Files
Jens GeyerandClaude Opus 5.5 a2748940bd THRIFT-6440: Let the .NET tool run where only .NET 9 or later is installed
Client: build

Apache.Thrift.Compiler targets net8.0 and set no RollForward, so on a
machine with only .NET 9 or 10 the tool installed and then refused to
start, asking for .NET 8. With RollForward=Major the launcher uses the
next newer .NET when 8 is missing; where 8 is installed, nothing
changes.

test-dotnet-tool.ps1 now checks the runtimeconfig.json in the package.
The CI runners have .NET 8 next to newer versions, so installing and
running the tool there could not show the problem.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 09:57:28 +02:00
..

Windows helpers

Scripts used by the Windows compiler build in .github/workflows/cmake.yml and by the Windows packaging workflows, and usable on their own from a developer machine.

check-compiler-imports.ps1

Checks what a Windows executable imports. The thrift.exe published with a release must depend only on Windows itself and on the Visual C++ runtime - anything else, such as Boost, OpenSSL, zlib or libevent, would have to be shipped alongside it, and the compiler is handed out as a single file.

PS C:\thrift> .\build\windows\check-compiler-imports.ps1 -Path C:\install\bin\thrift.exe

It finds dumpbin.exe itself through vswhere.exe, so it does not have to run from a Visual Studio developer prompt. Pass -DumpBin to point it somewhere else, and -AllowedImports to change the permitted list; wildcards are allowed, which is how one entry covers the whole UCRT.

The executable must also not be statically linked. The project moved away from a statically linked compiler in dd8ecde06, and doc/ReleaseManagement.md states that the released compiler requires the Visual C++ redistributable. A static build imports strictly fewer DLLs, every one of them permitted, so the allow list on its own would wave it through; -RequiredImport is what catches it. Pass -RequiredImport '' when a static build really is what you want.

The exit code is 0 when everything is in order and 1 otherwise, so it can be used as a build step.

check-compiler-imports-tests.ps1

Tests for the above. They feed canned dumpbin output through the parser, so they need neither a compiler nor Windows - any PowerShell will do:

$ pwsh build/windows/check-compiler-imports-tests.ps1

They exist because a check like this fails open in both directions: a parser that matches nothing reports every executable as clean, and an allow list that grew far enough rejects nothing. The tests pin down that a release build passes, that a static build and a third-party dependency are both rejected, and that the wildcard matching does what the default allow list relies on.

get-voted-compiler.ps1

Fetches the thrift-<version>.exe of a release candidate or a release from dist.apache.org and checks it before anything is built from it: every checksum file next to it (.sha512, .sha256) must match, at least one must be there, and its detached signature must be good and made with a key from the project's KEYS. Only dist.apache.org URLs naming a thrift-<version>.exe are accepted.

PS C:\thrift> .\build\windows\get-voted-compiler.ps1 -Url https://dist.apache.org/repos/dist/release/thrift/0.25.0/thrift-0.25.0.exe -OutputDir voted

The installer and the .NET tool of a release have to contain the executable the vote covered, and a new build of the same source is not the same bytes. The Windows packages and .NET tool workflows use this to get that executable. -Path and -KeysPath run the same checks on files that are already local. It needs gpg.

get-voted-compiler-tests.ps1

Tests for the above. They sign stand-in executables with a throwaway GPG key, so they need gpg but no network:

$ pwsh build/windows/get-voted-compiler-tests.ps1

A signed executable with matching checksums passes. Refused are: one changed after signing, also when its checksums were redone; one with a wrong checksum; one signed by a key that is not in KEYS; one without a signature or without a checksum; and URLs that are not on dist.apache.org or do not name a thrift-<version>.exe.

build-installer.ps1

Packages a built thrift.exe into a Windows installer with Inno Setup 6.3 or later, which is preinstalled on the GitHub Windows runner images.

PS C:\thrift> .\build\windows\build-installer.ps1 -Version 0.26.0 -Compiler C:\install\bin\thrift.exe -OutputDir dist

It writes thrift-<version>-setup.exe into the output directory and prints its path. ISCC.exe is located automatically; pass -Iscc to override. The installer also carries LICENSE and NOTICE, taken from the checkout unless -SourceRoot names another directory.

installer/thrift.iss

The Inno Setup script itself. It takes the version, the compiler and the output directory on the command line, so no version number is stored in it and build/veralign.sh has nothing extra to keep in step.

The installer installs per user by default and needs no elevation; an administrator can install for all users from the wizard or with /ALLUSERS. It optionally puts the install directory on PATH, and takes that entry out again on uninstall.

The compiler links the shared runtime, so it needs the Microsoft Visual C++ redistributable, which the ASF cannot ship. An interactive install says so when it cannot find vcruntime140.dll, rather than letting the first run fail with a missing-DLL dialog. It never blocks, and a silent install - which is what a package manager does - shows nothing; the WinGet and Chocolatey packages declare the redistributable as a dependency instead, so their users never see it. They ask for version 14.51 or later, because it has to be at least as new as the build tools the compiler was built with; a version already installed only counts from there on.

installer/test-installer.ps1

Installs and uninstalls a built installer silently and checks what it did: that the compiler is there and runs, that the install directory landed on PATH, and that uninstalling removed exactly that one PATH entry and nothing else. Inno Setup can append to PATH on its own but cannot undo it, so the removal is hand-written code in thrift.iss - the part of the installer most worth testing.

It changes the PATH of the user it runs as, so run it in CI or on a throwaway machine.

PS C:\thrift> .\build\windows\installer\test-installer.ps1 -Installer dist\thrift-0.26.0-setup.exe

-ExpectedVersion checks what the installed compiler reports, and -ExpectedSha256 that the installed thrift.exe is the very file the installer was built from.

build-dotnet-tool.ps1

Packs a built thrift.exe as a .NET tool, published to NuGet as Apache.Thrift.Compiler.

PS C:\thrift> .\build\windows\build-dotnet-tool.ps1 -Version 0.26.0 -Compiler C:\install\bin\thrift.exe -OutputDir nupkg

As with the installer, the version is passed in rather than stored in the project file, so build/veralign.sh gains nothing to keep in step. Building without -Compiler fails rather than producing a package with no compiler in it. LICENSE and NOTICE come from the checkout unless -SourceRoot names another directory.

For a release, the .NET tool workflow packs the voted thrift-<version>.exe from dist/release, fetched and checked with get-voted-compiler.ps1, together with the LICENSE and NOTICE of the release tag. Started by hand with compiler_url, it does the same with the executable at that URL and publishes nothing.

dotnet-tool/

The tool project. A .NET tool package needs a managed entry point, so Program.cs is a launcher that finds thrift.exe next to itself and hands over to it - arguments, the standard streams and the exit code all pass straight through.

.NET 10 can declare a native executable as the entry point directly, with no 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.

dotnet-tool/test-dotnet-tool.ps1

Looks inside a packed .nupkg, then installs it into a throwaway directory and runs it.

$ pwsh build/windows/dotnet-tool/test-dotnet-tool.ps1 -Package nupkg/Apache.Thrift.Compiler.0.26.0.nupkg -Version 0.26.0

Two things can go wrong with a tool package that wraps a native executable, and 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 keeps a Linux user from a confusing failure. -ExpectedSha256 checks that the bundled thrift.exe is the very file the package was built from.

build-chocolatey-package.ps1 and chocolatey/

Builds the Chocolatey package, so that the compiler can be installed with choco install thrift.

PS C:\thrift> .\build\windows\build-chocolatey-package.ps1 -Version 0.26.0

The package does not carry the compiler. It downloads the installer published with the release and runs it silently for all users, so what a user installs is the voted compiler, and no binary is redistributed through a third party CDN. As with the WinGet manifest, the URL is archive.apache.org and the checksum is computed from the file at that very URL, so the package cannot record a checksum the published file does not have. For a release whose vote did not cover the installer, -InstallerSource github takes the copy on the GitHub release instead.

Pass -StageOnly to render the package without packing it, which works on a machine that has no Chocolatey.

chocolatey/test-chocolatey-package.ps1

Tests the builder: that the URL, checksum and silent install arguments reach the install script, that no placeholder survives, that LICENSE and NOTICE are staged and not empty, and that a malformed version or checksum, or an unknown installer source, is refused. Given -Package, it also looks inside a packed .nupkg - including that it carries no executable, since the compiler is downloaded at install time.

Runs anywhere PowerShell does; only the choco pack step needs Windows.

build-winget-manifests.ps1 and winget/

Renders the three manifest files a submission to microsoft/winget-pkgs needs, from the templates in winget/, so that the compiler can be installed with winget install Apache.Thrift.

$ pwsh build/windows/build-winget-manifests.ps1 -Version 0.26.0

The manifest points at archive.apache.org, which keeps every release. downloads.apache.org only carries the current one, so a manifest naming it would stop working at the next release and take every older version in winget-pkgs with it.

An installer the release vote did not cover is not on dist.apache.org, and so not in the archive either. For such a release, -InstallerSource github points the manifest at the copy on the GitHub release, whose URL is just as permanent.

Unless -Sha256 is given, the installer is downloaded from the very URL that goes into the manifest and hashed, so the manifest cannot claim a checksum the published file does not have. That also means this cannot be run before the installer is there: for the archive, before the release has reached it.

winget/test-winget-manifests.ps1

Tests the renderer: that the values land where they belong, that no placeholder survives into a file that would be submitted verbatim, that the output is UTF-8 without a BOM and with LF endings, and that a malformed version, checksum or date, or an unknown installer source, is refused.

winget/validate_manifests.py

Validates rendered manifests against the WinGet JSON schemas, fetched from Microsoft for the schema version each manifest names. A manifest that does not match is otherwise rejected only after the pull request has been opened.

$ python3 -m pip install pyyaml jsonschema
$ python3 build/windows/winget/validate_manifests.py winget-manifests/manifests/a/Apache/Thrift/0.26.0