.NET 8: MSBuild improvements for containers
See original GitHub issue.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
.
Issue Analytics
- State:
- Created a year ago
- Reactions:4
- Comments:20 (20 by maintainers)
Top GitHub Comments
It seems there are certainly similarities. Our .NET default yaml files for GitHub Actions and AzDO Pipelines break out into separate steps for restore, build, test, publish, etc. along with opted out of the implied dependent stages for the later steps (e.g.
--no-build
ondotnet publish
), which means passing the various options to each step is required in order for the stage to work correctly. Once you add a RID you need to ensure it’s passed to every stage, which today is a completely manual thing.I want to be super clear that you don’t need parent directories here. If you use Central Package Version Management, for instance, copying
*.csproj
is flat wrong. Likewise if you have aDirectory.Build.props
next to your solution or in any subfolder that influences restore in any way. Basically I think the suggestions in the docs are useful only for trivial codebases.