Skip to content

Fix NativeAOT publish including satellite assemblies despite embedding them#124192

Merged
sbomer merged 5 commits intomainfrom
copilot/fix-aot-publish-satellite-assemblies
Mar 24, 2026
Merged

Fix NativeAOT publish including satellite assemblies despite embedding them#124192
sbomer merged 5 commits intomainfrom
copilot/fix-aot-publish-satellite-assemblies

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 9, 2026

Description

NativeAOT embeds satellite assemblies into the native binary but still copies them to the publish folder. After PR #124801 refactored the NativeAOT build integration to work with ResolvedFileToPublish directly, this fix removes project satellite assemblies from that item group.

Fix: Add removal of IntermediateSatelliteAssembliesWithTargetPath from ResolvedFileToPublish in the ComputeLinkedFilesToPublish target:

<ItemGroup>
  <ResolvedFileToPublish Remove="@(_IlcManagedInputAssemblies)" />
  <!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
  <ResolvedFileToPublish Remove="@(_DebugSymbolsIntermediatePath)" />
  <!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
  <ResolvedFileToPublish Remove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
  <!-- replace apphost with binary we generated during native compilation -->
  <ResolvedFileToPublish Include="$(NativeBinary)">
    <RelativePath>$(NativeBinaryPrefix)$(TargetName)$(NativeBinaryExt)</RelativePath>
    <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
  </ResolvedFileToPublish>
</ItemGroup>

This follows the same pattern as removing managed assemblies and debug symbols, which are also embedded or replaced in the native binary.

Customer Impact

  • Affected customers: NativeAOT users with localized resources
  • Regression: No
  • Source incompatibility: No
  • Breaking change: No (removes extraneous files from publish output)

Testing

Testing will be added in the SDK repo per review feedback. The fix can be validated by publishing a NativeAOT app with satellite assemblies and verifying that:

  • Localized resources are accessible at runtime (embedded correctly)
  • No satellite assembly subdirectories exist in publish output

Risk

Minimal. One-line change following established pattern. Satellite assemblies remain embedded and functional; only removes redundant disk copies.

Original prompt

This section details on the original issue you should resolve

<issue_title>AOT publish includes satellite assemblies in publish folder</issue_title>
<issue_description>### Describe the bug
Publishing an AOT app will include satellite resource assemblies (for example <language>\AppName.resources.dll in the output folder. It appears that these are also embedded in the NativeAOT executable, the app can still show localized strings even if these are deleted.

Ideally if these satellite assemblies are not needed, they should not be copied to the publish folder. As it is, it's confusing and makes it look like they need to be deployed with the app.

To Reproduce

  • Create a console app
  • Set PublishAot to true in the .csproj file
  • Add a resx file and a localized resx file with a string resource in them (for example Strings.resx and Strings.es.resx)
  • Publish the app

Expected: No language subfolders and satellite assemblies in the publish folder

Actual: Language subfolders with satellite assemblies are present in the publish folder

Repro project

Binlog

Further technical details

.NET SDK version: 10.0.102</issue_description>

<agent_instructions>Fix this bug. Pay attention to the analysis from @baronfel about how to fix it.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@baronfel I gave this problem to Copilot CLI equipped with the viktorhofer/dotnet-skills plugin, and here was its analysis after publishing + inspecting the binlog:

Root Cause Analysis

The satellite assemblies (e.g. es\AppName.resources.dll) end up in the NativeAOT publish output because the NativeAOT build integration correctly embeds them into the native binary but fails to remove them from the publish file list, so the SDK's generic publish pipeline copies them to the output directory anyway.

How satellite assemblies flow through the pipeline

Step 1: NativeAOT collects satellite assemblies for embedding

In Microsoft.NETCore.Native.Publish.targets (dotnet/runtime), _ComputeIlcCompileInputs gathers satellite assemblies from two sources:

<IlcSatelliteAssembly Include="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssembly Include="@(IntermediateSatelliteAssembliesWithTargetPath)" />
  • _SatelliteAssembliesToPublish = satellite assemblies from package/project references (extracted from _ResolvedCopyLocalPublishAssets by ComputeManagedAssembliesToCompileToNative)
  • IntermediateSatelliteAssembliesWithTargetPath = the project's own satellite assemblies (e.g. es\52913-resx-in-nativeaot.resources.dll)

These are passed to ILC via --satellite: in Microsoft.NETCore.Native.targets, which embeds them into the native binary. This part works correctly — the app can resolve localized strings even if the satellite DLLs are deleted from disk.

Step 2: ComputeLinkedFilesToPublish cleans up the publish list — but misses the project's own satellites

ComputeLinkedFilesToPublish runs BeforeTargets="ComputeResolvedFilesToPublishList" and modifies the publish list:

<_ResolvedCopyLocalPublishAssets Remove="@(_AssembliesToSkipPublish)" />  <!-- removes package satellites -->
<_ResolvedCopyLocalPublishAssets Include="@(_LinkedResolvedAssemblies)" />
<_DebugSymbolsIntermediatePath Remove="@(_DebugSymbolsIntermediatePath)" />

<IntermediateAssembly Remove="@(IntermediateAssembly)" />  <!-- replaces managed .dll with native binary -->
<IntermediateAssembly Include="$(NativeBinary)" />

This successfully removes package-reference satellite assemblies (via _AssembliesToSkipPublish) and replaces the managed assembly with the native binary. But it does NOT remove IntermediateSatelliteAssembliesWithTargetPath.

Step 3: The SDK unconditionally re-adds the project's satellite assemblies to publish

In Microsoft.NET.Publish.targets (dotnet/sdk), ComputeResolvedFilesToPublishList unconditionally includes:

<!-- Copy satellite assemblies. -->
<ResolvedFileToPublish Include="@(IntermediateSatelliteAssembliesWithTargetPath)">
  <RelativePath>%(IntermediateSatelliteAss...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes dotnet/runtime#124191

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

@github-actions github-actions Bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Feb 9, 2026
… test

Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix AOT publish to exclude satellite assemblies Fix NativeAOT publish including satellite assemblies despite embedding them Feb 9, 2026
Copilot AI requested a review from dsplaisted February 9, 2026 18:37
@jkotas jkotas added area-NativeAOT-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Feb 9, 2026
Comment thread src/tests/nativeaot/SmokeTests/SatelliteAssemblies/Program.cs Outdated
Comment thread src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets Outdated
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
@baronfel
Copy link
Copy Markdown
Member

@nagilson @agocke here is that PR about NAOT and satellite assemblies.

@agocke
Copy link
Copy Markdown
Member

agocke commented Mar 16, 2026

@sbomer has been refactoring the targets so I'm not sure if there's a better place for this now

Copy link
Copy Markdown
Member

@sbomer sbomer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right target to do the update. Once #124801 merges, I think we should remove these items from ResolvedFileToPublish instead

@sbomer
Copy link
Copy Markdown
Member

sbomer commented Mar 16, 2026

@copilot fix merge conflict (ensure the satellite assemblies are removed from ResolvedFileToPublish).

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 16, 2026

@copilot fix merge conflict (ensure the satellite assemblies are removed from ResolvedFileToPublish).

Merge conflict fixed in commit fffa5f9. Satellite assemblies are now removed from ResolvedFileToPublish on line 31 of the targets file, following the refactored approach from PR #124801.

@dotnet-policy-service dotnet-policy-service Bot added the linkable-framework Issues associated with delivering a linker friendly framework label Mar 16, 2026
Copilot AI requested a review from sbomer March 16, 2026 18:21
@sbomer sbomer force-pushed the copilot/fix-aot-publish-satellite-assemblies branch from fffa5f9 to 3feb4a7 Compare March 16, 2026 18:28
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets
@sbomer
Copy link
Copy Markdown
Member

sbomer commented Mar 16, 2026

Actually it looks like the fix falls out of #124801. That's because it's removing _IlcManagedInputAssemblies from the publish set, and that appears to include satellites. Although I'm not sure if the satellite assemblies should flow through that ItemGroup in the first place - I think they get passed to ILC both as --satellite and as normal references. Maybe that could be cleaned up.

@sbomer
Copy link
Copy Markdown
Member

sbomer commented Mar 16, 2026

@sbomer
Copy link
Copy Markdown
Member

sbomer commented Mar 23, 2026

Tested again with P3 bits and that doesn't seem to be the case, the satellite assemblies still show up. I think removing them from ResolvedFileToPublish is the proper fix (validated locally).

@sbomer sbomer marked this pull request as ready for review March 23, 2026 23:37
Copilot AI review requested due to automatic review settings March 23, 2026 23:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Removes redundant project satellite resource assemblies from NativeAOT publish output, aligning publish artifacts with NativeAOT behavior where satellites are embedded into the native binary.

Changes:

  • Remove @(IntermediateSatelliteAssembliesWithTargetPath) entries from @(ResolvedFileToPublish) during NativeAOT publish.
  • Keeps existing publish-list cleanup (managed inputs, managed PDBs) and native-binary replacement behavior intact.

@sbomer sbomer enabled auto-merge (squash) March 24, 2026 17:40
@sbomer
Copy link
Copy Markdown
Member

sbomer commented Mar 24, 2026

/ba-g "timeouts"

@sbomer sbomer merged commit d4cc2d9 into main Mar 24, 2026
95 of 105 checks passed
@sbomer sbomer deleted the copilot/fix-aot-publish-satellite-assemblies branch March 24, 2026 17:41
@nagilson
Copy link
Copy Markdown
Member

I don't think this fix completely resolved the issue: #127086 (comment)

@MichalStrehovsky
Copy link
Copy Markdown
Member

I don't think this fix completely resolved the issue: dotnet/runtime#127086 (comment)

We regressed that one, that one used to work. In my comment above (#124192 (comment)) I'm asking copilot about where are we dropping satellite assemblies from references because those I was certain we did drop already at that time.

I just double checked with a simple Console.WriteLine(typeof(System.CommandLine.RootCommand)); with <PackageReference Include="System.CommandLine" Version="2.0.6" /> (because I know System.CommandLine has satellite assemblies). .NET 11 Preview 2 drops them. With .NET 11 Preview 3 we keep them :(.

@baronfel
Copy link
Copy Markdown
Member

@MichalStrehovsky should we open an issue on runtime to track? Or do you think the answer is on our side?

@sbomer
Copy link
Copy Markdown
Member

sbomer commented Apr 17, 2026

Taking a look - I suspect the issue is on the runtime side. I moved that issue over to track.

sbomer added a commit that referenced this pull request Apr 21, 2026
…em from publish output (#127089)

NativeAOT publish was leaking satellite `.dll` files from NuGet packages
into the publish directory and failing to pass them to ILC via
`--satellite:` (required for resource embedding at link time).

**Root cause:** `ComputeManagedAssembliesToCompileToNative` classified
satellites by PE-inspecting items flagged `PostprocessAssembly=true`.
The SDK never sets this metadata on resource items
(`AssetType=resources`), so the satellite branch was dead code —
`SatelliteAssemblies` output was always empty. PR #111514 introduced
this regression; PR #124192 partially fixed project-own satellites via
`IntermediateSatelliteAssembliesWithTargetPath` but left the package
case broken.

## Changes

**`Microsoft.NETCore.Native.Publish.targets`**
- Populate `IlcSatelliteAssembly` via
`Condition="'%(ResolvedFileToPublish.AssetType)' == 'resources'"`
(package satellites) in addition to the existing
`IntermediateSatelliteAssembliesWithTargetPath` (project satellites)
- Replace `Remove="@(IntermediateSatelliteAssembliesWithTargetPath)"`
with `Remove="@(IlcSatelliteAssembly)"` so both package and project
satellites are pruned from `ResolvedFileToPublish`
- Drop the now-unused `<Output TaskParameter="SatelliteAssemblies" ...>`
wire-up

**`ComputeManagedAssembliesToCompileToNative.cs`**
- Remove dead `SatelliteAssemblies` `[Output]` property, PE-reading
culture classification loop, `PostprocessAssembly` guard block, and the
now-unused `System.Reflection.Metadata` /
`System.Reflection.PortableExecutable` usings (~45 lines removed, none
of it ever ran)

# Customer Impact

NativeAOT apps that reference NuGet packages containing satellite
assemblies (e.g. `System.CommandLine`) would fail to resolve localized
resources at runtime and would have stray `.dll` files in the publish
output, defeating the single-file native binary guarantee.

# Regression

Yes — regressed in PR #111514 ("Run ILC after
ComputeResolvedFilesToPublishList"). PR #124192 closed the gap for
project-own satellites but missed package satellites.

# Testing

End-to-end validated with a repro app (`PublishAot=true`,
`System.CommandLine 2.0.6` package dependency, project `Strings.resx` +
`Strings.es.resx`): publish output is clean (only native binary + debug
symbols), ILC receives the expected `--satellite:` arguments for all
cultures, and runtime correctly resolves `es`/`de`/`ja` resources from
both package and project sources.

# Risk

Low. The targets change is purely additive for the satellite item
population and replaces a narrower `Remove` with a broader one that is a
strict superset of the previous behavior. The task change removes code
that was provably never executed under the current SDK contract.

# Package authoring no longer needed in .NET 9

IMPORTANT: Starting with .NET 9, you no longer need to edit a NuGet
package's csproj to enable building and bump the version.
Keep in mind that we still need package authoring in .NET 8 and older
versions.

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

<analysis>
Chronological review:
1. User asked to read dotnet/runtime PR #124192 + related SDK issue
comments
2. Asked for a repro in /tmp
3. Asked to root cause
4. Asked for logging / both app and package resources in repro
5. Asked about runtime behavior
6. Asked how SDK populates ResolvedFileToPublish
7. Asked what gets passed as --satellite
8. Asked to fix
9. Asked if I tested
10. Asked about task's satellite filtering being moot → led to cleanup
11. Asked about `_IlcReferenceSatelliteAssemblies` naming → kept
(consistency)
12. Then asked: "What if we consider IlcSatelliteAssembly to be the
snapshot?" → simplified to inline include +
`Remove="@(IlcSatelliteAssembly)"`
13. Asked to remove two comments
14. Asked to re-test
15. Asked to commit → committed as `e765e126dcb`
16. Most recent: asked "Why weren't we passing satellites, did that
regress at some point?" → I investigated git history, found PR #111514
was the regression point.

Recent commands and results:
- `git log --oneline --all -S 'SatelliteAssemblies'` on task file →
found b134fa2 (#86689, 2023, MichalStrehovsky, original satellite
support)
- `git show b134fa2` → showed original implementation: task read ALL
items, PE-inspected, classified by Culture
- `git log --oneline --all -S 'PostprocessAssembly'` → found e144870
"Skip non-PostprocessAssembly files..." (Feb 17 2026) and a5cee7c
"Derive ILC inputs from ResolvedFileToPublish..." (Feb 25 2026), both by
Sven Boemer
- `git log --all --oneline --ancestry-path` → showed these were part of
branch ilc-compile-order / ilcPublish
- `git log main --grep='Run ILC after'` → confirmed squash-merged as
`44e21b71149 Run ILC after ComputeResolvedFilesToPublishList (#111514)`

Final answer given: PR #111514 regressed satellite passing. Original PR
#86689 (2023) worked. PR #124192 partially fixed project-own case. This
commit closes package case.

Commit created: e765e126dcb on branch satellite-fix, not pushed.
</analysis>

<summary>
1. Conversation Overview:
- Primary Objectives: Investigate dotnet/runtime PR #124192 /
dotnet/sdk#53422 regression where NativeAOT publish leaks package
satellite `.dll` files AND fails to embed them via ILC. Root-cause, fix,
validate, commit.
- Session Context: Progressed through reading comments → repro → root
cause → logging → SDK trace → fix implementation → code review/cleanup
iterations → test → commit → regression history.
- User Intent Evolution: From fixing the surface bug to cleaning up dead
code, then simplifying the fix (inlining `IlcSatelliteAssembly` as the
snapshot), then historical attribution.

2. Technical Foundation:
- .NET 11 preview 3 SDK (`11.0.100-preview.3.26172.108`) pinned via
global.json in the repro
- NativeAOT publish pipeline: `_ComputeIlcCompileInputs` →
`NativeCompile` (post PR #125629 at HEAD) /
`ComputeLinkedFilesToPublish` (pre #125629 in installed SDK)
- SDK `_ComputeAssembliesToPostprocessOnPublish` deliberately excludes
satellites from `PostprocessAssembly=true` metadata
- Package/ref satellites land in `ResolvedFileToPublish` with
`AssetType=='resources'`; project's own satellites arrive via
`IntermediateSatelliteAssembliesWithTargetPath`
- `IlcSatelliteAssembly` item → `--satellite:` ILC args (required for
embedding; NAOT can't load from disk)

3. Codebase Status:
-
`/home/sven/src/satellite-fix/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets`:
- Changed `<IlcSatelliteAssembly
Include="@(_SatelliteAssembliesToPublish)" />` to `<IlcSatelliteAssembly
Include="@(ResolvedFileToPublish)"
Condition="'%(ResolvedFileToPublish.AssetType)' == 'resources'" />`
- Changed `<ResolvedFileToPublish
Remove="@(IntermediateSatelliteAssembliesWithTargetPath)" />` to
`<ResolvedFileToPublish Remove="@(IlcSatelliteAssembly)" />`
- Dropped `<Output TaskParameter="SatelliteAssemblies"
ItemName="_SatelliteAssembliesToPublish" />`
-
`/home/sven/src/satellite-fix/src/coreclr/tools/aot/ILCompiler.Build.Tasks/ComputeManagedAssembliesToCompileToNative.cs`:
- Removed `SatelliteAssemblies` `[Output]` property, PE-reading culture
classification, `PostprocessAssembly` late-filter check,
`System.Reflection.Metadata`/`PortableExecutable` usings
     - Net −45 lines; +3/−3 on targets file
- Repro at `/tmp/aot-satellite-repro/app/` with diagnostic dump targets,
`PublishAot=true`, `System.CommandLine 2.0.6`,
`Strings.resx`+`Strings.es.resx`

4. Problem Resolution:
- Root cause: Task's classification logic gated on
`PostprocessAssembly=true` which SDK never sets on satellites, making
satellite branch unreachable
- Fix: Classify via `AssetType=='resources'` in MSBuild directly; use
`IlcSatelliteAssembly` as snapshot consumed by both ILC and the
`ResolvedFileToPublish` remove
- Dead code cleanup: task's `SatelliteAssemblies` output never produced
anything under current SDK contract

5. Progress Tracking:
   - Completed: investigation, repro, fix, cleanup, end-to-end val...

</details>

Adding a test for this in dotnet/sdk#53971.

<!-- START COPILOT CODING AGENT SUFFIX -->

Created from Copilot CLI via the copilot delegate command.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-NativeAOT-coreclr linkable-framework Issues associated with delivering a linker friendly framework

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants