Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions documentation/general/publish-build-optimization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Publish Build Optimization

## Overview

When `dotnet publish` runs, it implicitly runs a full `Build` before the publish step.
This is necessary because the SDK cannot assume the previous `dotnet build` used the same
configuration (e.g., the default build configuration is `Debug` while publish defaults to
`Release`), runtime identifier, or other settings.

However, for some publish modes, the full `Build` output (written to `bin\<config>\<tfm>\<rid>\`)
is never used by the publish pipeline. The publish steps read from intermediate outputs
(`obj\`) and resolution items, not from the `bin\` directory. This means the `Build` step
produces artifacts that are confusing to users and automation, as `bin\<config>\<tfm>\<rid>\`
contains a full self-contained managed deployment that is not the intended output.

## How Publish Modes Work Today

### Common Architecture

All publish modes share this flow:

1. **Build** (or equivalent) — produces the IL assembly at `@(IntermediateAssembly)` in `obj\`
2. **ComputeResolvedFilesToPublishList** — collects files to publish from `@(IntermediateAssembly)`,
`@(RuntimeCopyLocalItems)`, `@(RuntimePackAsset)`, and content items
3. **Post-processing** — mode-specific transformations (ILC, ILLink, crossgen2, bundler)
4. **Copy to PublishDir** — final output written to `bin\<config>\<tfm>\<rid>\publish\`

Key insight: All post-processing steps read from `@(IntermediateAssembly)` (obj) and
`@(ResolvedFileToPublish)` (resolved from NuGet/project references), **never** from the
`Build` output directory.

### PublishAot (Native AOT)

**Status: Optimized**

- `IlcCompile` reads `@(IntermediateAssembly)` from `obj\` and produces a native binary
- The full `Build` was running a self-contained deployment to `bin\<config>\<tfm>\<rid>\`,
including apphost, managed DLLs, deps.json, runtimeconfig.json, and runtime pack files
- **None of these files are used** by the AOT pipeline
- **Optimization**: Replace `Build` with `Compile` (plus resource/satellite targets)
- **Opt-out**: Set `UseOptimizedPublish=false` to restore full Build behavior
- `UseOptimizedPublish` currently has no effect when `PublishAot` is not enabled

Target chain for optimized AOT publish:
```
BuildOnlySettings → PrepareForBuild → PrepareResources → Compile → CreateSatelliteAssemblies
```

Where `Compile` includes `ResolveReferences → ResolveProjectReferences → CoreCompile`.

### PublishTrimmed (IL Trimming)

**Status: Not yet optimized — candidate for future optimization**

- `ILLink` (the IL trimmer) processes `@(ResolvedFileToPublish)` items marked with
`PostprocessAssembly=true`
- These items come from `ComputeResolvedFilesToPublishList` which reads from
`@(IntermediateAssembly)` (obj) and resolved references
- The `Build` output in `bin\` is not consumed by the trimmer
- The same `Compile`-based optimization would apply here

### PublishReadyToRun (R2R / Crossgen2)

**Status: Not yet optimized — candidate for future optimization**

- `RunCrossgen2` processes `@(ResolvedFileToPublish)` items
- Input assemblies come from resolution, not from `Build` output
- Same optimization opportunity as trimming

### PublishSingleFile (Single-File Bundling)

**Status: Not yet optimized — candidate for future optimization**

- `GenerateSingleFileBundle` bundles `@(ResolvedFileToPublish)` items into one executable
- All inputs come from resolved items and `@(IntermediateAssembly)`
- Same optimization opportunity

### Combined Modes

These modes can be combined (e.g., `PublishAot` implies `PublishTrimmed`). The optimization
applies when the outermost mode is optimized:

| Combination | Optimized? | Notes |
|---|---|---|
| PublishAot (implies trimmed) | ✅ Yes | AOT is the outermost mode |
| PublishTrimmed + PublishSingleFile | ❌ Not yet | Future candidate |
| PublishReadyToRun + PublishSingleFile | ❌ Not yet | Future candidate |
| PublishTrimmed alone | ❌ Not yet | Future candidate |
| PublishReadyToRun alone | ❌ Not yet | Future candidate |

## Breaking Change: AOT Publish Build Optimization

### What Changed

Starting in .NET 11, `dotnet publish` with `PublishAot=true` no longer runs a full `Build`
before publish. Instead, it runs only `Compile` (and resource/satellite assembly targets).

### Impact

- **BeforeBuild / AfterBuild targets**: These will not execute during AOT publish. If you have
custom targets attached to `BeforeBuild`, `AfterBuild`, or using
`BeforeTargets="Build"` / `AfterTargets="Build"`, they will not run.
- **Workaround**: Attach your targets to `BeforeTargets="Publish"` / `AfterTargets="Publish"`,
or to `BeforeTargets="Compile"` / `AfterTargets="Compile"` instead.
- **PreBuildEvent / PostBuildEvent**: These will not execute during AOT publish (consistent
with `--no-build` behavior).
- **Third-party NuGet Build hooks**: Targets from NuGet packages that hook into `Build` will be
skipped (consistent with `--no-build` behavior).
- **Output directory**: `bin\<config>\<tfm>\<rid>\` will no longer contain managed apphost,
DLLs, deps.json, runtimeconfig.json, or runtime pack files. Only `native\` and `publish\`
subdirectories will be present.

### Opt-Out

To restore the previous behavior of running a full `Build` before AOT publish, set:

```xml
<PropertyGroup>
<UseOptimizedPublish>false</UseOptimizedPublish>
</PropertyGroup>
```

Or pass it on the command line:

```
dotnet publish /p:UseOptimizedPublish=false
```

## Future Work

The same optimization could be applied to `PublishTrimmed`, `PublishReadyToRun`, and
`PublishSingleFile` modes, as they all share the same architecture of reading from
`@(IntermediateAssembly)` and resolved references rather than `Build` output. Each mode
would need:

1. Compatibility analysis for Build hooks used by projects and NuGet packages
2. The same target chain: `BuildOnlySettings → PrepareForBuild → PrepareResources → Compile → CreateSatelliteAssemblies`
3. Tests verifying the expected build and publish outputs
4. Documentation of the breaking change for that mode
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,13 @@ Copyright (c) .NET Foundation. All rights reserved.

<GenerateStaticWebAssetsManifestDependsOn>$(GenerateStaticWebAssetsManifestDependsOn);ResolveBuildStaticWebAssets</GenerateStaticWebAssetsManifestDependsOn>

<!-- The AOT-optimized publish path (UseOptimizedPublish) runs Compile instead of full Build,
which skips PrepareForRun and therefore GenerateStaticWebAssetsManifest. The publish pipeline
still expects the static web assets build manifest (staticwebassets.build.json) to exist, so
contribute manifest generation to the optimized publish extension point. This writes only
to the intermediate (obj\) directory and does not copy assets to the output (bin\) directory. -->
<_AdditionalOptimizedPublishTargets>$(_AdditionalOptimizedPublishTargets);GenerateStaticWebAssetsManifest</_AdditionalOptimizedPublishTargets>

<!-- There are now three stages:
ResolveCoreStaticWebAssets: This is the stage where we resolve the static web assets for the current project, packages and project references. These assets exist on disk
and are part of the project or one of its dependencies.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,19 +148,43 @@ Copyright (c) .NET Foundation. All rights reserved.
</_CorePublishTargets>

<_PublishNoBuildAlternativeDependsOn>$(_BeforePublishNoBuildTargets);$(_CorePublishTargets)</_PublishNoBuildAlternativeDependsOn>

<!-- For AOT publish, we only need Compile (not full Build) since IlcCompile reads from
@(IntermediateAssembly) in obj\, not from bin\ output. Skipping Build avoids producing
unnecessary self-contained managed output (apphost, deps.json, runtimeconfig, runtime
pack files) in the output directory that would confuse users and automation.
Set UseOptimizedPublish=false to restore the previous behavior of running full Build. -->
<UseOptimizedPublish Condition="'$(UseOptimizedPublish)' == '' and '$(PublishAot)' == 'true'">true</UseOptimizedPublish>
<!-- Native AOT is currently the only publish mode that supports optimized publish. -->
<_UseOptimizedPublish Condition="'$(PublishAot)' == 'true' and '$(UseOptimizedPublish)' == 'true'">true</_UseOptimizedPublish>
<_BeforeOptimizedPublishTargets Condition="'$(_UseOptimizedPublish)' == 'true'">
BuildOnlySettings;
PrepareForBuild;
PrepareResources;
Compile;
CreateSatelliteAssemblies;
</_BeforeOptimizedPublishTargets>
</PropertyGroup>

<Target Name="_PublishBuildAlternative"
Condition="'$(NoBuild)' != 'true'"
Condition="'$(NoBuild)' != 'true' and '$(_UseOptimizedPublish)' != 'true'"
DependsOnTargets="Build;$(_CorePublishTargets)" />

<!-- $(_AdditionalOptimizedPublishTargets) is an extension point for SDKs (e.g. Static Web Assets)
that hook additional work into the full Build via targets which the AOT-optimized path skips.
It runs after Compile so contributors can rely on the intermediate assembly, and before the core
publish targets that consume its outputs. -->
<Target Name="_PublishOptimizedBuildAlternative"
Condition="'$(NoBuild)' != 'true' and '$(_UseOptimizedPublish)' == 'true'"
DependsOnTargets="$(_BeforeOptimizedPublishTargets);$(_AdditionalOptimizedPublishTargets);$(_CorePublishTargets)" />

<Target Name="_PublishNoBuildAlternative"
Condition="'$(NoBuild)' == 'true'"
DependsOnTargets="$(_PublishNoBuildAlternativeDependsOn)" />

<Target Name="Publish"
Condition="$(IsPublishable) == 'true'"
DependsOnTargets="_PublishBuildAlternative;_PublishNoBuildAlternative" >
DependsOnTargets="_PublishBuildAlternative;_PublishOptimizedBuildAlternative;_PublishNoBuildAlternative" >

<!-- Ensure there is minimal verbosity output pointing to the publish directory and not just the
build step's minimal output. Otherwise there is no indication at minimal verbosity of where
Expand Down
Loading
Loading