Skip to content

AOT publish includes satellite assemblies in publish folder #124191

Closed

Description

@dsplaisted

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

Activity

  1. removed
    untriagedNew issue has not been triaged by the area owner
    on Feb 9, 2026
  2. baronfel commented on Feb 9, 2026

    @baronfel
    Member

    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>%(IntermediateSatelliteAssembliesWithTargetPath.Culture)\%(Filename)%(Extension)</RelativePath>
      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
    </ResolvedFileToPublish>

    Since IntermediateSatelliteAssembliesWithTargetPath was never cleared, the project's own satellite assemblies get added to ResolvedFileToPublish and are copied to the publish directory — even though they've already been embedded into the native binary.

    Note that this same target does have a NativeAOT-aware condition for the runtime config file (line 525):

    Condition="'$(GenerateRuntimeConfigurationFiles)' == 'true'  and '$(PublishAot)' != 'true'"

    Suggested fix

    The ComputeLinkedFilesToPublish target in Microsoft.NETCore.Native.Publish.targets should also clear the project's intermediate satellite assemblies:

    <IntermediateSatelliteAssembliesWithTargetPath Remove="@(IntermediateSatelliteAssembliesWithTargetPath)" />

    This mirrors the pattern already used for IntermediateAssembly and _DebugSymbolsIntermediatePath in the same target.

  3. transferred this issue fromdotnet/sdkon Feb 9, 2026
  4. added
    needs-area-labelAn area label is needed to ensure this gets routed to the appropriate area owners
    on Feb 9, 2026
  5. added and removed
    needs-area-labelAn area label is needed to ensure this gets routed to the appropriate area owners
    on Feb 9, 2026
  6. dotnet-policy-service commented on Feb 9, 2026

    @dotnet-policy-service
    Contributor

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

  7. added a commit that references this issue on Mar 24, 2026
    d4cc2d9
  8. locked and limited conversation to collaborators on Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

  • Status
    No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions