NuGet Product(s) Involved
- MSBuild.exe
- dotnet.exe
- NuGet SDK
The Elevator Pitch
Add a supported MSBuild Pack item for arbitrary symbol-package-only files and directory trees with an exact PackagePath, including files without extensions.
For example:
<ItemGroup>
<SymbolsPackageFile
Include="path/to/Foo.framework.dSYM/**/*"
PackagePath="symbols/Foo.framework.dSYM" />
</ItemGroup>
With SymbolPackageFormat=symbols.nupkg, dotnet pack should:
- Keep these files out of the normal
.nupkg.
- Include them at the exact requested paths in
.symbols.nupkg.
- Preserve extensionless filenames and recursive directory structure.
- Include the normal package payload and metadata in the symbol package as it does today.
This is related to #10860, which already tracks a public SymbolsPackageFile-style item and PackagePath support. This issue calls out an additional requirement for that design: extensionless files and complete symbol directory trees must be representable without renaming, flattening, nesting, or duplicating their payloads.
Additional Context and Details
Generic limitation
The current public extension point is TfmSpecificDebugSymbolsFile:
https://github.com/NuGet/NuGet.Client/blob/ebc9615813b3d475f848339310f1ced8e9fc1182/src/NuGet.Core/NuGet.Build.Tasks.Pack/NuGet.Build.Tasks.Pack.targets#L462-L472
It feeds TargetPathsToSymbols, but MSBuildProjectFactory.AddOutputLibFiles filters files using the extension of FinalOutputPath:
https://github.com/NuGet/NuGet.Client/blob/ebc9615813b3d475f848339310f1ced8e9fc1182/src/NuGet.Core/NuGet.Commands/MSBuildProjectFactory.cs#L128-L152
Empty entries cannot be added to AllowedOutputExtensionsInSymbolsPackageBuildOutputFolder, because Pack trims and excludes empty values. As a result, an extensionless symbol input is skipped.
Copying the input to a temporary file with an accepted extension does not preserve the requested package filename. PackageBuilder.ResolvePackagePath only treats a target as an exact filename when the source and target extensions match:
https://github.com/NuGet/NuGet.Client/blob/ebc9615813b3d475f848339310f1ced8e9fc1182/src/NuGet.Core/NuGet.Packaging/PackageCreation/Authoring/PackageBuilder.cs#L1019-L1051
For an extensionless target and a staged .dwarf source, NuGet treats the target as a directory and appends the staged filename.
Concrete Apple dSYM example
Apple's standard dSYM format is a directory bundle:
Foo.framework.dSYM/
└── Contents/
├── Info.plist
└── Resources/
├── DWARF/
│ └── Foo # extensionless MH_DSYM Mach-O
└── Relocations/...
The complete bundle is useful to Apple crash-reporting tools such as Crashlytics, Sentry, Raygun, Datadog, Bugsnag, New Relic, Embrace, and Rollbar. The extensionless Contents/Resources/DWARF/Foo file is also what Microsoft.SymbolStore indexes by Mach-O UUID for MSDL/SymWeb and dotnet-symbol retrieval.
Desired packages:
Foo.NativeAssets.iOS.nupkg
└── runtimes/ios/native/Foo.framework
Foo.NativeAssets.iOS.symbols.nupkg
├── runtimes/ios/native/Foo.framework
└── symbols/ios-arm64/Foo.framework.dSYM/
└── Contents/Resources/DWARF/Foo
Attempting to stage Foo as Foo.dwarf and set an extensionless TargetPath currently produces:
Contents/Resources/DWARF/Foo/Foo.dwarf
That is not a valid dSYM layout.
This also applies beyond Apple: native debug artifacts may be extensionless, may consist of a directory tree with auxiliary metadata, or may need to live outside lib/<tfm> and ref/<tfm>.
Why normal Pack=true content cannot solve it
Normal None/Content/PackageFile items are processed by AddContentFiles for both the normal and symbol builders. MSBuild is not reevaluated between those builders, so there is no supported target where a project can remove content only from the normal package.
IncludeSource=true is symbol-builder-only and can preserve extensionless names, but it forces files under src/, includes project/source files unless private items are filtered, and semantically misrepresents native symbols as source code.
Current workarounds
- Second Pack evaluation: run the same project's regular
Pack target again with symbol-only Pack=true content, write to an isolated directory, then name that generated archive .symbols.nupkg. This preserves the exact directory tree and does not modify an archive, but doubles Pack evaluation and requires recursion/output guards.
- Duplicate payload: include a raw renamed
.dwarf for symbol-server indexing plus a nested ZIP containing the complete dSYM for crash vendors. This approximately doubles native symbol size and nested ZIPs are not recursively indexed by Arcade.
- Post-process
.symbols.nupkg: reopen and edit the archive after Pack. This is fragile and bypasses NuGet's normal package construction.
- Abuse
IncludeSource: inject native symbols through SourceFilesProjectOutputGroup. This avoids the normal package but forces src/ paths and conflates native symbols with source content.
None is a good replacement for a public symbol-only content item.
Tested versions
- .NET SDK: 10.0.203
- NuGet Command Line: 7.3.1
SymbolPackageFormat: symbols.nupkg
Suggested acceptance criteria
- A public, supported item usable from outer or inner builds.
- Exact
PackagePath semantics for files and recursive directory globs.
- Files appear only in
.symbols.nupkg, never the normal .nupkg.
- Extensionless source and target filenames are supported.
- Normal package metadata/runtime payload is preserved in the symbol package.
- Incremental Pack inputs/outputs include these files.
snupkg may retain its existing portable-PDB-only restrictions; this request primarily targets legacy/general .symbols.nupkg.
NuGet Product(s) Involved
The Elevator Pitch
Add a supported MSBuild Pack item for arbitrary symbol-package-only files and directory trees with an exact
PackagePath, including files without extensions.For example:
With
SymbolPackageFormat=symbols.nupkg,dotnet packshould:.nupkg..symbols.nupkg.This is related to #10860, which already tracks a public
SymbolsPackageFile-style item andPackagePathsupport. This issue calls out an additional requirement for that design: extensionless files and complete symbol directory trees must be representable without renaming, flattening, nesting, or duplicating their payloads.Additional Context and Details
Generic limitation
The current public extension point is
TfmSpecificDebugSymbolsFile:https://github.com/NuGet/NuGet.Client/blob/ebc9615813b3d475f848339310f1ced8e9fc1182/src/NuGet.Core/NuGet.Build.Tasks.Pack/NuGet.Build.Tasks.Pack.targets#L462-L472
It feeds
TargetPathsToSymbols, butMSBuildProjectFactory.AddOutputLibFilesfilters files using the extension ofFinalOutputPath:https://github.com/NuGet/NuGet.Client/blob/ebc9615813b3d475f848339310f1ced8e9fc1182/src/NuGet.Core/NuGet.Commands/MSBuildProjectFactory.cs#L128-L152
Empty entries cannot be added to
AllowedOutputExtensionsInSymbolsPackageBuildOutputFolder, because Pack trims and excludes empty values. As a result, an extensionless symbol input is skipped.Copying the input to a temporary file with an accepted extension does not preserve the requested package filename.
PackageBuilder.ResolvePackagePathonly treats a target as an exact filename when the source and target extensions match:https://github.com/NuGet/NuGet.Client/blob/ebc9615813b3d475f848339310f1ced8e9fc1182/src/NuGet.Core/NuGet.Packaging/PackageCreation/Authoring/PackageBuilder.cs#L1019-L1051
For an extensionless target and a staged
.dwarfsource, NuGet treats the target as a directory and appends the staged filename.Concrete Apple dSYM example
Apple's standard dSYM format is a directory bundle:
The complete bundle is useful to Apple crash-reporting tools such as Crashlytics, Sentry, Raygun, Datadog, Bugsnag, New Relic, Embrace, and Rollbar. The extensionless
Contents/Resources/DWARF/Foofile is also what Microsoft.SymbolStore indexes by Mach-O UUID for MSDL/SymWeb anddotnet-symbolretrieval.Desired packages:
Attempting to stage
FooasFoo.dwarfand set an extensionlessTargetPathcurrently produces:That is not a valid dSYM layout.
This also applies beyond Apple: native debug artifacts may be extensionless, may consist of a directory tree with auxiliary metadata, or may need to live outside
lib/<tfm>andref/<tfm>.Why normal
Pack=truecontent cannot solve itNormal
None/Content/PackageFileitems are processed byAddContentFilesfor both the normal and symbol builders. MSBuild is not reevaluated between those builders, so there is no supported target where a project can remove content only from the normal package.IncludeSource=trueis symbol-builder-only and can preserve extensionless names, but it forces files undersrc/, includes project/source files unless private items are filtered, and semantically misrepresents native symbols as source code.Current workarounds
Packtarget again with symbol-onlyPack=truecontent, write to an isolated directory, then name that generated archive.symbols.nupkg. This preserves the exact directory tree and does not modify an archive, but doubles Pack evaluation and requires recursion/output guards..dwarffor symbol-server indexing plus a nested ZIP containing the complete dSYM for crash vendors. This approximately doubles native symbol size and nested ZIPs are not recursively indexed by Arcade..symbols.nupkg: reopen and edit the archive after Pack. This is fragile and bypasses NuGet's normal package construction.IncludeSource: inject native symbols throughSourceFilesProjectOutputGroup. This avoids the normal package but forcessrc/paths and conflates native symbols with source content.None is a good replacement for a public symbol-only content item.
Tested versions
SymbolPackageFormat:symbols.nupkgSuggested acceptance criteria
PackagePathsemantics for files and recursive directory globs..symbols.nupkg, never the normal.nupkg.snupkgmay retain its existing portable-PDB-only restrictions; this request primarily targets legacy/general.symbols.nupkg.