Skip to content

Fix NativeAOT publish including satellite assemblies despite embedding them#124192

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/fix-aot-publish-satellite-assemblies
Draft

Fix NativeAOT publish including satellite assemblies despite embedding them#124192
Copilot wants to merge 3 commits intomainfrom
copilot/fix-aot-publish-satellite-assemblies

Conversation

Copy link
Contributor

Copilot AI commented Feb 9, 2026

Description

NativeAOT embeds satellite assemblies into the native binary but still copies them to the publish folder. This occurs because ComputeLinkedFilesToPublish removes IntermediateAssembly and _DebugSymbolsIntermediatePath to prevent redundant publishing, but doesn't remove IntermediateSatelliteAssembliesWithTargetPath.

Fix: Add removal of IntermediateSatelliteAssembliesWithTargetPath in ComputeLinkedFilesToPublish target:

<ItemGroup>
  <IntermediateAssembly Remove="@(IntermediateAssembly)" />
  <IntermediateAssembly Include="$(NativeBinary)" />
  <IntermediateSatelliteAssembliesWithTargetPath Remove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
</ItemGroup>

This follows the same pattern already used for IntermediateAssembly and _DebugSymbolsIntermediatePath.

Customer Impact

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

Testing

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

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

Risk

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

Original prompt

This section details on the original issue you should resolve

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

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

To Reproduce

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

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

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

Repro project

Binlog

Further technical details

.NET SDK version: 10.0.102</issue_description>

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

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

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

Root Cause Analysis

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

How satellite assemblies flow through the pipeline

Step 1: NativeAOT collects satellite assemblies for embedding

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

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

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

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

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

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

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

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

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

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

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

</details>



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

- Fixes dotnet/runtime#124191

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

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

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

Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix AOT publish to exclude satellite assemblies Fix NativeAOT publish including satellite assemblies despite embedding them Feb 9, 2026
Copilot AI requested a review from dsplaisted February 9, 2026 18:37
@jkotas jkotas added area-NativeAOT-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Feb 9, 2026
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib
See info in area-owners.md if you want to be subscribed.

Comment on lines 37 to 45
string exePath = Environment.ProcessPath ?? Assembly.GetExecutingAssembly().Location;
string publishDir = Path.GetDirectoryName(exePath);

if (string.IsNullOrEmpty(publishDir))
{
Console.WriteLine("ERROR: Could not determine publish directory");
return 1;
}

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the test in commit 3feb4a7. Testing will be handled in the SDK repo as you suggested.

<ItemGroup>
<IntermediateAssembly Remove="@(IntermediateAssembly)" />
<IntermediateAssembly Include="$(NativeBinary)" />
<IntermediateSatelliteAssembliesWithTargetPath Remove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
Copy link
Member

Choose a reason for hiding this comment

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

@copilot where is the logic that filters out satellite assemblies from NuGet references? I can see that we correctly don't preserve satellite assemblies from NuGet references. The issue is isolated to satellite assemblies from current assembly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The logic for filtering NuGet satellite assemblies is in the ComputeManagedAssembliesToCompileToNative task (lines 74-85), which outputs _AssembliesToSkipPublish. Line 25 then removes those from _ResolvedCopyLocalPublishAssets.

The fix in line 33 addresses the project's own satellite assemblies by removing IntermediateSatelliteAssembliesWithTargetPath, which the SDK would otherwise unconditionally add back to the publish list.

Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants