Fix NativeAOT publish including satellite assemblies despite embedding them - #124192

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

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

Conversation

CopilotAI commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

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>
<ResolvedFileToPublishRemove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublishRemove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublishRemove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublishInclude="$(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:

<IlcSatelliteAssemblyInclude="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssemblyInclude="@(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:

<_ResolvedCopyLocalPublishAssetsRemove="@(_AssembliesToSkipPublish)" /> <!-- removes package satellites -->
<_ResolvedCopyLocalPublishAssetsInclude="@(_LinkedResolvedAssemblies)" />
<_DebugSymbolsIntermediatePathRemove="@(_DebugSymbolsIntermediatePath)" />
<IntermediateAssemblyRemove="@(IntermediateAssembly)" /> <!-- replaces managed .dll with native binary -->
<IntermediateAssemblyInclude="$(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. -->
<ResolvedFileToPublishInclude="@(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-actionsgithub-actionsBot 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>
CopilotAI changed the title [WIP] Fix AOT publish to exclude satellite assembliesFix NativeAOT publish including satellite assemblies despite embedding themFeb 9, 2026
CopilotAI requested a review from dsplaistedFebruary 9, 2026 18:37
@jkotasjkotas 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 threadsrc/tests/nativeaot/SmokeTests/SatelliteAssemblies/Program.cs 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

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

@sbomersbomer left a comment

Copy link
Copy Markdown
Member

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

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

CopilotAI commented Mar 16, 2026

Copy link
Copy Markdown
ContributorAuthor

@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-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Mar 16, 2026
CopilotAI requested a review from sbomerMarch 16, 2026 18:21
@sbomer
sbomerforce-pushed the copilot/fix-aot-publish-satellite-assemblies branch from fffa5f9 to 3feb4a7CompareMarch 16, 2026 18:28
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets
@sbomer

Copy link
Copy Markdown
Member

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

I believe single-file handles this similarly and removes satellite assemblies in this line:
https://github.com/dotnet/sdk/blob/a6af77909c46799080943ee15c028462867c60f5/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L1184

@sbomer

Copy link
Copy Markdown
Member

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
CopilotAI review requested due to automatic review settings March 23, 2026 23:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

/ba-g "timeouts"

@sbomer
sbomer merged commit d4cc2d9 into mainMar 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

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>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrlinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@baronfel@agocke@sbomer@nagilson@MichalStrehovsky@dsplaisted@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix NativeAOT publish including satellite assemblies despite embedding them - #124192

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

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

Conversation

CopilotAI commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

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>
<ResolvedFileToPublishRemove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublishRemove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublishRemove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublishInclude="$(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:

<IlcSatelliteAssemblyInclude="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssemblyInclude="@(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:

<_ResolvedCopyLocalPublishAssetsRemove="@(_AssembliesToSkipPublish)" /> <!-- removes package satellites -->
<_ResolvedCopyLocalPublishAssetsInclude="@(_LinkedResolvedAssemblies)" />
<_DebugSymbolsIntermediatePathRemove="@(_DebugSymbolsIntermediatePath)" />
<IntermediateAssemblyRemove="@(IntermediateAssembly)" /> <!-- replaces managed .dll with native binary -->
<IntermediateAssemblyInclude="$(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. -->
<ResolvedFileToPublishInclude="@(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-actionsgithub-actionsBot 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>
CopilotAI changed the title [WIP] Fix AOT publish to exclude satellite assembliesFix NativeAOT publish including satellite assemblies despite embedding themFeb 9, 2026
CopilotAI requested a review from dsplaistedFebruary 9, 2026 18:37
@jkotasjkotas 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 threadsrc/tests/nativeaot/SmokeTests/SatelliteAssemblies/Program.cs 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

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

@sbomersbomer left a comment

Copy link
Copy Markdown
Member

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

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

CopilotAI commented Mar 16, 2026

Copy link
Copy Markdown
ContributorAuthor

@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-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Mar 16, 2026
CopilotAI requested a review from sbomerMarch 16, 2026 18:21
@sbomer
sbomerforce-pushed the copilot/fix-aot-publish-satellite-assemblies branch from fffa5f9 to 3feb4a7CompareMarch 16, 2026 18:28
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets
@sbomer

Copy link
Copy Markdown
Member

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

I believe single-file handles this similarly and removes satellite assemblies in this line:
https://github.com/dotnet/sdk/blob/a6af77909c46799080943ee15c028462867c60f5/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L1184

@sbomer

Copy link
Copy Markdown
Member

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
CopilotAI review requested due to automatic review settings March 23, 2026 23:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

/ba-g "timeouts"

@sbomer
sbomer merged commit d4cc2d9 into mainMar 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

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>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrlinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@baronfel@agocke@sbomer@nagilson@MichalStrehovsky@dsplaisted@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix NativeAOT publish including satellite assemblies despite embedding them - #124192

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

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

Conversation

CopilotAI commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

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>
<ResolvedFileToPublishRemove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublishRemove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublishRemove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublishInclude="$(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:

<IlcSatelliteAssemblyInclude="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssemblyInclude="@(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:

<_ResolvedCopyLocalPublishAssetsRemove="@(_AssembliesToSkipPublish)" /> <!-- removes package satellites -->
<_ResolvedCopyLocalPublishAssetsInclude="@(_LinkedResolvedAssemblies)" />
<_DebugSymbolsIntermediatePathRemove="@(_DebugSymbolsIntermediatePath)" />
<IntermediateAssemblyRemove="@(IntermediateAssembly)" /> <!-- replaces managed .dll with native binary -->
<IntermediateAssemblyInclude="$(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. -->
<ResolvedFileToPublishInclude="@(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-actionsgithub-actionsBot 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>
CopilotAI changed the title [WIP] Fix AOT publish to exclude satellite assembliesFix NativeAOT publish including satellite assemblies despite embedding themFeb 9, 2026
CopilotAI requested a review from dsplaistedFebruary 9, 2026 18:37
@jkotasjkotas 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 threadsrc/tests/nativeaot/SmokeTests/SatelliteAssemblies/Program.cs 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

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

@sbomersbomer left a comment

Copy link
Copy Markdown
Member

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

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

CopilotAI commented Mar 16, 2026

Copy link
Copy Markdown
ContributorAuthor

@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-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Mar 16, 2026
CopilotAI requested a review from sbomerMarch 16, 2026 18:21
@sbomer
sbomerforce-pushed the copilot/fix-aot-publish-satellite-assemblies branch from fffa5f9 to 3feb4a7CompareMarch 16, 2026 18:28
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets
@sbomer

Copy link
Copy Markdown
Member

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

I believe single-file handles this similarly and removes satellite assemblies in this line:
https://github.com/dotnet/sdk/blob/a6af77909c46799080943ee15c028462867c60f5/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L1184

@sbomer

Copy link
Copy Markdown
Member

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
CopilotAI review requested due to automatic review settings March 23, 2026 23:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

/ba-g "timeouts"

@sbomer
sbomer merged commit d4cc2d9 into mainMar 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

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>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrlinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@baronfel@agocke@sbomer@nagilson@MichalStrehovsky@dsplaisted@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix NativeAOT publish including satellite assemblies despite embedding them - #124192

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

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

Conversation

CopilotAI commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

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>
<ResolvedFileToPublishRemove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublishRemove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublishRemove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublishInclude="$(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:

<IlcSatelliteAssemblyInclude="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssemblyInclude="@(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:

<_ResolvedCopyLocalPublishAssetsRemove="@(_AssembliesToSkipPublish)" /> <!-- removes package satellites -->
<_ResolvedCopyLocalPublishAssetsInclude="@(_LinkedResolvedAssemblies)" />
<_DebugSymbolsIntermediatePathRemove="@(_DebugSymbolsIntermediatePath)" />
<IntermediateAssemblyRemove="@(IntermediateAssembly)" /> <!-- replaces managed .dll with native binary -->
<IntermediateAssemblyInclude="$(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. -->
<ResolvedFileToPublishInclude="@(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-actionsgithub-actionsBot 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>
CopilotAI changed the title [WIP] Fix AOT publish to exclude satellite assembliesFix NativeAOT publish including satellite assemblies despite embedding themFeb 9, 2026
CopilotAI requested a review from dsplaistedFebruary 9, 2026 18:37
@jkotasjkotas 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 threadsrc/tests/nativeaot/SmokeTests/SatelliteAssemblies/Program.cs 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

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

@sbomersbomer left a comment

Copy link
Copy Markdown
Member

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

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

CopilotAI commented Mar 16, 2026

Copy link
Copy Markdown
ContributorAuthor

@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-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Mar 16, 2026
CopilotAI requested a review from sbomerMarch 16, 2026 18:21
@sbomer
sbomerforce-pushed the copilot/fix-aot-publish-satellite-assemblies branch from fffa5f9 to 3feb4a7CompareMarch 16, 2026 18:28
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets
@sbomer

Copy link
Copy Markdown
Member

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

I believe single-file handles this similarly and removes satellite assemblies in this line:
https://github.com/dotnet/sdk/blob/a6af77909c46799080943ee15c028462867c60f5/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L1184

@sbomer

Copy link
Copy Markdown
Member

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
CopilotAI review requested due to automatic review settings March 23, 2026 23:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

/ba-g "timeouts"

@sbomer
sbomer merged commit d4cc2d9 into mainMar 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

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>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrlinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@baronfel@agocke@sbomer@nagilson@MichalStrehovsky@dsplaisted@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix NativeAOT publish including satellite assemblies despite embedding them - #124192

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

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

Conversation

CopilotAI commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

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>
<ResolvedFileToPublishRemove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublishRemove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublishRemove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublishInclude="$(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:

<IlcSatelliteAssemblyInclude="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssemblyInclude="@(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:

<_ResolvedCopyLocalPublishAssetsRemove="@(_AssembliesToSkipPublish)" /> <!-- removes package satellites -->
<_ResolvedCopyLocalPublishAssetsInclude="@(_LinkedResolvedAssemblies)" />
<_DebugSymbolsIntermediatePathRemove="@(_DebugSymbolsIntermediatePath)" />
<IntermediateAssemblyRemove="@(IntermediateAssembly)" /> <!-- replaces managed .dll with native binary -->
<IntermediateAssemblyInclude="$(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. -->
<ResolvedFileToPublishInclude="@(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-actionsgithub-actionsBot 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>
CopilotAI changed the title [WIP] Fix AOT publish to exclude satellite assembliesFix NativeAOT publish including satellite assemblies despite embedding themFeb 9, 2026
CopilotAI requested a review from dsplaistedFebruary 9, 2026 18:37
@jkotasjkotas 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 threadsrc/tests/nativeaot/SmokeTests/SatelliteAssemblies/Program.cs 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

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

@sbomersbomer left a comment

Copy link
Copy Markdown
Member

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

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

CopilotAI commented Mar 16, 2026

Copy link
Copy Markdown
ContributorAuthor

@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-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Mar 16, 2026
CopilotAI requested a review from sbomerMarch 16, 2026 18:21
@sbomer
sbomerforce-pushed the copilot/fix-aot-publish-satellite-assemblies branch from fffa5f9 to 3feb4a7CompareMarch 16, 2026 18:28
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets
@sbomer

Copy link
Copy Markdown
Member

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

I believe single-file handles this similarly and removes satellite assemblies in this line:
https://github.com/dotnet/sdk/blob/a6af77909c46799080943ee15c028462867c60f5/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L1184

@sbomer

Copy link
Copy Markdown
Member

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
CopilotAI review requested due to automatic review settings March 23, 2026 23:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

/ba-g "timeouts"

@sbomer
sbomer merged commit d4cc2d9 into mainMar 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

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>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrlinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@baronfel@agocke@sbomer@nagilson@MichalStrehovsky@dsplaisted@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix NativeAOT publish including satellite assemblies despite embedding them - #124192

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

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

Conversation

CopilotAI commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

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>
<ResolvedFileToPublishRemove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublishRemove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublishRemove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublishInclude="$(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:

<IlcSatelliteAssemblyInclude="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssemblyInclude="@(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:

<_ResolvedCopyLocalPublishAssetsRemove="@(_AssembliesToSkipPublish)" /> <!-- removes package satellites -->
<_ResolvedCopyLocalPublishAssetsInclude="@(_LinkedResolvedAssemblies)" />
<_DebugSymbolsIntermediatePathRemove="@(_DebugSymbolsIntermediatePath)" />
<IntermediateAssemblyRemove="@(IntermediateAssembly)" /> <!-- replaces managed .dll with native binary -->
<IntermediateAssemblyInclude="$(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. -->
<ResolvedFileToPublishInclude="@(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-actionsgithub-actionsBot 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>
CopilotAI changed the title [WIP] Fix AOT publish to exclude satellite assembliesFix NativeAOT publish including satellite assemblies despite embedding themFeb 9, 2026
CopilotAI requested a review from dsplaistedFebruary 9, 2026 18:37
@jkotasjkotas 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 threadsrc/tests/nativeaot/SmokeTests/SatelliteAssemblies/Program.cs 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

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

@sbomersbomer left a comment

Copy link
Copy Markdown
Member

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

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

CopilotAI commented Mar 16, 2026

Copy link
Copy Markdown
ContributorAuthor

@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-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Mar 16, 2026
CopilotAI requested a review from sbomerMarch 16, 2026 18:21
@sbomer
sbomerforce-pushed the copilot/fix-aot-publish-satellite-assemblies branch from fffa5f9 to 3feb4a7CompareMarch 16, 2026 18:28
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets
@sbomer

Copy link
Copy Markdown
Member

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

I believe single-file handles this similarly and removes satellite assemblies in this line:
https://github.com/dotnet/sdk/blob/a6af77909c46799080943ee15c028462867c60f5/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L1184

@sbomer

Copy link
Copy Markdown
Member

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
CopilotAI review requested due to automatic review settings March 23, 2026 23:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

/ba-g "timeouts"

@sbomer
sbomer merged commit d4cc2d9 into mainMar 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

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>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrlinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@baronfel@agocke@sbomer@nagilson@MichalStrehovsky@dsplaisted@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix NativeAOT publish including satellite assemblies despite embedding them - #124192

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

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

Conversation

CopilotAI commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

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>
<ResolvedFileToPublishRemove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublishRemove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublishRemove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublishInclude="$(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:

<IlcSatelliteAssemblyInclude="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssemblyInclude="@(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:

<_ResolvedCopyLocalPublishAssetsRemove="@(_AssembliesToSkipPublish)" /> <!-- removes package satellites -->
<_ResolvedCopyLocalPublishAssetsInclude="@(_LinkedResolvedAssemblies)" />
<_DebugSymbolsIntermediatePathRemove="@(_DebugSymbolsIntermediatePath)" />
<IntermediateAssemblyRemove="@(IntermediateAssembly)" /> <!-- replaces managed .dll with native binary -->
<IntermediateAssemblyInclude="$(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. -->
<ResolvedFileToPublishInclude="@(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-actionsgithub-actionsBot 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>
CopilotAI changed the title [WIP] Fix AOT publish to exclude satellite assembliesFix NativeAOT publish including satellite assemblies despite embedding themFeb 9, 2026
CopilotAI requested a review from dsplaistedFebruary 9, 2026 18:37
@jkotasjkotas 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 threadsrc/tests/nativeaot/SmokeTests/SatelliteAssemblies/Program.cs 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

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

@sbomersbomer left a comment

Copy link
Copy Markdown
Member

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

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

CopilotAI commented Mar 16, 2026

Copy link
Copy Markdown
ContributorAuthor

@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-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Mar 16, 2026
CopilotAI requested a review from sbomerMarch 16, 2026 18:21
@sbomer
sbomerforce-pushed the copilot/fix-aot-publish-satellite-assemblies branch from fffa5f9 to 3feb4a7CompareMarch 16, 2026 18:28
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets
@sbomer

Copy link
Copy Markdown
Member

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

I believe single-file handles this similarly and removes satellite assemblies in this line:
https://github.com/dotnet/sdk/blob/a6af77909c46799080943ee15c028462867c60f5/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L1184

@sbomer

Copy link
Copy Markdown
Member

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
CopilotAI review requested due to automatic review settings March 23, 2026 23:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

/ba-g "timeouts"

@sbomer
sbomer merged commit d4cc2d9 into mainMar 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

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>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrlinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@baronfel@agocke@sbomer@nagilson@MichalStrehovsky@dsplaisted@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix NativeAOT publish including satellite assemblies despite embedding them - #124192

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

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

Conversation

CopilotAI commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

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>
<ResolvedFileToPublishRemove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublishRemove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublishRemove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublishInclude="$(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:

<IlcSatelliteAssemblyInclude="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssemblyInclude="@(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:

<_ResolvedCopyLocalPublishAssetsRemove="@(_AssembliesToSkipPublish)" /> <!-- removes package satellites -->
<_ResolvedCopyLocalPublishAssetsInclude="@(_LinkedResolvedAssemblies)" />
<_DebugSymbolsIntermediatePathRemove="@(_DebugSymbolsIntermediatePath)" />
<IntermediateAssemblyRemove="@(IntermediateAssembly)" /> <!-- replaces managed .dll with native binary -->
<IntermediateAssemblyInclude="$(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. -->
<ResolvedFileToPublishInclude="@(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-actionsgithub-actionsBot 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>
CopilotAI changed the title [WIP] Fix AOT publish to exclude satellite assembliesFix NativeAOT publish including satellite assemblies despite embedding themFeb 9, 2026
CopilotAI requested a review from dsplaistedFebruary 9, 2026 18:37
@jkotasjkotas 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 threadsrc/tests/nativeaot/SmokeTests/SatelliteAssemblies/Program.cs 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

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

@sbomersbomer left a comment

Copy link
Copy Markdown
Member

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

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

CopilotAI commented Mar 16, 2026

Copy link
Copy Markdown
ContributorAuthor

@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-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Mar 16, 2026
CopilotAI requested a review from sbomerMarch 16, 2026 18:21
@sbomer
sbomerforce-pushed the copilot/fix-aot-publish-satellite-assemblies branch from fffa5f9 to 3feb4a7CompareMarch 16, 2026 18:28
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets
@sbomer

Copy link
Copy Markdown
Member

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

I believe single-file handles this similarly and removes satellite assemblies in this line:
https://github.com/dotnet/sdk/blob/a6af77909c46799080943ee15c028462867c60f5/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L1184

@sbomer

Copy link
Copy Markdown
Member

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
CopilotAI review requested due to automatic review settings March 23, 2026 23:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

/ba-g "timeouts"

@sbomer
sbomer merged commit d4cc2d9 into mainMar 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

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>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrlinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@baronfel@agocke@sbomer@nagilson@MichalStrehovsky@dsplaisted@jkotas