Wednesday, March 19, 2014

Pack nuget packages locally and with TFS Build

Currently I’m working on a project at a customer where we are integrating a new ASP.NET MVC application into an existing ASP.NET WebForms application. Due the complexity of the legacy system (70+ projects) I decided to start a brand new solution for the new development and put this into a nuget package. Every time a new version of the ASP.NET MVC needs to be published, a new version of the nuget package gets installed on the existing legacy system.

To automate this, I started by adding a post-build event to the ASP.NET MVC Project that got triggered every time I was building a release build. So far, so good.

Next step was configuring the TFS Build so I could push the package to a share, this way every any of the team could install the package and I could use package restore. This way I no longer needed to check-in the package folder.

Note: Don’t use ‘enable Nuget Package Restore’ function on the solution, but enable it on ‘Tools > Options > Package Manager > General’ on this tab page you have a group box called Package Restore. Check both checkboxes. More information about this can be found on this blog by Xavier Decoster.

image

This is where the hell began. Building the project succeeded, but the build failed on packaging the nuget package. One of the issues I had was that nuget looked for was looking for the bin folder to copy the dll’s, but a TFS build doesn’t place the dll files in the bin folder after build, but to a binaries folder at solution level. A possible fix was changing the output path for a release build to ‘..\..\Binaries’ on the properties of the project file (Build tab). But this is rather a workaround then a good solution. So I looked further for a better solution.

Next I started to take a look at the nuget.targets file that gets added when you use the ‘enable Nuget Package Restore’ on the solution. I know I just told not to use this, and you shouldn’t, but this targets file also contained a task for packaging nuget packages. So the next thing I did was copying the content of the .nuget folder to my own folder and modified the NuGet.targets file.

If you thought this would solve all my problems, think again. The problem of the packaging  looking for the bin folder of the project was solved by adding –OutputDirectory “$(OutDir) “ to the pack command. Note the space after the $(OutDir). This must be present and is necessary to handle the double slashes that occurs when packaging on TFS. This results in something like 'bin\ \package.nupkg'. Not nice, but seems to work. Next problem I had was the fact I was using the “-IncludeReferencedProjects” option. This was still using the output path configured for this project instead of the binaries folder.

After some googling I found a helpfull comment on a nuget issue. (The last comment by athinton). So by adding -Properties OutputPath="$(OutDir) " to the pack statement, I solved the issue and packaging after a TFS build finally worked with the in“-IncludeReferencedProjects” option.

But … this broke the packaging inside visual studio. For making it work in VS, the -Properties OutputPath="$(OutDir) " must be removed, so that is why I’m using an MSBuild task to define all this rules.

The NuGetToolsPath must be change to the path where the nuget.exe file is located. (Note that it appears 2 times)
Extra options for packaging the nuget package can be added to the buildcommand element.

Last but not least, don’t forget to import this file in your project file.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">$(MSBuildProjectDirectory)\..\</SolutionDir>

<!-- Property that enables building a package from a project -->
<BuildPackage Condition=" '$(BuildPackage)' == '' ">true</BuildPackage>

<!-- Download NuGet.exe if it does not already exist -->
<DownloadNuGetExe Condition=" '$(DownloadNuGetExe)' == '' ">false</DownloadNuGetExe>
</PropertyGroup>

<PropertyGroup Condition=" '$(OS)' == 'Windows_NT'">
<!-- Windows specific commands -->
<NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), "Nuget"))</NuGetToolsPath>
</PropertyGroup>

<PropertyGroup Condition=" '$(OS)' != 'Windows_NT'">
<!-- We need to launch nuget.exe with the mono command if we're not on windows -->
<NuGetToolsPath>$(SolutionDir)Nuget</NuGetToolsPath>
</PropertyGroup>

<PropertyGroup>
<!-- NuGet command -->
<NuGetExePath Condition=" '$(NuGetExePath)' == '' ">$(NuGetToolsPath)\NuGet.exe</NuGetExePath>

<NuGetCommand Condition=" '$(OS)' == 'Windows_NT'">"$(NuGetExePath)"</NuGetCommand>
<NuGetCommand Condition=" '$(OS)' != 'Windows_NT' ">mono --runtime=v4.0.30319 $(NuGetExePath)</NuGetCommand>

<PackageOutputDir Condition="$(PackageOutputDir) == ''">$(OutDir) </PackageOutputDir>

<OutputPath Condition="'$(BuildingInsideVisualStudio)' == 'false' " >OutputPath="$(OutDir) "</OutputPath>
<OutputPath Condition="'$(BuildingInsideVisualStudio)' == 'true' " ></OutputPath>

<NonInteractiveSwitch Condition=" '$(VisualStudioVersion)' != '' AND '$(OS)' == 'Windows_NT' ">-NonInteractive</NonInteractiveSwitch>

<!-- Commands -->
<BuildCommand>$(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform);$(OutputPath)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -IncludeReferencedProjects</BuildCommand>

<!-- Make the build depend on restore packages -->
<BuildDependsOn Condition="$(BuildPackage) == 'true'">
$(BuildDependsOn);
BuildPackage;
</BuildDependsOn>
</PropertyGroup>
<Target Name="BuildPackage">
<Exec Command="$(BuildCommand)"
Condition=" '$(OS)' != 'Windows_NT' " />

<Exec Command="$(BuildCommand)"
LogStandardErrorAsError="true"
Condition=" '$(OS)' == 'Windows_NT' " />
</Target>
</Project>