NuGet Packages for unstable, in-development Libraries
This article discusses a way how to create and use a NuGet Package for Libraries which are not yet stable and which are still under active development.
Especially, the core idea is to develop such an library and a project consuming this library in an efficient way.
As we develop on Windows with Visual Studio, NuGet is a powerful tool to manage library dependencies in Projects, for external 3rd Party libraries, as well as for our own libraries. Using Git our alternatives would be:
- Git Submodules – with their known problems (detached head, multiple commits, etc.)
- cmake – vs. Visual Studio solutions
- manual paths / zip packages – manual work
Using NuGet seems the way to go for Windows-Only projects. Even for native c++ components. (Side note: coapp is dead and is not missed.)
However: if we have a library component and a consuming project, which we have to actively develop both at the same time, the NuGet process gets prohibitively tiring. For each change in the library, we need to build the lib, repack the NuGet package, increasing its (build) version number, update the consuming project’s NuGet reference, and build the project… For every tiny typo.
The core idea is to prepare the NuGet package in such a way, that a local override of the package’s content is possible. Think of an environment variable holding the file system path to the library’s development directory. If this environment variable is not set, the content of the NuGet package is use. If the environment variable is set, then include paths and library paths are set to directly use the files found in that development directory (obviously, resulting in an unclean in-development version of the project).
Since native NuGet packages simply use an MSBuild script to integrate themselves into a project, we have (almost) all the possibilities MSBuild has to offer.
Set up the NuGet Package
Creating a native C++ library NuGet package without coapp is not very difficult, but out-of-focus of this article. For the sake of simplicity I only discuss the (second) easiest task: a static library. We need some header file and some static library files. All in all the whole file looks like this:
<?xml version="1.0" encoding="utf-8"?> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <!-- Create package: cd solution directory nuget pack NugetDevPackageTest.testLib.nuspec -Version 0.2.0-build001 --> <!-- package metadata --> <metadata> <id>NugetDevPackageTest.testLib</id> <version>$version$</version> <title>NugetDevPackageTest testLib</title> <authors>Sebastian Grottel</authors> <projectUrl>https://bitbucket.org/sgr_faro/nuget-dev-package-test/overview</projectUrl> <requireLicenseAcceptance>false</requireLicenseAcceptance> <description>Nuget package of `someLib` to test dev nuget package concept</description> <tags>nugetdevpackagetest testLib native</tags> </metadata> <!-- content --> <files> <!-- include header files --> <file src="DemoClass.h" target="build\native\include" /> <!-- static libraries --> <file src="lib\Win32\Debug\some.lib" target="build\native\lib\Win32\Debug" /> <file src="lib\Win32\Release\some.lib" target="build\native\lib\Win32\Release" /> <file src="lib\x64\Debug\some.lib" target="build\native\lib\x64\Debug" /> <file src="lib\x64\Release\some.lib" target="build\native\lib\x64\Release" /> <!-- build rules --> <file src="NugetDevPackageTest.testLib.targets" target="build\native" /> </files> </package>
There are two interesting aspects to highlight here:
- The static libraries can be collected from any directories. Important is that you specify the correct target directories. Follow the semantic rules NuGet specifies.
- The first build rule file, NugetDevPackageTest.testLib.targets, controls the integration of the NuGet Package’s content into the Visual Studio project. This file must have the same name as the package itself.
We set up the targets file as follows:
<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <!-- If development directory is empty, use content from the package --> <PropertyGroup Condition="'$(NugetDevPackageTest_testLib_DevDir)' == ''"> <__NugetDevPackageTest_testLib_IncludeDir>$(MSBuildThisFileDirectory)include/</__NugetDevPackageTest_testLib_IncludeDir> <__NugetDevPackageTest_testLib_LibrarayPath>$(MSBuildThisFileDirectory)lib\$(Platform)\$(Configuration)\some.lib</__NugetDevPackageTest_testLib_LibrarayPath> </PropertyGroup> <!-- If development directory is set, use the content found there --> <PropertyGroup Condition="'$(NugetDevPackageTest_testLib_DevDir)' != ''"> <__NugetDevPackageTest_testLib_IncludeDir>$(NugetDevPackageTest_testLib_DevDir)/</__NugetDevPackageTest_testLib_IncludeDir> <__NugetDevPackageTest_testLib_LibrarayPath>$(NugetDevPackageTest_testLib_DevDir)\lib\$(Platform)\$(Configuration)\some.lib</__NugetDevPackageTest_testLib_LibrarayPath> </PropertyGroup> <!-- If development directory is set, define an additional macro --> <ItemDefinitionGroup Condition="'$(NugetDevPackageTest_testLib_DevDir)' != ''"> <ClCompile> <PreprocessorDefinitions>HAS_NUGETDEVPACKAGETEST_TESTLIB_DEVDIR;%(PreprocessorDefinitions)</PreprocessorDefinitions> </ClCompile> </ItemDefinitionGroup> <!-- Compiler options: package macro and include paths --> <ItemDefinitionGroup> <ClCompile> <PreprocessorDefinitions>HAS_NUGETDEVPACKAGETEST_TESTLIB;%(PreprocessorDefinitions)</PreprocessorDefinitions> <AdditionalIncludeDirectories>$(__NugetDevPackageTest_testLib_IncludeDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> </ClCompile> <ResourceCompile> <AdditionalIncludeDirectories>$(__NugetDevPackageTest_testLib_IncludeDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> </ResourceCompile> </ItemDefinitionGroup> <!-- Linker options: library (full) path --> <ItemDefinitionGroup> <Link> <AdditionalDependencies>$(__NugetDevPackageTest_testLib_LibrarayPath);%(AdditionalDependencies)</AdditionalDependencies> </Link> </ItemDefinitionGroup> </Project>
Create the NuGet package:
nuget pack NugetDevPackageTest.testLib.nuspec -Version 0.2.0-build002
(obviously, you adjust the version number as you like to.) and be happy that it already does work as you intended: if the environment variable NugetDevPackageTest_testLib_DevDir is set, the content of this folder is used instead of the NuGet package’s content.
Setting an environment variable and the need to restart Visual Studio any time you change it, this is obviously not great. We can improve on that by adding a GUI.
Visual Studio Property Page
We add an Visual Studio property page for your project to the NuGet package, allowing us to change the setting of the variable from within Visual Studio. For this we add an XML description of the values we want to change:
<?xml version="1.0" encoding="utf-8"?> <ProjectSchemaDefinitions xmlns="clr-namespace:Microsoft.Build.Framework.XamlTypes;assembly=Microsoft.Build.Framework"> <!-- Project property settings to override content of nuget development packages --> <Rule Name="ProjectSettings_NugetDevOverride" PageTemplate="tool" DisplayName="Nuget Development Override" SwitchPrefix="/" Order="1"> <!-- Category to identify this package --> <Rule.Categories> <Category Name="NugetDevPackageTest_testLib" DisplayName="NugetDevPackageTest.testLib" /> </Rule.Categories> <!-- Store override settings only in the user file --> <Rule.DataSource> <DataSource Persistence="UserFile" ItemType="" /> </Rule.DataSource> <!-- Override development path --> <StringProperty Name="NugetDevPackageTest_testLib_DevDir" DisplayName="Development Directory" Subtype="folder" Description="Override content of Nuget package with content of this directory. If this field is empty (default) the content of the nuget package is used. This value is persistent as user setting only." Category="NugetDevPackageTest_testLib" /> </Rule> </ProjectSchemaDefinitions>
Note that the StringProperty directly stores it’s value in the variable we are already using. Make sure not to use this variable as an Environment variable anymore!
Now, we reference this property page from the other files of our NuGet package:
... <!-- build rules --> <file src="NugetDevPackageTest.testLib.targets" target="build\native" /> <file src="overrideSettings.xml" target="build\native" /> </files> </package>
<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <!-- include the gui property page to override the development directory --> <ItemGroup> <PropertyPageSchema Include="$(MSBuildThisFileDirectory)\overrideSettings.xml" /> </ItemGroup> <!-- If development directory is empty, use content from the package --> ...
Now create the new version of the package. Update the package reference in your consuming project. You might need to restart visual studio once. And you should find the new property page in the settings of your project.
Use the NuGet Package or the local Version
Be Aware of all the pitfalls
- Never commit the .user file of your projects to the repository!
- If your project code requires local changes within the code usually packed into NuGet, your project will not compile correctly on any other machine!
- Before you push, or at least before you create a pull request, you need to push and publish the changed NuGet project and you need to reference the NuGet package cleanly without override.
- You need to use two Visual Studios running, one for your project and one for the NuGet-ted project.
- If you run your project in the debugger, and you step into functions from the NuGet project, the corresponding source file is opened in the Visual Studio of your project. You can edit the files there, but of course you cannot compile the NuGet library here. Remember to switch to the right Visual Studio for that.
The Proposed Process
|testApp – Visual Studio #1||testLib – Visual Studio #2||Comment|
|Open testAppsolution in Visual Studio #1|
|Build testApp||This fetches the NuGet package of testLib and builds the App with the content of that package, e.g. version 1.0.0|
|Open testLib solution in Visual Studio #2|
|(Build testApp)||Nothing changes. Still the NuGet package is used. Build should not do anything (up to date)|
|Open Project settings and override the nuget settings.||Reference the directory where you cloned and built testLib|
|(Build testApp)||Known Issue: Visual Studio still believes the project would be up to date|
|Rebuild testApp||App is now built with the testLib built in your local directory!|
|(Build testApp)||The project is up to date (which is correct)|
|Change some code and build testLib|
|Build testApp||The project correctly detects that it needs to build (at least to link) again. The changes of the local testLib can directly be used.|
|You start testApp in the Debugger||You can even enter code of testLib, look at local / private variables, etc. The corresponding source files of testLib are opened in Visual Studio #1!|
|You edit source of testLib||WRONG Visual Studio!|
|You hit Build → testApp is up to date||It is the wrong Visual Studio. You cannot build testLib there. You need to switch to the other Visual Studio|
|Visual Studio might detect the changes at the source files, if they are opened.||If the files are not opened, everything is ok. If Visual Studio does ask you, select to keep changes made outside Visual Studio #2|
|Hit F5 (start Debugging)||Binary and source code do not match! Visual Studio #1 should warn you about it if you try, e.g., to set a breakpoint in source code of testLib|
|Build testLib||The testLib is update with the changes to the source code you made in Visual Studio #1|
|Hit F5 (start Debugging)||Visual Studio #1 correctly detects the updates of testLib, builds again, and executes testApp with the newest version of testLib.|
Develop your changes in testApp and testLib. Now you are finished and want to push a stable version to your CI system
|Be sure testLib is correct in all configurations|
|Commit + Push testLib|
|Trigger built / update of new NuGet package (we call it Ver++)|
|Disable the override settings for the NuGet package|
|Update the testApp project to use the NuGet package Ver++||Ideally this package should be the result of your CI system.|
|Compile and testApp in all configurations||Since you had the identical code tested before locally, this should not show any problems. If it does, you likely missed something before.|
|Commit + Push your final update of testApp||This should be solely the increment of the NuGet package version|
|Build / Test your testApp on your CI system||All should work fine.|
Visual Studio Property Pages GUI
The Visual Studio GUI is sometimes not correctly updated. One such situation is created by the property page descriptions injected via NuGet packages.
If you initially add a NuGet package with such a property page, or if the NuGet package you are using changed its property page, Visual Studio might not show you the right proptery page (or might not show any property page).
If this happens, restart Visual Studio after installing / restoring the NuGet package and you should be fine.
Visual Studio’s Fast Up to Date Check misses Changes of the StringProperty
When in Visual Studio a build is triggered (e.g. by pressing F5 for a Debug start), Visual Studio does not invoke MSBuild to determine if the project is up to date. Instead is uses an internal mechanism to quickly decide if a rebuild is required. This internal mechanism, known as fast up to date check, does not perform all required checks. E.g., it misses changed to the paths defined by the variable defined within the property page GUI created by our NuGet package.
This means, whenever the value of this variable is changed, you should trigger a rebuild of the consuming project.
One workaround would be to disable the fast up to date check for this project. However, for large projects, like SCENE, this introduces more pain than the required rebuild.
There is an semantic problem: you reference a Nuget package version, e.g. 1.0.0. But you override the content with changed files. Those will eventually be published as a new version, e.g. 1.0.1. But until then, your consuming project will reference those as version 1.0.0.
Your build process must be aware of this issue.
You should introduce integration tests to make sure such information mismatch never enters release branches.
Deep Dependency Graph Version Conflict
Consuming the unstable library from the development directory, is a project setting, not a solution setting. If you have a deep dependency graph and you are consuming this library via multiple routes, the override setting only influences one single route! Especially, if you use another Nuget package which uses your lib nuget, you cannot do anything about it. You need to be carefully aware of potential binary compatibility issues in such a construct.
Workarounds could be based compile-time and run-time checks of version numbers and the “dirty-lib-flag” (cf. HAS_NUGETDEVPACKAGETEST_TESTLIB_DEVDIR).