|
| 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 |
0 commit comments