Skip to content

ComputeIlcCompileInputs should not need to run before PrepareForILLink #124800

@sbomer

Description

@sbomer

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

Assignees

Labels

area-Tools-ILLink.NET linker development as well as trimming analyzersuntriagedNew issue has not been triaged by the area owner

Type

No type

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions