From ed395dd3d08f46b1cdf19f4a25b89ddbe30d4b31 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 24 Aug 2026 13:32:05 +0200 Subject: [PATCH 1/5] [build] Share provisioned .NET SDK across worktrees Allow local builds to opt into a versioned, configuration-specific shared SDK root through DOTNET_INSTALL_DIR while keeping CI on worktree-local installations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cbf40b05-e7fb-454f-a214-feb9a301ae95 --- Directory.Build.props | 3 +++ Documentation/building/unix/instructions.md | 15 +++++++++-- .../building/windows/instructions.md | 17 ++++++++++-- build-tools/scripts/msbuild.mk | 5 ++-- dotnet-local.cmd | 27 ++++++++++++------- dotnet-local.sh | 11 +++++--- eng/install-dotnet.ps1 | 22 +++++++++++++-- eng/install-dotnet.sh | 23 +++++++++++++--- 8 files changed, 100 insertions(+), 23 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index baf6a832968..d2cbd89c580 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,6 +16,9 @@ $(MSBuildThisFileDirectory)bin\Test$(Configuration)\ $(BootstrapOutputDirectory)$(DotNetStableTargetFramework)\Xamarin.Android.Tools.BootstrapTasks.dll $(BootstrapOutputDirectory)$(DotNetStableTargetFramework)\xa-prep-tasks.dll + <_DotNetInstallBase Condition=" '$(DOTNET_INSTALL_DIR)' != '' and '$(TF_BUILD)' == '' and '$(GITHUB_ACTIONS)' == '' and '$(CI)' == '' ">$([System.IO.Path]::Combine('$(MSBuildThisFileDirectory)', '$(DOTNET_INSTALL_DIR)')) + $([MSBuild]::EnsureTrailingSlash('$(_DotNetInstallBase)\$(MicrosoftNETSdkPackageVersion)\$(Configuration)')) + $([System.IO.File]::ReadAllText('$(BuildOutputDirectory)dotnet-install-location.txt').Trim()) $(BuildOutputDirectory)dotnet\ $(DotNetPreviewPath)dotnet dotnet diff --git a/Documentation/building/unix/instructions.md b/Documentation/building/unix/instructions.md index 24deeb20938..8c95b8d0ae5 100644 --- a/Documentation/building/unix/instructions.md +++ b/Documentation/building/unix/instructions.md @@ -58,6 +58,17 @@ on Windows, many of the concepts should still apply: `make prepare` provisions a specific build of .NET to `bin/$(Configuration)/dotnet`. +To share the provisioned SDK between multiple worktrees, set +`DOTNET_INSTALL_DIR` to a common base directory before running `make prepare`: + + $ export DOTNET_INSTALL_DIR="$HOME/android-dotnet-sdk" + $ make prepare + +The SDK will be installed under +`$DOTNET_INSTALL_DIR//`. The override is ignored +when `TF_BUILD`, `GITHUB_ACTIONS`, or `CI` is set, so CI builds and local builds +without `DOTNET_INSTALL_DIR` continue to use `bin/$(Configuration)/dotnet`. + Once `make all` or `make jenkins` have completed, your local `bin/$(Configuration)/lib/packs` directory will be populated with a local Android "workload" in `Microsoft.Android.Sdk.$(HostOS)` matching @@ -78,8 +89,8 @@ Build the project with: $ ./dotnet-local.sh build foo.csproj -Using the `dotnet-local` script will execute the `dotnet` provisioned in -`bin/$(Configuration)/dotnet` and will use the locally built binaries. +Using the `dotnet-local` script will execute the provisioned `dotnet` and +will use the locally built binaries. See the [One .NET Documentation](../../guides/OneDotNet.md) for further details. diff --git a/Documentation/building/windows/instructions.md b/Documentation/building/windows/instructions.md index b6ece0df558..5402ad19997 100644 --- a/Documentation/building/windows/instructions.md +++ b/Documentation/building/windows/instructions.md @@ -91,8 +91,21 @@ Or in powershell: > dotnet-local.cmd build foo.csproj -Using the `dotnet-local` script will execute the `dotnet` provisioned in -`bin\$(Configuration)\dotnet` and will use the locally built binaries. +Using the `dotnet-local` script will execute the provisioned `dotnet` and +will use the locally built binaries. + +To share the provisioned SDK between multiple worktrees, set +`DOTNET_INSTALL_DIR` to a common base directory before running the `Prepare` +target: + + $env:DOTNET_INSTALL_DIR = Join-Path $HOME 'android-dotnet-sdk' + dotnet msbuild Xamarin.Android.slnx -t:Prepare + +The SDK will be installed under +`$env:DOTNET_INSTALL_DIR\\`. The override is +ignored when `TF_BUILD`, `GITHUB_ACTIONS`, or `CI` is set, so CI builds and +local builds without `DOTNET_INSTALL_DIR` continue to use +`bin\$(Configuration)\dotnet`. See the [One .NET Documentation](../../guides/OneDotNet.md) for further details. diff --git a/build-tools/scripts/msbuild.mk b/build-tools/scripts/msbuild.mk index bc9e989405f..e8e8e20def1 100644 --- a/build-tools/scripts/msbuild.mk +++ b/build-tools/scripts/msbuild.mk @@ -23,7 +23,8 @@ # $(MSBUILD_FLAGS): Additional MSBuild flags; contains $(CONFIGURATION), $(V), $(MSBUILD_ARGS). MSBUILD = msbuild -DOTNET_ROOT = $(topdir)/bin/$(CONFIGURATION)/dotnet/ +DOTNET_INSTALL_LOCATION = $(topdir)/bin/$(CONFIGURATION)/dotnet-install-location.txt +DOTNET_ROOT = $(if $(wildcard $(DOTNET_INSTALL_LOCATION)),$(shell cat "$(DOTNET_INSTALL_LOCATION)"),$(topdir)/bin/$(CONFIGURATION)/dotnet/) DOTNET_TOOL = $(DOTNET_ROOT)dotnet DOTNET_VERB = build MSBUILD_FLAGS = /p:Configuration=$(CONFIGURATION) $(MSBUILD_ARGS) @@ -52,7 +53,7 @@ endef # $(call DOTNET_BINLOG,name,build=$(DOTNET_VERB),dotnet=$(DOTNET_TOOL)) define DOTNET_BINLOG - $(if $(3),,PATH="$(DOTNET_ROOT):$(PATH)") $(if $(3),$(3),$(DOTNET_TOOL)) $(if $(2),$(2),$(DOTNET_VERB)) -p:Configuration=$(CONFIGURATION) -v:n $(MSBUILD_ARGS) \ + $(if $(3),,PATH="$(DOTNET_ROOT):$(PATH)") $(if $(3),$(3),"$(DOTNET_TOOL)") $(if $(2),$(2),$(DOTNET_VERB)) -p:Configuration=$(CONFIGURATION) -v:n $(MSBUILD_ARGS) \ -bl:"$(dir $(realpath $(firstword $(MAKEFILE_LIST))))/bin/Build$(CONFIGURATION)/msbuild-`date +%Y%m%dT%H%M%S`-$(1).binlog" endef diff --git a/dotnet-local.cmd b/dotnet-local.cmd index 448303606bb..fa86a51ae59 100644 --- a/dotnet-local.cmd +++ b/dotnet-local.cmd @@ -2,21 +2,30 @@ SETLOCAL SET ROOT=%~dp0 +SET XA_CONFIG= +SET XA_DOTNET_ROOT= -IF EXIST "%ROOT%\bin\Release\dotnet\dotnet.exe" ( - SET XA_CONFIG=Release -) ELSE IF EXIST "%ROOT%\bin\Debug\dotnet\dotnet.exe" ( - SET XA_CONFIG=Debug -) ELSE ( - echo "You need to run 'msbuild Xamarin.Android.slnx /t:Prepare' first." - goto :exit -) +CALL :find_dotnet Release +IF DEFINED XA_CONFIG GOTO :dotnet_found +CALL :find_dotnet Debug +IF DEFINED XA_CONFIG GOTO :dotnet_found -SET XA_DOTNET_ROOT=%ROOT%\bin\%XA_CONFIG%\dotnet +echo "You need to run 'msbuild Xamarin.Android.slnx /t:Prepare' first." +GOTO :exit + +:dotnet_found SET PATH=%XA_DOTNET_ROOT%;%PATH% SET DOTNETSDK_WORKLOAD_MANIFEST_ROOTS=%ROOT%\bin\%XA_CONFIG%\lib\sdk-manifests SET DOTNETSDK_WORKLOAD_PACK_ROOTS=%ROOT%\bin\%XA_CONFIG%\lib call "%XA_DOTNET_ROOT%\dotnet.exe" %* +GOTO :exit + +:find_dotnet +SET XA_DOTNET_ROOT= +IF EXIST "%ROOT%\bin\%1\dotnet-install-location.txt" SET /P XA_DOTNET_ROOT=<"%ROOT%\bin\%1\dotnet-install-location.txt" +IF NOT DEFINED XA_DOTNET_ROOT SET XA_DOTNET_ROOT=%ROOT%\bin\%1\dotnet\ +IF EXIST "%XA_DOTNET_ROOT%\dotnet.exe" SET XA_CONFIG=%1 +EXIT /B :exit diff --git a/dotnet-local.sh b/dotnet-local.sh index ca26fa1f75e..dd545df3abb 100755 --- a/dotnet-local.sh +++ b/dotnet-local.sh @@ -2,14 +2,19 @@ ROOT="$(dirname "${BASH_SOURCE}")" FULLROOT="$(cd "${ROOT}"; pwd)" for config in Release Debug ; do - if [[ ! -x "${ROOT}/bin/${config}/dotnet/dotnet" ]] ; then + install_location="${FULLROOT}/bin/${config}/dotnet-install-location.txt" + if [[ -f "${install_location}" ]] ; then + IFS= read -r XA_DOTNET_ROOT < "${install_location}" + else + XA_DOTNET_ROOT="${FULLROOT}/bin/${config}/dotnet" + fi + if [[ ! -x "${XA_DOTNET_ROOT}/dotnet" ]] ; then continue fi - XA_DOTNET_ROOT="${FULLROOT}/bin/${config}/dotnet" export PATH="${XA_DOTNET_ROOT}:${PATH}" export DOTNETSDK_WORKLOAD_MANIFEST_ROOTS="${FULLROOT}/bin/${config}/lib/sdk-manifests" export DOTNETSDK_WORKLOAD_PACK_ROOTS="${FULLROOT}/bin/${config}/lib" - exec "${ROOT}/bin/${config}/dotnet/dotnet" "$@" + exec "${XA_DOTNET_ROOT}/dotnet" "$@" done echo "You need to run 'make prepare' first." >&2 diff --git a/eng/install-dotnet.ps1 b/eng/install-dotnet.ps1 index aa10f476cf9..0f00bc6f52c 100644 --- a/eng/install-dotnet.ps1 +++ b/eng/install-dotnet.ps1 @@ -1,11 +1,14 @@ <# .SYNOPSIS - Provisions the .NET SDK into bin\$Configuration\dotnet\. + Provisions the .NET SDK into bin\$Configuration\dotnet\ by default. .DESCRIPTION The SDK version is read from eng\Versions.props (single source of truth kept up to date by darc when Microsoft.NET.Sdk flows from dotnet/dotnet), so global.json does not need a 'tools.dotnet' pin. + + Set DOTNET_INSTALL_DIR to a shared base directory to install the SDK under + \\\. #> [CmdletBinding(PositionalBinding=$false)] param( @@ -26,7 +29,17 @@ if ($null -eq $sdkNode -or [string]::IsNullOrWhiteSpace($sdkNode.InnerText)) { } $sdkVersion = $sdkNode.InnerText -$installDir = Join-Path $repoRoot "bin\$configuration\dotnet" +$useSharedInstall = -not [string]::IsNullOrWhiteSpace($env:DOTNET_INSTALL_DIR) -and + [string]::IsNullOrEmpty($env:TF_BUILD) -and + [string]::IsNullOrEmpty($env:GITHUB_ACTIONS) -and + [string]::IsNullOrEmpty($env:CI) + +if (-not $useSharedInstall) { + $installDir = Join-Path $repoRoot "bin\$configuration\dotnet" +} else { + $installBase = [IO.Path]::GetFullPath((Join-Path $repoRoot $env:DOTNET_INSTALL_DIR)) + $installDir = Join-Path $installBase "$sdkVersion\$configuration" +} New-Item -ItemType Directory -Force -Path $installDir | Out-Null # Download Microsoft's official dotnet-install.ps1 (cached under $installDir @@ -52,3 +65,8 @@ Write-Host "Installing .NET SDK $sdkVersion into $installDir" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +$installLocationFile = Join-Path $repoRoot "bin\$configuration\dotnet-install-location.txt" +$installLocation = $installDir.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar +New-Item -ItemType Directory -Force -Path (Split-Path -Parent $installLocationFile) | Out-Null +Set-Content -LiteralPath $installLocationFile -Value $installLocation -NoNewline diff --git a/eng/install-dotnet.sh b/eng/install-dotnet.sh index 9574bf1845a..ce65d203c09 100644 --- a/eng/install-dotnet.sh +++ b/eng/install-dotnet.sh @@ -1,13 +1,15 @@ #!/usr/bin/env bash # -# Provisions the .NET SDK into bin/$Configuration/dotnet/. +# Provisions the .NET SDK into bin/$Configuration/dotnet/ by default. # # The SDK version is read from eng/Versions.props (single source of truth # kept up to date by darc when Microsoft.NET.Sdk flows from dotnet/dotnet), # so global.json does not need a 'tools.dotnet' pin. # # Inputs (env vars): -# CONFIGURATION - Debug (default) or Release; controls install path. +# CONFIGURATION - Debug (default) or Release; controls install path. +# DOTNET_INSTALL_DIR - Optional shared base directory. The SDK is installed +# under ///. # set -euo pipefail @@ -24,7 +26,18 @@ if [[ -z "$sdk_version" ]]; then exit 1 fi -install_dir="$repo_root/bin/$configuration/dotnet" +if [[ -n "${DOTNET_INSTALL_DIR:-}" && -z "${TF_BUILD:-}" && -z "${GITHUB_ACTIONS:-}" && -z "${CI:-}" ]]; then + if [[ "$DOTNET_INSTALL_DIR" = /* ]]; then + install_base="$DOTNET_INSTALL_DIR" + else + install_base="$repo_root/$DOTNET_INSTALL_DIR" + fi + mkdir -p "$install_base" + install_base="$(cd -P "$install_base" && pwd)" + install_dir="$install_base/$sdk_version/$configuration" +else + install_dir="$repo_root/bin/$configuration/dotnet" +fi mkdir -p "$install_dir" # Download Microsoft's official dotnet-install.sh (cached under @@ -41,3 +54,7 @@ fi echo "Installing .NET SDK $sdk_version into $install_dir" bash "$install_script" --version "$sdk_version" --install-dir "$install_dir" --no-path + +install_location_file="$repo_root/bin/$configuration/dotnet-install-location.txt" +mkdir -p "$(dirname "$install_location_file")" +printf '%s/\n' "${install_dir%/}" > "$install_location_file" From b2aa2d11dd9ac51d66f82ec4e8cca8af6a7fc385 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 24 Aug 2026 14:29:23 +0200 Subject: [PATCH 2/5] [build] Share SDK between configurations Use one versioned shared SDK root for both Debug and Release while retaining configuration-specific checkout output and location marker files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cbf40b05-e7fb-454f-a214-feb9a301ae95 --- Directory.Build.props | 2 +- Documentation/building/unix/instructions.md | 7 ++++--- Documentation/building/windows/instructions.md | 6 +++--- eng/install-dotnet.ps1 | 10 +++++++--- eng/install-dotnet.sh | 6 +++--- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index d2cbd89c580..1fab676d3af 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -17,7 +17,7 @@ $(BootstrapOutputDirectory)$(DotNetStableTargetFramework)\Xamarin.Android.Tools.BootstrapTasks.dll $(BootstrapOutputDirectory)$(DotNetStableTargetFramework)\xa-prep-tasks.dll <_DotNetInstallBase Condition=" '$(DOTNET_INSTALL_DIR)' != '' and '$(TF_BUILD)' == '' and '$(GITHUB_ACTIONS)' == '' and '$(CI)' == '' ">$([System.IO.Path]::Combine('$(MSBuildThisFileDirectory)', '$(DOTNET_INSTALL_DIR)')) - $([MSBuild]::EnsureTrailingSlash('$(_DotNetInstallBase)\$(MicrosoftNETSdkPackageVersion)\$(Configuration)')) + $([MSBuild]::EnsureTrailingSlash('$(_DotNetInstallBase)\$(MicrosoftNETSdkPackageVersion)')) $([System.IO.File]::ReadAllText('$(BuildOutputDirectory)dotnet-install-location.txt').Trim()) $(BuildOutputDirectory)dotnet\ $(DotNetPreviewPath)dotnet diff --git a/Documentation/building/unix/instructions.md b/Documentation/building/unix/instructions.md index 8c95b8d0ae5..318e3df70da 100644 --- a/Documentation/building/unix/instructions.md +++ b/Documentation/building/unix/instructions.md @@ -65,9 +65,10 @@ To share the provisioned SDK between multiple worktrees, set $ make prepare The SDK will be installed under -`$DOTNET_INSTALL_DIR//`. The override is ignored -when `TF_BUILD`, `GITHUB_ACTIONS`, or `CI` is set, so CI builds and local builds -without `DOTNET_INSTALL_DIR` continue to use `bin/$(Configuration)/dotnet`. +`$DOTNET_INSTALL_DIR/`, shared by Debug and Release builds. The +override is ignored when `TF_BUILD`, `GITHUB_ACTIONS`, or `CI` is set, so CI +builds and local builds without `DOTNET_INSTALL_DIR` continue to use +`bin/$(Configuration)/dotnet`. Once `make all` or `make jenkins` have completed, your local `bin/$(Configuration)/lib/packs` directory will be populated with a diff --git a/Documentation/building/windows/instructions.md b/Documentation/building/windows/instructions.md index 5402ad19997..1c46447ac0d 100644 --- a/Documentation/building/windows/instructions.md +++ b/Documentation/building/windows/instructions.md @@ -102,9 +102,9 @@ target: dotnet msbuild Xamarin.Android.slnx -t:Prepare The SDK will be installed under -`$env:DOTNET_INSTALL_DIR\\`. The override is -ignored when `TF_BUILD`, `GITHUB_ACTIONS`, or `CI` is set, so CI builds and -local builds without `DOTNET_INSTALL_DIR` continue to use +`$env:DOTNET_INSTALL_DIR\`, shared by Debug and Release builds. +The override is ignored when `TF_BUILD`, `GITHUB_ACTIONS`, or `CI` is set, so +CI builds and local builds without `DOTNET_INSTALL_DIR` continue to use `bin\$(Configuration)\dotnet`. See the [One .NET Documentation](../../guides/OneDotNet.md) for further details. diff --git a/eng/install-dotnet.ps1 b/eng/install-dotnet.ps1 index 0f00bc6f52c..05922eb0310 100644 --- a/eng/install-dotnet.ps1 +++ b/eng/install-dotnet.ps1 @@ -8,7 +8,7 @@ so global.json does not need a 'tools.dotnet' pin. Set DOTNET_INSTALL_DIR to a shared base directory to install the SDK under - \\\. + \\. #> [CmdletBinding(PositionalBinding=$false)] param( @@ -37,8 +37,12 @@ $useSharedInstall = -not [string]::IsNullOrWhiteSpace($env:DOTNET_INSTALL_DIR) - if (-not $useSharedInstall) { $installDir = Join-Path $repoRoot "bin\$configuration\dotnet" } else { - $installBase = [IO.Path]::GetFullPath((Join-Path $repoRoot $env:DOTNET_INSTALL_DIR)) - $installDir = Join-Path $installBase "$sdkVersion\$configuration" + if ([IO.Path]::IsPathRooted($env:DOTNET_INSTALL_DIR)) { + $installBase = [IO.Path]::GetFullPath($env:DOTNET_INSTALL_DIR) + } else { + $installBase = [IO.Path]::GetFullPath((Join-Path $repoRoot $env:DOTNET_INSTALL_DIR)) + } + $installDir = Join-Path $installBase $sdkVersion } New-Item -ItemType Directory -Force -Path $installDir | Out-Null diff --git a/eng/install-dotnet.sh b/eng/install-dotnet.sh index ce65d203c09..438772d3630 100644 --- a/eng/install-dotnet.sh +++ b/eng/install-dotnet.sh @@ -7,9 +7,9 @@ # so global.json does not need a 'tools.dotnet' pin. # # Inputs (env vars): -# CONFIGURATION - Debug (default) or Release; controls install path. +# CONFIGURATION - Debug (default) or Release; controls checkout output. # DOTNET_INSTALL_DIR - Optional shared base directory. The SDK is installed -# under ///. +# under //. # set -euo pipefail @@ -34,7 +34,7 @@ if [[ -n "${DOTNET_INSTALL_DIR:-}" && -z "${TF_BUILD:-}" && -z "${GITHUB_ACTIONS fi mkdir -p "$install_base" install_base="$(cd -P "$install_base" && pwd)" - install_dir="$install_base/$sdk_version/$configuration" + install_dir="$install_base/$sdk_version" else install_dir="$repo_root/bin/$configuration/dotnet" fi From b8574833b3e8b2f05725131bc1c9ec67662d32b6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 25 Aug 2026 08:32:14 +0200 Subject: [PATCH 3/5] Address shared SDK review feedback Use a repository-specific environment variable for the shared SDK base and prefer the persisted per-configuration install location during MSBuild evaluation.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Directory.Build.props | 4 ++-- Documentation/building/unix/instructions.md | 13 +++++++------ Documentation/building/windows/instructions.md | 14 +++++++------- eng/install-dotnet.ps1 | 12 ++++++------ eng/install-dotnet.sh | 12 ++++++------ 5 files changed, 28 insertions(+), 27 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 1fab676d3af..bbde47e742b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,9 +16,9 @@ $(MSBuildThisFileDirectory)bin\Test$(Configuration)\ $(BootstrapOutputDirectory)$(DotNetStableTargetFramework)\Xamarin.Android.Tools.BootstrapTasks.dll $(BootstrapOutputDirectory)$(DotNetStableTargetFramework)\xa-prep-tasks.dll - <_DotNetInstallBase Condition=" '$(DOTNET_INSTALL_DIR)' != '' and '$(TF_BUILD)' == '' and '$(GITHUB_ACTIONS)' == '' and '$(CI)' == '' ">$([System.IO.Path]::Combine('$(MSBuildThisFileDirectory)', '$(DOTNET_INSTALL_DIR)')) - $([MSBuild]::EnsureTrailingSlash('$(_DotNetInstallBase)\$(MicrosoftNETSdkPackageVersion)')) $([System.IO.File]::ReadAllText('$(BuildOutputDirectory)dotnet-install-location.txt').Trim()) + <_DotNetSharedInstallBase Condition=" '$(XA_DOTNET_SHARED_INSTALL_BASE)' != '' and '$(TF_BUILD)' == '' and '$(GITHUB_ACTIONS)' == '' and '$(CI)' == '' ">$([System.IO.Path]::Combine('$(MSBuildThisFileDirectory)', '$(XA_DOTNET_SHARED_INSTALL_BASE)')) + $([MSBuild]::EnsureTrailingSlash('$(_DotNetSharedInstallBase)\$(MicrosoftNETSdkPackageVersion)')) $(BuildOutputDirectory)dotnet\ $(DotNetPreviewPath)dotnet dotnet diff --git a/Documentation/building/unix/instructions.md b/Documentation/building/unix/instructions.md index 318e3df70da..13e76ccb2ee 100644 --- a/Documentation/building/unix/instructions.md +++ b/Documentation/building/unix/instructions.md @@ -59,16 +59,17 @@ on Windows, many of the concepts should still apply: `bin/$(Configuration)/dotnet`. To share the provisioned SDK between multiple worktrees, set -`DOTNET_INSTALL_DIR` to a common base directory before running `make prepare`: +`XA_DOTNET_SHARED_INSTALL_BASE` to a common base directory before running +`make prepare`: - $ export DOTNET_INSTALL_DIR="$HOME/android-dotnet-sdk" + $ export XA_DOTNET_SHARED_INSTALL_BASE="$HOME/android-dotnet-sdk" $ make prepare The SDK will be installed under -`$DOTNET_INSTALL_DIR/`, shared by Debug and Release builds. The -override is ignored when `TF_BUILD`, `GITHUB_ACTIONS`, or `CI` is set, so CI -builds and local builds without `DOTNET_INSTALL_DIR` continue to use -`bin/$(Configuration)/dotnet`. +`$XA_DOTNET_SHARED_INSTALL_BASE/`, shared by Debug and Release +builds. The override is ignored when `TF_BUILD`, `GITHUB_ACTIONS`, or `CI` is +set, so CI builds and local builds without `XA_DOTNET_SHARED_INSTALL_BASE` +continue to use `bin/$(Configuration)/dotnet`. Once `make all` or `make jenkins` have completed, your local `bin/$(Configuration)/lib/packs` directory will be populated with a diff --git a/Documentation/building/windows/instructions.md b/Documentation/building/windows/instructions.md index 1c46447ac0d..55379db1bbb 100644 --- a/Documentation/building/windows/instructions.md +++ b/Documentation/building/windows/instructions.md @@ -95,17 +95,17 @@ Using the `dotnet-local` script will execute the provisioned `dotnet` and will use the locally built binaries. To share the provisioned SDK between multiple worktrees, set -`DOTNET_INSTALL_DIR` to a common base directory before running the `Prepare` -target: +`XA_DOTNET_SHARED_INSTALL_BASE` to a common base directory before running the +`Prepare` target: - $env:DOTNET_INSTALL_DIR = Join-Path $HOME 'android-dotnet-sdk' + $env:XA_DOTNET_SHARED_INSTALL_BASE = Join-Path $HOME 'android-dotnet-sdk' dotnet msbuild Xamarin.Android.slnx -t:Prepare The SDK will be installed under -`$env:DOTNET_INSTALL_DIR\`, shared by Debug and Release builds. -The override is ignored when `TF_BUILD`, `GITHUB_ACTIONS`, or `CI` is set, so -CI builds and local builds without `DOTNET_INSTALL_DIR` continue to use -`bin\$(Configuration)\dotnet`. +`$env:XA_DOTNET_SHARED_INSTALL_BASE\`, shared by Debug and Release +builds. The override is ignored when `TF_BUILD`, `GITHUB_ACTIONS`, or `CI` is +set, so CI builds and local builds without `XA_DOTNET_SHARED_INSTALL_BASE` +continue to use `bin\$(Configuration)\dotnet`. See the [One .NET Documentation](../../guides/OneDotNet.md) for further details. diff --git a/eng/install-dotnet.ps1 b/eng/install-dotnet.ps1 index 05922eb0310..02c9939b3bb 100644 --- a/eng/install-dotnet.ps1 +++ b/eng/install-dotnet.ps1 @@ -7,8 +7,8 @@ kept up to date by darc when Microsoft.NET.Sdk flows from dotnet/dotnet), so global.json does not need a 'tools.dotnet' pin. - Set DOTNET_INSTALL_DIR to a shared base directory to install the SDK under - \\. + Set XA_DOTNET_SHARED_INSTALL_BASE to a shared base directory to install the + SDK under \\. #> [CmdletBinding(PositionalBinding=$false)] param( @@ -29,7 +29,7 @@ if ($null -eq $sdkNode -or [string]::IsNullOrWhiteSpace($sdkNode.InnerText)) { } $sdkVersion = $sdkNode.InnerText -$useSharedInstall = -not [string]::IsNullOrWhiteSpace($env:DOTNET_INSTALL_DIR) -and +$useSharedInstall = -not [string]::IsNullOrWhiteSpace($env:XA_DOTNET_SHARED_INSTALL_BASE) -and [string]::IsNullOrEmpty($env:TF_BUILD) -and [string]::IsNullOrEmpty($env:GITHUB_ACTIONS) -and [string]::IsNullOrEmpty($env:CI) @@ -37,10 +37,10 @@ $useSharedInstall = -not [string]::IsNullOrWhiteSpace($env:DOTNET_INSTALL_DIR) - if (-not $useSharedInstall) { $installDir = Join-Path $repoRoot "bin\$configuration\dotnet" } else { - if ([IO.Path]::IsPathRooted($env:DOTNET_INSTALL_DIR)) { - $installBase = [IO.Path]::GetFullPath($env:DOTNET_INSTALL_DIR) + if ([IO.Path]::IsPathRooted($env:XA_DOTNET_SHARED_INSTALL_BASE)) { + $installBase = [IO.Path]::GetFullPath($env:XA_DOTNET_SHARED_INSTALL_BASE) } else { - $installBase = [IO.Path]::GetFullPath((Join-Path $repoRoot $env:DOTNET_INSTALL_DIR)) + $installBase = [IO.Path]::GetFullPath((Join-Path $repoRoot $env:XA_DOTNET_SHARED_INSTALL_BASE)) } $installDir = Join-Path $installBase $sdkVersion } diff --git a/eng/install-dotnet.sh b/eng/install-dotnet.sh index 438772d3630..1d02d330db1 100644 --- a/eng/install-dotnet.sh +++ b/eng/install-dotnet.sh @@ -8,8 +8,8 @@ # # Inputs (env vars): # CONFIGURATION - Debug (default) or Release; controls checkout output. -# DOTNET_INSTALL_DIR - Optional shared base directory. The SDK is installed -# under //. +# XA_DOTNET_SHARED_INSTALL_BASE - Optional shared base directory. The SDK is +# installed under //. # set -euo pipefail @@ -26,11 +26,11 @@ if [[ -z "$sdk_version" ]]; then exit 1 fi -if [[ -n "${DOTNET_INSTALL_DIR:-}" && -z "${TF_BUILD:-}" && -z "${GITHUB_ACTIONS:-}" && -z "${CI:-}" ]]; then - if [[ "$DOTNET_INSTALL_DIR" = /* ]]; then - install_base="$DOTNET_INSTALL_DIR" +if [[ -n "${XA_DOTNET_SHARED_INSTALL_BASE:-}" && -z "${TF_BUILD:-}" && -z "${GITHUB_ACTIONS:-}" && -z "${CI:-}" ]]; then + if [[ "$XA_DOTNET_SHARED_INSTALL_BASE" = /* ]]; then + install_base="$XA_DOTNET_SHARED_INSTALL_BASE" else - install_base="$repo_root/$DOTNET_INSTALL_DIR" + install_base="$repo_root/$XA_DOTNET_SHARED_INSTALL_BASE" fi mkdir -p "$install_base" install_base="$(cd -P "$install_base" && pwd)" From 0ba1067664217964b96a78341e4df2803cf6136e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 25 Aug 2026 11:35:38 +0200 Subject: [PATCH 4/5] Keep shared dotnet SDK cache immutable Route workload manifests, packs, metadata, CLI state, and temporary files to configuration-local roots when using a shared SDK. Preserve the existing local and CI layouts, reject stale SDK pointers, and enable the SDK's user-local workload mode only for shared installs.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: c73b6275-d847-40c9-9d3e-cf15cfdee0ee --- Configuration.props | 2 ++ Directory.Build.props | 19 ++++++++++-- Documentation/building/unix/instructions.md | 8 +++-- .../building/windows/instructions.md | 8 +++-- .../ConfigureLocalWorkload.targets | 4 ++- .../create-packs/Directory.Build.targets | 29 ++++++++++--------- build-tools/scripts/DotNet.targets | 10 ++++--- build-tools/scripts/Prepare.proj | 2 ++ build-tools/scripts/msbuild.mk | 17 ++++++++++- dotnet-local.cmd | 11 ++++++- dotnet-local.sh | 13 ++++++++- eng/install-dotnet.ps1 | 17 +++++++++-- eng/install-dotnet.sh | 22 +++++++++++++- src/workloads/workloads.csproj | 10 +++---- 14 files changed, 132 insertions(+), 40 deletions(-) diff --git a/Configuration.props b/Configuration.props index c6f829d80d2..06fcb18457c 100644 --- a/Configuration.props +++ b/Configuration.props @@ -150,6 +150,8 @@ $(NUGET_PACKAGES) $(userprofile)\.nuget\packages $(HOME)/.nuget/packages + <_DotNetWorkloadEnvironment Condition=" '$(_DotNetUsingSharedSdk)' == 'true' ">DOTNET_CLI_HOME=$(_DotNetWorkloadUserHome);DOTNETSDK_WORKLOAD_MANIFEST_ROOTS=$(_DotNetWorkloadManifestRoot);DOTNETSDK_WORKLOAD_PACK_ROOTS=$(_DotNetWorkloadPackRoot) + <_DotNetBuildEnvironment Condition=" '$(_DotNetUsingSharedSdk)' == 'true' ">$(_DotNetWorkloadEnvironment);NUGET_PACKAGES=$([MSBuild]::EnsureTrailingSlash('$(XAPackagesDir)')) $([System.IO.Path]::PathSeparator) <_TestsAotName Condition=" '$(AotAssemblies)' == 'true' ">-Aot <_TestsProfiledAotName Condition=" '$(AndroidEnableProfiledAot)' == 'true' ">-Profiled diff --git a/Directory.Build.props b/Directory.Build.props index bbde47e742b..0b98012337c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,12 +16,25 @@ $(MSBuildThisFileDirectory)bin\Test$(Configuration)\ $(BootstrapOutputDirectory)$(DotNetStableTargetFramework)\Xamarin.Android.Tools.BootstrapTasks.dll $(BootstrapOutputDirectory)$(DotNetStableTargetFramework)\xa-prep-tasks.dll - $([System.IO.File]::ReadAllText('$(BuildOutputDirectory)dotnet-install-location.txt').Trim()) + <_DotNetInstallLocationFile>$(BuildOutputDirectory)dotnet-install-location.txt + <_DotNetInstallLocation Condition=" '$(TF_BUILD)' == '' and '$(GITHUB_ACTIONS)' == '' and '$(CI)' == '' and Exists('$(_DotNetInstallLocationFile)') ">$([System.IO.File]::ReadAllText('$(_DotNetInstallLocationFile)').Trim()) <_DotNetSharedInstallBase Condition=" '$(XA_DOTNET_SHARED_INSTALL_BASE)' != '' and '$(TF_BUILD)' == '' and '$(GITHUB_ACTIONS)' == '' and '$(CI)' == '' ">$([System.IO.Path]::Combine('$(MSBuildThisFileDirectory)', '$(XA_DOTNET_SHARED_INSTALL_BASE)')) - $([MSBuild]::EnsureTrailingSlash('$(_DotNetSharedInstallBase)\$(MicrosoftNETSdkPackageVersion)')) + <_DotNetSharedInstallPath Condition=" '$(_DotNetInstallLocation)' != '' and Exists('$(_DotNetInstallLocation)sdk\$(MicrosoftNETSdkPackageVersion)') ">$(_DotNetInstallLocation) + <_DotNetSharedInstallPath Condition=" '$(_DotNetSharedInstallPath)' == '' and '$(_DotNetSharedInstallBase)' != '' ">$(_DotNetSharedInstallBase)\$(MicrosoftNETSdkPackageVersion) + <_DotNetUsingSharedSdk Condition=" '$(_DotNetSharedInstallPath)' != '' ">true + $([MSBuild]::EnsureTrailingSlash('$(_DotNetSharedInstallPath)')) $(BuildOutputDirectory)dotnet\ - $(DotNetPreviewPath)dotnet + $(DotNetPreviewPath)dotnet dotnet + <_DotNetWorkloadPackRoot Condition=" '$(_DotNetUsingSharedSdk)' == 'true' ">$(BuildOutputDirectory)lib\ + <_DotNetWorkloadPackRoot Condition=" '$(_DotNetWorkloadPackRoot)' == '' ">$(DotNetPreviewPath) + <_DotNetWorkloadManifestRoot Condition=" '$(_DotNetUsingSharedSdk)' == 'true' ">$(_DotNetWorkloadPackRoot)sdk-manifests\ + <_DotNetWorkloadManifestRoot Condition=" '$(_DotNetWorkloadManifestRoot)' == '' ">$(DotNetPreviewPath)sdk-manifests\ + <_DotNetWorkloadUserHome Condition=" '$(_DotNetUsingSharedSdk)' == 'true' ">$(BuildOutputDirectory)dotnet-home\ + <_DotNetWorkloadUserProfileRoot Condition=" '$(_DotNetUsingSharedSdk)' == 'true' ">$(_DotNetWorkloadUserHome).dotnet\ + <_DotNetWorkloadUserProfileRoot Condition=" '$(_DotNetWorkloadUserProfileRoot)' == '' ">$(DotNetPreviewPath) + <_DotNetWorkloadUserManifestRoot>$(_DotNetWorkloadUserProfileRoot)sdk-manifests\ + <_DotNetWorkloadUserPackRoot>$(_DotNetWorkloadUserProfileRoot)packs\ true diff --git a/Documentation/building/unix/instructions.md b/Documentation/building/unix/instructions.md index 13e76ccb2ee..21e0968c309 100644 --- a/Documentation/building/unix/instructions.md +++ b/Documentation/building/unix/instructions.md @@ -67,9 +67,11 @@ To share the provisioned SDK between multiple worktrees, set The SDK will be installed under `$XA_DOTNET_SHARED_INSTALL_BASE/`, shared by Debug and Release -builds. The override is ignored when `TF_BUILD`, `GITHUB_ACTIONS`, or `CI` is -set, so CI builds and local builds without `XA_DOTNET_SHARED_INSTALL_BASE` -continue to use `bin/$(Configuration)/dotnet`. +builds. The shared SDK is treated as read-only after provisioning; workload +packs, manifests, and installation state remain under `bin/$(Configuration)`. +The override is ignored when `TF_BUILD`, `GITHUB_ACTIONS`, or `CI` is set, so +CI builds and local builds without `XA_DOTNET_SHARED_INSTALL_BASE` continue to +use `bin/$(Configuration)/dotnet`. Once `make all` or `make jenkins` have completed, your local `bin/$(Configuration)/lib/packs` directory will be populated with a diff --git a/Documentation/building/windows/instructions.md b/Documentation/building/windows/instructions.md index 55379db1bbb..cd6723e706e 100644 --- a/Documentation/building/windows/instructions.md +++ b/Documentation/building/windows/instructions.md @@ -103,9 +103,11 @@ To share the provisioned SDK between multiple worktrees, set The SDK will be installed under `$env:XA_DOTNET_SHARED_INSTALL_BASE\`, shared by Debug and Release -builds. The override is ignored when `TF_BUILD`, `GITHUB_ACTIONS`, or `CI` is -set, so CI builds and local builds without `XA_DOTNET_SHARED_INSTALL_BASE` -continue to use `bin\$(Configuration)\dotnet`. +builds. The shared SDK is treated as read-only after provisioning; workload +packs, manifests, and installation state remain under `bin\$(Configuration)`. +The override is ignored when `TF_BUILD`, `GITHUB_ACTIONS`, or `CI` is set, so +CI builds and local builds without `XA_DOTNET_SHARED_INSTALL_BASE` continue to +use `bin\$(Configuration)\dotnet`. See the [One .NET Documentation](../../guides/OneDotNet.md) for further details. diff --git a/build-tools/create-packs/ConfigureLocalWorkload.targets b/build-tools/create-packs/ConfigureLocalWorkload.targets index 1cb543b424b..81ee12867ea 100644 --- a/build-tools/create-packs/ConfigureLocalWorkload.targets +++ b/build-tools/create-packs/ConfigureLocalWorkload.targets @@ -95,6 +95,8 @@ <_LocalSdkManifestsFolder>$(BuildOutputDirectory)lib\sdk-manifests\$(_LocalSdkFeatureBand)\ <_LocalAndroidManifestFolder>$(_LocalSdkManifestsFolder)microsoft.net.sdk.android\$(AndroidPackVersionLong)\ <_EmptyWorkloadDir>$(_LocalSdkManifestsFolder)android.deps.workload\0.0.1\ + <_InstallManifestEnvironment Condition=" '$(_DotNetUsingSharedSdk)' == 'true' ">$(_DotNetBuildEnvironment) + <_InstallManifestEnvironment Condition=" '$(_InstallManifestEnvironment)' == '' ">DOTNETSDK_WORKLOAD_MANIFEST_ROOTS=$(BuildOutputDirectory)lib\sdk-manifests @@ -128,7 +130,7 @@ /> diff --git a/build-tools/create-packs/Directory.Build.targets b/build-tools/create-packs/Directory.Build.targets index f69919ef4b2..53f99c2649b 100644 --- a/build-tools/create-packs/Directory.Build.targets +++ b/build-tools/create-packs/Directory.Build.targets @@ -88,7 +88,7 @@ <_WLManifest Include="$(XamarinAndroidSourcePath)bin\Build$(Configuration)\nuget-unsigned\Microsoft.NET.Sdk.Android.Manifest-*.nupkg" /> - <_SdkManifestsFolder>$(DotNetPreviewPath)sdk-manifests\$(DotNetSdkManifestsFolder)\ + <_SdkManifestsFolder>$(_DotNetWorkloadUserManifestRoot)$(DotNetSdkManifestsFolder)\ <_InstallArguments Include="--temp-dir "$(_TempDirectory)"" /> - + @@ -151,18 +152,18 @@ <_PackApiLevels Include="$(AndroidLatestUnstableApiLevel)" /> - <_PackFoldersToDelete Include="$(DotNetPreviewPath)metadata" /> - <_PackFoldersToDelete Include="$(DotNetPreviewPath)sdk-manifests\$(DotNetSdkManifestsFolder)\workloadsets" /> - <_PackFoldersToDelete Include="$(DotNetPreviewPath)sdk-manifests\$(DotNetSdkManifestsFolder)\microsoft.net.sdk.android" /> - <_PackFoldersToDelete Include="$(DotNetPreviewPath)packs\Microsoft.Android.Ref.%(_PackApiLevels.Identity)" /> - <_PackFoldersToDelete Include="$(DotNetPreviewPath)packs\Microsoft.Android.Runtime.Mono.%(_PackApiLevels.Identity).android-arm" /> - <_PackFoldersToDelete Include="$(DotNetPreviewPath)packs\Microsoft.Android.Runtime.Mono.%(_PackApiLevels.Identity).android-arm64" /> - <_PackFoldersToDelete Include="$(DotNetPreviewPath)packs\Microsoft.Android.Runtime.Mono.%(_PackApiLevels.Identity).android-x86" /> - <_PackFoldersToDelete Include="$(DotNetPreviewPath)packs\Microsoft.Android.Runtime.Mono.%(_PackApiLevels.Identity).android-x64" /> - <_PackFoldersToDelete Include="$(DotNetPreviewPath)packs\Microsoft.Android.Sdk.Darwin" /> - <_PackFoldersToDelete Include="$(DotNetPreviewPath)packs\Microsoft.Android.Sdk.Linux" /> - <_PackFoldersToDelete Include="$(DotNetPreviewPath)packs\Microsoft.Android.Sdk.Windows" /> - <_PackFoldersToDelete Include="$(DotNetPreviewPath)template-packs" /> + <_PackFoldersToDelete Include="$(_DotNetWorkloadUserProfileRoot)metadata" /> + <_PackFoldersToDelete Include="$(_DotNetWorkloadUserManifestRoot)$(DotNetSdkManifestsFolder)\workloadsets" /> + <_PackFoldersToDelete Include="$(_DotNetWorkloadUserManifestRoot)$(DotNetSdkManifestsFolder)\microsoft.net.sdk.android" /> + <_PackFoldersToDelete Include="$(_DotNetWorkloadUserPackRoot)Microsoft.Android.Ref.%(_PackApiLevels.Identity)" /> + <_PackFoldersToDelete Include="$(_DotNetWorkloadUserPackRoot)Microsoft.Android.Runtime.Mono.%(_PackApiLevels.Identity).android-arm" /> + <_PackFoldersToDelete Include="$(_DotNetWorkloadUserPackRoot)Microsoft.Android.Runtime.Mono.%(_PackApiLevels.Identity).android-arm64" /> + <_PackFoldersToDelete Include="$(_DotNetWorkloadUserPackRoot)Microsoft.Android.Runtime.Mono.%(_PackApiLevels.Identity).android-x86" /> + <_PackFoldersToDelete Include="$(_DotNetWorkloadUserPackRoot)Microsoft.Android.Runtime.Mono.%(_PackApiLevels.Identity).android-x64" /> + <_PackFoldersToDelete Include="$(_DotNetWorkloadUserPackRoot)Microsoft.Android.Sdk.Darwin" /> + <_PackFoldersToDelete Include="$(_DotNetWorkloadUserPackRoot)Microsoft.Android.Sdk.Linux" /> + <_PackFoldersToDelete Include="$(_DotNetWorkloadUserPackRoot)Microsoft.Android.Sdk.Windows" /> + <_PackFoldersToDelete Include="$(_DotNetWorkloadUserProfileRoot)template-packs" /> diff --git a/build-tools/scripts/DotNet.targets b/build-tools/scripts/DotNet.targets index 6be7b0637be..07c0d814a2f 100644 --- a/build-tools/scripts/DotNet.targets +++ b/build-tools/scripts/DotNet.targets @@ -17,6 +17,7 @@ @@ -25,6 +26,7 @@ DependsOnTargets="PrepareOpenJDK"> @@ -92,7 +94,7 @@ - <_TempDirectory>$(DotNetPreviewPath)..\.xa-workload-temp-$([System.IO.Path]::GetRandomFileName()) + <_TempDirectory>$(BuildOutputDirectory)obj\.xa-workload-temp-$([System.IO.Path]::GetRandomFileName()) $(DotNetSdkManifestsFolder) @@ -102,7 +104,7 @@ Condition=" '$(MauiUseLocalPacks)' != 'true' " Command=""$(DotNetPreviewTool)" restore maui.proj -p:MauiVersion=$(MauiVersion) -p:MauiVersionBand=$(MauiVersionBand)" WorkingDirectory="$(MSBuildThisFileDirectory)" - EnvironmentVariables="NUGET_PACKAGES=$(_TempDirectory);DOTNET_MULTILEVEL_LOOKUP=0" + EnvironmentVariables="NUGET_PACKAGES=$(_TempDirectory);DOTNET_MULTILEVEL_LOOKUP=0;$(_DotNetWorkloadEnvironment)" /> <_WLManifestPack Include="$(MauiPackagePath)\Microsoft.NET.Sdk.Maui.Manifest-$(MauiVersionBand.Substring (0,3))*.nupkg" /> @@ -120,7 +122,7 @@ - + diff --git a/build-tools/scripts/Prepare.proj b/build-tools/scripts/Prepare.proj index 8c51b180699..47e89706af4 100644 --- a/build-tools/scripts/Prepare.proj +++ b/build-tools/scripts/Prepare.proj @@ -22,10 +22,12 @@ diff --git a/build-tools/scripts/msbuild.mk b/build-tools/scripts/msbuild.mk index e8e8e20def1..3296adf4d6b 100644 --- a/build-tools/scripts/msbuild.mk +++ b/build-tools/scripts/msbuild.mk @@ -24,10 +24,25 @@ MSBUILD = msbuild DOTNET_INSTALL_LOCATION = $(topdir)/bin/$(CONFIGURATION)/dotnet-install-location.txt -DOTNET_ROOT = $(if $(wildcard $(DOTNET_INSTALL_LOCATION)),$(shell cat "$(DOTNET_INSTALL_LOCATION)"),$(topdir)/bin/$(CONFIGURATION)/dotnet/) +DOTNET_SDK_VERSION = $(shell sed -n 's|.*\([^<]*\).*|\1|p' "$(topdir)/eng/Versions.props" | head -n 1) +ifeq ($(strip $(TF_BUILD)$(GITHUB_ACTIONS)$(CI)),) +_DOTNET_SHARED_ROOT = $(if $(wildcard $(DOTNET_INSTALL_LOCATION)),$(shell root=$$(cat "$(DOTNET_INSTALL_LOCATION)"); test -x "$${root}dotnet" && test -d "$${root}sdk/$(DOTNET_SDK_VERSION)" && printf '%s' "$$root")) +endif +DOTNET_ROOT = $(if $(_DOTNET_SHARED_ROOT),$(_DOTNET_SHARED_ROOT),$(topdir)/bin/$(CONFIGURATION)/dotnet/) DOTNET_TOOL = $(DOTNET_ROOT)dotnet DOTNET_VERB = build MSBUILD_FLAGS = /p:Configuration=$(CONFIGURATION) $(MSBUILD_ARGS) +ifneq ($(_DOTNET_SHARED_ROOT),) +DOTNET_CLI_HOME = $(topdir)/bin/$(CONFIGURATION)/dotnet-home +DOTNETSDK_WORKLOAD_MANIFEST_ROOTS = $(topdir)/bin/$(CONFIGURATION)/lib/sdk-manifests +DOTNETSDK_WORKLOAD_PACK_ROOTS = $(topdir)/bin/$(CONFIGURATION)/lib +NUGET_PACKAGES := $(patsubst %/,%,$(if $(NUGET_PACKAGES),$(NUGET_PACKAGES),$(HOME)/.nuget/packages))/ + +export DOTNET_CLI_HOME +export DOTNETSDK_WORKLOAD_MANIFEST_ROOTS +export DOTNETSDK_WORKLOAD_PACK_ROOTS +export NUGET_PACKAGES +endif ifeq ($(OS_NAME),Darwin) _PKG_CONFIG = /Library/Frameworks/Mono.framework/Commands/pkg-config diff --git a/dotnet-local.cmd b/dotnet-local.cmd index fa86a51ae59..057b5be3aad 100644 --- a/dotnet-local.cmd +++ b/dotnet-local.cmd @@ -4,6 +4,10 @@ SETLOCAL SET ROOT=%~dp0 SET XA_CONFIG= SET XA_DOTNET_ROOT= +SET XA_DOTNET_SDK_VERSION= +SET XA_USING_SHARED_DOTNET= + +FOR /F "tokens=2 delims=<>" %%V IN ('findstr /C:"" "%ROOT%\eng\Versions.props"') DO SET XA_DOTNET_SDK_VERSION=%%V CALL :find_dotnet Release IF DEFINED XA_CONFIG GOTO :dotnet_found @@ -15,6 +19,10 @@ GOTO :exit :dotnet_found SET PATH=%XA_DOTNET_ROOT%;%PATH% +IF DEFINED XA_USING_SHARED_DOTNET ( + IF NOT DEFINED NUGET_PACKAGES SET NUGET_PACKAGES=%USERPROFILE%\.nuget\packages\ + SET DOTNET_CLI_HOME=%ROOT%\bin\%XA_CONFIG%\dotnet-home +) SET DOTNETSDK_WORKLOAD_MANIFEST_ROOTS=%ROOT%\bin\%XA_CONFIG%\lib\sdk-manifests SET DOTNETSDK_WORKLOAD_PACK_ROOTS=%ROOT%\bin\%XA_CONFIG%\lib @@ -24,7 +32,8 @@ GOTO :exit :find_dotnet SET XA_DOTNET_ROOT= IF EXIST "%ROOT%\bin\%1\dotnet-install-location.txt" SET /P XA_DOTNET_ROOT=<"%ROOT%\bin\%1\dotnet-install-location.txt" -IF NOT DEFINED XA_DOTNET_ROOT SET XA_DOTNET_ROOT=%ROOT%\bin\%1\dotnet\ +IF EXIST "%XA_DOTNET_ROOT%\dotnet.exe" IF EXIST "%XA_DOTNET_ROOT%\sdk\%XA_DOTNET_SDK_VERSION%\" SET XA_USING_SHARED_DOTNET=true +IF NOT DEFINED XA_USING_SHARED_DOTNET SET XA_DOTNET_ROOT=%ROOT%\bin\%1\dotnet\ IF EXIST "%XA_DOTNET_ROOT%\dotnet.exe" SET XA_CONFIG=%1 EXIT /B diff --git a/dotnet-local.sh b/dotnet-local.sh index dd545df3abb..b6afe050fcd 100755 --- a/dotnet-local.sh +++ b/dotnet-local.sh @@ -1,17 +1,28 @@ #!/bin/bash ROOT="$(dirname "${BASH_SOURCE}")" FULLROOT="$(cd "${ROOT}"; pwd)" +sdk_version="$(sed -n 's|.*\([^<]*\).*|\1|p' "${FULLROOT}/eng/Versions.props" | head -n 1)" for config in Release Debug ; do + XA_USING_SHARED_DOTNET=false install_location="${FULLROOT}/bin/${config}/dotnet-install-location.txt" if [[ -f "${install_location}" ]] ; then IFS= read -r XA_DOTNET_ROOT < "${install_location}" - else + if [[ -x "${XA_DOTNET_ROOT}/dotnet" && -d "${XA_DOTNET_ROOT}/sdk/${sdk_version}" ]] ; then + XA_USING_SHARED_DOTNET=true + fi + fi + if [[ "$XA_USING_SHARED_DOTNET" != true ]] ; then XA_DOTNET_ROOT="${FULLROOT}/bin/${config}/dotnet" fi if [[ ! -x "${XA_DOTNET_ROOT}/dotnet" ]] ; then continue fi export PATH="${XA_DOTNET_ROOT}:${PATH}" + if [[ "$XA_USING_SHARED_DOTNET" == true ]] ; then + NUGET_PACKAGES="${NUGET_PACKAGES:-${HOME}/.nuget/packages}" + export NUGET_PACKAGES="${NUGET_PACKAGES%/}/" + export DOTNET_CLI_HOME="${FULLROOT}/bin/${config}/dotnet-home" + fi export DOTNETSDK_WORKLOAD_MANIFEST_ROOTS="${FULLROOT}/bin/${config}/lib/sdk-manifests" export DOTNETSDK_WORKLOAD_PACK_ROOTS="${FULLROOT}/bin/${config}/lib" exec "${XA_DOTNET_ROOT}/dotnet" "$@" diff --git a/eng/install-dotnet.ps1 b/eng/install-dotnet.ps1 index 02c9939b3bb..3c46ed2b07a 100644 --- a/eng/install-dotnet.ps1 +++ b/eng/install-dotnet.ps1 @@ -28,6 +28,8 @@ if ($null -eq $sdkNode -or [string]::IsNullOrWhiteSpace($sdkNode.InnerText)) { exit 1 } $sdkVersion = $sdkNode.InnerText +$sdkBaseVersion = [Version]($sdkVersion.Split('-', 2)[0]) +$sdkFeatureBand = '{0}.{1}.{2}' -f $sdkBaseVersion.Major, $sdkBaseVersion.Minor, ([Math]::Floor($sdkBaseVersion.Build / 100) * 100) $useSharedInstall = -not [string]::IsNullOrWhiteSpace($env:XA_DOTNET_SHARED_INSTALL_BASE) -and [string]::IsNullOrEmpty($env:TF_BUILD) -and @@ -71,6 +73,15 @@ if ($LASTEXITCODE -ne 0) { } $installLocationFile = Join-Path $repoRoot "bin\$configuration\dotnet-install-location.txt" -$installLocation = $installDir.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar -New-Item -ItemType Directory -Force -Path (Split-Path -Parent $installLocationFile) | Out-Null -Set-Content -LiteralPath $installLocationFile -Value $installLocation -NoNewline +if ($useSharedInstall) { + $userLocalMarker = Join-Path $installDir "metadata\workloads\$sdkFeatureBand\userlocal" + if (-not (Test-Path $userLocalMarker)) { + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $userLocalMarker) | Out-Null + New-Item -ItemType File -Path $userLocalMarker | Out-Null + } + $installLocation = $installDir.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $installLocationFile) | Out-Null + Set-Content -LiteralPath $installLocationFile -Value $installLocation -NoNewline +} elseif (Test-Path $installLocationFile) { + Remove-Item -LiteralPath $installLocationFile -Force +} diff --git a/eng/install-dotnet.sh b/eng/install-dotnet.sh index 1d02d330db1..674af8ee222 100644 --- a/eng/install-dotnet.sh +++ b/eng/install-dotnet.sh @@ -26,7 +26,17 @@ if [[ -z "$sdk_version" ]]; then exit 1 fi +sdk_version_core="${sdk_version%%-*}" +IFS=. read -r sdk_major sdk_minor sdk_patch _ <<< "$sdk_version_core" +if [[ ! "$sdk_major" =~ ^[0-9]+$ || ! "$sdk_minor" =~ ^[0-9]+$ || ! "$sdk_patch" =~ ^[0-9]+$ ]]; then + echo "error: could not determine the SDK feature band from '$sdk_version'" >&2 + exit 1 +fi +sdk_feature_band="$sdk_major.$sdk_minor.$((10#$sdk_patch / 100 * 100))" + +use_shared_install=false if [[ -n "${XA_DOTNET_SHARED_INSTALL_BASE:-}" && -z "${TF_BUILD:-}" && -z "${GITHUB_ACTIONS:-}" && -z "${CI:-}" ]]; then + use_shared_install=true if [[ "$XA_DOTNET_SHARED_INSTALL_BASE" = /* ]]; then install_base="$XA_DOTNET_SHARED_INSTALL_BASE" else @@ -57,4 +67,14 @@ bash "$install_script" --version "$sdk_version" --install-dir "$install_dir" --n install_location_file="$repo_root/bin/$configuration/dotnet-install-location.txt" mkdir -p "$(dirname "$install_location_file")" -printf '%s/\n' "${install_dir%/}" > "$install_location_file" +if [[ "$use_shared_install" == true ]]; then + # Keep workload packs, manifests, and installation records outside the shared SDK. + userlocal_marker="$install_dir/metadata/workloads/$sdk_feature_band/userlocal" + if [[ ! -f "$userlocal_marker" ]]; then + mkdir -p "$(dirname "$userlocal_marker")" + : > "$userlocal_marker" + fi + printf '%s/\n' "${install_dir%/}" > "$install_location_file" +else + rm -f "$install_location_file" +fi diff --git a/src/workloads/workloads.csproj b/src/workloads/workloads.csproj index d35b82dc2dd..4909e4373c8 100644 --- a/src/workloads/workloads.csproj +++ b/src/workloads/workloads.csproj @@ -2,7 +2,7 @@ *********************************************************************************************** workloads.csproj -Provisions the Android-specific .NET workloads against the .NET SDK that +Provisions the Android-specific .NET workloads for the .NET SDK that `eng/install-dotnet.{sh,ps1}` has already installed at `bin/$(Configuration)/dotnet/`: @@ -10,8 +10,8 @@ Provisions the Android-specific .NET workloads against the .NET SDK that * Restores the Mono and Emscripten workload manifest packages (Microsoft.NET.Workload.{Mono,Emscripten}.*.Manifest-*). * Copies the workload manifests from the NuGet package cache into the - local SDK's `sdk-manifests/` folder so the SDK can resolve the - workloads against the locally installed runtime packs. + checkout-local workload root so the SDK can resolve the workloads + against the locally installed runtime packs. Run via: @@ -61,14 +61,14 @@ inside xaprepare. <_DotNetSdk>$(DotNetPreviewPath.TrimEnd('\').TrimEnd('/')) - <_SdkManifests>$(_DotNetSdk)\sdk-manifests + <_SdkManifests>$(_DotNetWorkloadManifestRoot) Date: Tue, 25 Aug 2026 12:58:45 +0200 Subject: [PATCH 5/5] Fix shared SDK wrapper isolation Ignore persisted shared SDK pointers in CI and normalize custom Windows NuGet package paths before invoking the shared SDK. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c73b6275-d847-40c9-9d3e-cf15cfdee0ee --- dotnet-local.cmd | 21 ++++++++++++++++----- dotnet-local.sh | 2 +- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/dotnet-local.cmd b/dotnet-local.cmd index 057b5be3aad..306008f2862 100644 --- a/dotnet-local.cmd +++ b/dotnet-local.cmd @@ -6,6 +6,11 @@ SET XA_CONFIG= SET XA_DOTNET_ROOT= SET XA_DOTNET_SDK_VERSION= SET XA_USING_SHARED_DOTNET= +SET XA_RUNNING_ON_CI= + +IF DEFINED TF_BUILD SET XA_RUNNING_ON_CI=true +IF DEFINED GITHUB_ACTIONS SET XA_RUNNING_ON_CI=true +IF DEFINED CI SET XA_RUNNING_ON_CI=true FOR /F "tokens=2 delims=<>" %%V IN ('findstr /C:"" "%ROOT%\eng\Versions.props"') DO SET XA_DOTNET_SDK_VERSION=%%V @@ -19,10 +24,7 @@ GOTO :exit :dotnet_found SET PATH=%XA_DOTNET_ROOT%;%PATH% -IF DEFINED XA_USING_SHARED_DOTNET ( - IF NOT DEFINED NUGET_PACKAGES SET NUGET_PACKAGES=%USERPROFILE%\.nuget\packages\ - SET DOTNET_CLI_HOME=%ROOT%\bin\%XA_CONFIG%\dotnet-home -) +IF DEFINED XA_USING_SHARED_DOTNET CALL :configure_shared_dotnet SET DOTNETSDK_WORKLOAD_MANIFEST_ROOTS=%ROOT%\bin\%XA_CONFIG%\lib\sdk-manifests SET DOTNETSDK_WORKLOAD_PACK_ROOTS=%ROOT%\bin\%XA_CONFIG%\lib @@ -31,10 +33,19 @@ GOTO :exit :find_dotnet SET XA_DOTNET_ROOT= -IF EXIST "%ROOT%\bin\%1\dotnet-install-location.txt" SET /P XA_DOTNET_ROOT=<"%ROOT%\bin\%1\dotnet-install-location.txt" +IF NOT DEFINED XA_RUNNING_ON_CI IF EXIST "%ROOT%\bin\%1\dotnet-install-location.txt" SET /P XA_DOTNET_ROOT=<"%ROOT%\bin\%1\dotnet-install-location.txt" IF EXIST "%XA_DOTNET_ROOT%\dotnet.exe" IF EXIST "%XA_DOTNET_ROOT%\sdk\%XA_DOTNET_SDK_VERSION%\" SET XA_USING_SHARED_DOTNET=true IF NOT DEFINED XA_USING_SHARED_DOTNET SET XA_DOTNET_ROOT=%ROOT%\bin\%1\dotnet\ IF EXIST "%XA_DOTNET_ROOT%\dotnet.exe" SET XA_CONFIG=%1 EXIT /B +:configure_shared_dotnet +IF NOT DEFINED NUGET_PACKAGES SET NUGET_PACKAGES=%USERPROFILE%\.nuget\packages +IF "%NUGET_PACKAGES:~-1%"=="\" GOTO :shared_dotnet_configured +IF "%NUGET_PACKAGES:~-1%"=="/" GOTO :shared_dotnet_configured +SET NUGET_PACKAGES=%NUGET_PACKAGES%\ +:shared_dotnet_configured +SET DOTNET_CLI_HOME=%ROOT%\bin\%XA_CONFIG%\dotnet-home +EXIT /B + :exit diff --git a/dotnet-local.sh b/dotnet-local.sh index b6afe050fcd..c36fed26c2b 100755 --- a/dotnet-local.sh +++ b/dotnet-local.sh @@ -5,7 +5,7 @@ sdk_version="$(sed -n 's|.*\([^<]*\)