Skip to content

Auto-derive BootstrapSdkVersion from global.json so it can never lag tools.dotnet #13693

@OvesN

Description

@OvesN

Background

eng/Versions.props::BootstrapSdkVersion and global.json::tools.dotnet are two pinned .NET SDK versions in this
repo that must stay in sync, but today they are maintained independently. When they drift, the bootstrap shared
runtime (installed at the version BootstrapSdkVersion says) ends up older than the runtime the tools shipped in
global.json::tools.dotnet's SDK require — and host-resolution fails.

We hit this in real life:

  • PR [main] Update dependencies from dotnet/arcade #13434 (Maestro arcade flow) bumped global.json::tools.dotnet from
    10.0.10410.0.106 but left BootstrapSdkVersion at 10.0.104.

  • Several days later, PRs started failing in
    MSBuild.EndToEndTests.MultithreadedExecution_Tests.MultithreadedBuild_BinaryLogging on Windows with:

    error : You must install or update .NET to run this application.
    App: D:\a\_work\1\s\.dotnet\sdk\10.0.106\Roslyn\bincore\csc.exe
    Framework: 'Microsoft.NETCore.App', version '10.0.6' (x64)
    The following frameworks were found:
    10.0.4 at [D:\a\_work\1\s\artifacts\bin\bootstrap\core\shared\Microsoft.NETCore.App]

    i.e. csc.exe (from SDK 10.0.106) needs runtime 10.0.6, but the bootstrap was installed against SDK 10.0.104 and
    only has runtime 10.0.4. Roll-forward, not roll-back.

  • The "fix" was a manual second PR (#13687) that simply mirrors the
    value global.json already had. This is busywork, easy to forget, and doesn't fail fast on the original PR. Reviewer
    (@ViktorHofer) suggested:

    "We should really start using some kind of Math.Min function here and pass the NetCoreSdkVersion in so that we
    are never behind the SDK version."

Why PR #13434's CI didn't catch this:

  • The Linux Core Multithreaded Mode lane in .vsts-dotnet-ci.yml runs with --skipTests, so it doesn't execute
    the failing xunit test.
  • There is no Windows Core Multithreaded Mode PR-CI lane at all.
  • The MultithreadedBuild_BinaryLogging test only fires /m:8 /mt end-to-end and is the one that surfaces the
    runtime-mismatch — every other lane uses paths that mask it.

Proposal

Make drift structurally impossible by deriving BootstrapSdkVersion from global.json::tools.dotnet rather than
maintaining a separate value. We never legitimately want bootstrap to be a different SDK from what Arcade installs
— only ever the same — so a single source of truth is the correct model.

1. eng/Versions.props — derive at evaluation time

Replace the hard-coded <BootstrapSdkVersion> with a parse of global.json:

<PropertyGroup>
  
<_GlobalJsonContent>$([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)..\global.json'))</_GlobalJsonContent>
  <BootstrapSdkVersion>$([System.Text.RegularExpressions.Regex]::Match($(_GlobalJsonContent), 
'"dotnet"\s*:\s*"([^"]+)"').Groups[1].Value)</BootstrapSdkVersion>
</PropertyGroup>

Add a defensive check: if the regex returns empty, fail evaluation with a clear error (<Error
Condition="'$(BootstrapSdkVersion)' == ''" Text="Could not parse tools.dotnet from global.json. Verify the file is
valid JSON." /> in a target that runs before bootstrap consumers).

2. eng/cibuild_bootstrapped_msbuild.ps1 — read from global.json too

The launcher currently parses Versions.props as raw XML, which means it would see the literal $(...) expression after
change (1) instead of the evaluated value. Switch it to global.json:

# was:
# $propsFile = Join-Path $PSScriptRoot "Versions.props"
# $bootstrapSdkVersion = ([xml](Get-Content 
$propsFile)).SelectSingleNode("//PropertyGroup/BootstrapSdkVersion").InnerText
$globalJsonPath = Join-Path $PSScriptRoot "..\global.json"
$bootstrapSdkVersion = (Get-Content $globalJsonPath -Raw | ConvertFrom-Json).tools.dotnet
if ([string]::IsNullOrWhiteSpace($bootstrapSdkVersion)) {
    throw "Could not read tools.dotnet from $globalJsonPath."
}

3. eng/cibuild_bootstrapped_msbuild.sh — same on Linux/macOS

# was:
# props_file="$script_dir/Versions.props"
# sdk_version=$(grep -A1 "BootstrapSdkVersion" "$props_file" | grep -o ">.*<" | sed 's/[><]//g')
global_json="$script_dir/../global.json"
sdk_version=$(grep -o '"dotnet"[[:space:]]*:[[:space:]]*"[^"]*"' "$global_json" | head -1 | sed 
's/.*"dotnet"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
if [ -z "$sdk_version" ]; then
    echo "ERROR: Could not read tools.dotnet from $global_json." >&2
    exit 1
fi

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions