Skip to content

Commit 771e843

Browse files
marcpopMSFTCopilot
andcommitted
Add UseAotOptimizedPublish opt-out, improve test assertions, add documentation
- Add UseAotOptimizedPublish property (default true for PublishAot) as opt-out to restore old Build behavior during AOT publish - Improve test to verify runtime pack files (System.Private.CoreLib.dll, coreclr.dll) are not present in build output directory - Add publish-build-optimization.md documenting the breaking change, all publish modes, opt-out mechanism, and future work Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fbeec77 commit 771e843

3 files changed

Lines changed: 150 additions & 4 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Publish Build Optimization
2+
3+
## Overview
4+
5+
When `dotnet publish` runs, it implicitly runs a full `Build` before the publish step.
6+
This is necessary because the SDK cannot assume the previous `dotnet build` used the same
7+
configuration (e.g., the default build configuration is `Debug` while publish defaults to
8+
`Release`), runtime identifier, or other settings.
9+
10+
However, for some publish modes, the full `Build` output (written to `bin\<config>\<tfm>\<rid>\`)
11+
is never used by the publish pipeline. The publish steps read from intermediate outputs
12+
(`obj\`) and resolution items, not from the `bin\` directory. This means the `Build` step
13+
produces artifacts that are confusing to users and automation, as `bin\<config>\<tfm>\<rid>\`
14+
contains a full self-contained managed deployment that is not the intended output.
15+
16+
## How Publish Modes Work Today
17+
18+
### Common Architecture
19+
20+
All publish modes share this flow:
21+
22+
1. **Build** (or equivalent) — produces the IL assembly at `@(IntermediateAssembly)` in `obj\`
23+
2. **ComputeResolvedFilesToPublishList** — collects files to publish from `@(IntermediateAssembly)`,
24+
`@(RuntimeCopyLocalItems)`, `@(RuntimePackAsset)`, and content items
25+
3. **Post-processing** — mode-specific transformations (ILC, ILLink, crossgen2, bundler)
26+
4. **Copy to PublishDir** — final output written to `bin\<config>\<tfm>\<rid>\publish\`
27+
28+
Key insight: All post-processing steps read from `@(IntermediateAssembly)` (obj) and
29+
`@(ResolvedFileToPublish)` (resolved from NuGet/project references), **never** from the
30+
`Build` output directory.
31+
32+
### PublishAot (Native AOT)
33+
34+
**Status: Optimized (this PR)**
35+
36+
- `IlcCompile` reads `@(IntermediateAssembly)` from `obj\` and produces a native binary
37+
- The full `Build` was running a self-contained deployment to `bin\<config>\<tfm>\<rid>\`,
38+
including apphost, managed DLLs, deps.json, runtimeconfig.json, and runtime pack files
39+
- **None of these files are used** by the AOT pipeline
40+
- **Optimization**: Replace `Build` with `Compile` (plus resource/satellite targets)
41+
- **Opt-out**: Set `UseAotOptimizedPublish=false` to restore full Build behavior
42+
43+
Target chain for optimized AOT publish:
44+
```
45+
BuildOnlySettings → PrepareForBuild → PrepareResources → Compile → CreateSatelliteAssemblies
46+
```
47+
48+
Where `Compile` includes `ResolveReferences → ResolveProjectReferences → CoreCompile`.
49+
50+
### PublishTrimmed (IL Trimming)
51+
52+
**Status: Not yet optimized — candidate for future optimization**
53+
54+
- `ILLink` (the IL trimmer) processes `@(ResolvedFileToPublish)` items marked with
55+
`PostprocessAssembly=true`
56+
- These items come from `ComputeResolvedFilesToPublishList` which reads from
57+
`@(IntermediateAssembly)` (obj) and resolved references
58+
- The `Build` output in `bin\` is not consumed by the trimmer
59+
- The same `Compile`-based optimization would apply here
60+
61+
### PublishReadyToRun (R2R / Crossgen2)
62+
63+
**Status: Not yet optimized — candidate for future optimization**
64+
65+
- `RunCrossgen2` processes `@(ResolvedFileToPublish)` items
66+
- Input assemblies come from resolution, not from `Build` output
67+
- Same optimization opportunity as trimming
68+
69+
### PublishSingleFile (Single-File Bundling)
70+
71+
**Status: Not yet optimized — candidate for future optimization**
72+
73+
- `GenerateSingleFileBundle` bundles `@(ResolvedFileToPublish)` items into one executable
74+
- All inputs come from resolved items and `@(IntermediateAssembly)`
75+
- Same optimization opportunity
76+
77+
### Combined Modes
78+
79+
These modes can be combined (e.g., `PublishAot` implies `PublishTrimmed`). The optimization
80+
applies when the outermost mode is optimized:
81+
82+
| Combination | Optimized? | Notes |
83+
|---|---|---|
84+
| PublishAot (implies trimmed) | ✅ Yes | AOT is the outermost mode |
85+
| PublishTrimmed + PublishSingleFile | ❌ Not yet | Future candidate |
86+
| PublishReadyToRun + PublishSingleFile | ❌ Not yet | Future candidate |
87+
| PublishTrimmed alone | ❌ Not yet | Future candidate |
88+
| PublishReadyToRun alone | ❌ Not yet | Future candidate |
89+
90+
## Breaking Change: AOT Publish Build Optimization
91+
92+
### What Changed
93+
94+
Starting in .NET 10, `dotnet publish` with `PublishAot=true` no longer runs a full `Build`
95+
before publish. Instead, it runs only `Compile` (and resource/satellite assembly targets).
96+
97+
### Impact
98+
99+
- **BeforeBuild / AfterBuild targets**: These will not execute during AOT publish. If you have
100+
custom targets attached to `BeforeBuild`, `AfterBuild`, or using
101+
`BeforeTargets="Build"` / `AfterTargets="Build"`, they will not run.
102+
- **Workaround**: Attach your targets to `BeforeTargets="Publish"` / `AfterTargets="Publish"`,
103+
or to `BeforeTargets="Compile"` / `AfterTargets="Compile"` instead.
104+
- **PostBuildEvent**: Will not execute during AOT publish (consistent with `--no-build` behavior).
105+
- **Third-party NuGet Build hooks**: Targets from NuGet packages that hook into `Build` will be
106+
skipped (consistent with `--no-build` behavior).
107+
- **Output directory**: `bin\<config>\<tfm>\<rid>\` will no longer contain managed apphost,
108+
DLLs, deps.json, runtimeconfig.json, or runtime pack files. Only `native\` and `publish\`
109+
subdirectories will be present.
110+
111+
### Opt-Out
112+
113+
To restore the previous behavior of running a full `Build` before AOT publish, set:
114+
115+
```xml
116+
<PropertyGroup>
117+
<UseAotOptimizedPublish>false</UseAotOptimizedPublish>
118+
</PropertyGroup>
119+
```
120+
121+
Or pass it on the command line:
122+
123+
```
124+
dotnet publish /p:UseAotOptimizedPublish=false
125+
```
126+
127+
## Future Work
128+
129+
The same optimization could be applied to `PublishTrimmed`, `PublishReadyToRun`, and
130+
`PublishSingleFile` modes, as they all share the same architecture of reading from
131+
`@(IntermediateAssembly)` and resolved references rather than `Build` output. Each mode
132+
would need:
133+
134+
1. Its own condition check (e.g., `UseTrimmingOptimizedPublish`)
135+
2. The same target chain: `BuildOnlySettings → PrepareForBuild → PrepareResources → Compile → CreateSatelliteAssemblies`
136+
3. Tests verifying no managed artifacts in the output directory
137+
4. Documentation of the breaking change for that mode

src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,10 @@ Copyright (c) .NET Foundation. All rights reserved.
152152
<!-- For AOT publish, we only need Compile (not full Build) since IlcCompile reads from
153153
@(IntermediateAssembly) in obj\, not from bin\ output. Skipping Build avoids producing
154154
unnecessary self-contained managed output (apphost, deps.json, runtimeconfig, runtime
155-
pack files) in the output directory that would confuse users and automation. -->
156-
<_BeforePublishAotBuildTargets Condition="'$(PublishAot)' == 'true'">
155+
pack files) in the output directory that would confuse users and automation.
156+
Set UseAotOptimizedPublish=false to restore the previous behavior of running full Build. -->
157+
<UseAotOptimizedPublish Condition="'$(UseAotOptimizedPublish)' == '' and '$(PublishAot)' == 'true'">true</UseAotOptimizedPublish>
158+
<_BeforePublishAotBuildTargets Condition="'$(UseAotOptimizedPublish)' == 'true'">
157159
BuildOnlySettings;
158160
PrepareForBuild;
159161
PrepareResources;
@@ -164,11 +166,11 @@ Copyright (c) .NET Foundation. All rights reserved.
164166
</PropertyGroup>
165167

166168
<Target Name="_PublishBuildAlternative"
167-
Condition="'$(NoBuild)' != 'true' and '$(PublishAot)' != 'true'"
169+
Condition="'$(NoBuild)' != 'true' and '$(UseAotOptimizedPublish)' != 'true'"
168170
DependsOnTargets="Build;$(_CorePublishTargets)" />
169171

170172
<Target Name="_PublishAotBuildAlternative"
171-
Condition="'$(NoBuild)' != 'true' and '$(PublishAot)' == 'true'"
173+
Condition="'$(NoBuild)' != 'true' and '$(UseAotOptimizedPublish)' == 'true'"
172174
DependsOnTargets="$(_PublishAotBuildAlternativeDependsOn)" />
173175

174176
<Target Name="_PublishNoBuildAlternative"

test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,13 @@ public void NativeAot_publish_does_not_produce_managed_build_output()
14391439
"the output directory should not contain deps.json from Build");
14401440
managedBuildArtifacts.Should().NotContain(f => f.Equals($"{projectName}.runtimeconfig.json", StringComparison.OrdinalIgnoreCase),
14411441
"the output directory should not contain runtimeconfig.json from Build");
1442+
// Also verify no runtime pack assemblies leaked from a self-contained Build
1443+
managedBuildArtifacts.Should().NotContain(f => f.Equals("System.Private.CoreLib.dll", StringComparison.OrdinalIgnoreCase),
1444+
"the output directory should not contain runtime pack assemblies from Build");
1445+
managedBuildArtifacts.Should().NotContain(f => f.Equals("coreclr.dll", StringComparison.OrdinalIgnoreCase) ||
1446+
f.Equals("libcoreclr.so", StringComparison.OrdinalIgnoreCase) ||
1447+
f.Equals("libcoreclr.dylib", StringComparison.OrdinalIgnoreCase),
1448+
"the output directory should not contain the CoreCLR runtime from Build");
14421449
}
14431450

14441451
[RequiresMSBuildVersionFact("17.0.0.32901")]

0 commit comments

Comments
 (0)