-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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 before ComputeIlcCompileInputs.
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>
<Import Project="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>
<Import Project="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:
<Target Name="_AndroidFixManagedAssemblyToLink"
AfterTargets="_ComputeManagedAssemblyForILLink">
<ItemGroup>
<ManagedAssemblyToLink Include="@(IntermediateAssembly)" />
</ItemGroup>
</Target>Metadata
Metadata
Labels
Type
Projects
Status