Skip to content

Support extensionless files and directory trees as symbol-only pack content #15056

Description

@mattleibow

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:

  1. Keep these files out of the normal .nupkg.
  2. Include them at the exact requested paths in .symbols.nupkg.
  3. Preserve extensionless filenames and recursive directory structure.
  4. 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

  1. 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.
  2. 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.
  3. Post-process .symbols.nupkg: reopen and edit the archive after Pack. This is fragile and bypasses NuGet's normal package construction.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions