Enabling Reproducible builds when building NuGet packages

 
 
  • Gérald Barré

Reproducible builds are important when building NuGet packages from public sources. Indeed, it gives your consumers confidence in your packages by allowing them to validate the package has actually been built using the public sources. Indeed, they would be able to rebuild the package and compare the result with the one you published. To be able to reproduce a build, you need the source files, the referenced DLLs, the compiler version, and the compiler options (language version, defines, nullables, etc.)

All these information are stored in the pdb file. Note that you quickly find them using NuGet Package Explorer:

NuGet Package Explorer shows the compilation information stored in the pdb file

NuGet Package Explorer shows the list of files stored in the pdb file

#How to create a reproducible build in .NET

To create a reproducible build in .NET, you need to use a recent version of the compiler and set a few options in the project file. You could do it manually or just use the NuGet package DotNet.ReproducibleBuilds. This package does many things for you:

  • Ensure MSBuild 16.10 or above is used
  • Enable SourceLink for GitHub, GitLab, Azure DevOps, BitBucket
  • Set ContinuousIntegrationBuild to true if it detects a known build environment (GitHub Actions, Azure Pipelines, AWS CodeBuild, GitLab, AppVeyor, etc.)
  • Set PublishRepositoryUrl to true to publish the repository URL and the commit in the NuGet package
  • Set EmbedUntrackedSources to true to include generated files in the NuGet package
  • Set DebugType to embedded if not already set to another mode

You can add the DotNet.ReproducibleBuilds package to your project file:

csproj (MSBuild project file)
<Project>
  <!-- Enabling reproducible builds -->
  <ItemGroup>
    <PackageReference Include="DotNet.ReproducibleBuilds" Version="0.1.66">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    </PackageReference>
  </ItemGroup>

  <!-- Optional but recommended: NuGet Package configuration -->
  <PropertyGroup>
    <Authors>Meziantou</Authors>
    <PackageIcon>icon.png</PackageIcon>
    <PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
    <PackageReadmeFile>readme.md</PackageReadmeFile>
  </PropertyGroup>

  <ItemGroup>
    <None Include="$(MSBuildThisFileDirectory)\icon.png" Pack="true" PackagePath="" Visible="false" />
    <None Include="$(MSBuildThisFileDirectory)\LICENSE.txt" Pack="true" PackagePath="" Visible="false" />
    <None Include="$(MSBuildThisFileDirectory)\readme.md" Pack="true" PackagePath="" />
  </ItemGroup>
</Project>

If you use dotnet pack to build the package on the CI server, you should get a valid NuGet package. You can valide everything is ok by using NuGet Package Explorer:

NuGet Package Explorer shows the package is valid

You can also use the dotnet-validate tool to validate a package

PowerShell
dotnet tool update -g dotnet-validate --version 0.0.1-preview.169
dotnet validate package local package.nupkg

Result of dotnet-validate

#Deterministic build vs Reproducible build

Deterministic builds ensure identical outputs, byte for byte, when given the same inputs. The inputs means the same file paths (in the same order), the same references, the same compiler options, and so on.

Reproducibility concerns whether a build can be recreated in a different environment with the same outcome. For instance, the .NET compiler allows reproducible builds allow rewriting the file paths, so they are not dependent on the local environment. For example, c:\users\meziantou\file1.cs can be rewritten as /_/file1.cs by specifying a RootPath, so it doesn't depend on the local environment. The same applies for files in NuGet packages.

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub