Summary
SaveChangedAssemblyStep in AssemblyModifierPipeline unconditionally calls File.SetLastWriteTimeUtc(destination, DateTime.UtcNow) on every assembly it processes, even when the content is unchanged. This causes unnecessary downstream target re-runs on incremental builds.
Root Cause
In AssemblyModifierPipeline.cs, SaveChangedAssemblyStep.CopyIfChanged():
// NOTE: We still need to update the timestamp on this file, or this target would run again
File.SetLastWriteTimeUtc (destination.ItemSpec, DateTime.UtcNow);
This timestamp bump was added to prevent the calling target from re-running. However, _AfterILLinkAdditionalSteps (which calls AssemblyModifierPipeline with SourceFiles == DestinationFiles, i.e. in-place) uses flag files (_AndroidLinkFlag / _AdditionalPostLinkerStepsFlag) for its incrementalism — the per-assembly timestamp bumps are unnecessary for this target.
Impact
NativeAOT (confirmed)
For NativeAOT builds, @(ResolvedAssemblies) points directly to obj/Release/_Microsoft.Android.Resource.Designer.dll (no R2R intermediate copy). The in-place timestamp bump makes the designer DLL newer than CoreCompile's outputs, causing:
CoreCompile re-runs → regenerates UnnamedProject.dll
_GenerateTrimmableTypeMap re-runs → regenerates TypeMap DLLs
_GenerateJavaStubs re-runs → not skipped on incremental build
CoreCLR (suspected)
For CoreCLR, @(ResolvedAssemblies) points to R2R copies (obj/Release/.../R2R/*.dll), so CoreCompile is not affected. However, the bumped R2R timestamps may cause downstream targets (e.g., _RemoveRegisterAttribute via CopyIfChanged) to do unnecessary work on incremental builds.
Why CoreCLR appears unaffected
CoreCLR's R2R step copies assemblies to a separate directory before _AfterILLinkAdditionalSteps runs. The timestamp bump hits the R2R copies, not the original assemblies that CoreCompile references. NativeAOT has no R2R step, so the pipeline operates directly on the originals.
Suggested Fix
Review whether the timestamp bump is needed for each caller:
_LinkAssembliesNoShrink: source ≠ destination, uses assembly-level Inputs/Outputs → bump IS needed
_AfterILLinkAdditionalSteps: source == destination (in-place), uses flag files → bump is NOT needed
Options:
- Skip the timestamp bump when source == destination (in-place mode)
- Eliminate the in-place mode entirely by having
_AfterILLinkAdditionalSteps write to a separate directory
- Audit all callers and remove the bump where flag-based incrementalism makes it unnecessary
Repro
# Build a NativeAOT + trimmable typemap project twice
dotnet build -c Release -p:_AndroidTypeMapImplementation=trimmable
dotnet build -c Release -p:_AndroidTypeMapImplementation=trimmable -v:diagnostic 2>&1 | grep "Building target.*CoreCompile.*completely"
# CoreCompile will re-run due to designer DLL timestamp
This content was created with assistance from AI.
Summary
SaveChangedAssemblyStepinAssemblyModifierPipelineunconditionally callsFile.SetLastWriteTimeUtc(destination, DateTime.UtcNow)on every assembly it processes, even when the content is unchanged. This causes unnecessary downstream target re-runs on incremental builds.Root Cause
In
AssemblyModifierPipeline.cs,SaveChangedAssemblyStep.CopyIfChanged():This timestamp bump was added to prevent the calling target from re-running. However,
_AfterILLinkAdditionalSteps(which callsAssemblyModifierPipelinewithSourceFiles == DestinationFiles, i.e. in-place) uses flag files (_AndroidLinkFlag/_AdditionalPostLinkerStepsFlag) for its incrementalism — the per-assembly timestamp bumps are unnecessary for this target.Impact
NativeAOT (confirmed)
For NativeAOT builds,
@(ResolvedAssemblies)points directly toobj/Release/_Microsoft.Android.Resource.Designer.dll(no R2R intermediate copy). The in-place timestamp bump makes the designer DLL newer thanCoreCompile's outputs, causing:CoreCompilere-runs → regeneratesUnnamedProject.dll_GenerateTrimmableTypeMapre-runs → regenerates TypeMap DLLs_GenerateJavaStubsre-runs → not skipped on incremental buildCoreCLR (suspected)
For CoreCLR,
@(ResolvedAssemblies)points to R2R copies (obj/Release/.../R2R/*.dll), so CoreCompile is not affected. However, the bumped R2R timestamps may cause downstream targets (e.g.,_RemoveRegisterAttributeviaCopyIfChanged) to do unnecessary work on incremental builds.Why CoreCLR appears unaffected
CoreCLR's R2R step copies assemblies to a separate directory before
_AfterILLinkAdditionalStepsruns. The timestamp bump hits the R2R copies, not the original assemblies that CoreCompile references. NativeAOT has no R2R step, so the pipeline operates directly on the originals.Suggested Fix
Review whether the timestamp bump is needed for each caller:
_LinkAssembliesNoShrink: source ≠ destination, uses assembly-levelInputs/Outputs→ bump IS needed_AfterILLinkAdditionalSteps: source == destination (in-place), uses flag files → bump is NOT neededOptions:
_AfterILLinkAdditionalStepswrite to a separate directoryRepro
This content was created with assistance from AI.