diff --git a/build/windows/README.md b/build/windows/README.md
index 715a4515fb..26bb12bf90 100644
--- a/build/windows/README.md
+++ b/build/windows/README.md
@@ -189,6 +189,10 @@ managed code, but a package built that way cannot be installed by the .NET 8 or
.NET 9 SDK at all, and the MSBuild properties involved are undocumented. The
launcher costs one small assembly and works everywhere.
+The launcher targets .NET 8 and rolls forward to a newer major version when
+.NET 8 is not installed. Otherwise a machine with only .NET 9 or later would
+install the tool and then refuse to start it, asking for .NET 8.
+
The package carries the Windows compiler only. NuGet installs a tool package on
any platform, so the launcher checks and reports that rather than failing to
start a Windows executable.
@@ -207,6 +211,8 @@ neither shows up before somebody installs it: the payload can be missing, and
the layout can be such that the tool installs but does not run. So the test
checks the package contents - including that `LICENSE` and `NOTICE` really are
in there and are not empty - and then proves the installed command works.
+It also checks that the launcher may roll forward to a newer .NET, because
+running the tool on a machine that has .NET 8 as well cannot show that.
It runs anywhere PowerShell and the .NET SDK do. Off Windows it checks the
launcher's refusal instead of the compiler's output, which is the behaviour that
diff --git a/build/windows/dotnet-tool/Apache.Thrift.Compiler.csproj b/build/windows/dotnet-tool/Apache.Thrift.Compiler.csproj
index 5b8662a45e..09c2d00628 100644
--- a/build/windows/dotnet-tool/Apache.Thrift.Compiler.csproj
+++ b/build/windows/dotnet-tool/Apache.Thrift.Compiler.csproj
@@ -34,6 +34,10 @@
Exe
net8.0
+
+ Major
enable
disable
latest
diff --git a/build/windows/dotnet-tool/test-dotnet-tool.ps1 b/build/windows/dotnet-tool/test-dotnet-tool.ps1
index 905ef687b5..2c27fcb599 100644
--- a/build/windows/dotnet-tool/test-dotnet-tool.ps1
+++ b/build/windows/dotnet-tool/test-dotnet-tool.ps1
@@ -155,6 +155,18 @@ try {
Assert-True 'the tool entry point is the managed launcher' `
($settings -match "EntryPoint\s*=\s*`"$([regex]::Escape($PackageId)).dll`"") $settings
+ # The launcher targets the oldest .NET the tool has to install on. Unless it
+ # may roll forward to a newer major version, a machine with only .NET 9 or
+ # later installs the tool and then refuses to start it, asking for .NET 8.
+ # Runners that also have .NET 8 never show that, so check the setting.
+ $runtimeConfig = Read-Entry "$toolDir/$PackageId.runtimeconfig.json"
+ $rollForward = ''
+ if ($runtimeConfig) {
+ $rollForward = [string](($runtimeConfig | ConvertFrom-Json).runtimeOptions.rollForward)
+ }
+ Assert-True 'the launcher may run on a newer major .NET' `
+ ($rollForward -in @('Major', 'LatestMajor')) "rollForward '$rollForward'"
+
$nuspec = Read-Entry "$PackageId.nuspec"
Assert-True 'the package is a DotnetTool package' ($nuspec -match 'packageType\s+name="DotnetTool"')
Assert-True "the package id is $PackageId" ($nuspec -match "$([regex]::Escape($PackageId))")