Symbols, Source Link and Deterministic Packages

This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its dotnet CLI) and NuGet 7.9, as published in the MSBuild, .NET and NuGet documentation on learn.microsoft.com, which is the reference these pages are written and verified against. Material about .NET 11 / NuGet 8.0 is still in preview and is always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The C# Reference bibliography lists the reference material consulted while preparing these pages.

A consumer debugging into a library needs two things: symbols (PDB files that map machine code back to source lines) and the source itself at the exact commit. Source Link puts the commit and a URL template into the PDB; symbol packages or embedded PDBs deliver the PDB; a deterministic build lets anyone check that the binaries came from that commit. This page does all three for Ledgerline with .NET SDK 10.0.402 (NuGet 7.9.0), and checks the results with dotnet-validate. The debugger side is covered in Build and Tooling — Symbols and Source Link.

Symbol packages (.snupkg)

IncludeSymbols=true with SymbolPackageFormat=snupkg (NuGet 4.9+) produces a second file next to the package, containing only the PDBs in the same folder layout:

dotnet pack src/Ledgerline -o artifacts/sym -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg
unzip -l artifacts/sym/Ledgerline.1.0.0.snupkg
  Successfully created package '.../artifacts/sym/Ledgerline.1.0.0.nupkg'.
  Successfully created package '.../artifacts/sym/Ledgerline.1.0.0.snupkg'.

_rels/.rels
Ledgerline.nuspec
lib/net10.0/Ledgerline.pdb
lib/net8.0/Ledgerline.pdb
lib/netstandard2.0/Ledgerline.pdb
[Content_Types].xml
package/services/metadata/core-properties/nuget.psmdcp

dotnet nuget push of the .nupkg pushes the .snupkg beside it automatically (to the source’s symbol endpoint, or -ss/-sk for another one; --no-symbols to skip). nuget.org indexes it on its symbol server (https://symbols.nuget.org/download/symbols, preconfigured in Visual Studio), after validation that usually takes under 15 minutes. Its constraints: only .pdb, .nuspec, .xml, .psmdcp, .rels and .p7s files; only portable PDBs (the SDK default), so native C++ projects with Windows PDBs must use the legacy format.

The legacy .symbols.nupkg (SymbolPackageFormat=symbols.nupkg, or --include-symbols alone) is a full copy of the package plus PDBs — lib/net8.0/Ledgerline.dll, .pdb and .xml together — and with --include-source also the sources under src/. nuget.org’s symbol server indexes .snupkg files; the legacy format is for other symbol servers.

Embedded PDBs

DebugType=embedded puts the PDB inside each DLL: no second file to push or host, at the cost of larger assemblies for everyone. It is the simplest choice for feeds without a symbol server (GitHub Packages and most private feeds). Applications can also copy PDBs from their package references into the output with CopyDebugSymbolFilesFromPackages (SDK 7.0.100+), for better stack traces in production.

Since the .NET 8 SDK, Source Link for GitHub, Azure Repos, GitLab and Bitbucket is part of the SDK and on by default — Microsoft.SourceLink.GitHub now ships in the SDK (10.0.402 is also the latest version on nuget.org). With the repository’s origin pointing at GitHub, the build writes a sourcelink.json into each PDB:

{"documents":{"/_/*":"https://raw.githubusercontent.com/ledgerline-example/ledgerline/e33226ff8326b40770b01db9cc78bfd2d7044fc8/*"}}

(ledgerline-example is a placeholder remote; nothing was pushed.) /_/ is the normalized source root that ContinuousIntegrationBuild produces. Source Link also fills the package’s <repository> element; with PublishRepositoryUrl=true and no explicit RepositoryUrl, it publishes the origin URL and the branch:

<repository type="git" url="https://github.com/ledgerline-example/ledgerline.git" branch="refs/heads/main" commit="e33226ff8326b40770b01db9cc78bfd2d7044fc8" />

Other properties: EmbedUntrackedSources (on by default in SDK 8+) embeds generated files that are not in git, such as obj/*.AssemblyInfo.cs; IncludeSourceRevisionInInformationalVersion=false removes the +<commit> suffix that .NET 8 added to InformationalVersion (a source-compatibility breaking change for code that parsed it). Referencing a Source Link package explicitly in an SDK 8+ project overrides the built-in one.

The documentation lags here: the library guidance’s Source Link page (2019) sends readers to the dotnet/sourcelink repository’s instructions for adding a package, and the authoring best practices say ✔️ CONSIDER setting up Source Link as an extra step; neither says that SDK 8+ does it by default for the four hosts.

ContinuousIntegrationBuild

ContinuousIntegrationBuild=true enables settings meant for official builds only — above all, normalizing the source paths stored in PDBs (/_/src/... instead of /home/runner/work/...), which Source Link and deterministic builds need, but which stops a local debugger from finding local files. Set it from the CI system’s variable; the Directory.Build.props of Ledgerline does:

<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true' or '$(TF_BUILD)' == 'true'">
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

Checking a package

dotnet-validate (third-party, from the NuGet Package Explorer authors, in preview: the newest listed version is 0.0.1-preview.582; 0.0.1 exists but is unlisted, and dnx dotnet-validate@0.0.1 reported it "not found") checks Source Link, determinism and compiler flags:

dnx dotnet-validate@0.0.1-preview.582 --yes -- package local artifacts/sym/Ledgerline.1.0.0.nupkg

Four builds of the same Ledgerline 1.0.0, checked this way:

Build Source Link Deterministic Compiler flags

ContinuousIntegrationBuild=true + .snupkg

✅ Valid with Symbol Server

✅ Valid

✅ Valid

ContinuousIntegrationBuild=true + DebugType=embedded

✅ Valid

✅ Valid

✅ Valid

local build + .snupkg

⚠️ Contains untracked sources (obj)

❌ Non deterministic

✅ Valid

local build, no symbols

❌ Missing Symbols

❌ Non deterministic

❌ Missing

For the local build it printed Ensure that the following property is enabled for CI builds and listed every source file with its absolute path — the paths ContinuousIntegrationBuild would have normalized. The desktop app NuGet Package Explorer (third-party, same authors; its web version is nuget.info) shows the same checks graphically, as the library guidance suggests.

Deterministic packs

A deterministic build (the default since the .NET Core SDK) produces byte-identical assemblies from the same source. The package around them is another matter: the ZIP entries carry timestamps, and NuGet wrote files in varying order until NuGet 7.3 (SDK 10.0.200) fixed the order. NuGet 7.9 (SDK 10.0.400) "revisited deterministic pack", adding the Deterministic and DeterministicTimestamp pack properties and SOURCE_DATE_EPOCH support, opt-in until .NET 11.

Measured on 10.0.402, packing the same build twice two seconds apart:

dotnet pack src/Ledgerline --no-build -o artifacts/det1    # and det2
SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) dotnet pack src/Ledgerline --no-build -o artifacts/det5 \
  -p:Deterministic=true -p:DeterministicTimestamp=true     # and det6
sha256sum artifacts/det*/Ledgerline.1.0.0.nupkg
c37e20a3375219a4  artifacts/det1/Ledgerline.1.0.0.nupkg
9ebf32be13873b45  artifacts/det2/Ledgerline.1.0.0.nupkg
8e897dcf6cf0aa34  artifacts/det5/Ledgerline.1.0.0.nupkg
14587c990fdf3bc9  artifacts/det6/Ledgerline.1.0.0.nupkg

(hashes shortened). Every pair differed, and a comparison of the entries showed that only the ZIP timestamps differed — the DLLs, XML files and manifests inside were identical, and the entries were still stamped with the current time even with DeterministicTimestamp=true and SOURCE_DATE_EPOCH set, although the diagnostic log showed the property reaching the pack task. So on this SDK, compare packages by their contents, not by hashing the .nupkg — the "Deterministic" check of dotnet-validate looks at the assemblies, which is what matters for verification.

Common mistakes

  • Pushing the .nupkg without its .snupkg — or the reverse, a .snupkg without the matching package; the symbol server rejects it.

  • ContinuousIntegrationBuild on developer machines — the debugger no longer finds local sources.

  • Adding Microsoft.SourceLink.GitHub to an SDK 8+ project "because the docs say so" — it pins an older version over the built-in one.

  • Windows PDBs (DebugType=full) in a .snupkg — nuget.org rejects them.

  • Expecting identical .nupkg hashes — not with 10.0.402, as measured above.