Symbols, Source Link and Deterministic Packages
|
This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its 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.
Source Link
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>
Source: ContinuousIntegrationBuild.
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 |
|---|---|---|---|
|
✅ Valid with Symbol Server |
✅ Valid |
✅ Valid |
|
✅ Valid |
✅ Valid |
✅ Valid |
local build + |
⚠️ 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
.nupkgwithout its.snupkg— or the reverse, a.snupkgwithout the matching package; the symbol server rejects it. -
ContinuousIntegrationBuildon developer machines — the debugger no longer finds local sources. -
Adding
Microsoft.SourceLink.GitHubto 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
.nupkghashes — not with 10.0.402, as measured above.