Decouple ILC from ManagedAssemblyToLink - #124801

Merged
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order
Mar 16, 2026
Merged

Decouple ILC from ManagedAssemblyToLink#124801
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order

Conversation

CopilotAI commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

_ComputeManagedAssemblyForILLink in NativeAOT was indirectly dependent on ComputeIlcCompileInputs via @(ManagedBinary), which made PrepareForILLink ordering-sensitive and broke incremental ILLink behavior when ILLink is run before ILC input computation. This change removes that coupling so ILLink preparation no longer relies on ComputeIlcCompileInputs side effects.

  • What changed

    • Updated src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets in _ComputeManagedAssemblyForILLink.
    • Replaced @(ManagedBinary) with @(IntermediateAssembly) when reconstructing @(ManagedAssemblyToLink).
  • Why this matters

    • @(IntermediateAssembly) is available during PrepareForILLink, independent of ComputeIlcCompileInputs.
    • This preserves inclusion of the project assembly in @(ManagedAssemblyToLink) regardless of whether ComputeIlcCompileInputs runs before or after PrepareForILLink/ILLink.
  • Code change

    <ManagedAssemblyToLinkInclude="@(DefaultFrameworkAssemblies);@(_ManagedResolvedAssembliesToPublish);@(IntermediateAssembly)" />
Original prompt

This section details on the original issue you should resolve

<issue_title>ComputeIlcCompileInputs should not need to run before PrepareForILLink</issue_title>
<issue_description>## Description

The default IlcCompileDependsOn in Microsoft.NETCore.Native.targets orders ComputeIlcCompileInputs before PrepareForILLink:

Compile;ComputeIlcCompileInputs;SetupOSSpecificProps;PrepareForILLink

This couples ILC's input computation to ILLink's preparation phase. _ComputeManagedAssemblyForILLink (which runs AfterTargets="_ComputeManagedAssemblyToLink" during PrepareForILLink) consumes @(ManagedBinary), a side effect of ComputeIlcCompileInputs. This ordering works for the standard pipeline because it doesn't actually run ILLink (RunILLink=false), but it breaks consumers that need PrepareForILLink and ILLink to run beforeComputeIlcCompileInputs.

Why a consumer would need the opposite order

The standard NativeAOT pipeline sets RunILLink=false — ILLink never actually runs, and @(ManagedAssemblyToLink) is only used as metadata for ILC. A consumer that sets RunILLink=true to actually trim assemblies before ILC needs ILLink to complete first, so that ILC consumes the trimmed output. This requires PrepareForILLink and ILLink to precede ComputeIlcCompileInputs — the opposite of the default order. This is the case in .NET for Android's NativeAOT pipeline.

Impact

When PrepareForILLink runs before ComputeIlcCompileInputs, _ComputeManagedAssemblyForILLink builds its replacement @(ManagedAssemblyToLink) list before @(ManagedBinary) has been populated. The project assembly ends up missing from @(ManagedAssemblyToLink), which is used as Inputs by _RunILLink (in Microsoft.NET.ILLink.targets). This causes ILLink's incremental build check to miss changes to the project assembly, so ILLink skips on rebuild even though the assembly changed.

First builds still succeed because PrepareForILLink independently adds @(IntermediateAssembly) as a TrimmerRootAssembly, so ILLink loads and processes it regardless. Only incremental builds are affected.

Reproduction

Create a dotnet new console project with the following csproj (note: explicit SDK imports are needed because Microsoft.NETCore.Native.targets unconditionally sets RunILLink=false, so it must be overridden after the SDK targets load):

<Project>
<ImportProject="Sdk.props"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<PublishAot>true</PublishAot>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>
<ImportProject="Sdk.targets"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<RunILLink>true</RunILLink>
<IlcCompileDependsOn>
Compile;
SetupOSSpecificProps;
PrepareForILLink;
ILLink;
ComputeIlcCompileInputs
</IlcCompileDependsOn>
</PropertyGroup>
</Project>

Run dotnet publish, then change "Hello, World!" to "Hello, Changed!" in Program.cs and publish again. On the second publish, ILLink is skipped (no "Optimizing assemblies for size" message) and the trimmed assembly in obj/.../linked/ReproApp.dll still contains Hello, World!.

Suggestion

Decouple ComputeIlcCompileInputs from PrepareForILLink so neither depends on having run before the other. Ideally _ComputeManagedAssemblyForILLink should not rely on state produced by ComputeIlcCompileInputs, and ComputeIlcCompileInputs should be free to run after ILLink without breaking the ILLink preparation phase.

Workaround

.NET for Android can work around this by injecting @(IntermediateAssembly) into @(ManagedAssemblyToLink) after _ComputeManagedAssemblyForILLink replaces it:

<TargetName="_AndroidFixManagedAssemblyToLink"AfterTargets="_ComputeManagedAssemblyForILLink">
<ItemGroup>
<ManagedAssemblyToLinkInclude="@(IntermediateAssembly)" />
</ItemGroup>
</Target>
```</issue_description>
<agent_instructions>Decouple these targets. The ILC inputs should not be resolved from ManagedAssemblyToLink.</agent_instructions>
## Comments on the Issue (you are @copilot in this section)
<comments>
</comments>

💡 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 in the docs.

@github-actionsgithub-actionsBot added the area-Tools-ILLink .NET linker development as well as trimming analyzers label Feb 24, 2026
…puts
Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
CopilotAI changed the title [WIP] Fix ComputeIlcCompileInputs execution order before PrepareForILLinkDecouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsFeb 24, 2026
CopilotAI requested a review from sbomerFebruary 24, 2026 17:25
…lyToLink
Factor _PrepareTrimConfiguration out of PrepareForILLink so shared trim
configuration operates on ResolvedFileToPublish and flows to both ILLink
and ILC without duplication.
For NativeAOT, _ComputeAssembliesToCompileToNative replaces CoreCLR
runtime pack files with DefaultFrameworkAssemblies (tagged
PostprocessAssembly=true) before _PrepareTrimConfiguration runs.
_ComputeIlcCompileInputs then derives IlcReference from
ResolvedFileToPublish via PostprocessAssembly metadata, ensuring ILC
sees ILLink-relocated paths when both run.
ComputeLinkedFilesToPublish hooks AfterTargets=ILLink (instead of
ComputeResolvedFilesToPublishList) for correct ordering when both
ILLink and ILC run. RunILLink is made conditional so projects can
opt in.
Remove ManagedAssemblies output from the C# task (replaced by
ResolvedFileToPublish filtering). Add RuntimePackFilesToSkipPublish
output for early removal of CoreCLR runtime pack files. Fix OOB
assembly handling so overrides stay in ResolvedFileToPublish.
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Feb 25, 2026
@sbomersbomer changed the title Decouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsDecouple ILC from ManagedAssemblyToLinkFeb 25, 2026
Skip _ComputeIlcCompileInputs for framework library builds
(BuildingFrameworkLibrary=true) since they use BuildOneFrameworkLibrary
for input computation, not the publish-pipeline targets.
When NativeCompilationDuringPublish is false (e.g. Apple non-library-mode
builds), Publish.targets is not imported so _ComputeIlcCompileInputs does
not exist to populate IlcReference from ResolvedFileToPublish. Fall back
to DefaultFrameworkAssemblies directly in ComputeIlcCompileInputs so ILC
can find System.Private.CoreLib and other framework references.
@sbomer

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-nativeaot-outerloop

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

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

Decouples NativeAOT’s ILLink preparation from ComputeIlcCompileInputs side effects by moving shared trim configuration into a dedicated target and rebuilding ILC/ILLink inputs from publish items rather than @(ManagedBinary).

Changes:

  • Introduced _PrepareTrimConfiguration in ILLink targets and made _ComputeManagedAssemblyToLink depend on it.
  • Updated NativeAOT publish pipeline to compute ILC inputs from ResolvedFileToPublish (PostprocessAssembly=true) and adjusted ordering around ILLink.
  • Simplified/changed ComputeManagedAssembliesToCompileToNative outputs to focus on runtime-pack files to remove and satellite assemblies.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targetsAdds _PrepareTrimConfiguration and moves shared trim defaults/metadata out of PrepareForILLink.
src/coreclr/tools/aot/ILCompiler.Build.Tasks/ComputeManagedAssembliesToCompileToNative.csAlters MSBuild task outputs and logic to support the updated publish/trim flow.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targetsRemoves NativeAOT’s PrepareForILLink dependency and shifts ILC trim metadata consumption.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targetsReorders publish hooks around ILLink and selects ILC managed inputs via PostprocessAssembly.

…esToPublishList
The AfterTargets="ILLink" hook is unnecessary because
ComputeLinkedFilesToPublish's own DependsOnTargets chain
(via LinkNative -> IlcCompile) transitively pulls in the
correct prerequisites regardless of the AfterTargets anchor.
Keeping ComputeResolvedFilesToPublishList avoids an artificial
coupling to ILLink and removes the need for the ILLink insertion
in the test infrastructure's LinkNativeIfBuildAndRun target.
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets
#	src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets
The _PrepareTrimConfiguration target was passing the full path of
IntermediateAssembly to TrimmerRootAssembly, but ILLink expects
assembly names (without path). This caused IL1032 errors across
all CI platforms: 'Root assembly with name ...ilc.dll could not be found.'
Restores the %(Filename) transform that was on main but got lost
when moving this line from PrepareForILLink to _PrepareTrimConfiguration.
@sbomer

Copy link
Copy Markdown
Member

/ba-g "deadletter"

@sbomer
sbomer merged commit 15da421 into mainMar 16, 2026
119 of 128 checks passed
@sbomer
sbomer deleted the copilot/fix-ilc-compile-order branch March 16, 2026 18:12
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
_ComputeAssembliesToCompileToNative now populates
@(_IlcManagedInputAssemblies) and ComputeLinkedFilesToPublish
removes them from @(ResolvedFileToPublish).
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in @(ResolvedFileToPublish) for the outer build's _ResolveAssemblies
target. Clear @(_IlcManagedInputAssemblies) in our
_AndroidComputeIlcCompileInputs target so the runtime's
ComputeLinkedFilesToPublish doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Changes: dotnet/dotnet@5ff448a...803eb28
- **Dependency Updates**:
- From [11.0.0-preview.3.26165.107 to 11.0.0-preview.3.26168.106][1]
- Microsoft.NET.Workload.Mono.ToolChain.Current.Manifest-11.0.100-preview.3
- Microsoft.NET.ILLink
- Microsoft.NETCore.App.Ref
- From [11.0.0-beta.26165.107 to 11.0.0-beta.26168.106][1]
- Microsoft.DotNet.Build.Tasks.Feed
- From [0.11.5-preview.26165.107 to 0.11.5-preview.26168.106][1]
- Microsoft.DotNet.Cecil
- From [11.0.100-preview.3.26165.107 to 11.0.100-preview.3.26168.106][1]
- Microsoft.NET.Sdk
- Microsoft.NET.Workload.Emscripten.Current.Manifest-11.0.100-preview.3
- Microsoft.TemplateEngine.Authoring.Tasks
[1]: dotnet/dotnet@5ff448a...803eb28
## Other changes ##
[xabt] Prevent `ComputeLinkedFilesToPublish` from stripping assemblies
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
`_ComputeAssembliesToCompileToNative` now populates
`@(_IlcManagedInputAssemblies)` and `ComputeLinkedFilesToPublish`
removes them from `@(ResolvedFileToPublish)`.
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in `@(ResolvedFileToPublish)` for the outer build's `_ResolveAssemblies`
target. Clear `@(_IlcManagedInputAssemblies)` in our
`_AndroidComputeIlcCompileInputs` target so the runtime's
`ComputeLinkedFilesToPublish` doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
sbomer added a commit that referenced this pull request Mar 24, 2026
…g them (#124192)
## 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:
```xml
<ItemGroup>
<ResolvedFileToPublish Remove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublish Remove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublish Remove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublish Include="$(NativeBinary)">
<RelativePath>$(NativeBinaryPrefix)$(TargetName)$(NativeBinaryExt)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
```
This follows the same pattern as removing managed assemblies and debug
symbols, which are also embedded or replaced in the native binary.
## Customer Impact
- **Affected customers:** NativeAOT users with localized resources
- **Regression:** No
- **Source incompatibility:** No
- **Breaking change:** No (removes extraneous files from publish output)
## Testing
Testing will be added in the SDK repo per review feedback. The fix can
be validated by publishing a NativeAOT app with satellite assemblies and
verifying that:
- Localized resources are accessible at runtime (embedded correctly)
- No satellite assembly subdirectories exist in publish output
## Risk
Minimal. One-line change following established pattern. Satellite
assemblies remain embedded and functional; only removes redundant disk
copies.
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
----
*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](https://github.com/user-attachments/files/25188858/AotLocalization.zip)
[Binlog](https://github.com/user-attachments/files/25188847/AotLocalizationBinlog.zip)
### 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)
<comments>
<comment_new><author>@baronfel</author><body>
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)](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L3-L11),
`_ComputeIlcCompileInputs` gathers satellite assemblies from two
sources:
```xml
<IlcSatelliteAssembly Include="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssembly Include="@(IntermediateSatelliteAssembliesWithTargetPath)" />
```
- `_SatelliteAssembliesToPublish` = satellite assemblies from
package/project references (extracted from
`_ResolvedCopyLocalPublishAssets` by
[`ComputeManagedAssembliesToCompileToNative`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L73-L84))
- `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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets#L236),
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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L20-L34)
runs `BeforeTargets="ComputeResolvedFilesToPublishList"` and modifies
the publish list:
```xml
<_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)](https://github.com/dotnet/sdk/blob/main/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L545-L549),
`ComputeResolvedFilesToPublishList` unconditionally includes:
```xml
<!-- Copy satellite assemblies. -->
<ResolvedFileToPublish Include="@(IntermediateSatelliteAssembliesWithTargetPath)">
<RelativePath>%(IntermediateSatelliteAss...
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes#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.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com>
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Co-authored-by: Sven Boemer <sbomer@gmail.com>
sbomer added a commit that referenced this pull request Apr 10, 2026
… item Update (#125630)
## Description
Follow-up to PR #124801 review feedback: the "intersection via
include/remove, then remove+re-include" pattern in
`_PrepareTrimConfiguration` was complex, mutated item ordering, and used
two throwaway item groups. Replace with a direct MSBuild `Update`.
### Change
**Before** — compute intersection manually to set metadata, then
remove+re-add items:
```xml
<__SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<__SingleWarnIntermediateAssembly Remove="@(IntermediateAssembly)" />
<_SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<_SingleWarnIntermediateAssembly Remove="@(__SingleWarnIntermediateAssembly)" />
<_SingleWarnIntermediateAssembly>
<TrimmerSingleWarn Condition="...">false</TrimmerSingleWarn>
</_SingleWarnIntermediateAssembly>
<ResolvedFileToPublish Remove="@(_SingleWarnIntermediateAssembly)" />
<ResolvedFileToPublish Include="@(_SingleWarnIntermediateAssembly)" />
```
**After** — update matching items directly, preserving order:
```xml
<ResolvedFileToPublish Update="@(IntermediateAssembly)">
<TrimmerSingleWarn Condition=" '%(ResolvedFileToPublish.TrimmerSingleWarn)' == '' ">false</TrimmerSingleWarn>
</ResolvedFileToPublish>
```
## Changes proposed in this pull request
- [`Microsoft.NET.ILLink.targets`] Replace 13-line intersection pattern
with a 3-line `Update` in `_PrepareTrimConfiguration`
- [`Microsoft.NET.ILLink.targets`] Qualify `%(TrimmerSingleWarn)` as
`%(ResolvedFileToPublish.TrimmerSingleWarn)` in the `Update` condition
to prevent MSB4096 (unqualified metadata batching over all
`ResolvedFileToPublish` items, including those without the metadata
defined)
## Additional context
<!-- START COPILOT CODING AGENT TIPS -->
---
🔒 GitHub Advanced Security automatically protects Copilot coding agent
pull requests. You can protect all pull requests by enabling Advanced
Security for your repositories. [Learn more about Advanced
Security.](https://gh.io/cca-advanced-security)
---------
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 Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-Tools-ILLink.NET linker development as well as trimming analyzerslinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

ComputeIlcCompileInputs should not need to run before PrepareForILLink

4 participants

@sbomer@MichalStrehovsky
, '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

Decouple ILC from ManagedAssemblyToLink - #124801

Merged
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order
Mar 16, 2026
Merged

Decouple ILC from ManagedAssemblyToLink#124801
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order

Conversation

CopilotAI commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

_ComputeManagedAssemblyForILLink in NativeAOT was indirectly dependent on ComputeIlcCompileInputs via @(ManagedBinary), which made PrepareForILLink ordering-sensitive and broke incremental ILLink behavior when ILLink is run before ILC input computation. This change removes that coupling so ILLink preparation no longer relies on ComputeIlcCompileInputs side effects.

  • What changed

    • Updated src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets in _ComputeManagedAssemblyForILLink.
    • Replaced @(ManagedBinary) with @(IntermediateAssembly) when reconstructing @(ManagedAssemblyToLink).
  • Why this matters

    • @(IntermediateAssembly) is available during PrepareForILLink, independent of ComputeIlcCompileInputs.
    • This preserves inclusion of the project assembly in @(ManagedAssemblyToLink) regardless of whether ComputeIlcCompileInputs runs before or after PrepareForILLink/ILLink.
  • Code change

    <ManagedAssemblyToLinkInclude="@(DefaultFrameworkAssemblies);@(_ManagedResolvedAssembliesToPublish);@(IntermediateAssembly)" />
Original prompt

This section details on the original issue you should resolve

<issue_title>ComputeIlcCompileInputs should not need to run before PrepareForILLink</issue_title>
<issue_description>## Description

The default IlcCompileDependsOn in Microsoft.NETCore.Native.targets orders ComputeIlcCompileInputs before PrepareForILLink:

Compile;ComputeIlcCompileInputs;SetupOSSpecificProps;PrepareForILLink

This couples ILC's input computation to ILLink's preparation phase. _ComputeManagedAssemblyForILLink (which runs AfterTargets="_ComputeManagedAssemblyToLink" during PrepareForILLink) consumes @(ManagedBinary), a side effect of ComputeIlcCompileInputs. This ordering works for the standard pipeline because it doesn't actually run ILLink (RunILLink=false), but it breaks consumers that need PrepareForILLink and ILLink to run beforeComputeIlcCompileInputs.

Why a consumer would need the opposite order

The standard NativeAOT pipeline sets RunILLink=false — ILLink never actually runs, and @(ManagedAssemblyToLink) is only used as metadata for ILC. A consumer that sets RunILLink=true to actually trim assemblies before ILC needs ILLink to complete first, so that ILC consumes the trimmed output. This requires PrepareForILLink and ILLink to precede ComputeIlcCompileInputs — the opposite of the default order. This is the case in .NET for Android's NativeAOT pipeline.

Impact

When PrepareForILLink runs before ComputeIlcCompileInputs, _ComputeManagedAssemblyForILLink builds its replacement @(ManagedAssemblyToLink) list before @(ManagedBinary) has been populated. The project assembly ends up missing from @(ManagedAssemblyToLink), which is used as Inputs by _RunILLink (in Microsoft.NET.ILLink.targets). This causes ILLink's incremental build check to miss changes to the project assembly, so ILLink skips on rebuild even though the assembly changed.

First builds still succeed because PrepareForILLink independently adds @(IntermediateAssembly) as a TrimmerRootAssembly, so ILLink loads and processes it regardless. Only incremental builds are affected.

Reproduction

Create a dotnet new console project with the following csproj (note: explicit SDK imports are needed because Microsoft.NETCore.Native.targets unconditionally sets RunILLink=false, so it must be overridden after the SDK targets load):

<Project>
<ImportProject="Sdk.props"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<PublishAot>true</PublishAot>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>
<ImportProject="Sdk.targets"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<RunILLink>true</RunILLink>
<IlcCompileDependsOn>
Compile;
SetupOSSpecificProps;
PrepareForILLink;
ILLink;
ComputeIlcCompileInputs
</IlcCompileDependsOn>
</PropertyGroup>
</Project>

Run dotnet publish, then change "Hello, World!" to "Hello, Changed!" in Program.cs and publish again. On the second publish, ILLink is skipped (no "Optimizing assemblies for size" message) and the trimmed assembly in obj/.../linked/ReproApp.dll still contains Hello, World!.

Suggestion

Decouple ComputeIlcCompileInputs from PrepareForILLink so neither depends on having run before the other. Ideally _ComputeManagedAssemblyForILLink should not rely on state produced by ComputeIlcCompileInputs, and ComputeIlcCompileInputs should be free to run after ILLink without breaking the ILLink preparation phase.

Workaround

.NET for Android can work around this by injecting @(IntermediateAssembly) into @(ManagedAssemblyToLink) after _ComputeManagedAssemblyForILLink replaces it:

<TargetName="_AndroidFixManagedAssemblyToLink"AfterTargets="_ComputeManagedAssemblyForILLink">
<ItemGroup>
<ManagedAssemblyToLinkInclude="@(IntermediateAssembly)" />
</ItemGroup>
</Target>
```</issue_description>
<agent_instructions>Decouple these targets. The ILC inputs should not be resolved from ManagedAssemblyToLink.</agent_instructions>
## Comments on the Issue (you are @copilot in this section)
<comments>
</comments>

💡 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 in the docs.

@github-actionsgithub-actionsBot added the area-Tools-ILLink .NET linker development as well as trimming analyzers label Feb 24, 2026
…puts
Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
CopilotAI changed the title [WIP] Fix ComputeIlcCompileInputs execution order before PrepareForILLinkDecouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsFeb 24, 2026
CopilotAI requested a review from sbomerFebruary 24, 2026 17:25
…lyToLink
Factor _PrepareTrimConfiguration out of PrepareForILLink so shared trim
configuration operates on ResolvedFileToPublish and flows to both ILLink
and ILC without duplication.
For NativeAOT, _ComputeAssembliesToCompileToNative replaces CoreCLR
runtime pack files with DefaultFrameworkAssemblies (tagged
PostprocessAssembly=true) before _PrepareTrimConfiguration runs.
_ComputeIlcCompileInputs then derives IlcReference from
ResolvedFileToPublish via PostprocessAssembly metadata, ensuring ILC
sees ILLink-relocated paths when both run.
ComputeLinkedFilesToPublish hooks AfterTargets=ILLink (instead of
ComputeResolvedFilesToPublishList) for correct ordering when both
ILLink and ILC run. RunILLink is made conditional so projects can
opt in.
Remove ManagedAssemblies output from the C# task (replaced by
ResolvedFileToPublish filtering). Add RuntimePackFilesToSkipPublish
output for early removal of CoreCLR runtime pack files. Fix OOB
assembly handling so overrides stay in ResolvedFileToPublish.
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Feb 25, 2026
@sbomersbomer changed the title Decouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsDecouple ILC from ManagedAssemblyToLinkFeb 25, 2026
Skip _ComputeIlcCompileInputs for framework library builds
(BuildingFrameworkLibrary=true) since they use BuildOneFrameworkLibrary
for input computation, not the publish-pipeline targets.
When NativeCompilationDuringPublish is false (e.g. Apple non-library-mode
builds), Publish.targets is not imported so _ComputeIlcCompileInputs does
not exist to populate IlcReference from ResolvedFileToPublish. Fall back
to DefaultFrameworkAssemblies directly in ComputeIlcCompileInputs so ILC
can find System.Private.CoreLib and other framework references.
@sbomer

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-nativeaot-outerloop

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

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

Decouples NativeAOT’s ILLink preparation from ComputeIlcCompileInputs side effects by moving shared trim configuration into a dedicated target and rebuilding ILC/ILLink inputs from publish items rather than @(ManagedBinary).

Changes:

  • Introduced _PrepareTrimConfiguration in ILLink targets and made _ComputeManagedAssemblyToLink depend on it.
  • Updated NativeAOT publish pipeline to compute ILC inputs from ResolvedFileToPublish (PostprocessAssembly=true) and adjusted ordering around ILLink.
  • Simplified/changed ComputeManagedAssembliesToCompileToNative outputs to focus on runtime-pack files to remove and satellite assemblies.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targetsAdds _PrepareTrimConfiguration and moves shared trim defaults/metadata out of PrepareForILLink.
src/coreclr/tools/aot/ILCompiler.Build.Tasks/ComputeManagedAssembliesToCompileToNative.csAlters MSBuild task outputs and logic to support the updated publish/trim flow.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targetsRemoves NativeAOT’s PrepareForILLink dependency and shifts ILC trim metadata consumption.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targetsReorders publish hooks around ILLink and selects ILC managed inputs via PostprocessAssembly.

…esToPublishList
The AfterTargets="ILLink" hook is unnecessary because
ComputeLinkedFilesToPublish's own DependsOnTargets chain
(via LinkNative -> IlcCompile) transitively pulls in the
correct prerequisites regardless of the AfterTargets anchor.
Keeping ComputeResolvedFilesToPublishList avoids an artificial
coupling to ILLink and removes the need for the ILLink insertion
in the test infrastructure's LinkNativeIfBuildAndRun target.
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets
#	src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets
The _PrepareTrimConfiguration target was passing the full path of
IntermediateAssembly to TrimmerRootAssembly, but ILLink expects
assembly names (without path). This caused IL1032 errors across
all CI platforms: 'Root assembly with name ...ilc.dll could not be found.'
Restores the %(Filename) transform that was on main but got lost
when moving this line from PrepareForILLink to _PrepareTrimConfiguration.
@sbomer

Copy link
Copy Markdown
Member

/ba-g "deadletter"

@sbomer
sbomer merged commit 15da421 into mainMar 16, 2026
119 of 128 checks passed
@sbomer
sbomer deleted the copilot/fix-ilc-compile-order branch March 16, 2026 18:12
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
_ComputeAssembliesToCompileToNative now populates
@(_IlcManagedInputAssemblies) and ComputeLinkedFilesToPublish
removes them from @(ResolvedFileToPublish).
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in @(ResolvedFileToPublish) for the outer build's _ResolveAssemblies
target. Clear @(_IlcManagedInputAssemblies) in our
_AndroidComputeIlcCompileInputs target so the runtime's
ComputeLinkedFilesToPublish doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Changes: dotnet/dotnet@5ff448a...803eb28
- **Dependency Updates**:
- From [11.0.0-preview.3.26165.107 to 11.0.0-preview.3.26168.106][1]
- Microsoft.NET.Workload.Mono.ToolChain.Current.Manifest-11.0.100-preview.3
- Microsoft.NET.ILLink
- Microsoft.NETCore.App.Ref
- From [11.0.0-beta.26165.107 to 11.0.0-beta.26168.106][1]
- Microsoft.DotNet.Build.Tasks.Feed
- From [0.11.5-preview.26165.107 to 0.11.5-preview.26168.106][1]
- Microsoft.DotNet.Cecil
- From [11.0.100-preview.3.26165.107 to 11.0.100-preview.3.26168.106][1]
- Microsoft.NET.Sdk
- Microsoft.NET.Workload.Emscripten.Current.Manifest-11.0.100-preview.3
- Microsoft.TemplateEngine.Authoring.Tasks
[1]: dotnet/dotnet@5ff448a...803eb28
## Other changes ##
[xabt] Prevent `ComputeLinkedFilesToPublish` from stripping assemblies
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
`_ComputeAssembliesToCompileToNative` now populates
`@(_IlcManagedInputAssemblies)` and `ComputeLinkedFilesToPublish`
removes them from `@(ResolvedFileToPublish)`.
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in `@(ResolvedFileToPublish)` for the outer build's `_ResolveAssemblies`
target. Clear `@(_IlcManagedInputAssemblies)` in our
`_AndroidComputeIlcCompileInputs` target so the runtime's
`ComputeLinkedFilesToPublish` doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
sbomer added a commit that referenced this pull request Mar 24, 2026
…g them (#124192)
## 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:
```xml
<ItemGroup>
<ResolvedFileToPublish Remove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublish Remove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublish Remove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublish Include="$(NativeBinary)">
<RelativePath>$(NativeBinaryPrefix)$(TargetName)$(NativeBinaryExt)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
```
This follows the same pattern as removing managed assemblies and debug
symbols, which are also embedded or replaced in the native binary.
## Customer Impact
- **Affected customers:** NativeAOT users with localized resources
- **Regression:** No
- **Source incompatibility:** No
- **Breaking change:** No (removes extraneous files from publish output)
## Testing
Testing will be added in the SDK repo per review feedback. The fix can
be validated by publishing a NativeAOT app with satellite assemblies and
verifying that:
- Localized resources are accessible at runtime (embedded correctly)
- No satellite assembly subdirectories exist in publish output
## Risk
Minimal. One-line change following established pattern. Satellite
assemblies remain embedded and functional; only removes redundant disk
copies.
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
----
*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](https://github.com/user-attachments/files/25188858/AotLocalization.zip)
[Binlog](https://github.com/user-attachments/files/25188847/AotLocalizationBinlog.zip)
### 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)
<comments>
<comment_new><author>@baronfel</author><body>
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)](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L3-L11),
`_ComputeIlcCompileInputs` gathers satellite assemblies from two
sources:
```xml
<IlcSatelliteAssembly Include="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssembly Include="@(IntermediateSatelliteAssembliesWithTargetPath)" />
```
- `_SatelliteAssembliesToPublish` = satellite assemblies from
package/project references (extracted from
`_ResolvedCopyLocalPublishAssets` by
[`ComputeManagedAssembliesToCompileToNative`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L73-L84))
- `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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets#L236),
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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L20-L34)
runs `BeforeTargets="ComputeResolvedFilesToPublishList"` and modifies
the publish list:
```xml
<_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)](https://github.com/dotnet/sdk/blob/main/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L545-L549),
`ComputeResolvedFilesToPublishList` unconditionally includes:
```xml
<!-- Copy satellite assemblies. -->
<ResolvedFileToPublish Include="@(IntermediateSatelliteAssembliesWithTargetPath)">
<RelativePath>%(IntermediateSatelliteAss...
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes#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.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com>
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Co-authored-by: Sven Boemer <sbomer@gmail.com>
sbomer added a commit that referenced this pull request Apr 10, 2026
… item Update (#125630)
## Description
Follow-up to PR #124801 review feedback: the "intersection via
include/remove, then remove+re-include" pattern in
`_PrepareTrimConfiguration` was complex, mutated item ordering, and used
two throwaway item groups. Replace with a direct MSBuild `Update`.
### Change
**Before** — compute intersection manually to set metadata, then
remove+re-add items:
```xml
<__SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<__SingleWarnIntermediateAssembly Remove="@(IntermediateAssembly)" />
<_SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<_SingleWarnIntermediateAssembly Remove="@(__SingleWarnIntermediateAssembly)" />
<_SingleWarnIntermediateAssembly>
<TrimmerSingleWarn Condition="...">false</TrimmerSingleWarn>
</_SingleWarnIntermediateAssembly>
<ResolvedFileToPublish Remove="@(_SingleWarnIntermediateAssembly)" />
<ResolvedFileToPublish Include="@(_SingleWarnIntermediateAssembly)" />
```
**After** — update matching items directly, preserving order:
```xml
<ResolvedFileToPublish Update="@(IntermediateAssembly)">
<TrimmerSingleWarn Condition=" '%(ResolvedFileToPublish.TrimmerSingleWarn)' == '' ">false</TrimmerSingleWarn>
</ResolvedFileToPublish>
```
## Changes proposed in this pull request
- [`Microsoft.NET.ILLink.targets`] Replace 13-line intersection pattern
with a 3-line `Update` in `_PrepareTrimConfiguration`
- [`Microsoft.NET.ILLink.targets`] Qualify `%(TrimmerSingleWarn)` as
`%(ResolvedFileToPublish.TrimmerSingleWarn)` in the `Update` condition
to prevent MSB4096 (unqualified metadata batching over all
`ResolvedFileToPublish` items, including those without the metadata
defined)
## Additional context
<!-- START COPILOT CODING AGENT TIPS -->
---
🔒 GitHub Advanced Security automatically protects Copilot coding agent
pull requests. You can protect all pull requests by enabling Advanced
Security for your repositories. [Learn more about Advanced
Security.](https://gh.io/cca-advanced-security)
---------
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 Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-Tools-ILLink.NET linker development as well as trimming analyzerslinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

ComputeIlcCompileInputs should not need to run before PrepareForILLink

4 participants

@sbomer@MichalStrehovsky
, '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

Decouple ILC from ManagedAssemblyToLink - #124801

Merged
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order
Mar 16, 2026
Merged

Decouple ILC from ManagedAssemblyToLink#124801
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order

Conversation

CopilotAI commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

_ComputeManagedAssemblyForILLink in NativeAOT was indirectly dependent on ComputeIlcCompileInputs via @(ManagedBinary), which made PrepareForILLink ordering-sensitive and broke incremental ILLink behavior when ILLink is run before ILC input computation. This change removes that coupling so ILLink preparation no longer relies on ComputeIlcCompileInputs side effects.

  • What changed

    • Updated src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets in _ComputeManagedAssemblyForILLink.
    • Replaced @(ManagedBinary) with @(IntermediateAssembly) when reconstructing @(ManagedAssemblyToLink).
  • Why this matters

    • @(IntermediateAssembly) is available during PrepareForILLink, independent of ComputeIlcCompileInputs.
    • This preserves inclusion of the project assembly in @(ManagedAssemblyToLink) regardless of whether ComputeIlcCompileInputs runs before or after PrepareForILLink/ILLink.
  • Code change

    <ManagedAssemblyToLinkInclude="@(DefaultFrameworkAssemblies);@(_ManagedResolvedAssembliesToPublish);@(IntermediateAssembly)" />
Original prompt

This section details on the original issue you should resolve

<issue_title>ComputeIlcCompileInputs should not need to run before PrepareForILLink</issue_title>
<issue_description>## Description

The default IlcCompileDependsOn in Microsoft.NETCore.Native.targets orders ComputeIlcCompileInputs before PrepareForILLink:

Compile;ComputeIlcCompileInputs;SetupOSSpecificProps;PrepareForILLink

This couples ILC's input computation to ILLink's preparation phase. _ComputeManagedAssemblyForILLink (which runs AfterTargets="_ComputeManagedAssemblyToLink" during PrepareForILLink) consumes @(ManagedBinary), a side effect of ComputeIlcCompileInputs. This ordering works for the standard pipeline because it doesn't actually run ILLink (RunILLink=false), but it breaks consumers that need PrepareForILLink and ILLink to run beforeComputeIlcCompileInputs.

Why a consumer would need the opposite order

The standard NativeAOT pipeline sets RunILLink=false — ILLink never actually runs, and @(ManagedAssemblyToLink) is only used as metadata for ILC. A consumer that sets RunILLink=true to actually trim assemblies before ILC needs ILLink to complete first, so that ILC consumes the trimmed output. This requires PrepareForILLink and ILLink to precede ComputeIlcCompileInputs — the opposite of the default order. This is the case in .NET for Android's NativeAOT pipeline.

Impact

When PrepareForILLink runs before ComputeIlcCompileInputs, _ComputeManagedAssemblyForILLink builds its replacement @(ManagedAssemblyToLink) list before @(ManagedBinary) has been populated. The project assembly ends up missing from @(ManagedAssemblyToLink), which is used as Inputs by _RunILLink (in Microsoft.NET.ILLink.targets). This causes ILLink's incremental build check to miss changes to the project assembly, so ILLink skips on rebuild even though the assembly changed.

First builds still succeed because PrepareForILLink independently adds @(IntermediateAssembly) as a TrimmerRootAssembly, so ILLink loads and processes it regardless. Only incremental builds are affected.

Reproduction

Create a dotnet new console project with the following csproj (note: explicit SDK imports are needed because Microsoft.NETCore.Native.targets unconditionally sets RunILLink=false, so it must be overridden after the SDK targets load):

<Project>
<ImportProject="Sdk.props"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<PublishAot>true</PublishAot>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>
<ImportProject="Sdk.targets"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<RunILLink>true</RunILLink>
<IlcCompileDependsOn>
Compile;
SetupOSSpecificProps;
PrepareForILLink;
ILLink;
ComputeIlcCompileInputs
</IlcCompileDependsOn>
</PropertyGroup>
</Project>

Run dotnet publish, then change "Hello, World!" to "Hello, Changed!" in Program.cs and publish again. On the second publish, ILLink is skipped (no "Optimizing assemblies for size" message) and the trimmed assembly in obj/.../linked/ReproApp.dll still contains Hello, World!.

Suggestion

Decouple ComputeIlcCompileInputs from PrepareForILLink so neither depends on having run before the other. Ideally _ComputeManagedAssemblyForILLink should not rely on state produced by ComputeIlcCompileInputs, and ComputeIlcCompileInputs should be free to run after ILLink without breaking the ILLink preparation phase.

Workaround

.NET for Android can work around this by injecting @(IntermediateAssembly) into @(ManagedAssemblyToLink) after _ComputeManagedAssemblyForILLink replaces it:

<TargetName="_AndroidFixManagedAssemblyToLink"AfterTargets="_ComputeManagedAssemblyForILLink">
<ItemGroup>
<ManagedAssemblyToLinkInclude="@(IntermediateAssembly)" />
</ItemGroup>
</Target>
```</issue_description>
<agent_instructions>Decouple these targets. The ILC inputs should not be resolved from ManagedAssemblyToLink.</agent_instructions>
## Comments on the Issue (you are @copilot in this section)
<comments>
</comments>

💡 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 in the docs.

@github-actionsgithub-actionsBot added the area-Tools-ILLink .NET linker development as well as trimming analyzers label Feb 24, 2026
…puts
Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
CopilotAI changed the title [WIP] Fix ComputeIlcCompileInputs execution order before PrepareForILLinkDecouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsFeb 24, 2026
CopilotAI requested a review from sbomerFebruary 24, 2026 17:25
…lyToLink
Factor _PrepareTrimConfiguration out of PrepareForILLink so shared trim
configuration operates on ResolvedFileToPublish and flows to both ILLink
and ILC without duplication.
For NativeAOT, _ComputeAssembliesToCompileToNative replaces CoreCLR
runtime pack files with DefaultFrameworkAssemblies (tagged
PostprocessAssembly=true) before _PrepareTrimConfiguration runs.
_ComputeIlcCompileInputs then derives IlcReference from
ResolvedFileToPublish via PostprocessAssembly metadata, ensuring ILC
sees ILLink-relocated paths when both run.
ComputeLinkedFilesToPublish hooks AfterTargets=ILLink (instead of
ComputeResolvedFilesToPublishList) for correct ordering when both
ILLink and ILC run. RunILLink is made conditional so projects can
opt in.
Remove ManagedAssemblies output from the C# task (replaced by
ResolvedFileToPublish filtering). Add RuntimePackFilesToSkipPublish
output for early removal of CoreCLR runtime pack files. Fix OOB
assembly handling so overrides stay in ResolvedFileToPublish.
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Feb 25, 2026
@sbomersbomer changed the title Decouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsDecouple ILC from ManagedAssemblyToLinkFeb 25, 2026
Skip _ComputeIlcCompileInputs for framework library builds
(BuildingFrameworkLibrary=true) since they use BuildOneFrameworkLibrary
for input computation, not the publish-pipeline targets.
When NativeCompilationDuringPublish is false (e.g. Apple non-library-mode
builds), Publish.targets is not imported so _ComputeIlcCompileInputs does
not exist to populate IlcReference from ResolvedFileToPublish. Fall back
to DefaultFrameworkAssemblies directly in ComputeIlcCompileInputs so ILC
can find System.Private.CoreLib and other framework references.
@sbomer

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-nativeaot-outerloop

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

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

Decouples NativeAOT’s ILLink preparation from ComputeIlcCompileInputs side effects by moving shared trim configuration into a dedicated target and rebuilding ILC/ILLink inputs from publish items rather than @(ManagedBinary).

Changes:

  • Introduced _PrepareTrimConfiguration in ILLink targets and made _ComputeManagedAssemblyToLink depend on it.
  • Updated NativeAOT publish pipeline to compute ILC inputs from ResolvedFileToPublish (PostprocessAssembly=true) and adjusted ordering around ILLink.
  • Simplified/changed ComputeManagedAssembliesToCompileToNative outputs to focus on runtime-pack files to remove and satellite assemblies.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targetsAdds _PrepareTrimConfiguration and moves shared trim defaults/metadata out of PrepareForILLink.
src/coreclr/tools/aot/ILCompiler.Build.Tasks/ComputeManagedAssembliesToCompileToNative.csAlters MSBuild task outputs and logic to support the updated publish/trim flow.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targetsRemoves NativeAOT’s PrepareForILLink dependency and shifts ILC trim metadata consumption.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targetsReorders publish hooks around ILLink and selects ILC managed inputs via PostprocessAssembly.

…esToPublishList
The AfterTargets="ILLink" hook is unnecessary because
ComputeLinkedFilesToPublish's own DependsOnTargets chain
(via LinkNative -> IlcCompile) transitively pulls in the
correct prerequisites regardless of the AfterTargets anchor.
Keeping ComputeResolvedFilesToPublishList avoids an artificial
coupling to ILLink and removes the need for the ILLink insertion
in the test infrastructure's LinkNativeIfBuildAndRun target.
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets
#	src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets
The _PrepareTrimConfiguration target was passing the full path of
IntermediateAssembly to TrimmerRootAssembly, but ILLink expects
assembly names (without path). This caused IL1032 errors across
all CI platforms: 'Root assembly with name ...ilc.dll could not be found.'
Restores the %(Filename) transform that was on main but got lost
when moving this line from PrepareForILLink to _PrepareTrimConfiguration.
@sbomer

Copy link
Copy Markdown
Member

/ba-g "deadletter"

@sbomer
sbomer merged commit 15da421 into mainMar 16, 2026
119 of 128 checks passed
@sbomer
sbomer deleted the copilot/fix-ilc-compile-order branch March 16, 2026 18:12
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
_ComputeAssembliesToCompileToNative now populates
@(_IlcManagedInputAssemblies) and ComputeLinkedFilesToPublish
removes them from @(ResolvedFileToPublish).
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in @(ResolvedFileToPublish) for the outer build's _ResolveAssemblies
target. Clear @(_IlcManagedInputAssemblies) in our
_AndroidComputeIlcCompileInputs target so the runtime's
ComputeLinkedFilesToPublish doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Changes: dotnet/dotnet@5ff448a...803eb28
- **Dependency Updates**:
- From [11.0.0-preview.3.26165.107 to 11.0.0-preview.3.26168.106][1]
- Microsoft.NET.Workload.Mono.ToolChain.Current.Manifest-11.0.100-preview.3
- Microsoft.NET.ILLink
- Microsoft.NETCore.App.Ref
- From [11.0.0-beta.26165.107 to 11.0.0-beta.26168.106][1]
- Microsoft.DotNet.Build.Tasks.Feed
- From [0.11.5-preview.26165.107 to 0.11.5-preview.26168.106][1]
- Microsoft.DotNet.Cecil
- From [11.0.100-preview.3.26165.107 to 11.0.100-preview.3.26168.106][1]
- Microsoft.NET.Sdk
- Microsoft.NET.Workload.Emscripten.Current.Manifest-11.0.100-preview.3
- Microsoft.TemplateEngine.Authoring.Tasks
[1]: dotnet/dotnet@5ff448a...803eb28
## Other changes ##
[xabt] Prevent `ComputeLinkedFilesToPublish` from stripping assemblies
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
`_ComputeAssembliesToCompileToNative` now populates
`@(_IlcManagedInputAssemblies)` and `ComputeLinkedFilesToPublish`
removes them from `@(ResolvedFileToPublish)`.
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in `@(ResolvedFileToPublish)` for the outer build's `_ResolveAssemblies`
target. Clear `@(_IlcManagedInputAssemblies)` in our
`_AndroidComputeIlcCompileInputs` target so the runtime's
`ComputeLinkedFilesToPublish` doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
sbomer added a commit that referenced this pull request Mar 24, 2026
…g them (#124192)
## 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:
```xml
<ItemGroup>
<ResolvedFileToPublish Remove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublish Remove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublish Remove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublish Include="$(NativeBinary)">
<RelativePath>$(NativeBinaryPrefix)$(TargetName)$(NativeBinaryExt)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
```
This follows the same pattern as removing managed assemblies and debug
symbols, which are also embedded or replaced in the native binary.
## Customer Impact
- **Affected customers:** NativeAOT users with localized resources
- **Regression:** No
- **Source incompatibility:** No
- **Breaking change:** No (removes extraneous files from publish output)
## Testing
Testing will be added in the SDK repo per review feedback. The fix can
be validated by publishing a NativeAOT app with satellite assemblies and
verifying that:
- Localized resources are accessible at runtime (embedded correctly)
- No satellite assembly subdirectories exist in publish output
## Risk
Minimal. One-line change following established pattern. Satellite
assemblies remain embedded and functional; only removes redundant disk
copies.
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
----
*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](https://github.com/user-attachments/files/25188858/AotLocalization.zip)
[Binlog](https://github.com/user-attachments/files/25188847/AotLocalizationBinlog.zip)
### 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)
<comments>
<comment_new><author>@baronfel</author><body>
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)](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L3-L11),
`_ComputeIlcCompileInputs` gathers satellite assemblies from two
sources:
```xml
<IlcSatelliteAssembly Include="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssembly Include="@(IntermediateSatelliteAssembliesWithTargetPath)" />
```
- `_SatelliteAssembliesToPublish` = satellite assemblies from
package/project references (extracted from
`_ResolvedCopyLocalPublishAssets` by
[`ComputeManagedAssembliesToCompileToNative`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L73-L84))
- `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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets#L236),
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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L20-L34)
runs `BeforeTargets="ComputeResolvedFilesToPublishList"` and modifies
the publish list:
```xml
<_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)](https://github.com/dotnet/sdk/blob/main/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L545-L549),
`ComputeResolvedFilesToPublishList` unconditionally includes:
```xml
<!-- Copy satellite assemblies. -->
<ResolvedFileToPublish Include="@(IntermediateSatelliteAssembliesWithTargetPath)">
<RelativePath>%(IntermediateSatelliteAss...
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes#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.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com>
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Co-authored-by: Sven Boemer <sbomer@gmail.com>
sbomer added a commit that referenced this pull request Apr 10, 2026
… item Update (#125630)
## Description
Follow-up to PR #124801 review feedback: the "intersection via
include/remove, then remove+re-include" pattern in
`_PrepareTrimConfiguration` was complex, mutated item ordering, and used
two throwaway item groups. Replace with a direct MSBuild `Update`.
### Change
**Before** — compute intersection manually to set metadata, then
remove+re-add items:
```xml
<__SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<__SingleWarnIntermediateAssembly Remove="@(IntermediateAssembly)" />
<_SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<_SingleWarnIntermediateAssembly Remove="@(__SingleWarnIntermediateAssembly)" />
<_SingleWarnIntermediateAssembly>
<TrimmerSingleWarn Condition="...">false</TrimmerSingleWarn>
</_SingleWarnIntermediateAssembly>
<ResolvedFileToPublish Remove="@(_SingleWarnIntermediateAssembly)" />
<ResolvedFileToPublish Include="@(_SingleWarnIntermediateAssembly)" />
```
**After** — update matching items directly, preserving order:
```xml
<ResolvedFileToPublish Update="@(IntermediateAssembly)">
<TrimmerSingleWarn Condition=" '%(ResolvedFileToPublish.TrimmerSingleWarn)' == '' ">false</TrimmerSingleWarn>
</ResolvedFileToPublish>
```
## Changes proposed in this pull request
- [`Microsoft.NET.ILLink.targets`] Replace 13-line intersection pattern
with a 3-line `Update` in `_PrepareTrimConfiguration`
- [`Microsoft.NET.ILLink.targets`] Qualify `%(TrimmerSingleWarn)` as
`%(ResolvedFileToPublish.TrimmerSingleWarn)` in the `Update` condition
to prevent MSB4096 (unqualified metadata batching over all
`ResolvedFileToPublish` items, including those without the metadata
defined)
## Additional context
<!-- START COPILOT CODING AGENT TIPS -->
---
🔒 GitHub Advanced Security automatically protects Copilot coding agent
pull requests. You can protect all pull requests by enabling Advanced
Security for your repositories. [Learn more about Advanced
Security.](https://gh.io/cca-advanced-security)
---------
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 Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-Tools-ILLink.NET linker development as well as trimming analyzerslinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

ComputeIlcCompileInputs should not need to run before PrepareForILLink

4 participants

@sbomer@MichalStrehovsky
, '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

Decouple ILC from ManagedAssemblyToLink - #124801

Merged
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order
Mar 16, 2026
Merged

Decouple ILC from ManagedAssemblyToLink#124801
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order

Conversation

CopilotAI commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

_ComputeManagedAssemblyForILLink in NativeAOT was indirectly dependent on ComputeIlcCompileInputs via @(ManagedBinary), which made PrepareForILLink ordering-sensitive and broke incremental ILLink behavior when ILLink is run before ILC input computation. This change removes that coupling so ILLink preparation no longer relies on ComputeIlcCompileInputs side effects.

  • What changed

    • Updated src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets in _ComputeManagedAssemblyForILLink.
    • Replaced @(ManagedBinary) with @(IntermediateAssembly) when reconstructing @(ManagedAssemblyToLink).
  • Why this matters

    • @(IntermediateAssembly) is available during PrepareForILLink, independent of ComputeIlcCompileInputs.
    • This preserves inclusion of the project assembly in @(ManagedAssemblyToLink) regardless of whether ComputeIlcCompileInputs runs before or after PrepareForILLink/ILLink.
  • Code change

    <ManagedAssemblyToLinkInclude="@(DefaultFrameworkAssemblies);@(_ManagedResolvedAssembliesToPublish);@(IntermediateAssembly)" />
Original prompt

This section details on the original issue you should resolve

<issue_title>ComputeIlcCompileInputs should not need to run before PrepareForILLink</issue_title>
<issue_description>## Description

The default IlcCompileDependsOn in Microsoft.NETCore.Native.targets orders ComputeIlcCompileInputs before PrepareForILLink:

Compile;ComputeIlcCompileInputs;SetupOSSpecificProps;PrepareForILLink

This couples ILC's input computation to ILLink's preparation phase. _ComputeManagedAssemblyForILLink (which runs AfterTargets="_ComputeManagedAssemblyToLink" during PrepareForILLink) consumes @(ManagedBinary), a side effect of ComputeIlcCompileInputs. This ordering works for the standard pipeline because it doesn't actually run ILLink (RunILLink=false), but it breaks consumers that need PrepareForILLink and ILLink to run beforeComputeIlcCompileInputs.

Why a consumer would need the opposite order

The standard NativeAOT pipeline sets RunILLink=false — ILLink never actually runs, and @(ManagedAssemblyToLink) is only used as metadata for ILC. A consumer that sets RunILLink=true to actually trim assemblies before ILC needs ILLink to complete first, so that ILC consumes the trimmed output. This requires PrepareForILLink and ILLink to precede ComputeIlcCompileInputs — the opposite of the default order. This is the case in .NET for Android's NativeAOT pipeline.

Impact

When PrepareForILLink runs before ComputeIlcCompileInputs, _ComputeManagedAssemblyForILLink builds its replacement @(ManagedAssemblyToLink) list before @(ManagedBinary) has been populated. The project assembly ends up missing from @(ManagedAssemblyToLink), which is used as Inputs by _RunILLink (in Microsoft.NET.ILLink.targets). This causes ILLink's incremental build check to miss changes to the project assembly, so ILLink skips on rebuild even though the assembly changed.

First builds still succeed because PrepareForILLink independently adds @(IntermediateAssembly) as a TrimmerRootAssembly, so ILLink loads and processes it regardless. Only incremental builds are affected.

Reproduction

Create a dotnet new console project with the following csproj (note: explicit SDK imports are needed because Microsoft.NETCore.Native.targets unconditionally sets RunILLink=false, so it must be overridden after the SDK targets load):

<Project>
<ImportProject="Sdk.props"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<PublishAot>true</PublishAot>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>
<ImportProject="Sdk.targets"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<RunILLink>true</RunILLink>
<IlcCompileDependsOn>
Compile;
SetupOSSpecificProps;
PrepareForILLink;
ILLink;
ComputeIlcCompileInputs
</IlcCompileDependsOn>
</PropertyGroup>
</Project>

Run dotnet publish, then change "Hello, World!" to "Hello, Changed!" in Program.cs and publish again. On the second publish, ILLink is skipped (no "Optimizing assemblies for size" message) and the trimmed assembly in obj/.../linked/ReproApp.dll still contains Hello, World!.

Suggestion

Decouple ComputeIlcCompileInputs from PrepareForILLink so neither depends on having run before the other. Ideally _ComputeManagedAssemblyForILLink should not rely on state produced by ComputeIlcCompileInputs, and ComputeIlcCompileInputs should be free to run after ILLink without breaking the ILLink preparation phase.

Workaround

.NET for Android can work around this by injecting @(IntermediateAssembly) into @(ManagedAssemblyToLink) after _ComputeManagedAssemblyForILLink replaces it:

<TargetName="_AndroidFixManagedAssemblyToLink"AfterTargets="_ComputeManagedAssemblyForILLink">
<ItemGroup>
<ManagedAssemblyToLinkInclude="@(IntermediateAssembly)" />
</ItemGroup>
</Target>
```</issue_description>
<agent_instructions>Decouple these targets. The ILC inputs should not be resolved from ManagedAssemblyToLink.</agent_instructions>
## Comments on the Issue (you are @copilot in this section)
<comments>
</comments>

💡 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 in the docs.

@github-actionsgithub-actionsBot added the area-Tools-ILLink .NET linker development as well as trimming analyzers label Feb 24, 2026
…puts
Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
CopilotAI changed the title [WIP] Fix ComputeIlcCompileInputs execution order before PrepareForILLinkDecouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsFeb 24, 2026
CopilotAI requested a review from sbomerFebruary 24, 2026 17:25
…lyToLink
Factor _PrepareTrimConfiguration out of PrepareForILLink so shared trim
configuration operates on ResolvedFileToPublish and flows to both ILLink
and ILC without duplication.
For NativeAOT, _ComputeAssembliesToCompileToNative replaces CoreCLR
runtime pack files with DefaultFrameworkAssemblies (tagged
PostprocessAssembly=true) before _PrepareTrimConfiguration runs.
_ComputeIlcCompileInputs then derives IlcReference from
ResolvedFileToPublish via PostprocessAssembly metadata, ensuring ILC
sees ILLink-relocated paths when both run.
ComputeLinkedFilesToPublish hooks AfterTargets=ILLink (instead of
ComputeResolvedFilesToPublishList) for correct ordering when both
ILLink and ILC run. RunILLink is made conditional so projects can
opt in.
Remove ManagedAssemblies output from the C# task (replaced by
ResolvedFileToPublish filtering). Add RuntimePackFilesToSkipPublish
output for early removal of CoreCLR runtime pack files. Fix OOB
assembly handling so overrides stay in ResolvedFileToPublish.
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Feb 25, 2026
@sbomersbomer changed the title Decouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsDecouple ILC from ManagedAssemblyToLinkFeb 25, 2026
Skip _ComputeIlcCompileInputs for framework library builds
(BuildingFrameworkLibrary=true) since they use BuildOneFrameworkLibrary
for input computation, not the publish-pipeline targets.
When NativeCompilationDuringPublish is false (e.g. Apple non-library-mode
builds), Publish.targets is not imported so _ComputeIlcCompileInputs does
not exist to populate IlcReference from ResolvedFileToPublish. Fall back
to DefaultFrameworkAssemblies directly in ComputeIlcCompileInputs so ILC
can find System.Private.CoreLib and other framework references.
@sbomer

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-nativeaot-outerloop

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

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

Decouples NativeAOT’s ILLink preparation from ComputeIlcCompileInputs side effects by moving shared trim configuration into a dedicated target and rebuilding ILC/ILLink inputs from publish items rather than @(ManagedBinary).

Changes:

  • Introduced _PrepareTrimConfiguration in ILLink targets and made _ComputeManagedAssemblyToLink depend on it.
  • Updated NativeAOT publish pipeline to compute ILC inputs from ResolvedFileToPublish (PostprocessAssembly=true) and adjusted ordering around ILLink.
  • Simplified/changed ComputeManagedAssembliesToCompileToNative outputs to focus on runtime-pack files to remove and satellite assemblies.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targetsAdds _PrepareTrimConfiguration and moves shared trim defaults/metadata out of PrepareForILLink.
src/coreclr/tools/aot/ILCompiler.Build.Tasks/ComputeManagedAssembliesToCompileToNative.csAlters MSBuild task outputs and logic to support the updated publish/trim flow.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targetsRemoves NativeAOT’s PrepareForILLink dependency and shifts ILC trim metadata consumption.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targetsReorders publish hooks around ILLink and selects ILC managed inputs via PostprocessAssembly.

…esToPublishList
The AfterTargets="ILLink" hook is unnecessary because
ComputeLinkedFilesToPublish's own DependsOnTargets chain
(via LinkNative -> IlcCompile) transitively pulls in the
correct prerequisites regardless of the AfterTargets anchor.
Keeping ComputeResolvedFilesToPublishList avoids an artificial
coupling to ILLink and removes the need for the ILLink insertion
in the test infrastructure's LinkNativeIfBuildAndRun target.
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets
#	src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets
The _PrepareTrimConfiguration target was passing the full path of
IntermediateAssembly to TrimmerRootAssembly, but ILLink expects
assembly names (without path). This caused IL1032 errors across
all CI platforms: 'Root assembly with name ...ilc.dll could not be found.'
Restores the %(Filename) transform that was on main but got lost
when moving this line from PrepareForILLink to _PrepareTrimConfiguration.
@sbomer

Copy link
Copy Markdown
Member

/ba-g "deadletter"

@sbomer
sbomer merged commit 15da421 into mainMar 16, 2026
119 of 128 checks passed
@sbomer
sbomer deleted the copilot/fix-ilc-compile-order branch March 16, 2026 18:12
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
_ComputeAssembliesToCompileToNative now populates
@(_IlcManagedInputAssemblies) and ComputeLinkedFilesToPublish
removes them from @(ResolvedFileToPublish).
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in @(ResolvedFileToPublish) for the outer build's _ResolveAssemblies
target. Clear @(_IlcManagedInputAssemblies) in our
_AndroidComputeIlcCompileInputs target so the runtime's
ComputeLinkedFilesToPublish doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Changes: dotnet/dotnet@5ff448a...803eb28
- **Dependency Updates**:
- From [11.0.0-preview.3.26165.107 to 11.0.0-preview.3.26168.106][1]
- Microsoft.NET.Workload.Mono.ToolChain.Current.Manifest-11.0.100-preview.3
- Microsoft.NET.ILLink
- Microsoft.NETCore.App.Ref
- From [11.0.0-beta.26165.107 to 11.0.0-beta.26168.106][1]
- Microsoft.DotNet.Build.Tasks.Feed
- From [0.11.5-preview.26165.107 to 0.11.5-preview.26168.106][1]
- Microsoft.DotNet.Cecil
- From [11.0.100-preview.3.26165.107 to 11.0.100-preview.3.26168.106][1]
- Microsoft.NET.Sdk
- Microsoft.NET.Workload.Emscripten.Current.Manifest-11.0.100-preview.3
- Microsoft.TemplateEngine.Authoring.Tasks
[1]: dotnet/dotnet@5ff448a...803eb28
## Other changes ##
[xabt] Prevent `ComputeLinkedFilesToPublish` from stripping assemblies
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
`_ComputeAssembliesToCompileToNative` now populates
`@(_IlcManagedInputAssemblies)` and `ComputeLinkedFilesToPublish`
removes them from `@(ResolvedFileToPublish)`.
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in `@(ResolvedFileToPublish)` for the outer build's `_ResolveAssemblies`
target. Clear `@(_IlcManagedInputAssemblies)` in our
`_AndroidComputeIlcCompileInputs` target so the runtime's
`ComputeLinkedFilesToPublish` doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
sbomer added a commit that referenced this pull request Mar 24, 2026
…g them (#124192)
## 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:
```xml
<ItemGroup>
<ResolvedFileToPublish Remove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublish Remove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublish Remove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublish Include="$(NativeBinary)">
<RelativePath>$(NativeBinaryPrefix)$(TargetName)$(NativeBinaryExt)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
```
This follows the same pattern as removing managed assemblies and debug
symbols, which are also embedded or replaced in the native binary.
## Customer Impact
- **Affected customers:** NativeAOT users with localized resources
- **Regression:** No
- **Source incompatibility:** No
- **Breaking change:** No (removes extraneous files from publish output)
## Testing
Testing will be added in the SDK repo per review feedback. The fix can
be validated by publishing a NativeAOT app with satellite assemblies and
verifying that:
- Localized resources are accessible at runtime (embedded correctly)
- No satellite assembly subdirectories exist in publish output
## Risk
Minimal. One-line change following established pattern. Satellite
assemblies remain embedded and functional; only removes redundant disk
copies.
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
----
*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](https://github.com/user-attachments/files/25188858/AotLocalization.zip)
[Binlog](https://github.com/user-attachments/files/25188847/AotLocalizationBinlog.zip)
### 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)
<comments>
<comment_new><author>@baronfel</author><body>
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)](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L3-L11),
`_ComputeIlcCompileInputs` gathers satellite assemblies from two
sources:
```xml
<IlcSatelliteAssembly Include="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssembly Include="@(IntermediateSatelliteAssembliesWithTargetPath)" />
```
- `_SatelliteAssembliesToPublish` = satellite assemblies from
package/project references (extracted from
`_ResolvedCopyLocalPublishAssets` by
[`ComputeManagedAssembliesToCompileToNative`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L73-L84))
- `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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets#L236),
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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L20-L34)
runs `BeforeTargets="ComputeResolvedFilesToPublishList"` and modifies
the publish list:
```xml
<_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)](https://github.com/dotnet/sdk/blob/main/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L545-L549),
`ComputeResolvedFilesToPublishList` unconditionally includes:
```xml
<!-- Copy satellite assemblies. -->
<ResolvedFileToPublish Include="@(IntermediateSatelliteAssembliesWithTargetPath)">
<RelativePath>%(IntermediateSatelliteAss...
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes#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.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com>
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Co-authored-by: Sven Boemer <sbomer@gmail.com>
sbomer added a commit that referenced this pull request Apr 10, 2026
… item Update (#125630)
## Description
Follow-up to PR #124801 review feedback: the "intersection via
include/remove, then remove+re-include" pattern in
`_PrepareTrimConfiguration` was complex, mutated item ordering, and used
two throwaway item groups. Replace with a direct MSBuild `Update`.
### Change
**Before** — compute intersection manually to set metadata, then
remove+re-add items:
```xml
<__SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<__SingleWarnIntermediateAssembly Remove="@(IntermediateAssembly)" />
<_SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<_SingleWarnIntermediateAssembly Remove="@(__SingleWarnIntermediateAssembly)" />
<_SingleWarnIntermediateAssembly>
<TrimmerSingleWarn Condition="...">false</TrimmerSingleWarn>
</_SingleWarnIntermediateAssembly>
<ResolvedFileToPublish Remove="@(_SingleWarnIntermediateAssembly)" />
<ResolvedFileToPublish Include="@(_SingleWarnIntermediateAssembly)" />
```
**After** — update matching items directly, preserving order:
```xml
<ResolvedFileToPublish Update="@(IntermediateAssembly)">
<TrimmerSingleWarn Condition=" '%(ResolvedFileToPublish.TrimmerSingleWarn)' == '' ">false</TrimmerSingleWarn>
</ResolvedFileToPublish>
```
## Changes proposed in this pull request
- [`Microsoft.NET.ILLink.targets`] Replace 13-line intersection pattern
with a 3-line `Update` in `_PrepareTrimConfiguration`
- [`Microsoft.NET.ILLink.targets`] Qualify `%(TrimmerSingleWarn)` as
`%(ResolvedFileToPublish.TrimmerSingleWarn)` in the `Update` condition
to prevent MSB4096 (unqualified metadata batching over all
`ResolvedFileToPublish` items, including those without the metadata
defined)
## Additional context
<!-- START COPILOT CODING AGENT TIPS -->
---
🔒 GitHub Advanced Security automatically protects Copilot coding agent
pull requests. You can protect all pull requests by enabling Advanced
Security for your repositories. [Learn more about Advanced
Security.](https://gh.io/cca-advanced-security)
---------
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 Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-Tools-ILLink.NET linker development as well as trimming analyzerslinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

ComputeIlcCompileInputs should not need to run before PrepareForILLink

4 participants

@sbomer@MichalStrehovsky
, '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

Decouple ILC from ManagedAssemblyToLink - #124801

Merged
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order
Mar 16, 2026
Merged

Decouple ILC from ManagedAssemblyToLink#124801
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order

Conversation

CopilotAI commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

_ComputeManagedAssemblyForILLink in NativeAOT was indirectly dependent on ComputeIlcCompileInputs via @(ManagedBinary), which made PrepareForILLink ordering-sensitive and broke incremental ILLink behavior when ILLink is run before ILC input computation. This change removes that coupling so ILLink preparation no longer relies on ComputeIlcCompileInputs side effects.

  • What changed

    • Updated src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets in _ComputeManagedAssemblyForILLink.
    • Replaced @(ManagedBinary) with @(IntermediateAssembly) when reconstructing @(ManagedAssemblyToLink).
  • Why this matters

    • @(IntermediateAssembly) is available during PrepareForILLink, independent of ComputeIlcCompileInputs.
    • This preserves inclusion of the project assembly in @(ManagedAssemblyToLink) regardless of whether ComputeIlcCompileInputs runs before or after PrepareForILLink/ILLink.
  • Code change

    <ManagedAssemblyToLinkInclude="@(DefaultFrameworkAssemblies);@(_ManagedResolvedAssembliesToPublish);@(IntermediateAssembly)" />
Original prompt

This section details on the original issue you should resolve

<issue_title>ComputeIlcCompileInputs should not need to run before PrepareForILLink</issue_title>
<issue_description>## Description

The default IlcCompileDependsOn in Microsoft.NETCore.Native.targets orders ComputeIlcCompileInputs before PrepareForILLink:

Compile;ComputeIlcCompileInputs;SetupOSSpecificProps;PrepareForILLink

This couples ILC's input computation to ILLink's preparation phase. _ComputeManagedAssemblyForILLink (which runs AfterTargets="_ComputeManagedAssemblyToLink" during PrepareForILLink) consumes @(ManagedBinary), a side effect of ComputeIlcCompileInputs. This ordering works for the standard pipeline because it doesn't actually run ILLink (RunILLink=false), but it breaks consumers that need PrepareForILLink and ILLink to run beforeComputeIlcCompileInputs.

Why a consumer would need the opposite order

The standard NativeAOT pipeline sets RunILLink=false — ILLink never actually runs, and @(ManagedAssemblyToLink) is only used as metadata for ILC. A consumer that sets RunILLink=true to actually trim assemblies before ILC needs ILLink to complete first, so that ILC consumes the trimmed output. This requires PrepareForILLink and ILLink to precede ComputeIlcCompileInputs — the opposite of the default order. This is the case in .NET for Android's NativeAOT pipeline.

Impact

When PrepareForILLink runs before ComputeIlcCompileInputs, _ComputeManagedAssemblyForILLink builds its replacement @(ManagedAssemblyToLink) list before @(ManagedBinary) has been populated. The project assembly ends up missing from @(ManagedAssemblyToLink), which is used as Inputs by _RunILLink (in Microsoft.NET.ILLink.targets). This causes ILLink's incremental build check to miss changes to the project assembly, so ILLink skips on rebuild even though the assembly changed.

First builds still succeed because PrepareForILLink independently adds @(IntermediateAssembly) as a TrimmerRootAssembly, so ILLink loads and processes it regardless. Only incremental builds are affected.

Reproduction

Create a dotnet new console project with the following csproj (note: explicit SDK imports are needed because Microsoft.NETCore.Native.targets unconditionally sets RunILLink=false, so it must be overridden after the SDK targets load):

<Project>
<ImportProject="Sdk.props"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<PublishAot>true</PublishAot>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>
<ImportProject="Sdk.targets"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<RunILLink>true</RunILLink>
<IlcCompileDependsOn>
Compile;
SetupOSSpecificProps;
PrepareForILLink;
ILLink;
ComputeIlcCompileInputs
</IlcCompileDependsOn>
</PropertyGroup>
</Project>

Run dotnet publish, then change "Hello, World!" to "Hello, Changed!" in Program.cs and publish again. On the second publish, ILLink is skipped (no "Optimizing assemblies for size" message) and the trimmed assembly in obj/.../linked/ReproApp.dll still contains Hello, World!.

Suggestion

Decouple ComputeIlcCompileInputs from PrepareForILLink so neither depends on having run before the other. Ideally _ComputeManagedAssemblyForILLink should not rely on state produced by ComputeIlcCompileInputs, and ComputeIlcCompileInputs should be free to run after ILLink without breaking the ILLink preparation phase.

Workaround

.NET for Android can work around this by injecting @(IntermediateAssembly) into @(ManagedAssemblyToLink) after _ComputeManagedAssemblyForILLink replaces it:

<TargetName="_AndroidFixManagedAssemblyToLink"AfterTargets="_ComputeManagedAssemblyForILLink">
<ItemGroup>
<ManagedAssemblyToLinkInclude="@(IntermediateAssembly)" />
</ItemGroup>
</Target>
```</issue_description>
<agent_instructions>Decouple these targets. The ILC inputs should not be resolved from ManagedAssemblyToLink.</agent_instructions>
## Comments on the Issue (you are @copilot in this section)
<comments>
</comments>

💡 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 in the docs.

@github-actionsgithub-actionsBot added the area-Tools-ILLink .NET linker development as well as trimming analyzers label Feb 24, 2026
…puts
Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
CopilotAI changed the title [WIP] Fix ComputeIlcCompileInputs execution order before PrepareForILLinkDecouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsFeb 24, 2026
CopilotAI requested a review from sbomerFebruary 24, 2026 17:25
…lyToLink
Factor _PrepareTrimConfiguration out of PrepareForILLink so shared trim
configuration operates on ResolvedFileToPublish and flows to both ILLink
and ILC without duplication.
For NativeAOT, _ComputeAssembliesToCompileToNative replaces CoreCLR
runtime pack files with DefaultFrameworkAssemblies (tagged
PostprocessAssembly=true) before _PrepareTrimConfiguration runs.
_ComputeIlcCompileInputs then derives IlcReference from
ResolvedFileToPublish via PostprocessAssembly metadata, ensuring ILC
sees ILLink-relocated paths when both run.
ComputeLinkedFilesToPublish hooks AfterTargets=ILLink (instead of
ComputeResolvedFilesToPublishList) for correct ordering when both
ILLink and ILC run. RunILLink is made conditional so projects can
opt in.
Remove ManagedAssemblies output from the C# task (replaced by
ResolvedFileToPublish filtering). Add RuntimePackFilesToSkipPublish
output for early removal of CoreCLR runtime pack files. Fix OOB
assembly handling so overrides stay in ResolvedFileToPublish.
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Feb 25, 2026
@sbomersbomer changed the title Decouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsDecouple ILC from ManagedAssemblyToLinkFeb 25, 2026
Skip _ComputeIlcCompileInputs for framework library builds
(BuildingFrameworkLibrary=true) since they use BuildOneFrameworkLibrary
for input computation, not the publish-pipeline targets.
When NativeCompilationDuringPublish is false (e.g. Apple non-library-mode
builds), Publish.targets is not imported so _ComputeIlcCompileInputs does
not exist to populate IlcReference from ResolvedFileToPublish. Fall back
to DefaultFrameworkAssemblies directly in ComputeIlcCompileInputs so ILC
can find System.Private.CoreLib and other framework references.
@sbomer

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-nativeaot-outerloop

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

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

Decouples NativeAOT’s ILLink preparation from ComputeIlcCompileInputs side effects by moving shared trim configuration into a dedicated target and rebuilding ILC/ILLink inputs from publish items rather than @(ManagedBinary).

Changes:

  • Introduced _PrepareTrimConfiguration in ILLink targets and made _ComputeManagedAssemblyToLink depend on it.
  • Updated NativeAOT publish pipeline to compute ILC inputs from ResolvedFileToPublish (PostprocessAssembly=true) and adjusted ordering around ILLink.
  • Simplified/changed ComputeManagedAssembliesToCompileToNative outputs to focus on runtime-pack files to remove and satellite assemblies.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targetsAdds _PrepareTrimConfiguration and moves shared trim defaults/metadata out of PrepareForILLink.
src/coreclr/tools/aot/ILCompiler.Build.Tasks/ComputeManagedAssembliesToCompileToNative.csAlters MSBuild task outputs and logic to support the updated publish/trim flow.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targetsRemoves NativeAOT’s PrepareForILLink dependency and shifts ILC trim metadata consumption.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targetsReorders publish hooks around ILLink and selects ILC managed inputs via PostprocessAssembly.

…esToPublishList
The AfterTargets="ILLink" hook is unnecessary because
ComputeLinkedFilesToPublish's own DependsOnTargets chain
(via LinkNative -> IlcCompile) transitively pulls in the
correct prerequisites regardless of the AfterTargets anchor.
Keeping ComputeResolvedFilesToPublishList avoids an artificial
coupling to ILLink and removes the need for the ILLink insertion
in the test infrastructure's LinkNativeIfBuildAndRun target.
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets
#	src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets
The _PrepareTrimConfiguration target was passing the full path of
IntermediateAssembly to TrimmerRootAssembly, but ILLink expects
assembly names (without path). This caused IL1032 errors across
all CI platforms: 'Root assembly with name ...ilc.dll could not be found.'
Restores the %(Filename) transform that was on main but got lost
when moving this line from PrepareForILLink to _PrepareTrimConfiguration.
@sbomer

Copy link
Copy Markdown
Member

/ba-g "deadletter"

@sbomer
sbomer merged commit 15da421 into mainMar 16, 2026
119 of 128 checks passed
@sbomer
sbomer deleted the copilot/fix-ilc-compile-order branch March 16, 2026 18:12
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
_ComputeAssembliesToCompileToNative now populates
@(_IlcManagedInputAssemblies) and ComputeLinkedFilesToPublish
removes them from @(ResolvedFileToPublish).
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in @(ResolvedFileToPublish) for the outer build's _ResolveAssemblies
target. Clear @(_IlcManagedInputAssemblies) in our
_AndroidComputeIlcCompileInputs target so the runtime's
ComputeLinkedFilesToPublish doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Changes: dotnet/dotnet@5ff448a...803eb28
- **Dependency Updates**:
- From [11.0.0-preview.3.26165.107 to 11.0.0-preview.3.26168.106][1]
- Microsoft.NET.Workload.Mono.ToolChain.Current.Manifest-11.0.100-preview.3
- Microsoft.NET.ILLink
- Microsoft.NETCore.App.Ref
- From [11.0.0-beta.26165.107 to 11.0.0-beta.26168.106][1]
- Microsoft.DotNet.Build.Tasks.Feed
- From [0.11.5-preview.26165.107 to 0.11.5-preview.26168.106][1]
- Microsoft.DotNet.Cecil
- From [11.0.100-preview.3.26165.107 to 11.0.100-preview.3.26168.106][1]
- Microsoft.NET.Sdk
- Microsoft.NET.Workload.Emscripten.Current.Manifest-11.0.100-preview.3
- Microsoft.TemplateEngine.Authoring.Tasks
[1]: dotnet/dotnet@5ff448a...803eb28
## Other changes ##
[xabt] Prevent `ComputeLinkedFilesToPublish` from stripping assemblies
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
`_ComputeAssembliesToCompileToNative` now populates
`@(_IlcManagedInputAssemblies)` and `ComputeLinkedFilesToPublish`
removes them from `@(ResolvedFileToPublish)`.
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in `@(ResolvedFileToPublish)` for the outer build's `_ResolveAssemblies`
target. Clear `@(_IlcManagedInputAssemblies)` in our
`_AndroidComputeIlcCompileInputs` target so the runtime's
`ComputeLinkedFilesToPublish` doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
sbomer added a commit that referenced this pull request Mar 24, 2026
…g them (#124192)
## 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:
```xml
<ItemGroup>
<ResolvedFileToPublish Remove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublish Remove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublish Remove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublish Include="$(NativeBinary)">
<RelativePath>$(NativeBinaryPrefix)$(TargetName)$(NativeBinaryExt)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
```
This follows the same pattern as removing managed assemblies and debug
symbols, which are also embedded or replaced in the native binary.
## Customer Impact
- **Affected customers:** NativeAOT users with localized resources
- **Regression:** No
- **Source incompatibility:** No
- **Breaking change:** No (removes extraneous files from publish output)
## Testing
Testing will be added in the SDK repo per review feedback. The fix can
be validated by publishing a NativeAOT app with satellite assemblies and
verifying that:
- Localized resources are accessible at runtime (embedded correctly)
- No satellite assembly subdirectories exist in publish output
## Risk
Minimal. One-line change following established pattern. Satellite
assemblies remain embedded and functional; only removes redundant disk
copies.
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
----
*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](https://github.com/user-attachments/files/25188858/AotLocalization.zip)
[Binlog](https://github.com/user-attachments/files/25188847/AotLocalizationBinlog.zip)
### 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)
<comments>
<comment_new><author>@baronfel</author><body>
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)](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L3-L11),
`_ComputeIlcCompileInputs` gathers satellite assemblies from two
sources:
```xml
<IlcSatelliteAssembly Include="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssembly Include="@(IntermediateSatelliteAssembliesWithTargetPath)" />
```
- `_SatelliteAssembliesToPublish` = satellite assemblies from
package/project references (extracted from
`_ResolvedCopyLocalPublishAssets` by
[`ComputeManagedAssembliesToCompileToNative`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L73-L84))
- `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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets#L236),
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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L20-L34)
runs `BeforeTargets="ComputeResolvedFilesToPublishList"` and modifies
the publish list:
```xml
<_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)](https://github.com/dotnet/sdk/blob/main/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L545-L549),
`ComputeResolvedFilesToPublishList` unconditionally includes:
```xml
<!-- Copy satellite assemblies. -->
<ResolvedFileToPublish Include="@(IntermediateSatelliteAssembliesWithTargetPath)">
<RelativePath>%(IntermediateSatelliteAss...
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes#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.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com>
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Co-authored-by: Sven Boemer <sbomer@gmail.com>
sbomer added a commit that referenced this pull request Apr 10, 2026
… item Update (#125630)
## Description
Follow-up to PR #124801 review feedback: the "intersection via
include/remove, then remove+re-include" pattern in
`_PrepareTrimConfiguration` was complex, mutated item ordering, and used
two throwaway item groups. Replace with a direct MSBuild `Update`.
### Change
**Before** — compute intersection manually to set metadata, then
remove+re-add items:
```xml
<__SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<__SingleWarnIntermediateAssembly Remove="@(IntermediateAssembly)" />
<_SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<_SingleWarnIntermediateAssembly Remove="@(__SingleWarnIntermediateAssembly)" />
<_SingleWarnIntermediateAssembly>
<TrimmerSingleWarn Condition="...">false</TrimmerSingleWarn>
</_SingleWarnIntermediateAssembly>
<ResolvedFileToPublish Remove="@(_SingleWarnIntermediateAssembly)" />
<ResolvedFileToPublish Include="@(_SingleWarnIntermediateAssembly)" />
```
**After** — update matching items directly, preserving order:
```xml
<ResolvedFileToPublish Update="@(IntermediateAssembly)">
<TrimmerSingleWarn Condition=" '%(ResolvedFileToPublish.TrimmerSingleWarn)' == '' ">false</TrimmerSingleWarn>
</ResolvedFileToPublish>
```
## Changes proposed in this pull request
- [`Microsoft.NET.ILLink.targets`] Replace 13-line intersection pattern
with a 3-line `Update` in `_PrepareTrimConfiguration`
- [`Microsoft.NET.ILLink.targets`] Qualify `%(TrimmerSingleWarn)` as
`%(ResolvedFileToPublish.TrimmerSingleWarn)` in the `Update` condition
to prevent MSB4096 (unqualified metadata batching over all
`ResolvedFileToPublish` items, including those without the metadata
defined)
## Additional context
<!-- START COPILOT CODING AGENT TIPS -->
---
🔒 GitHub Advanced Security automatically protects Copilot coding agent
pull requests. You can protect all pull requests by enabling Advanced
Security for your repositories. [Learn more about Advanced
Security.](https://gh.io/cca-advanced-security)
---------
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 Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-Tools-ILLink.NET linker development as well as trimming analyzerslinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

ComputeIlcCompileInputs should not need to run before PrepareForILLink

4 participants

@sbomer@MichalStrehovsky
, '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

Decouple ILC from ManagedAssemblyToLink - #124801

Merged
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order
Mar 16, 2026
Merged

Decouple ILC from ManagedAssemblyToLink#124801
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order

Conversation

CopilotAI commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

_ComputeManagedAssemblyForILLink in NativeAOT was indirectly dependent on ComputeIlcCompileInputs via @(ManagedBinary), which made PrepareForILLink ordering-sensitive and broke incremental ILLink behavior when ILLink is run before ILC input computation. This change removes that coupling so ILLink preparation no longer relies on ComputeIlcCompileInputs side effects.

  • What changed

    • Updated src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets in _ComputeManagedAssemblyForILLink.
    • Replaced @(ManagedBinary) with @(IntermediateAssembly) when reconstructing @(ManagedAssemblyToLink).
  • Why this matters

    • @(IntermediateAssembly) is available during PrepareForILLink, independent of ComputeIlcCompileInputs.
    • This preserves inclusion of the project assembly in @(ManagedAssemblyToLink) regardless of whether ComputeIlcCompileInputs runs before or after PrepareForILLink/ILLink.
  • Code change

    <ManagedAssemblyToLinkInclude="@(DefaultFrameworkAssemblies);@(_ManagedResolvedAssembliesToPublish);@(IntermediateAssembly)" />
Original prompt

This section details on the original issue you should resolve

<issue_title>ComputeIlcCompileInputs should not need to run before PrepareForILLink</issue_title>
<issue_description>## Description

The default IlcCompileDependsOn in Microsoft.NETCore.Native.targets orders ComputeIlcCompileInputs before PrepareForILLink:

Compile;ComputeIlcCompileInputs;SetupOSSpecificProps;PrepareForILLink

This couples ILC's input computation to ILLink's preparation phase. _ComputeManagedAssemblyForILLink (which runs AfterTargets="_ComputeManagedAssemblyToLink" during PrepareForILLink) consumes @(ManagedBinary), a side effect of ComputeIlcCompileInputs. This ordering works for the standard pipeline because it doesn't actually run ILLink (RunILLink=false), but it breaks consumers that need PrepareForILLink and ILLink to run beforeComputeIlcCompileInputs.

Why a consumer would need the opposite order

The standard NativeAOT pipeline sets RunILLink=false — ILLink never actually runs, and @(ManagedAssemblyToLink) is only used as metadata for ILC. A consumer that sets RunILLink=true to actually trim assemblies before ILC needs ILLink to complete first, so that ILC consumes the trimmed output. This requires PrepareForILLink and ILLink to precede ComputeIlcCompileInputs — the opposite of the default order. This is the case in .NET for Android's NativeAOT pipeline.

Impact

When PrepareForILLink runs before ComputeIlcCompileInputs, _ComputeManagedAssemblyForILLink builds its replacement @(ManagedAssemblyToLink) list before @(ManagedBinary) has been populated. The project assembly ends up missing from @(ManagedAssemblyToLink), which is used as Inputs by _RunILLink (in Microsoft.NET.ILLink.targets). This causes ILLink's incremental build check to miss changes to the project assembly, so ILLink skips on rebuild even though the assembly changed.

First builds still succeed because PrepareForILLink independently adds @(IntermediateAssembly) as a TrimmerRootAssembly, so ILLink loads and processes it regardless. Only incremental builds are affected.

Reproduction

Create a dotnet new console project with the following csproj (note: explicit SDK imports are needed because Microsoft.NETCore.Native.targets unconditionally sets RunILLink=false, so it must be overridden after the SDK targets load):

<Project>
<ImportProject="Sdk.props"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<PublishAot>true</PublishAot>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>
<ImportProject="Sdk.targets"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<RunILLink>true</RunILLink>
<IlcCompileDependsOn>
Compile;
SetupOSSpecificProps;
PrepareForILLink;
ILLink;
ComputeIlcCompileInputs
</IlcCompileDependsOn>
</PropertyGroup>
</Project>

Run dotnet publish, then change "Hello, World!" to "Hello, Changed!" in Program.cs and publish again. On the second publish, ILLink is skipped (no "Optimizing assemblies for size" message) and the trimmed assembly in obj/.../linked/ReproApp.dll still contains Hello, World!.

Suggestion

Decouple ComputeIlcCompileInputs from PrepareForILLink so neither depends on having run before the other. Ideally _ComputeManagedAssemblyForILLink should not rely on state produced by ComputeIlcCompileInputs, and ComputeIlcCompileInputs should be free to run after ILLink without breaking the ILLink preparation phase.

Workaround

.NET for Android can work around this by injecting @(IntermediateAssembly) into @(ManagedAssemblyToLink) after _ComputeManagedAssemblyForILLink replaces it:

<TargetName="_AndroidFixManagedAssemblyToLink"AfterTargets="_ComputeManagedAssemblyForILLink">
<ItemGroup>
<ManagedAssemblyToLinkInclude="@(IntermediateAssembly)" />
</ItemGroup>
</Target>
```</issue_description>
<agent_instructions>Decouple these targets. The ILC inputs should not be resolved from ManagedAssemblyToLink.</agent_instructions>
## Comments on the Issue (you are @copilot in this section)
<comments>
</comments>

💡 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 in the docs.

@github-actionsgithub-actionsBot added the area-Tools-ILLink .NET linker development as well as trimming analyzers label Feb 24, 2026
…puts
Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
CopilotAI changed the title [WIP] Fix ComputeIlcCompileInputs execution order before PrepareForILLinkDecouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsFeb 24, 2026
CopilotAI requested a review from sbomerFebruary 24, 2026 17:25
…lyToLink
Factor _PrepareTrimConfiguration out of PrepareForILLink so shared trim
configuration operates on ResolvedFileToPublish and flows to both ILLink
and ILC without duplication.
For NativeAOT, _ComputeAssembliesToCompileToNative replaces CoreCLR
runtime pack files with DefaultFrameworkAssemblies (tagged
PostprocessAssembly=true) before _PrepareTrimConfiguration runs.
_ComputeIlcCompileInputs then derives IlcReference from
ResolvedFileToPublish via PostprocessAssembly metadata, ensuring ILC
sees ILLink-relocated paths when both run.
ComputeLinkedFilesToPublish hooks AfterTargets=ILLink (instead of
ComputeResolvedFilesToPublishList) for correct ordering when both
ILLink and ILC run. RunILLink is made conditional so projects can
opt in.
Remove ManagedAssemblies output from the C# task (replaced by
ResolvedFileToPublish filtering). Add RuntimePackFilesToSkipPublish
output for early removal of CoreCLR runtime pack files. Fix OOB
assembly handling so overrides stay in ResolvedFileToPublish.
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Feb 25, 2026
@sbomersbomer changed the title Decouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsDecouple ILC from ManagedAssemblyToLinkFeb 25, 2026
Skip _ComputeIlcCompileInputs for framework library builds
(BuildingFrameworkLibrary=true) since they use BuildOneFrameworkLibrary
for input computation, not the publish-pipeline targets.
When NativeCompilationDuringPublish is false (e.g. Apple non-library-mode
builds), Publish.targets is not imported so _ComputeIlcCompileInputs does
not exist to populate IlcReference from ResolvedFileToPublish. Fall back
to DefaultFrameworkAssemblies directly in ComputeIlcCompileInputs so ILC
can find System.Private.CoreLib and other framework references.
@sbomer

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-nativeaot-outerloop

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

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

Decouples NativeAOT’s ILLink preparation from ComputeIlcCompileInputs side effects by moving shared trim configuration into a dedicated target and rebuilding ILC/ILLink inputs from publish items rather than @(ManagedBinary).

Changes:

  • Introduced _PrepareTrimConfiguration in ILLink targets and made _ComputeManagedAssemblyToLink depend on it.
  • Updated NativeAOT publish pipeline to compute ILC inputs from ResolvedFileToPublish (PostprocessAssembly=true) and adjusted ordering around ILLink.
  • Simplified/changed ComputeManagedAssembliesToCompileToNative outputs to focus on runtime-pack files to remove and satellite assemblies.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targetsAdds _PrepareTrimConfiguration and moves shared trim defaults/metadata out of PrepareForILLink.
src/coreclr/tools/aot/ILCompiler.Build.Tasks/ComputeManagedAssembliesToCompileToNative.csAlters MSBuild task outputs and logic to support the updated publish/trim flow.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targetsRemoves NativeAOT’s PrepareForILLink dependency and shifts ILC trim metadata consumption.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targetsReorders publish hooks around ILLink and selects ILC managed inputs via PostprocessAssembly.

…esToPublishList
The AfterTargets="ILLink" hook is unnecessary because
ComputeLinkedFilesToPublish's own DependsOnTargets chain
(via LinkNative -> IlcCompile) transitively pulls in the
correct prerequisites regardless of the AfterTargets anchor.
Keeping ComputeResolvedFilesToPublishList avoids an artificial
coupling to ILLink and removes the need for the ILLink insertion
in the test infrastructure's LinkNativeIfBuildAndRun target.
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets
#	src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets
The _PrepareTrimConfiguration target was passing the full path of
IntermediateAssembly to TrimmerRootAssembly, but ILLink expects
assembly names (without path). This caused IL1032 errors across
all CI platforms: 'Root assembly with name ...ilc.dll could not be found.'
Restores the %(Filename) transform that was on main but got lost
when moving this line from PrepareForILLink to _PrepareTrimConfiguration.
@sbomer

Copy link
Copy Markdown
Member

/ba-g "deadletter"

@sbomer
sbomer merged commit 15da421 into mainMar 16, 2026
119 of 128 checks passed
@sbomer
sbomer deleted the copilot/fix-ilc-compile-order branch March 16, 2026 18:12
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
_ComputeAssembliesToCompileToNative now populates
@(_IlcManagedInputAssemblies) and ComputeLinkedFilesToPublish
removes them from @(ResolvedFileToPublish).
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in @(ResolvedFileToPublish) for the outer build's _ResolveAssemblies
target. Clear @(_IlcManagedInputAssemblies) in our
_AndroidComputeIlcCompileInputs target so the runtime's
ComputeLinkedFilesToPublish doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Changes: dotnet/dotnet@5ff448a...803eb28
- **Dependency Updates**:
- From [11.0.0-preview.3.26165.107 to 11.0.0-preview.3.26168.106][1]
- Microsoft.NET.Workload.Mono.ToolChain.Current.Manifest-11.0.100-preview.3
- Microsoft.NET.ILLink
- Microsoft.NETCore.App.Ref
- From [11.0.0-beta.26165.107 to 11.0.0-beta.26168.106][1]
- Microsoft.DotNet.Build.Tasks.Feed
- From [0.11.5-preview.26165.107 to 0.11.5-preview.26168.106][1]
- Microsoft.DotNet.Cecil
- From [11.0.100-preview.3.26165.107 to 11.0.100-preview.3.26168.106][1]
- Microsoft.NET.Sdk
- Microsoft.NET.Workload.Emscripten.Current.Manifest-11.0.100-preview.3
- Microsoft.TemplateEngine.Authoring.Tasks
[1]: dotnet/dotnet@5ff448a...803eb28
## Other changes ##
[xabt] Prevent `ComputeLinkedFilesToPublish` from stripping assemblies
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
`_ComputeAssembliesToCompileToNative` now populates
`@(_IlcManagedInputAssemblies)` and `ComputeLinkedFilesToPublish`
removes them from `@(ResolvedFileToPublish)`.
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in `@(ResolvedFileToPublish)` for the outer build's `_ResolveAssemblies`
target. Clear `@(_IlcManagedInputAssemblies)` in our
`_AndroidComputeIlcCompileInputs` target so the runtime's
`ComputeLinkedFilesToPublish` doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
sbomer added a commit that referenced this pull request Mar 24, 2026
…g them (#124192)
## 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:
```xml
<ItemGroup>
<ResolvedFileToPublish Remove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublish Remove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublish Remove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublish Include="$(NativeBinary)">
<RelativePath>$(NativeBinaryPrefix)$(TargetName)$(NativeBinaryExt)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
```
This follows the same pattern as removing managed assemblies and debug
symbols, which are also embedded or replaced in the native binary.
## Customer Impact
- **Affected customers:** NativeAOT users with localized resources
- **Regression:** No
- **Source incompatibility:** No
- **Breaking change:** No (removes extraneous files from publish output)
## Testing
Testing will be added in the SDK repo per review feedback. The fix can
be validated by publishing a NativeAOT app with satellite assemblies and
verifying that:
- Localized resources are accessible at runtime (embedded correctly)
- No satellite assembly subdirectories exist in publish output
## Risk
Minimal. One-line change following established pattern. Satellite
assemblies remain embedded and functional; only removes redundant disk
copies.
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
----
*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](https://github.com/user-attachments/files/25188858/AotLocalization.zip)
[Binlog](https://github.com/user-attachments/files/25188847/AotLocalizationBinlog.zip)
### 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)
<comments>
<comment_new><author>@baronfel</author><body>
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)](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L3-L11),
`_ComputeIlcCompileInputs` gathers satellite assemblies from two
sources:
```xml
<IlcSatelliteAssembly Include="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssembly Include="@(IntermediateSatelliteAssembliesWithTargetPath)" />
```
- `_SatelliteAssembliesToPublish` = satellite assemblies from
package/project references (extracted from
`_ResolvedCopyLocalPublishAssets` by
[`ComputeManagedAssembliesToCompileToNative`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L73-L84))
- `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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets#L236),
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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L20-L34)
runs `BeforeTargets="ComputeResolvedFilesToPublishList"` and modifies
the publish list:
```xml
<_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)](https://github.com/dotnet/sdk/blob/main/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L545-L549),
`ComputeResolvedFilesToPublishList` unconditionally includes:
```xml
<!-- Copy satellite assemblies. -->
<ResolvedFileToPublish Include="@(IntermediateSatelliteAssembliesWithTargetPath)">
<RelativePath>%(IntermediateSatelliteAss...
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes#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.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com>
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Co-authored-by: Sven Boemer <sbomer@gmail.com>
sbomer added a commit that referenced this pull request Apr 10, 2026
… item Update (#125630)
## Description
Follow-up to PR #124801 review feedback: the "intersection via
include/remove, then remove+re-include" pattern in
`_PrepareTrimConfiguration` was complex, mutated item ordering, and used
two throwaway item groups. Replace with a direct MSBuild `Update`.
### Change
**Before** — compute intersection manually to set metadata, then
remove+re-add items:
```xml
<__SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<__SingleWarnIntermediateAssembly Remove="@(IntermediateAssembly)" />
<_SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<_SingleWarnIntermediateAssembly Remove="@(__SingleWarnIntermediateAssembly)" />
<_SingleWarnIntermediateAssembly>
<TrimmerSingleWarn Condition="...">false</TrimmerSingleWarn>
</_SingleWarnIntermediateAssembly>
<ResolvedFileToPublish Remove="@(_SingleWarnIntermediateAssembly)" />
<ResolvedFileToPublish Include="@(_SingleWarnIntermediateAssembly)" />
```
**After** — update matching items directly, preserving order:
```xml
<ResolvedFileToPublish Update="@(IntermediateAssembly)">
<TrimmerSingleWarn Condition=" '%(ResolvedFileToPublish.TrimmerSingleWarn)' == '' ">false</TrimmerSingleWarn>
</ResolvedFileToPublish>
```
## Changes proposed in this pull request
- [`Microsoft.NET.ILLink.targets`] Replace 13-line intersection pattern
with a 3-line `Update` in `_PrepareTrimConfiguration`
- [`Microsoft.NET.ILLink.targets`] Qualify `%(TrimmerSingleWarn)` as
`%(ResolvedFileToPublish.TrimmerSingleWarn)` in the `Update` condition
to prevent MSB4096 (unqualified metadata batching over all
`ResolvedFileToPublish` items, including those without the metadata
defined)
## Additional context
<!-- START COPILOT CODING AGENT TIPS -->
---
🔒 GitHub Advanced Security automatically protects Copilot coding agent
pull requests. You can protect all pull requests by enabling Advanced
Security for your repositories. [Learn more about Advanced
Security.](https://gh.io/cca-advanced-security)
---------
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 Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-Tools-ILLink.NET linker development as well as trimming analyzerslinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

ComputeIlcCompileInputs should not need to run before PrepareForILLink

4 participants

@sbomer@MichalStrehovsky
, '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

Decouple ILC from ManagedAssemblyToLink - #124801

Merged
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order
Mar 16, 2026
Merged

Decouple ILC from ManagedAssemblyToLink#124801
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order

Conversation

CopilotAI commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

_ComputeManagedAssemblyForILLink in NativeAOT was indirectly dependent on ComputeIlcCompileInputs via @(ManagedBinary), which made PrepareForILLink ordering-sensitive and broke incremental ILLink behavior when ILLink is run before ILC input computation. This change removes that coupling so ILLink preparation no longer relies on ComputeIlcCompileInputs side effects.

  • What changed

    • Updated src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets in _ComputeManagedAssemblyForILLink.
    • Replaced @(ManagedBinary) with @(IntermediateAssembly) when reconstructing @(ManagedAssemblyToLink).
  • Why this matters

    • @(IntermediateAssembly) is available during PrepareForILLink, independent of ComputeIlcCompileInputs.
    • This preserves inclusion of the project assembly in @(ManagedAssemblyToLink) regardless of whether ComputeIlcCompileInputs runs before or after PrepareForILLink/ILLink.
  • Code change

    <ManagedAssemblyToLinkInclude="@(DefaultFrameworkAssemblies);@(_ManagedResolvedAssembliesToPublish);@(IntermediateAssembly)" />
Original prompt

This section details on the original issue you should resolve

<issue_title>ComputeIlcCompileInputs should not need to run before PrepareForILLink</issue_title>
<issue_description>## Description

The default IlcCompileDependsOn in Microsoft.NETCore.Native.targets orders ComputeIlcCompileInputs before PrepareForILLink:

Compile;ComputeIlcCompileInputs;SetupOSSpecificProps;PrepareForILLink

This couples ILC's input computation to ILLink's preparation phase. _ComputeManagedAssemblyForILLink (which runs AfterTargets="_ComputeManagedAssemblyToLink" during PrepareForILLink) consumes @(ManagedBinary), a side effect of ComputeIlcCompileInputs. This ordering works for the standard pipeline because it doesn't actually run ILLink (RunILLink=false), but it breaks consumers that need PrepareForILLink and ILLink to run beforeComputeIlcCompileInputs.

Why a consumer would need the opposite order

The standard NativeAOT pipeline sets RunILLink=false — ILLink never actually runs, and @(ManagedAssemblyToLink) is only used as metadata for ILC. A consumer that sets RunILLink=true to actually trim assemblies before ILC needs ILLink to complete first, so that ILC consumes the trimmed output. This requires PrepareForILLink and ILLink to precede ComputeIlcCompileInputs — the opposite of the default order. This is the case in .NET for Android's NativeAOT pipeline.

Impact

When PrepareForILLink runs before ComputeIlcCompileInputs, _ComputeManagedAssemblyForILLink builds its replacement @(ManagedAssemblyToLink) list before @(ManagedBinary) has been populated. The project assembly ends up missing from @(ManagedAssemblyToLink), which is used as Inputs by _RunILLink (in Microsoft.NET.ILLink.targets). This causes ILLink's incremental build check to miss changes to the project assembly, so ILLink skips on rebuild even though the assembly changed.

First builds still succeed because PrepareForILLink independently adds @(IntermediateAssembly) as a TrimmerRootAssembly, so ILLink loads and processes it regardless. Only incremental builds are affected.

Reproduction

Create a dotnet new console project with the following csproj (note: explicit SDK imports are needed because Microsoft.NETCore.Native.targets unconditionally sets RunILLink=false, so it must be overridden after the SDK targets load):

<Project>
<ImportProject="Sdk.props"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<PublishAot>true</PublishAot>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>
<ImportProject="Sdk.targets"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<RunILLink>true</RunILLink>
<IlcCompileDependsOn>
Compile;
SetupOSSpecificProps;
PrepareForILLink;
ILLink;
ComputeIlcCompileInputs
</IlcCompileDependsOn>
</PropertyGroup>
</Project>

Run dotnet publish, then change "Hello, World!" to "Hello, Changed!" in Program.cs and publish again. On the second publish, ILLink is skipped (no "Optimizing assemblies for size" message) and the trimmed assembly in obj/.../linked/ReproApp.dll still contains Hello, World!.

Suggestion

Decouple ComputeIlcCompileInputs from PrepareForILLink so neither depends on having run before the other. Ideally _ComputeManagedAssemblyForILLink should not rely on state produced by ComputeIlcCompileInputs, and ComputeIlcCompileInputs should be free to run after ILLink without breaking the ILLink preparation phase.

Workaround

.NET for Android can work around this by injecting @(IntermediateAssembly) into @(ManagedAssemblyToLink) after _ComputeManagedAssemblyForILLink replaces it:

<TargetName="_AndroidFixManagedAssemblyToLink"AfterTargets="_ComputeManagedAssemblyForILLink">
<ItemGroup>
<ManagedAssemblyToLinkInclude="@(IntermediateAssembly)" />
</ItemGroup>
</Target>
```</issue_description>
<agent_instructions>Decouple these targets. The ILC inputs should not be resolved from ManagedAssemblyToLink.</agent_instructions>
## Comments on the Issue (you are @copilot in this section)
<comments>
</comments>

💡 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 in the docs.

@github-actionsgithub-actionsBot added the area-Tools-ILLink .NET linker development as well as trimming analyzers label Feb 24, 2026
…puts
Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
CopilotAI changed the title [WIP] Fix ComputeIlcCompileInputs execution order before PrepareForILLinkDecouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsFeb 24, 2026
CopilotAI requested a review from sbomerFebruary 24, 2026 17:25
…lyToLink
Factor _PrepareTrimConfiguration out of PrepareForILLink so shared trim
configuration operates on ResolvedFileToPublish and flows to both ILLink
and ILC without duplication.
For NativeAOT, _ComputeAssembliesToCompileToNative replaces CoreCLR
runtime pack files with DefaultFrameworkAssemblies (tagged
PostprocessAssembly=true) before _PrepareTrimConfiguration runs.
_ComputeIlcCompileInputs then derives IlcReference from
ResolvedFileToPublish via PostprocessAssembly metadata, ensuring ILC
sees ILLink-relocated paths when both run.
ComputeLinkedFilesToPublish hooks AfterTargets=ILLink (instead of
ComputeResolvedFilesToPublishList) for correct ordering when both
ILLink and ILC run. RunILLink is made conditional so projects can
opt in.
Remove ManagedAssemblies output from the C# task (replaced by
ResolvedFileToPublish filtering). Add RuntimePackFilesToSkipPublish
output for early removal of CoreCLR runtime pack files. Fix OOB
assembly handling so overrides stay in ResolvedFileToPublish.
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Feb 25, 2026
@sbomersbomer changed the title Decouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsDecouple ILC from ManagedAssemblyToLinkFeb 25, 2026
Skip _ComputeIlcCompileInputs for framework library builds
(BuildingFrameworkLibrary=true) since they use BuildOneFrameworkLibrary
for input computation, not the publish-pipeline targets.
When NativeCompilationDuringPublish is false (e.g. Apple non-library-mode
builds), Publish.targets is not imported so _ComputeIlcCompileInputs does
not exist to populate IlcReference from ResolvedFileToPublish. Fall back
to DefaultFrameworkAssemblies directly in ComputeIlcCompileInputs so ILC
can find System.Private.CoreLib and other framework references.
@sbomer

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-nativeaot-outerloop

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

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

Decouples NativeAOT’s ILLink preparation from ComputeIlcCompileInputs side effects by moving shared trim configuration into a dedicated target and rebuilding ILC/ILLink inputs from publish items rather than @(ManagedBinary).

Changes:

  • Introduced _PrepareTrimConfiguration in ILLink targets and made _ComputeManagedAssemblyToLink depend on it.
  • Updated NativeAOT publish pipeline to compute ILC inputs from ResolvedFileToPublish (PostprocessAssembly=true) and adjusted ordering around ILLink.
  • Simplified/changed ComputeManagedAssembliesToCompileToNative outputs to focus on runtime-pack files to remove and satellite assemblies.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targetsAdds _PrepareTrimConfiguration and moves shared trim defaults/metadata out of PrepareForILLink.
src/coreclr/tools/aot/ILCompiler.Build.Tasks/ComputeManagedAssembliesToCompileToNative.csAlters MSBuild task outputs and logic to support the updated publish/trim flow.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targetsRemoves NativeAOT’s PrepareForILLink dependency and shifts ILC trim metadata consumption.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targetsReorders publish hooks around ILLink and selects ILC managed inputs via PostprocessAssembly.

…esToPublishList
The AfterTargets="ILLink" hook is unnecessary because
ComputeLinkedFilesToPublish's own DependsOnTargets chain
(via LinkNative -> IlcCompile) transitively pulls in the
correct prerequisites regardless of the AfterTargets anchor.
Keeping ComputeResolvedFilesToPublishList avoids an artificial
coupling to ILLink and removes the need for the ILLink insertion
in the test infrastructure's LinkNativeIfBuildAndRun target.
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets
#	src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets
The _PrepareTrimConfiguration target was passing the full path of
IntermediateAssembly to TrimmerRootAssembly, but ILLink expects
assembly names (without path). This caused IL1032 errors across
all CI platforms: 'Root assembly with name ...ilc.dll could not be found.'
Restores the %(Filename) transform that was on main but got lost
when moving this line from PrepareForILLink to _PrepareTrimConfiguration.
@sbomer

Copy link
Copy Markdown
Member

/ba-g "deadletter"

@sbomer
sbomer merged commit 15da421 into mainMar 16, 2026
119 of 128 checks passed
@sbomer
sbomer deleted the copilot/fix-ilc-compile-order branch March 16, 2026 18:12
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
_ComputeAssembliesToCompileToNative now populates
@(_IlcManagedInputAssemblies) and ComputeLinkedFilesToPublish
removes them from @(ResolvedFileToPublish).
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in @(ResolvedFileToPublish) for the outer build's _ResolveAssemblies
target. Clear @(_IlcManagedInputAssemblies) in our
_AndroidComputeIlcCompileInputs target so the runtime's
ComputeLinkedFilesToPublish doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Changes: dotnet/dotnet@5ff448a...803eb28
- **Dependency Updates**:
- From [11.0.0-preview.3.26165.107 to 11.0.0-preview.3.26168.106][1]
- Microsoft.NET.Workload.Mono.ToolChain.Current.Manifest-11.0.100-preview.3
- Microsoft.NET.ILLink
- Microsoft.NETCore.App.Ref
- From [11.0.0-beta.26165.107 to 11.0.0-beta.26168.106][1]
- Microsoft.DotNet.Build.Tasks.Feed
- From [0.11.5-preview.26165.107 to 0.11.5-preview.26168.106][1]
- Microsoft.DotNet.Cecil
- From [11.0.100-preview.3.26165.107 to 11.0.100-preview.3.26168.106][1]
- Microsoft.NET.Sdk
- Microsoft.NET.Workload.Emscripten.Current.Manifest-11.0.100-preview.3
- Microsoft.TemplateEngine.Authoring.Tasks
[1]: dotnet/dotnet@5ff448a...803eb28
## Other changes ##
[xabt] Prevent `ComputeLinkedFilesToPublish` from stripping assemblies
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
`_ComputeAssembliesToCompileToNative` now populates
`@(_IlcManagedInputAssemblies)` and `ComputeLinkedFilesToPublish`
removes them from `@(ResolvedFileToPublish)`.
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in `@(ResolvedFileToPublish)` for the outer build's `_ResolveAssemblies`
target. Clear `@(_IlcManagedInputAssemblies)` in our
`_AndroidComputeIlcCompileInputs` target so the runtime's
`ComputeLinkedFilesToPublish` doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
sbomer added a commit that referenced this pull request Mar 24, 2026
…g them (#124192)
## 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:
```xml
<ItemGroup>
<ResolvedFileToPublish Remove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublish Remove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublish Remove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublish Include="$(NativeBinary)">
<RelativePath>$(NativeBinaryPrefix)$(TargetName)$(NativeBinaryExt)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
```
This follows the same pattern as removing managed assemblies and debug
symbols, which are also embedded or replaced in the native binary.
## Customer Impact
- **Affected customers:** NativeAOT users with localized resources
- **Regression:** No
- **Source incompatibility:** No
- **Breaking change:** No (removes extraneous files from publish output)
## Testing
Testing will be added in the SDK repo per review feedback. The fix can
be validated by publishing a NativeAOT app with satellite assemblies and
verifying that:
- Localized resources are accessible at runtime (embedded correctly)
- No satellite assembly subdirectories exist in publish output
## Risk
Minimal. One-line change following established pattern. Satellite
assemblies remain embedded and functional; only removes redundant disk
copies.
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
----
*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](https://github.com/user-attachments/files/25188858/AotLocalization.zip)
[Binlog](https://github.com/user-attachments/files/25188847/AotLocalizationBinlog.zip)
### 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)
<comments>
<comment_new><author>@baronfel</author><body>
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)](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L3-L11),
`_ComputeIlcCompileInputs` gathers satellite assemblies from two
sources:
```xml
<IlcSatelliteAssembly Include="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssembly Include="@(IntermediateSatelliteAssembliesWithTargetPath)" />
```
- `_SatelliteAssembliesToPublish` = satellite assemblies from
package/project references (extracted from
`_ResolvedCopyLocalPublishAssets` by
[`ComputeManagedAssembliesToCompileToNative`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L73-L84))
- `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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets#L236),
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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L20-L34)
runs `BeforeTargets="ComputeResolvedFilesToPublishList"` and modifies
the publish list:
```xml
<_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)](https://github.com/dotnet/sdk/blob/main/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L545-L549),
`ComputeResolvedFilesToPublishList` unconditionally includes:
```xml
<!-- Copy satellite assemblies. -->
<ResolvedFileToPublish Include="@(IntermediateSatelliteAssembliesWithTargetPath)">
<RelativePath>%(IntermediateSatelliteAss...
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes#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.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com>
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Co-authored-by: Sven Boemer <sbomer@gmail.com>
sbomer added a commit that referenced this pull request Apr 10, 2026
… item Update (#125630)
## Description
Follow-up to PR #124801 review feedback: the "intersection via
include/remove, then remove+re-include" pattern in
`_PrepareTrimConfiguration` was complex, mutated item ordering, and used
two throwaway item groups. Replace with a direct MSBuild `Update`.
### Change
**Before** — compute intersection manually to set metadata, then
remove+re-add items:
```xml
<__SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<__SingleWarnIntermediateAssembly Remove="@(IntermediateAssembly)" />
<_SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<_SingleWarnIntermediateAssembly Remove="@(__SingleWarnIntermediateAssembly)" />
<_SingleWarnIntermediateAssembly>
<TrimmerSingleWarn Condition="...">false</TrimmerSingleWarn>
</_SingleWarnIntermediateAssembly>
<ResolvedFileToPublish Remove="@(_SingleWarnIntermediateAssembly)" />
<ResolvedFileToPublish Include="@(_SingleWarnIntermediateAssembly)" />
```
**After** — update matching items directly, preserving order:
```xml
<ResolvedFileToPublish Update="@(IntermediateAssembly)">
<TrimmerSingleWarn Condition=" '%(ResolvedFileToPublish.TrimmerSingleWarn)' == '' ">false</TrimmerSingleWarn>
</ResolvedFileToPublish>
```
## Changes proposed in this pull request
- [`Microsoft.NET.ILLink.targets`] Replace 13-line intersection pattern
with a 3-line `Update` in `_PrepareTrimConfiguration`
- [`Microsoft.NET.ILLink.targets`] Qualify `%(TrimmerSingleWarn)` as
`%(ResolvedFileToPublish.TrimmerSingleWarn)` in the `Update` condition
to prevent MSB4096 (unqualified metadata batching over all
`ResolvedFileToPublish` items, including those without the metadata
defined)
## Additional context
<!-- START COPILOT CODING AGENT TIPS -->
---
🔒 GitHub Advanced Security automatically protects Copilot coding agent
pull requests. You can protect all pull requests by enabling Advanced
Security for your repositories. [Learn more about Advanced
Security.](https://gh.io/cca-advanced-security)
---------
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 Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-Tools-ILLink.NET linker development as well as trimming analyzerslinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

ComputeIlcCompileInputs should not need to run before PrepareForILLink

4 participants

@sbomer@MichalStrehovsky
, '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

Decouple ILC from ManagedAssemblyToLink - #124801

Merged
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order
Mar 16, 2026
Merged

Decouple ILC from ManagedAssemblyToLink#124801
sbomer merged 12 commits into
mainfrom
copilot/fix-ilc-compile-order

Conversation

CopilotAI commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

_ComputeManagedAssemblyForILLink in NativeAOT was indirectly dependent on ComputeIlcCompileInputs via @(ManagedBinary), which made PrepareForILLink ordering-sensitive and broke incremental ILLink behavior when ILLink is run before ILC input computation. This change removes that coupling so ILLink preparation no longer relies on ComputeIlcCompileInputs side effects.

  • What changed

    • Updated src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets in _ComputeManagedAssemblyForILLink.
    • Replaced @(ManagedBinary) with @(IntermediateAssembly) when reconstructing @(ManagedAssemblyToLink).
  • Why this matters

    • @(IntermediateAssembly) is available during PrepareForILLink, independent of ComputeIlcCompileInputs.
    • This preserves inclusion of the project assembly in @(ManagedAssemblyToLink) regardless of whether ComputeIlcCompileInputs runs before or after PrepareForILLink/ILLink.
  • Code change

    <ManagedAssemblyToLinkInclude="@(DefaultFrameworkAssemblies);@(_ManagedResolvedAssembliesToPublish);@(IntermediateAssembly)" />
Original prompt

This section details on the original issue you should resolve

<issue_title>ComputeIlcCompileInputs should not need to run before PrepareForILLink</issue_title>
<issue_description>## Description

The default IlcCompileDependsOn in Microsoft.NETCore.Native.targets orders ComputeIlcCompileInputs before PrepareForILLink:

Compile;ComputeIlcCompileInputs;SetupOSSpecificProps;PrepareForILLink

This couples ILC's input computation to ILLink's preparation phase. _ComputeManagedAssemblyForILLink (which runs AfterTargets="_ComputeManagedAssemblyToLink" during PrepareForILLink) consumes @(ManagedBinary), a side effect of ComputeIlcCompileInputs. This ordering works for the standard pipeline because it doesn't actually run ILLink (RunILLink=false), but it breaks consumers that need PrepareForILLink and ILLink to run beforeComputeIlcCompileInputs.

Why a consumer would need the opposite order

The standard NativeAOT pipeline sets RunILLink=false — ILLink never actually runs, and @(ManagedAssemblyToLink) is only used as metadata for ILC. A consumer that sets RunILLink=true to actually trim assemblies before ILC needs ILLink to complete first, so that ILC consumes the trimmed output. This requires PrepareForILLink and ILLink to precede ComputeIlcCompileInputs — the opposite of the default order. This is the case in .NET for Android's NativeAOT pipeline.

Impact

When PrepareForILLink runs before ComputeIlcCompileInputs, _ComputeManagedAssemblyForILLink builds its replacement @(ManagedAssemblyToLink) list before @(ManagedBinary) has been populated. The project assembly ends up missing from @(ManagedAssemblyToLink), which is used as Inputs by _RunILLink (in Microsoft.NET.ILLink.targets). This causes ILLink's incremental build check to miss changes to the project assembly, so ILLink skips on rebuild even though the assembly changed.

First builds still succeed because PrepareForILLink independently adds @(IntermediateAssembly) as a TrimmerRootAssembly, so ILLink loads and processes it regardless. Only incremental builds are affected.

Reproduction

Create a dotnet new console project with the following csproj (note: explicit SDK imports are needed because Microsoft.NETCore.Native.targets unconditionally sets RunILLink=false, so it must be overridden after the SDK targets load):

<Project>
<ImportProject="Sdk.props"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<PublishAot>true</PublishAot>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>
<ImportProject="Sdk.targets"Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<RunILLink>true</RunILLink>
<IlcCompileDependsOn>
Compile;
SetupOSSpecificProps;
PrepareForILLink;
ILLink;
ComputeIlcCompileInputs
</IlcCompileDependsOn>
</PropertyGroup>
</Project>

Run dotnet publish, then change "Hello, World!" to "Hello, Changed!" in Program.cs and publish again. On the second publish, ILLink is skipped (no "Optimizing assemblies for size" message) and the trimmed assembly in obj/.../linked/ReproApp.dll still contains Hello, World!.

Suggestion

Decouple ComputeIlcCompileInputs from PrepareForILLink so neither depends on having run before the other. Ideally _ComputeManagedAssemblyForILLink should not rely on state produced by ComputeIlcCompileInputs, and ComputeIlcCompileInputs should be free to run after ILLink without breaking the ILLink preparation phase.

Workaround

.NET for Android can work around this by injecting @(IntermediateAssembly) into @(ManagedAssemblyToLink) after _ComputeManagedAssemblyForILLink replaces it:

<TargetName="_AndroidFixManagedAssemblyToLink"AfterTargets="_ComputeManagedAssemblyForILLink">
<ItemGroup>
<ManagedAssemblyToLinkInclude="@(IntermediateAssembly)" />
</ItemGroup>
</Target>
```</issue_description>
<agent_instructions>Decouple these targets. The ILC inputs should not be resolved from ManagedAssemblyToLink.</agent_instructions>
## Comments on the Issue (you are @copilot in this section)
<comments>
</comments>

💡 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 in the docs.

@github-actionsgithub-actionsBot added the area-Tools-ILLink .NET linker development as well as trimming analyzers label Feb 24, 2026
…puts
Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
CopilotAI changed the title [WIP] Fix ComputeIlcCompileInputs execution order before PrepareForILLinkDecouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsFeb 24, 2026
CopilotAI requested a review from sbomerFebruary 24, 2026 17:25
…lyToLink
Factor _PrepareTrimConfiguration out of PrepareForILLink so shared trim
configuration operates on ResolvedFileToPublish and flows to both ILLink
and ILC without duplication.
For NativeAOT, _ComputeAssembliesToCompileToNative replaces CoreCLR
runtime pack files with DefaultFrameworkAssemblies (tagged
PostprocessAssembly=true) before _PrepareTrimConfiguration runs.
_ComputeIlcCompileInputs then derives IlcReference from
ResolvedFileToPublish via PostprocessAssembly metadata, ensuring ILC
sees ILLink-relocated paths when both run.
ComputeLinkedFilesToPublish hooks AfterTargets=ILLink (instead of
ComputeResolvedFilesToPublishList) for correct ordering when both
ILLink and ILC run. RunILLink is made conditional so projects can
opt in.
Remove ManagedAssemblies output from the C# task (replaced by
ResolvedFileToPublish filtering). Add RuntimePackFilesToSkipPublish
output for early removal of CoreCLR runtime pack files. Fix OOB
assembly handling so overrides stay in ResolvedFileToPublish.
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Feb 25, 2026
@sbomersbomer changed the title Decouple NativeAOT ILLink assembly input computation from ComputeIlcCompileInputsDecouple ILC from ManagedAssemblyToLinkFeb 25, 2026
Skip _ComputeIlcCompileInputs for framework library builds
(BuildingFrameworkLibrary=true) since they use BuildOneFrameworkLibrary
for input computation, not the publish-pipeline targets.
When NativeCompilationDuringPublish is false (e.g. Apple non-library-mode
builds), Publish.targets is not imported so _ComputeIlcCompileInputs does
not exist to populate IlcReference from ResolvedFileToPublish. Fall back
to DefaultFrameworkAssemblies directly in ComputeIlcCompileInputs so ILC
can find System.Private.CoreLib and other framework references.
@sbomer

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-nativeaot-outerloop

@sbomer

Copy link
Copy Markdown
Member

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

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

Decouples NativeAOT’s ILLink preparation from ComputeIlcCompileInputs side effects by moving shared trim configuration into a dedicated target and rebuilding ILC/ILLink inputs from publish items rather than @(ManagedBinary).

Changes:

  • Introduced _PrepareTrimConfiguration in ILLink targets and made _ComputeManagedAssemblyToLink depend on it.
  • Updated NativeAOT publish pipeline to compute ILC inputs from ResolvedFileToPublish (PostprocessAssembly=true) and adjusted ordering around ILLink.
  • Simplified/changed ComputeManagedAssembliesToCompileToNative outputs to focus on runtime-pack files to remove and satellite assemblies.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

FileDescription
src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targetsAdds _PrepareTrimConfiguration and moves shared trim defaults/metadata out of PrepareForILLink.
src/coreclr/tools/aot/ILCompiler.Build.Tasks/ComputeManagedAssembliesToCompileToNative.csAlters MSBuild task outputs and logic to support the updated publish/trim flow.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targetsRemoves NativeAOT’s PrepareForILLink dependency and shifts ILC trim metadata consumption.
src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targetsReorders publish hooks around ILLink and selects ILC managed inputs via PostprocessAssembly.

…esToPublishList
The AfterTargets="ILLink" hook is unnecessary because
ComputeLinkedFilesToPublish's own DependsOnTargets chain
(via LinkNative -> IlcCompile) transitively pulls in the
correct prerequisites regardless of the AfterTargets anchor.
Keeping ComputeResolvedFilesToPublishList avoids an artificial
coupling to ILLink and removes the need for the ILLink insertion
in the test infrastructure's LinkNativeIfBuildAndRun target.
# Conflicts:
#	src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets
#	src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets
The _PrepareTrimConfiguration target was passing the full path of
IntermediateAssembly to TrimmerRootAssembly, but ILLink expects
assembly names (without path). This caused IL1032 errors across
all CI platforms: 'Root assembly with name ...ilc.dll could not be found.'
Restores the %(Filename) transform that was on main but got lost
when moving this line from PrepareForILLink to _PrepareTrimConfiguration.
@sbomer

Copy link
Copy Markdown
Member

/ba-g "deadletter"

@sbomer
sbomer merged commit 15da421 into mainMar 16, 2026
119 of 128 checks passed
@sbomer
sbomer deleted the copilot/fix-ilc-compile-order branch March 16, 2026 18:12
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
_ComputeAssembliesToCompileToNative now populates
@(_IlcManagedInputAssemblies) and ComputeLinkedFilesToPublish
removes them from @(ResolvedFileToPublish).
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in @(ResolvedFileToPublish) for the outer build's _ResolveAssemblies
target. Clear @(_IlcManagedInputAssemblies) in our
_AndroidComputeIlcCompileInputs target so the runtime's
ComputeLinkedFilesToPublish doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Changes: dotnet/dotnet@5ff448a...803eb28
- **Dependency Updates**:
- From [11.0.0-preview.3.26165.107 to 11.0.0-preview.3.26168.106][1]
- Microsoft.NET.Workload.Mono.ToolChain.Current.Manifest-11.0.100-preview.3
- Microsoft.NET.ILLink
- Microsoft.NETCore.App.Ref
- From [11.0.0-beta.26165.107 to 11.0.0-beta.26168.106][1]
- Microsoft.DotNet.Build.Tasks.Feed
- From [0.11.5-preview.26165.107 to 0.11.5-preview.26168.106][1]
- Microsoft.DotNet.Cecil
- From [11.0.100-preview.3.26165.107 to 11.0.100-preview.3.26168.106][1]
- Microsoft.NET.Sdk
- Microsoft.NET.Workload.Emscripten.Current.Manifest-11.0.100-preview.3
- Microsoft.TemplateEngine.Authoring.Tasks
[1]: dotnet/dotnet@5ff448a...803eb28
## Other changes ##
[xabt] Prevent `ComputeLinkedFilesToPublish` from stripping assemblies
Context: dotnet/runtime#124801
The runtime restructured NativeAOT build targets so that
`_ComputeAssembliesToCompileToNative` now populates
`@(_IlcManagedInputAssemblies)` and `ComputeLinkedFilesToPublish`
removes them from `@(ResolvedFileToPublish)`.
Since .NET for Android runs ILLink before ILC (to support custom
trimmer steps), our assemblies are already trimmed and must remain
in `@(ResolvedFileToPublish)` for the outer build's `_ResolveAssemblies`
target. Clear `@(_IlcManagedInputAssemblies)` in our
`_AndroidComputeIlcCompileInputs` target so the runtime's
`ComputeLinkedFilesToPublish` doesn't strip them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
sbomer added a commit that referenced this pull request Mar 24, 2026
…g them (#124192)
## 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:
```xml
<ItemGroup>
<ResolvedFileToPublish Remove="@(_IlcManagedInputAssemblies)" />
<!-- dotnet CLI produces managed debug symbols, which we will replace with native symbols instead -->
<ResolvedFileToPublish Remove="@(_DebugSymbolsIntermediatePath)" />
<!-- Satellite assemblies are embedded into the native binary, so we don't need to publish them -->
<ResolvedFileToPublish Remove="@(IntermediateSatelliteAssembliesWithTargetPath)" />
<!-- replace apphost with binary we generated during native compilation -->
<ResolvedFileToPublish Include="$(NativeBinary)">
<RelativePath>$(NativeBinaryPrefix)$(TargetName)$(NativeBinaryExt)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
```
This follows the same pattern as removing managed assemblies and debug
symbols, which are also embedded or replaced in the native binary.
## Customer Impact
- **Affected customers:** NativeAOT users with localized resources
- **Regression:** No
- **Source incompatibility:** No
- **Breaking change:** No (removes extraneous files from publish output)
## Testing
Testing will be added in the SDK repo per review feedback. The fix can
be validated by publishing a NativeAOT app with satellite assemblies and
verifying that:
- Localized resources are accessible at runtime (embedded correctly)
- No satellite assembly subdirectories exist in publish output
## Risk
Minimal. One-line change following established pattern. Satellite
assemblies remain embedded and functional; only removes redundant disk
copies.
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
----
*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](https://github.com/user-attachments/files/25188858/AotLocalization.zip)
[Binlog](https://github.com/user-attachments/files/25188847/AotLocalizationBinlog.zip)
### 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)
<comments>
<comment_new><author>@baronfel</author><body>
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)](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L3-L11),
`_ComputeIlcCompileInputs` gathers satellite assemblies from two
sources:
```xml
<IlcSatelliteAssembly Include="@(_SatelliteAssembliesToPublish)" />
<IlcSatelliteAssembly Include="@(IntermediateSatelliteAssembliesWithTargetPath)" />
```
- `_SatelliteAssembliesToPublish` = satellite assemblies from
package/project references (extracted from
`_ResolvedCopyLocalPublishAssets` by
[`ComputeManagedAssembliesToCompileToNative`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L73-L84))
- `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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets#L236),
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`](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Publish.targets#L20-L34)
runs `BeforeTargets="ComputeResolvedFilesToPublishList"` and modifies
the publish list:
```xml
<_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)](https://github.com/dotnet/sdk/blob/main/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L545-L549),
`ComputeResolvedFilesToPublishList` unconditionally includes:
```xml
<!-- Copy satellite assemblies. -->
<ResolvedFileToPublish Include="@(IntermediateSatelliteAssembliesWithTargetPath)">
<RelativePath>%(IntermediateSatelliteAss...
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes#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.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: dsplaisted <145043+dsplaisted@users.noreply.github.com>
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Co-authored-by: Sven Boemer <sbomer@gmail.com>
sbomer added a commit that referenced this pull request Apr 10, 2026
… item Update (#125630)
## Description
Follow-up to PR #124801 review feedback: the "intersection via
include/remove, then remove+re-include" pattern in
`_PrepareTrimConfiguration` was complex, mutated item ordering, and used
two throwaway item groups. Replace with a direct MSBuild `Update`.
### Change
**Before** — compute intersection manually to set metadata, then
remove+re-add items:
```xml
<__SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<__SingleWarnIntermediateAssembly Remove="@(IntermediateAssembly)" />
<_SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" />
<_SingleWarnIntermediateAssembly Remove="@(__SingleWarnIntermediateAssembly)" />
<_SingleWarnIntermediateAssembly>
<TrimmerSingleWarn Condition="...">false</TrimmerSingleWarn>
</_SingleWarnIntermediateAssembly>
<ResolvedFileToPublish Remove="@(_SingleWarnIntermediateAssembly)" />
<ResolvedFileToPublish Include="@(_SingleWarnIntermediateAssembly)" />
```
**After** — update matching items directly, preserving order:
```xml
<ResolvedFileToPublish Update="@(IntermediateAssembly)">
<TrimmerSingleWarn Condition=" '%(ResolvedFileToPublish.TrimmerSingleWarn)' == '' ">false</TrimmerSingleWarn>
</ResolvedFileToPublish>
```
## Changes proposed in this pull request
- [`Microsoft.NET.ILLink.targets`] Replace 13-line intersection pattern
with a 3-line `Update` in `_PrepareTrimConfiguration`
- [`Microsoft.NET.ILLink.targets`] Qualify `%(TrimmerSingleWarn)` as
`%(ResolvedFileToPublish.TrimmerSingleWarn)` in the `Update` condition
to prevent MSB4096 (unqualified metadata batching over all
`ResolvedFileToPublish` items, including those without the metadata
defined)
## Additional context
<!-- START COPILOT CODING AGENT TIPS -->
---
🔒 GitHub Advanced Security automatically protects Copilot coding agent
pull requests. You can protect all pull requests by enabling Advanced
Security for your repositories. [Learn more about Advanced
Security.](https://gh.io/cca-advanced-security)
---------
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 Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-Tools-ILLink.NET linker development as well as trimming analyzerslinkable-frameworkIssues associated with delivering a linker friendly framework

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

ComputeIlcCompileInputs should not need to run before PrepareForILLink

4 participants

@sbomer@MichalStrehovsky