Setting up multi-targeting build with Mono
In this post I’m going to show how I did my first PR to an open-source project, adding support for multi-target builds using Mono for FSUnit.
Finding an issue
I have always wanted to contribute to an open-source project. I’ve found myself many times looking at the issues list for tools I have used, unfortunately I never managed to actually do it. Then a tweet from Sergey Thion where he asked if anyone would try Don Syme’s suggestion for how to do multi-target builds using dotnet CLI and Mono. I thought why not, I’m coding F# on Linux and find it much simpler to maintain Linux dockers instead of Windows VMs for CI build agents.
Sergey had already created an issue in his project FSUnit
, which I said I was willing to try to fix for him. He was very greatful and I was feeling … semi confident :)
Step 1: Forking the project
Since you don’t have write access to someone elses repository, the first thing you’ll have to do is fork the repository to you own account. So, first I logged on to GitHub with my own account, then I moved to the fsprojects/FsUnit
repository and pressed the Fork
button in the top right corner.
This created a copy of FSUnit to my account, atlemann/FSUnit
.
Step 2: Cloning the fork
Next I cloned my copy of FSUnit
(Note the path containing my username) to my machine.
Here I’m cloning the repository and opening it in VSCode:
~src$ git clone git@github.com:atlemann/FsUnit.git
Cloning into 'FsUnit'...
remote: Counting objects: 9766, done.
remote: Compressing objects: 100% (57/57), done.
remote: Total 9766 (delta 61), reused 83 (delta 53), pack-reused 9655
Receiving objects: 100% (9766/9766), 72.63 MiB | 4.85 MiB/s, done.
Resolving deltas: 100% (4796/4796), done.
Checking connectivity... done.
~src$ code FsUnit
This is how it looks:
The integrated terminal in VSCode is great. At least when combined with git branch annotations so you always know which branch you’re in.
Step 3: How do I build this thing?
Nothing is as depressing as wanting to contribute to something and not being able to easily compile the thing. Fortunately, Sergey is following one of the most common conventions in the F# world: build using FAKE
, handle dependencies using Paket
. In the tree we find the usual build.sh/build.cmd
files which can be used to build on Linux and Windows respectivey. So here goes build.sh
:
Success! However, wasn’t the task to add multi-target build on Mono? Let’s check what it built:
Here we’ll have to check the FsUnit.NUnit.fsproj
file to see what’s going on.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net46</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition=" '$(OS)' != 'Windows_NT' ">
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<AssemblyName>FsUnit.NUnit</AssemblyName>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<Compile Include="AssemblyInfo.fs" />
<Compile Include="FsUnit.fs" />
<Compile Include="FsUnitTyped.fs" />
<Compile Include="GenericAssert.fs" />
<None Include="paket.references" />
<None Include="paket.template" />
<None Include="FsUnitSample.fs.pp">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="sample.paket.template" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'netstandard1.6' ">
<Reference Include="System" />
</ItemGroup>
<Import Project="..\..\.paket\Paket.Restore.targets" />
</Project>
Here we see <PropertyGroup Condition=" '$(OS)' != 'Windows_NT' ">
makes sure only netstandard2.0
target is build if not on windows, which explains it. Lets try to remove it and see what happens:
/usr/share/dotnet/sdk/2.1.4/Microsoft.Common.CurrentVersion.targets(1124,5):
error MSB3644: The reference assemblies for framework
".NETFramework,Version=v4.6" were not found. To resolve this, install the SDK
or Targeting Pack for this framework version or retarget your application to
a version of the framework for which you have the SDK or Targeting Pack
installed. Note that assemblies will be resolved from the Global Assembly
Cache (GAC) and will be used in place of reference assemblies. Therefore
your assembly may not be correctly targeted for the framework you intend.
[/home/atle/src/FsUnit/src/FsUnit.NUnit/FsUnit.NUnit.fsproj]
28 Warning(s)
1 Error(s)
Time Elapsed 00:00:59.80
Running build failed.
This fails, as expected.
Step 4: Fixing the issue
There are three projects in the solution that have to be fixed:
- FsUnit.NUnit
- FsUnit.MsTestUnit
- FsUnit.Xunit
First things first, now I will create a local branch to apply this fix:
atle@latle:~/src/FsUnit (master)$ checkout -b make.multitarget.build.on.linux
Branch make.multitarget.build.on.linux set up to track remote branch make.multitarget.build.on.linux from origin.
Switched to a new branch 'make.multitarget.build.on.linux'
atle@latle:~/src/FsUnit (make.multitarget.build.on.linux)$
In the issue posted by Sergey he links to a solution proposed by Don Syme, the creator of F#, so he probably knows what he’s talking about. The fix is to add the following content to a file in your repository, called e.g. netfx.props
, and import it to the relevant .fsproj
files.
<PropertyGroup>
<!-- When compiling .NET SDK 2.0 projects targeting .NET 4.x on Mono using 'dotnet build' you -->
<!-- have to teach MSBuild where the Mono copy of the reference asssemblies is -->
<TargetIsMono Condition="$(TargetFramework.StartsWith('net4')) and '$(OS)' == 'Unix'">true</TargetIsMono>
<!-- Look in the standard install locations -->
<BaseFrameworkPathOverrideForMono Condition="'$(BaseFrameworkPathOverrideForMono)' == '' AND '$(TargetIsMono)' == 'true' AND EXISTS('/Library/Frameworks/Mono.framework/Versions/Current/lib/mono')">/Library/Frameworks/Mono.framework/Versions/Current/lib/mono</BaseFrameworkPathOverrideForMono>
<BaseFrameworkPathOverrideForMono Condition="'$(BaseFrameworkPathOverrideForMono)' == '' AND '$(TargetIsMono)' == 'true' AND EXISTS('/usr/lib/mono')">/usr/lib/mono</BaseFrameworkPathOverrideForMono>
<BaseFrameworkPathOverrideForMono Condition="'$(BaseFrameworkPathOverrideForMono)' == '' AND '$(TargetIsMono)' == 'true' AND EXISTS('/usr/local/lib/mono')">/usr/local/lib/mono</BaseFrameworkPathOverrideForMono>
<!-- If we found Mono reference assemblies, then use them -->
<FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net45'">$(BaseFrameworkPathOverrideForMono)/4.5-api</FrameworkPathOverride>
<FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net451'">$(BaseFrameworkPathOverrideForMono)/4.5.1-api</FrameworkPathOverride>
<FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net452'">$(BaseFrameworkPathOverrideForMono)/4.5.2-api</FrameworkPathOverride>
<FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net46'">$(BaseFrameworkPathOverrideForMono)/4.6-api</FrameworkPathOverride>
<FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net461'">$(BaseFrameworkPathOverrideForMono)/4.6.1-api</FrameworkPathOverride>
<FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net462'">$(BaseFrameworkPathOverrideForMono)/4.6.2-api</FrameworkPathOverride>
<FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net47'">$(BaseFrameworkPathOverrideForMono)/4.7-api</FrameworkPathOverride>
<FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net471'">$(BaseFrameworkPathOverrideForMono)/4.7.1-api</FrameworkPathOverride>
<EnableFrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != ''">true</EnableFrameworkPathOverride>
<!-- Add the Facades directory. Not sure how else to do this. Necessary at least for .NET 4.5 -->
<AssemblySearchPaths Condition="'$(BaseFrameworkPathOverrideForMono)' != ''">$(FrameworkPathOverride)/Facades;$(AssemblySearchPaths)</AssemblySearchPaths>
</PropertyGroup>
FsUnit.NUnit
Now I changed the FsUnit.NUnit.fsproj
to look like this:
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\netfx.props" />
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net46</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<AssemblyName>FsUnit.NUnit</AssemblyName>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<Compile Include="AssemblyInfo.fs" />
<Compile Include="FsUnit.fs" />
<Compile Include="FsUnitTyped.fs" />
<Compile Include="GenericAssert.fs" />
<None Include="paket.references" />
<None Include="paket.template" />
<None Include="FsUnitSample.fs.pp">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="sample.paket.template" />
</ItemGroup>
<Import Project="..\..\.paket\Paket.Restore.targets" />
</Project>
I basically removed that condition for Windows_NT
and added an import for the netfx.props
in the second line:
<Import Project="..\..\netfx.props" />
Now we’ll try to build this project only, since it’s the only one that has been fixed:
$ dotnet build src/FsUnit.NUnit/FsUnit.NUnit.fsproj
...
Build succeeded
...
Success! For real this time:
FsUnit.MsTestUnit
I added the same fix there, but got the following error when trying to build:
$ dotnet build src/FsUnit.MsTestUnit/FsUnit.MsTest.fsproj
...
...
/home/atle/src/FsUnit/src/FsUnit.MsTestUnit/FsUnit.fs(14,20):
error FS1108: The type 'Exception' is required here and is
unavailable. You must add a reference to assembly 'System.Runtime,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'.
[/home/atle/src/FsUnit/src/FsUnit.MsTestUnit/FsUnit.MsTest.fsproj]
As Don Syme mentions in his comment, he had to add explicit references to some facade assemblies, System.Runtime
is one of those, so we’ll add it to FsUnit.MsTest.fsproj
and see what happens:
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\netfx.props" />
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net46</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<AssemblyName>FsUnit.MsTest</AssemblyName>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<Compile Include="AssemblyInfo.fs" />
<Compile Include="..\FsUnit.Xunit\CustomMatchers.fs">
<Link>CustomMatchers.fs</Link>
</Compile>
<Compile Include="FsUnit.fs" />
<None Include="paket.references" />
<None Include="paket.template" />
<None Include="FsUnitSample.fs.pp">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="sample.paket.template" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Runtime" />
</ItemGroup>
<Import Project="..\..\.paket\Paket.Restore.targets" />
</Project>
$ dotnet build src/FsUnit.MsTestUnit/FsUnit.MsTest.fsproj
...
Build succeeded
Success again!
FsUnit.Xunit
Adding netfx.props
and building gives a similar error message:
$ dotnet build src/FsUnit.Xunit/FsUnit.Xunit.fsproj
...
...
The type 'Exception' is required here and is unavailable. You must add a reference to assembly 'System.Runtime...'
The type 'Object' is required here and is unavailable. You must add a reference to assembly 'System.Runtime...'
We add the same reference as for MsTestUnit
and build again:
$ dotnet build src/FsUnit.Xunit/FsUnit.Xunit.fsproj
...
...
The type 'TypeInfo' is required here and is unavailable. You must add a reference to assembly 'System.Reflection
Now we add the a reference to System.Reflection
and build again:
$ dotnet build src/FsUnit.Xunit/FsUnit.Xunit.fsproj
...
...
Build succeeded
The FsUnit.Xunit.fsproj
now looks like this:
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\netfx.props" />
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net46</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<AssemblyName>FsUnit.Xunit</AssemblyName>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<Compile Include="AssemblyInfo.fs" />
<Compile Include="CustomMatchers.fs" />
<Compile Include="FsUnit.fs" />
<None Include="paket.references" />
<None Include="paket.template" />
<None Include="FsUnitSample.fs.pp">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="sample.paket.template" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Runtime" />
<Reference Include="System.Reflection" />
</ItemGroup>
<Import Project="..\..\.paket\Paket.Restore.targets" />
</Project>
Finally we should run build.sh
again, since it will build all projects and run all tests, just to make sure everything works.
Finished Target: All
---------------------------------------------------------------------
Build Time Report
---------------------------------------------------------------------
Target Duration
------ --------
Clean 00:00:00.0449729
AssemblyInfo 00:00:00.0276845
InstallDotNetCore 00:00:00.1632733
Build 00:01:02.8727158
CopyBinaries 00:00:00.0349621
NUnit 00:00:11.8522380
xUnit 00:00:12.2566259
RunTests 00:00:00.0001697
All 00:00:00.0000695
Total: 00:01:27.3429696
---------------------------------------------------------------------
Status: Ok
---------------------------------------------------------------------
Step 4: Submitting a PR
Now that we have made the changes to resolve the issue, we have to open a pull request to the original repository for the owner to merge. First I’ll commit my changes and push my local branch to my remote repository, also known as origin
.
I find VSCode's
git integration to be pretty good, so I usually stage files and write commit messages using it. (This shows my second commit, by the way)
Then we need to push it:
$ git push origin make.multitarget.build.on.linux
and then you’ll get a pull request button in your fork
:
When pressing the Compare & pull request
button, you will be sent to the original repository for opening a PR, where you can write some details on what is done. By adding Fixes: #IssueNr
, GitHub will automatically close the issue when the PR is closed, which is convenient.
Now press the Create pull request
button and your contribution is given to the owner of the repository.
And that’s it! My first PR to an open source project. And you should now be able to do multi-target builds using Mono for your own projects as well.