From d7a5d481f12796b7d4094b3e884670d1751b8878 Mon Sep 17 00:00:00 2001 From: Alex Soto Date: Mon, 16 Mar 2026 21:37:26 -0400 Subject: [PATCH] [build] Fix parallel make hang by disabling dotnet build servers Parallel make (e.g. 'make all -j8', 'make world') has been hanging indefinitely at the end of the build. This is a long-standing issue (xamarin/xamarin-macios#13355) that has been patched three times (#15407, #21315, #22300) without fully fixing the root cause. The problem: when using parallel make, GNU Make uses a jobserver with pipe-based file descriptors to coordinate sub-makes. The dotnet CLI can start background build servers (MSBuild server, Roslyn/VBCSCompiler) that inherit these file descriptors but never close them. Make then waits indefinitely for those file descriptors to close, thinking there are still active jobs. The previous workaround attempted to shut down and force-kill dotnet processes after the build via a 'shutdown-build-server' target. This approach was unreliable because: - The shutdown ran from a double-colon all-hook:: rule with no prerequisites, so with -j it could execute in parallel with (or before) the actual build, killing nothing. - Build servers started by later subdirectories (e.g. tests/) after the dotnet/ shutdown were never killed. - The process-matching regex pattern might not match all server processes. The fix: disable build servers entirely via environment variables in Make.config: - DOTNET_CLI_USE_MSBUILD_SERVER=0: prevents the MSBuild server https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-server https://github.com/dotnet/msbuild/blob/main/documentation/MSBuild-Server.md - UseSharedCompilation=false: prevents the Roslyn compiler server (VBCSCompiler) https://github.com/dotnet/roslyn/issues/27975 - MSBUILDDISABLENODEREUSE=1: prevents MSBuild node reuse https://github.com/dotnet/msbuild/wiki/MSBuild-Tips-&-Tricks This eliminates the root cause - no background servers means no inherited file descriptors means no hang. The shutdown-build-server target and its invocations are removed as they are no longer needed. Additionally, 'make world' now prints the installed workloads at the end of the build for visibility. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Make.config | 9 +++++++++ Makefile | 5 ++--- dotnet/Makefile | 20 -------------------- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/Make.config b/Make.config index c4671333d0ad..f020308accd4 100644 --- a/Make.config +++ b/Make.config @@ -403,6 +403,15 @@ DOTNET_DIR=$(abspath $(TOP)/builds/downloads/$(DOTNET_INSTALL_NAME)) export DOTNET_ROOT=$(DOTNET_DIR) # dotnet now is being looked up in the PATH export PATH := $(DOTNET_DIR):$(PATH) + +# Disable build servers to prevent parallel make from hanging. +# Build servers (MSBuild server, Roslyn/VBCSCompiler) inherit jobserver file +# descriptors from make, and don't close them when daemonizing. This prevents +# make from detecting that all jobs have finished, causing it to hang +# indefinitely at the end of the build. +export DOTNET_CLI_USE_MSBUILD_SERVER=0 +export UseSharedCompilation=false +export MSBUILDDISABLENODEREUSE=1 DOTNET=$(DOTNET_DIR)/dotnet DOTNET_BCL_DIR:=$(abspath $(TOP)/packages/microsoft.netcore.app.ref/$(DOTNET_BCL_VERSION)/ref/$(DOTNET_TFM)) # when bumping to a new .NET version, there may be a period when some parts of .NET is still on the old .NET version, so handle that here for DOTNET_BCL_DIR diff --git a/Makefile b/Makefile index 68482dab47b6..0dcd1bcd2fa1 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,8 @@ world: check-system @$(MAKE) reset-versions @$(MAKE) all -j8 @$(MAKE) install -j8 + @echo "Build is done, the following workloads were built:" + @$(DOTNET) workload list .PHONY: check-system check-system: @@ -68,9 +70,6 @@ install-hook:: exit 1; \ fi -all-hook install-hook:: - $(Q) $(MAKE) -C dotnet shutdown-build-server - dotnet-install-system: $(Q) $(MAKE) -C dotnet install-system diff --git a/dotnet/Makefile b/dotnet/Makefile index 5b9c9cc6ee94..64d81f786d56 100644 --- a/dotnet/Makefile +++ b/dotnet/Makefile @@ -536,23 +536,3 @@ clean-local:: $(Q) $(DOTNET) restore package/workaround-for-maccore-issue-2427/restore.csproj /bl:package/workaround-for-maccore-issue-2427/restore.binlog $(MSBUILD_VERBOSITY) $(Q) touch $@ -# We need to shut down the builder server, because: -# We're using parallel make, and parallel make will start a jobserver, managed by file descriptors, where these file descriptors must be closed in all subprocesses for make to realize it's done. -# 'dotnet pack' might have started a build server -# The build server does not close any file descriptors it may have inherited when daemonizing itself. -# Thus the build server (which will still be alive after we're done building here) might have a file descriptor open which make is waiting for. -# The proper fix is to fix the build server to close its file descriptors. -# The intermediate working is to shut down the build server instead. An alternative solution would be to pass /p:UseSharedCompilation=false to 'dotnet pack' to disable the usage of the build server. -# -# The 'shutdown-build-server' is executed in a sub-make (and not as a dependency to the all-hook target), -# to make sure it's executed after everything else is built in this file. -all-hook:: - $(Q) $(MAKE) shutdown-build-server - -shutdown-build-server: - $(Q) echo "Shutting down build servers:" - $(Q) $(DOTNET) build-server shutdown | sed 's/^/ /' || true - $(Q) echo "Listing .NET processes still alive:" - $(Q) pgrep -lf "^$(DOTNET)" | sed 's/^/ /' || true - $(Q) echo "Killing the above mentioned processes." - $(Q) pkill -9 -f "^$(DOTNET)" | sed 's/^/ /' || true