Skip to content

.NET 8: MSBuild improvements for containers #26249

@richlander

Description

@richlander

.NET 8: MSBuild improvements for containers

I get a lot of my inspiration for SDK improvements from container workflows. Containers constrain the MSBuild environment considerably, mostly due to the docker build context. That makes idiomatic experiences like global config at root less than convenient or performant. Separately, MSBuild is not optimized for an immutable by default build environment, which docker offers as a strength.

Samples

Let's take a look at one our Docker samples:

RUN dotnet restore -r linux-musl-arm

# copy and publish app and libraries
COPY . .
RUN dotnet publish -c release -o /app -r linux-musl-arm --self-contained false --no-restore

Here's another one:

RUN dotnet restore "Store.ProductApi/Store.ProductApi.csproj"
COPY . .
WORKDIR "/src/Store.ProductApi"
RUN dotnet build "Store.ProductApi.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "Store.ProductApi.csproj" -c Release -o /app/publish

There are two important aspects at play:

  • Repeated arguments
  • Careful use of no-blah commands to ensure no repeated work or bookkeeping in an immutable environment.

Ideally, would be a way to lock a configuration and to tell MSBuild that that we're in an immutable environment.

Locking in a configuraton

Directory.Build.props enables locking in a configuration. It's not always convenient to create a file for that purpose. However, that's the effect that I'm after.

Instead, something like the following would be awesome:

dotnet lock-config -c Release -r linux-x64 --self-contained false
dotnet restore
dotnet publish

That's an improvement since the configuration is just specified once. However, it doesn't feel right. I don't want a new command. I just want to be able to lock-in my configuration with an existing command.

Like:

dotnet restore -c Release -r linux-x64 --self-contained false
dotnet publish

Immutable mode

MSBuild (and possibly NuGet) does significant work to check if the environment has changed since the last run of the command. The time since the last command was run could have been as short as <1s ago! There are reasons why that behavior is a good one, but it's also very conservative.

Today, we have --no-restore and --no-build commands. That's a lot of extra ceremony.

Here's a naive option:

dotnet restore
dotnet build --immutable-mode
dotnet publish --immutable-mode

That's not much of an improvement on --no-restore and no-build.

How about:

dotnet restore --immutable-mode
dotnet build
dotnet build
dotnet build
dotnet publish

That's looking much better. MSBuild can manage states for me. In this mode, I would expect that only the first dotnet build would run and the second two would just early-exit. Same with the implicit build within restore. Actually, same with the implicit restore within build and publish.

Pulling it all together

The initial idea with locking in a configuration with restore is likely a breaking change. Let's avoid that.

We could introduce a new --lock argument on restore. It locks in a configuration until restore is run again. That overloads restore a bit, but its also the most basic command. I think it works.

Our experience now looks like the following

dotnet restore -c Release -r linux-x64 --self-contained false --lock
dotnet publish

It's reasonable for --immutable to also force --lock. That enables the following experience.

dotnet restore -c Release -r linux-x64 --self-contained false --immutable
dotnet build
dotnet build
dotnet build
dotnet publish

build is only run once, which includes build within publish.

Clearly, I don't want to run dotnet build multiple times. That's not the point I'm trying to make.

Really, I want just the following:

dotnet restore -c Release -r linux-x64 --self-contained false --immutable
dotnet publish

I want to ensure that three things:

  • I specify my configuration just once across a set of SDK commands.
  • Restore has all the information it needs to be run just once.
  • All commands run optimally or else they fail.

Closing thoughts

These changes have the potential to make it so much easier to make a high-performance docker build (with MSBuild). I'm certain we can make improvements here.

The examples are centered exclusively on locking with restore. That's likely not a good idea. We'd want to rationalize that.

These types of experiences always break with dotnet test and (to a lesser degree) dotnet run. We'd need to validate that. Both are reasonable to run within containers (particularly dotnet test.

Metadata

Metadata

Assignees

Labels

Area-NetSDKconfig-system-candidateIssues that could be solved cleanly if the .NET CLI had a git-like configuration systemuntriagedRequest triage from a team member

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions