Adding content files in nuget packages

Short description

Oleksandr, a senior developer in the document digitization team, describes how to add font files to a NuGet package. He initially attempts to add files directly to the package, but the fonts are only available in packages that directly reference the font package. After researching, he manually writes a .nuspec file and .props file with the logic to be executed during project compilation. He adds a buildTransitive folder to the project to include fonts, .nuspec, and .props files. He cites best practices for transferring metadata from the project file to the .nuspec file and notes that variable values can be viewed from the project file when building.

Adding content files in nuget packages

Greeting! My name is Oleksandr, I am a senior developer in the document digitization team. When we develop component library packages, we sometimes need to add some content to the executable code. Often this content is needed in the form of separate files, rather than built-in resources. Examples of such tasks are various .NET shell packages that typically require source libraries. We needed special fonts in the document conversion library.

We saw two solutions to the problem:

  1. Embed fonts as embedded resources and copy them when initializing the library to the target folder.

  2. Add files to the nuget package.

The first solution is actually hardcoded. If a user of the library wants to use their fonts instead of ours, we’ll still add them to the application folder on every startup. So we decided to add the font files to the nuget package.

I will describe the solutions and pitfalls I came across in the process of work. Habré already has one article on this topic. I would like to elaborate on my decision and discuss some points that were not discussed in that material.

First attempt (failed)

First, I decided to set the properties Build Action – None and Copy to Output Directory – Copy if never for all files. This solution works fine when directly referencing projects in the solution, but in nuget packages the fonts were only available in packages that directly referenced the font package.

For example, we have a LibA nuget package that contains fonts. LibA is used in the LibB nuget package, and when LibB is added to the project, the fonts remain available. LibB is used in the nuget package LibC, and adding LibC to a project does not add fonts.

Second attempt (successful)

After much research on MSDN and StackOverflow, I have come to the conclusion that it is better to do everything manually. That is, write a .nuspec file with a description of the package and a .props file with the logic that will be executed during the compilation of the project.

Add the buildTransitive folder to the project, add fonts, .nuspec and .props files to it. I named the folder buildTransitive because there will be a folder with the same name in the package. It is needed so that each subsequent package in the link chain has access to the fonts. You can learn more about her from the documentation.

Add references to .props and .nuspec in the project file.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    ...
    <NuspecFile>buildTransitive\LibA.nuspec</NuspecFile> 
  </PropertyGroup>
  ...
  <Import Project="buildTransitive\LibA.props" />
</Project>

We open the .nuspec file and describe which files to put where in our package.

<files>
    <file src="https://habr.com/ru/companies/tinkoff/articles/728242/LibA.props" target="buildTransitive" />
    <file src="https://habr.com/ru/companies/tinkoff/articles/728242/fonts\**" target="buildTransitive\fonts" />
    <file src="https://habr.com/ru/companies/tinkoff/articles/728242/..\bin\Debug\net6.0\LibA.dll" target="lib\net6.0\LibA.dll" />   
</files>

In this case, we copy the LibA.props files and the fonts folder to the buildTransitive package folder, and the compiled project file to the lib\net6.0\LibA.dll package folder.

WARNING! Always use a backslash in paths! Otherwise, you can get different results when compiling for Windows and Linux. The situation is described in more detail here.

We describe the logic of copying font files during compilation in a .props file.

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <None Include="$(MSBuildThisFileDirectory)fonts\**" >
      <Link>fonts\%(RecursiveDir)%(Filename)%(Extension)</Link>
      <PackageCopyToOutputDirectory>PreserveNewest</PackageCopyToOutputDirectory>
      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <Visible>False</Visible>
    </None>
  </ItemGroup>
</Project>

In this script, the contents of the fonts folder from the build package will be recursively copied to the fonts folder in the source directory.

We are adding functionality

At this stage, it is already possible to assemble a package with fonts, when using which fonts will be automatically copied to the source directory, regardless of the length of the chain of links to our package.

Many IDEs allow you to fill in information about the author, company, package description, and so on. Ideally, this metadata about the nuget package should be obtained from the project file. Additional variables can be passed during the build. In my case, it was the package version.

To transfer data from the project file to .nuspec, we use the NuspecProperties tag. As a result, the project file will look like this:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>disable</ImplicitUsings>
    <Nullable>disable</Nullable>
    <GeneratePackageOnBuild>True</GeneratePackageOnBuild>
    <Version>$(PkgVersion)</Version>
    <Authors>Authors list</Authors>
    <Description>Project description</Description>
    <NuspecFile>buildTransitive\LibA.nuspec</NuspecFile>
    <NuspecProperties>$(NuspecProperties);PackageId=$(MSBuildProjectName)</NuspecProperties>
    <NuspecProperties>$(NuspecProperties);PackageAuthors=$(Authors)</NuspecProperties>
    <NuspecProperties>$(NuspecProperties);PackageDescription=$(Description)</NuspecProperties>
  </PropertyGroup>
  <Target Name="NuspecProperties" AfterTargets="Build">
    <PropertyGroup>
      <NuspecProperties>$(NuspecProperties);PackageVersion=$(Version)</NuspecProperties>
      <NuspecProperties>$(NuspecProperties);PackageTargetPath=$(TargetPath)</NuspecProperties>
    </PropertyGroup>
  </Target>
  <Import Project="buildTransitive\LibA.props" />
</Project>

The version number will be passed during build in the PkgVersion variable and recorded in the Version tag. Variables from .nuspec are passed to the NuspecProperties variable in key-value pairs and separated by semicolons. In this case, data about the version and the final path to the source file of the assembly are written after the build.

If you do not pass the version of the PkgVersion variable and run the project build, its number will be 1.0.0.

Compilation of the project on the build machine is started using the command:

- dotnet build $ SOLUTION_FILE_PATH -c Release --no-restore -p:PkgVersion=$PACKAGE_VERSION --output outDir

In this case, if the GeneratePackageOnBuild tag is set to true in the project file, the project will be built and the nuget package will be created. If you set the GeneratePackageOnBuild tag to false and separate the dotnet build and dotnet pack operations, the data from the project file will not get into the .nuspec file.

An example of a .nuspec file with added variables declared in the project file:

<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
  <metadata>
    <id>$PackageId$</id>
    <version>$PackageVersion$</version>
    <authors>$PackageAuthors$</authors>
    <description>$PackageDescription$</description>
    <dependencies>
      <group targetFramework="net6.0" />
    </dependencies>
  </metadata>
  <files>
    <file src="https://habr.com/ru/companies/tinkoff/articles/728242/LibA.props" target="buildTransitive" />
    <file src="https://habr.com/ru/companies/tinkoff/articles/728242/fonts\**" target="buildTransitive\fonts" />
    <file src="https://habr.com/ru/companies/tinkoff/articles/728242/$PackageTargetPath$" target="lib\net6.0\LibA.dll" />
  </files>
</package>

That’s all. I hope my article will help someone save time and nerves! The test project can be viewed on GitHub.

Additional Information

My test project that contains the fonts does not reference any other packages. If you add a reference to another nuget package, it will be written to the project file and will have to be manually written into the .nuspec. To avoid manually maintaining referential integrity, in a working project I moved the fonts into a separate assembly that contains nothing but fonts, created a separate package, and referenced it from other projects.

If you need to view variable values ​​from the project file when building, you can do so using the Message tag.

<Project Sdk="Microsoft.NET.Sdk">
  ...
  <Target Name="Log" AfterTargets="Build">
    <Message Importance="High" Text="----------Build Variables-------------" />
    <Message Importance="High" Text="MSBuildProjectName = $(MSBuildProjectName)" />
    <Message Importance="High" Text="TargetPath = $(TargetPath)" />
    <Message Importance="High" Text="NuspecProperties = $(NuspecProperties)" />
    <Message Importance="High" Text="----------Build Variables-------------" />
  </Target>
  ...
</Project>

If you have any questions or want to share your experience with nuget packages, feel free to comment.

Related posts