How to create a multi NET framework NuGet package through TFS 2017.3
Here are the steps to make a regular .NET project become a multi framework NuGet package. I have also included the steps to build it with TFS 2017.3. This article came to life after I have come across a problem that took me some times to debug and finally figure it out. I needed that an existing NuGet package have more than one .NET framework to be available. The solution is so neat that I needed to share it.
Changes to the project file
The initial project file
The project I am working with is a regular C# project. Here is the content:
<?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="..\packages\xunit.runner.visualstudio.2.3.1\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\packages\xunit.runner.visualstudio.2.3.1\build\net20\xunit.runner.visualstudio.props')" /> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <ProjectGuid>{D645388E-F937-4310-9D13-F2107612C36C}</ProjectGuid> <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>BuildLogParsers.UnitTests</RootNamespace> <AssemblyName>BuildLogParsers.UnitTests</AssemblyName> <TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SccProjectName>SAK</SccProjectName> <SccLocalPath>SAK</SccLocalPath> <SccAuxPath>SAK</SccAuxPath> <SccProvider>SAK</SccProvider> <NuGetPackageImportStamp> </NuGetPackageImportStamp> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> <DebugType>full</DebugType> <Optimize>false</Optimize> <OutputPath>bin\Debug\</OutputPath> <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> <Optimize>true</Optimize> <OutputPath>bin\Release\</OutputPath> <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> </PropertyGroup> <ItemGroup> <Reference Include="Castle.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL"> <HintPath>..\packages\Castle.Core.4.0.0\lib\net45\Castle.Core.dll</HintPath> </Reference> <Reference Include="Moq, Version=4.7.10.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL"> <HintPath>..\packages\Moq.4.7.10\lib\net45\Moq.dll</HintPath> </Reference> <Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> <HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> <Reference Include="System.Xml.Linq" /> <Reference Include="System.Data.DataSetExtensions" /> <Reference Include="Microsoft.CSharp" /> <Reference Include="System.Data" /> <Reference Include="System.Net.Http" /> <Reference Include="System.Xml" /> <Reference Include="xunit.abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> <HintPath>..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll</HintPath> </Reference> <Reference Include="xunit.assert, Version=2.1.0.3179, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> <HintPath>..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll</HintPath> </Reference> <Reference Include="xunit.core, Version=2.1.0.3179, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> <HintPath>..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll</HintPath> </Reference> <Reference Include="xunit.execution.desktop, Version=2.1.0.3179, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> <HintPath>..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll</HintPath> </Reference> </ItemGroup> <ItemGroup> <Compile Include="Issues\IssuesTypeManagerTests.cs" /> <Compile Include="Loader\DataTextLoaderTests.cs" /> <Compile Include="Loader\IssuesJsonLoaderTests.cs" /> <Compile Include="Parsers\BuildLogParserTests.cs" /> <Compile Include="Parsers\SubProcesses\AbstractSubProcessLogParserTests.cs" /> <Compile Include="Parsers\SubProcesses\GenericLogParserTests.cs" /> <Compile Include="TestsFoundations\Helpers\AbstractSubProcessLogParserTestsHelper.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Streams\StreamsManagerTests.cs" /> <Compile Include="TestsFoundations\UnitTestsFoundation.cs" /> </ItemGroup> <ItemGroup> <None Include="App.config" /> <None Include="packages.config" /> <None Include="Settings.StyleCop" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\BuildLogParsers\BuildLogParsers.csproj"> <Project>{ff436071-73cc-4141-903c-c0b418120f49}</Project> <Name>BuildLogParsers</Name> </ProjectReference> </ItemGroup> <ItemGroup> <Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" /> </ItemGroup> <ItemGroup /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild"> <PropertyGroup> <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText> </PropertyGroup> </Target> </Project>
I would like to point you out a couple of concepts in this project file:
- All files to be included in the project are declared. It can become a source of problem when merging branches with big project files.
- All default information is listed for each configuration.
- The old format of NuGet is used (with the package folder).
On the way…
First, let me tell you a couple of ways I have tried and did not work. I tried to set one debug configuration and one release configuration per framework I wanted to build. Something like that:
- Debug-Net451
- Release-Net451
- Debug-Net452
- Release-Net452
- …
The problem is that the NuGet restore command will failed because NuGet does not know how to deal with multiple build configuration like that. Also, the build in TFS was getting complicated to setup with all the build configurations for each platform…
The Solution to Multi Framework NuGet package!
So… the solution is to take advantage of the new format for dot net core!
Unload the project, edit the csproj file and replace the content with the new dot net core project format:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks>net451;net452</TargetFrameworks> </PropertyGroup> </Project>
Note the s in TargetFrameworks tag. This is the way you can declare the number of frameworks you need to build against.
If you reload your project, you will see, Visual Studio will reload all the files correctly. In fact, it takes the reverse approach of including all the files and will add the files you want to exclude in the csproj when you specify them. Better than that, once your project will be reloaded, you can now edit the csproj directly without having to unload/reload the project. Reload your project, and have a look to the new option on the right click on the project: Edit YourProject.csproj.
Missing Packages
It’s possible that if you were using some packages for your project, by replacing the csproj by the code above, you flushed them out. You must now add them back and you can add conditions depending on the framework targeted. For example, in one of the unit tests projects, I added those lines with the conditions attributes:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks>net451;net452</TargetFrameworks> </PropertyGroup> <ItemGroup> <PackageReference Include="Moq" Version="4.8.1" /> <PackageReference Include="xUnit" Version="2.3.1" Condition="'$(TargetFramework)' == 'net452'" /> <!-- xunit 2.2.0 is not compatible with .net 4.5.1.--> <PackageReference Include="xUnit" Version="2.1.0" Condition="'$(TargetFramework)' == 'net451'" /> <PackageReference Include="xUnit.runner.visualstudio" Version="2.3.1" /> </ItemGroup> </Project>
As you can see, I have specified a condition for the xunit framework because the version 2.2.0 is not compatible anymore with .Net 4.5.1.
Duplicate information
If you try to build, you will probably run into compile errors specifying duplicate information in the AssemblyInfo.cs. With the new csproj format, the compiler is now generating all those information for us. Copy your information if you have some information customized to your csproj file:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks>net451;net452</TargetFrameworks> </PropertyGroup> <PropertyGroup> <Version>1.0.0</Version> <FileVersion>1.0.0</FileVersion> <Authors>The Futue of Code</Authors> <Description>Library used for demo purposes.</Description> <Copyright>Copyright (c) 2018 The Future Of Code</Copyright> </PropertyGroup> </Project>
The NuSpec information
When creating a NuGet package, you must usually specify the information inside a nuspec file. This file is now embedded into your new csproj as well (pretty interesting no?). You can enter the information from your nuspec file by specifying a property group just like this (add all the information you need):
<PropertyGroup> <PackageId>TheFutureOfCode.DevOps.LogParser</PackageId> <PackageVersion>1.0.0</PackageVersion> <PackagerequireLicenseAcceptance>false</PackagerequireLicenseAcceptance> <PackageReleaseNotes>This is my release notes.</PackageReleaseNotes> </PropertyGroup>
You should now be able to build your project. If you have any missing references, add them, but there is nothing else special to know. What is pretty cool about this approach when you build your project is that it just restore and build all the platforms needed in the bin/Debug or bin/Release folder.
MSBuild 15 to restore and pack
NuGet.exe does not know how to deal with this format. So, you must use msbuild 15 that comes with VS2017 to handle that. Open a developer command prompt for VS2017 and type:
msbuild YourProject.csproj /t:restore
I suggest you to have a look at this link for more information.
So, yes, we get rid of the nuspec file, and the AssemblyInfo.cs. Pretty cool no?
How to build the multi platform NuGet package into TFS 2017.3
Let see what it looks like to build such a project in TFS 2017.3. Here are the basic steps to make the restoration works properly:
The importants points are:
- MSBuild version 15 (2017) must be selected, otherwise, it won’t work.
- You can ask MSBuild to only restore, this is the important part. You can do the rest of your process the way you want.
- I enable the regular build, there is no need to ask MSBuild to do it, but I could have done it via
msbuild /t:build
. - I am running my tests using the regular way with the VSTests task. It will run all tests for each framework.
Finally the pack command must use the one from msbuild 15 also (msbuild /t:pack
):
Run your build and you should get a nice package with all the frameworks binaries you have specified.
References
Some of the good references I found that help me to figure out how to put all of that together:
- https://www.bartwolff.com/Blog/2017/03/20/targeting-multiple-net-platforms-in-a-single-nuget-package-w
- https://blog.nuget.org/20170316/NuGet-now-fully-integrated-into-MSBuild.html
- https://docs.microsoft.com/en-us/nuget/consume-packages/package-restore
- https://docs.microsoft.com/en-us/nuget/reference/msbuild-targets
How to build a NuGet package for Cake
I started to use Cake (www.cakebuild.net) a while ago and once I had gone through all the basis and got something working in my build.cake file, I wanted to extract those features to put them into external cake files to finally move them into a NuGet package. After many tries and errors, I finally figured it out and decided to share it on TheFutureOfCode.
Assumptions
- I assume that you know how Cake works, at least to create a basic build.cake. If you don't, please have a look here: https://cakebuild.net/docs/tutorials/getting-started
- I also assumed that you know the basics about NuGet.
1) The build.cake file
Let's create a build.cake file that will make all the calls we need for our future NuGet package. This script allows you to restore, build and run tests. It also defines a way to automatically find your solution file if not provided on the command line when calling the bootstrapper (build.ps1 or build.sh):
#load nuget:?package=TheFutureOfCode.Build.Cake var target = Argument<string>("Target", "Default"); var configuration = Argument("Configuration", "Release"); FilePath solutionFile = Argument<FilePath>("SolutionFile", null); Task("Default").IsDependentOn("UnitTests"); Task("Initialize") .Does(() => { solutionFile = GetSolutionFile(Context, solutionFile); }); Task("Restore") .IsDependentOn("Initialize") .Does(() => { NuGetRestore(Context, solutionFile); }); Task("Build") .IsDependentOn("Restore") .Does(() => { Build(Context, solutionFile, configuration, "Build"); }); Task("UnitTests") .IsDependentOn("Build") .WithCriteria(() => BuildSystem.IsLocalBuild) .Does(() => { RunUnitTestsUsingXUnit( Context, configuration); }); RunTarget(target);
Some considerations must be observed in this build.cake file:
- Each task is calling functions that we will define later into the NuGet package called TheFutureOfCode.Build.Cake. This allows any Build Master to let developers work around the main part of the build process, without compromising their freedom.
- There is no version on the NuGet package that we load. It's intentional if you end up with many build.cake files and you need to update all of them at once. You want them to use the latest version of the package.
- You need Cake version 0.24.0 and up to have the
#load
directive understand your NuGet call correctly.
2) The NuGet package
The NuGet package can consist of the number of files you need. In this example, I only use one, but in other projects I have been working on, I have many more files. To build your NuGet package, you need to follow the rules defined by NuGet:
- Put your cake files into the tools directory.
- Use a
nuget spec
to create your spec file and edit it.
My spec file looks like this:
<?xml version="1.0"?> <package > <metadata> <id>TheFutureOfCode.Build.Cake</id> <version>1.0.0.0</version> <authors>The Future Of Code</authors> <owners>The Future Of Code</owners> <requireLicenseAcceptance>false</requireLicenseAcceptance> <description>Cake scripts used to build any product.</description> <releaseNotes> 1.0.0.0: First version of package TheFutureOfCode.Build.Cake. </releaseNotes> <copyright>Copyright 2018</copyright> </metadata> </package>
I "hide" my functions into a file called Tasks.cake:
#tool nuget:?package=xunit.runner.console&version=2.3.1 public static FilePath GetSolutionFile(ICakeContext context, FilePath solutionFile) { if (solutionFile == null) { // Look for local solution file. IEnumerable<FilePath> slnFiles = context.Globber.GetFiles("*.sln"); if (slnFiles.Count() == 1) { context.Log.Information($"Solution that will be loaded: {slnFiles.First().ToString()}"); return slnFiles.First(); } else { if (slnFiles.Count() == 0) { throw new System.IO.FileNotFoundException($"No solution file found."); } else { foreach(FilePath sln in slnFiles) { context.Log.Information($"Solution found: {sln.ToString()}"); } throw new System.IO.IOException($"More than one solution file were found. Please specify which one to use."); } } } context.Log.Information($"Solution that will be loaded: {solutionFile.ToString()}"); return solutionFile; } public static void NuGetRestore(ICakeContext context, FilePath nugetFileToRestore) { context.StartProcess( "nuget", new ProcessSettings { Arguments = new ProcessArgumentBuilder() .Append("restore") .Append(nugetFileToRestore.ToString()) }); } public static void Build(ICakeContext context, FilePath fileToBuild, string configuration, string buildTarget) { context.MSBuild(fileToBuild, settings => settings.SetConfiguration(configuration) .WithTarget(buildTarget) ); } public static void RunUnitTestsUsingXUnit(ICakeContext context, string configuration) { context.XUnit2($"**/bin/{configuration}/*Test*.dll"); }
- To have each Cake API call working, you must call the desired function on the ICakeContext!
- I have not used the NuGet API call from Cake, since I wanted to use the version 4 of NuGet which was not supported at the time of writing this article.
3) Create your package, publish it and build your project
The only things left is to pack your package and push it to a repository (here is my publish.sh that I use on my mac):
#!/usr/bin/env bash nuget pack nuget push *.nupkg -source Local rm -f *.nupkg
Then run the Cake bootstrapper (build.sh or build.ps1) in your project, and it should automatically download your newly created NuGet package. You don't have to add anything to the tools/package.config file to specify the version of your NuGet package since the #load nuget:
command will take care of getting the package correctly.
I wish you a very good cake!
References
- NuGet: https://docs.microsoft.com/en-us/nuget/what-is-nuget
- Cake: https://cakebuild.net/
- Pluralsight classes: