From a331e6f592641b30c6ffc51c5b74675eb4fb48d5 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Sat, 20 Jun 2026 16:47:49 -0700 Subject: [PATCH 1/8] Exit after `versions --update` instead of falling through MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The argparse block in pyversions.main exits cleanly after `--update-deps` but `--update` had no `sys.exit(0)`, so it fell through into the `if args.version:` block. `--version` defaults to "3.14", so every `relenv versions --update` run ended by printing the latest 3.14.x — mistakable for "I just added 3.14.6" output when really it was just the implicit version lookup running. Mirror the --update-deps shape: exit after the update finishes. --- relenv/pyversions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/relenv/pyversions.py b/relenv/pyversions.py index cd504f08..e8a2f99e 100644 --- a/relenv/pyversions.py +++ b/relenv/pyversions.py @@ -1418,6 +1418,7 @@ def main(args: argparse.Namespace) -> None: if args.update: python_versions(create=True) + sys.exit(0) if args.list: for version in python_versions(): print(version) From dd3d63f6eb3ef8566bd07d07d30cc0c73be0867f Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Sun, 21 Jun 2026 16:15:43 -0700 Subject: [PATCH 2/8] Install v140 toolset on existing VS via VS bootstrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous attempt invoked vs_installer.exe with an explicitly-quoted installPath; it bailed with exit 87 (invalid parameter). Two changes: - Use `Installer\setup.exe` (the VS bootstrapper) and fall back to vs_installer.exe. setup.exe is what Microsoft documents as the bootstrapper for modify operations. - Pass --installPath as a bare PowerShell argument (let Start-Process quote it), drop the manual backtick-quotes that confused the parser. Also stop trusting the bootstrapper's exit code — VS Installer returns non-zero in several benign cases. Verify by checking that MSBuild can see the v140 platform toolset: ${env:ProgramFiles(x86)}\MSBuild\Microsoft.Cpp\v4.0\V140 If that path exists after the modify call, the toolset is registered and Python's PCbuild can use it. --- relenv/_scripts/install_vc_build.ps1 | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/relenv/_scripts/install_vc_build.ps1 b/relenv/_scripts/install_vc_build.ps1 index 5528ca7e..2745abb8 100644 --- a/relenv/_scripts/install_vc_build.ps1 +++ b/relenv/_scripts/install_vc_build.ps1 @@ -109,6 +109,48 @@ Write-Host "Confirming Presence of Visual Studio Build Tools: " -NoNewline # We're only gonna look for msbuild.exe if ( Test-Path -Path $MSBUILD_BIN ) { Write-Result "Success" -ForegroundColor Green + + # MSBuild is present, but Python's PCbuild for 3.10/3.11 pins + # PlatformToolset to v140. Current windows-latest runners ship VS + # without v140 by default, so use the VS bootstrapper to add the + # component to the existing install. We don't trust the installer's + # exit code (it returns non-zero in a few benign cases) — instead + # verify the v140 platform-toolset targets file exists afterwards. + $V140_TARGETS = "${env:ProgramFiles(x86)}\MSBuild\Microsoft.Cpp\v4.0\V140" + Write-Host "Ensuring v140 toolset is installed: " -NoNewline + if ( Test-Path -Path $V140_TARGETS ) { + Write-Result "Already present" -ForegroundColor Green + } else { + Write-Host "" + $VS_BOOTSTRAPPER = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\setup.exe" + if ( -not (Test-Path -Path $VS_BOOTSTRAPPER) ) { + $VS_BOOTSTRAPPER = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vs_installer.exe" + } + if ( -not (Test-Path -Path $VS_BOOTSTRAPPER) ) { + Write-Host " VS bootstrapper not found under Microsoft Visual Studio\Installer" + exit 1 + } + $installer_args = @( + "modify", + "--installPath", $VS_INST_LOC, + "--add", "Microsoft.VisualStudio.Component.VC.140", + "--add", "Microsoft.VisualStudio.Component.Windows81SDK", + "--quiet", "--norestart", "--wait", "--nocache" + ) + Write-Host " Running: $VS_BOOTSTRAPPER $($installer_args -join ' ')" + $proc = Start-Process ` + -FilePath $VS_BOOTSTRAPPER ` + -ArgumentList $installer_args ` + -PassThru -Wait -NoNewWindow + Write-Host " bootstrapper exit code: $($proc.ExitCode)" + Write-Host "Verifying v140 toolset: " -NoNewline + if ( Test-Path -Path $V140_TARGETS ) { + Write-Result "Success" -ForegroundColor Green + } else { + Write-Result "Failed (v140 still not registered with MSBuild)" -ForegroundColor Red + exit 1 + } + } } else { Write-Result "Missing" -ForegroundColor Yellow From d4d38612d3d4c430d3690917cc3b0f5fee5efb16 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Sun, 21 Jun 2026 16:46:27 -0700 Subject: [PATCH 3/8] Pass installer cmdline as single string so quoted path survives Previous attempt passed the bootstrapper arguments as a PowerShell array; Start-Process then concatenated them without re-quoting, so `C:\Program Files\Microsoft Visual Studio\18\Enterprise` was parsed as separate positional arguments and the installer rejected the call with exit 87. Build the command line as a single string with embedded double quotes around $VS_INST_LOC. Start-Process forwards that verbatim, the installer sees a properly-quoted --installPath, and the modify operation proceeds. --- relenv/_scripts/install_vc_build.ps1 | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/relenv/_scripts/install_vc_build.ps1 b/relenv/_scripts/install_vc_build.ps1 index 2745abb8..a4928440 100644 --- a/relenv/_scripts/install_vc_build.ps1 +++ b/relenv/_scripts/install_vc_build.ps1 @@ -130,17 +130,15 @@ if ( Test-Path -Path $MSBUILD_BIN ) { Write-Host " VS bootstrapper not found under Microsoft Visual Studio\Installer" exit 1 } - $installer_args = @( - "modify", - "--installPath", $VS_INST_LOC, - "--add", "Microsoft.VisualStudio.Component.VC.140", - "--add", "Microsoft.VisualStudio.Component.Windows81SDK", - "--quiet", "--norestart", "--wait", "--nocache" - ) - Write-Host " Running: $VS_BOOTSTRAPPER $($installer_args -join ' ')" + # Pass the entire command line as a single string so the embedded + # quotes around --installPath survive Start-Process intact. Passing + # an array would let Start-Process drop the quotes and the + # bootstrapper would parse "C:\Program" as the installPath value. + $installer_cmdline = 'modify --installPath "{0}" --add Microsoft.VisualStudio.Component.VC.140 --add Microsoft.VisualStudio.Component.Windows81SDK --quiet --norestart --wait --nocache' -f $VS_INST_LOC + Write-Host " Running: $VS_BOOTSTRAPPER $installer_cmdline" $proc = Start-Process ` -FilePath $VS_BOOTSTRAPPER ` - -ArgumentList $installer_args ` + -ArgumentList $installer_cmdline ` -PassThru -Wait -NoNewWindow Write-Host " bootstrapper exit code: $($proc.ExitCode)" Write-Host "Verifying v140 toolset: " -NoNewline From 1b7baf3bd50a79d48f971732fcd7ae85882b64c6 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Sun, 21 Jun 2026 17:46:20 -0700 Subject: [PATCH 4/8] Drop invalid --wait flag from VS Installer modify call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified against the real Visual Studio Installer (4.x) on a Windows 11 box: passing --wait makes the bootstrapper bail before parsing anything else with "Option 'wait' is unknown" and exit code 87 — the mystery exit-87 we kept hitting in CI. --quiet already runs the modify synchronously, and Start-Process -Wait in PowerShell still ensures we don't return until the bootstrapper process exits. Tested locally: the full install_vc_build.ps1 -CICD now reports "Ensuring v140 toolset is installed: Already present" on an existing VS install, and the standalone modify call returns exit 0. --- relenv/_scripts/install_vc_build.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/relenv/_scripts/install_vc_build.ps1 b/relenv/_scripts/install_vc_build.ps1 index a4928440..314a9682 100644 --- a/relenv/_scripts/install_vc_build.ps1 +++ b/relenv/_scripts/install_vc_build.ps1 @@ -134,7 +134,11 @@ if ( Test-Path -Path $MSBUILD_BIN ) { # quotes around --installPath survive Start-Process intact. Passing # an array would let Start-Process drop the quotes and the # bootstrapper would parse "C:\Program" as the installPath value. - $installer_cmdline = 'modify --installPath "{0}" --add Microsoft.VisualStudio.Component.VC.140 --add Microsoft.VisualStudio.Component.Windows81SDK --quiet --norestart --wait --nocache' -f $VS_INST_LOC + # `setup.exe modify` does NOT accept --wait (verified against + # Visual Studio Installer 4.x — `Option 'wait' is unknown`). + # --quiet already runs synchronously, and Start-Process -Wait + # below ensures we don't return until the bootstrapper exits. + $installer_cmdline = 'modify --installPath "{0}" --add Microsoft.VisualStudio.Component.VC.140 --add Microsoft.VisualStudio.Component.Windows81SDK --quiet --norestart --nocache' -f $VS_INST_LOC Write-Host " Running: $VS_BOOTSTRAPPER $installer_cmdline" $proc = Start-Process ` -FilePath $VS_BOOTSTRAPPER ` From bae46363541c72fe25bd222bf20c0051ff5bfd27 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Sun, 21 Jun 2026 18:12:32 -0700 Subject: [PATCH 5/8] Pair v140 with Windows 10 SDK 19041 instead of nonexistent Windows81SDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After v140 install succeeded, Python 3.11.15 PCbuild started failing on the windows-latest runner with: \ucrt\wchar.h(443): error C2440: 'initializing': cannot convert from 'int' to '__m128i' Two related causes, both fixed here: - The bootstrapper was warning "Cannot find package: Microsoft.VisualStudio.Component.Windows81SDK in product graph" — that component was removed in VS 2022+ / VS 18. No SDK was actually installed alongside v140, so PCbuild fell back to the runner's only SDK (Win11 SDK 10.0.26100.0), whose uses SSE intrinsics that v140 chokes on. - Even with an older SDK installed, MSBuild auto-picks the newest one unless told otherwise. Install `Windows10SDK.19041` (Win10 SDK 2004, the latest known to pair cleanly with v140 and Python's PCbuild) and pin the build step to it via `WindowsTargetPlatformVersion`. --- .github/workflows/build-native-action.yml | 5 +++++ relenv/_scripts/install_vc_build.ps1 | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-native-action.yml b/.github/workflows/build-native-action.yml index aefe1eed..88e3db9d 100644 --- a/.github/workflows/build-native-action.yml +++ b/.github/workflows/build-native-action.yml @@ -317,6 +317,11 @@ jobs: - name: Build Python with Relenv env: RELENV_NATIVE_PY_VERSION: 3.10.15 + # Pin MSBuild to the older Windows 10 SDK we just installed + # alongside v140; the newer 26100 SDK on windows-latest has + # SSE intrinsics in that break C compiles under the + # v140 toolset Python 3.10/3.11 PCbuild targets. + WindowsTargetPlatformVersion: "10.0.19041.0" run: | python -m relenv build --no-pretty --arch=${{ matrix.arch }} --python=${{ steps.python-version.outputs.version }} diff --git a/relenv/_scripts/install_vc_build.ps1 b/relenv/_scripts/install_vc_build.ps1 index 314a9682..eb4256f2 100644 --- a/relenv/_scripts/install_vc_build.ps1 +++ b/relenv/_scripts/install_vc_build.ps1 @@ -138,7 +138,12 @@ if ( Test-Path -Path $MSBUILD_BIN ) { # Visual Studio Installer 4.x — `Option 'wait' is unknown`). # --quiet already runs synchronously, and Start-Process -Wait # below ensures we don't return until the bootstrapper exits. - $installer_cmdline = 'modify --installPath "{0}" --add Microsoft.VisualStudio.Component.VC.140 --add Microsoft.VisualStudio.Component.Windows81SDK --quiet --norestart --nocache' -f $VS_INST_LOC + # Pair v140 with the Windows 10 SDK 19041 (the latest SDK known + # to compile cleanly against the v140 toolset and Python 3.10/3.11 + # PCbuild headers; newer SDKs put SSE intrinsics in + # that v140 chokes on with C2440). Windows81SDK is not present + # in the VS 2022+/VS 18 product graph and silently no-op'd here. + $installer_cmdline = 'modify --installPath "{0}" --add Microsoft.VisualStudio.Component.VC.140 --add Microsoft.VisualStudio.Component.Windows10SDK.19041 --quiet --norestart --nocache' -f $VS_INST_LOC Write-Host " Running: $VS_BOOTSTRAPPER $installer_cmdline" $proc = Start-Process ` -FilePath $VS_BOOTSTRAPPER ` From e313feca333fe05d2685041da332a3567e0a2021 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Tue, 23 Jun 2026 01:08:13 -0700 Subject: [PATCH 6/8] Pin Windows build runners to windows-2022 windows-latest has rolled to VS 18 (a.k.a. VS 2026), whose VS Installer product graph no longer exposes pre-26100 Windows SDKs by component name. We need v140 + an older Windows SDK to compile Python 3.10/3.11 PCbuild (newer SDKs put SSE intrinsics in that v140 chokes on with C2440), but on VS 18 we can't get there: Warning: Cannot find package: Microsoft.VisualStudio.Component.Windows10SDK.19041 in product graph. error MSB8036: The Windows SDK version 10.0.19041.0 was not found. windows-2022 still ships VS 2022 with multiple Windows SDKs (10240, 19041, 20348, 22621, 26100), so v140 + the env-var pin to 10.0.19041.0 works there. Pin both build_windows and the matching verify test_windows to that image until a path forward exists for VS 18. The install-v140/SDK-19041 helper in install_vc_build.ps1 and the WindowsTargetPlatformVersion env var stay in place: on windows-2022 the v140 check is a no-op (it's pre-installed), and the env var pins MSBuild to the SDK that v140 expects. --- .github/workflows/build-native-action.yml | 9 ++++++++- .github/workflows/verify-build-action.yml | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-native-action.yml b/.github/workflows/build-native-action.yml index 88e3db9d..acb671ea 100644 --- a/.github/workflows/build-native-action.yml +++ b/.github/workflows/build-native-action.yml @@ -263,7 +263,14 @@ jobs: build_windows: name: "Python Windows" - runs-on: windows-latest + # Pinned to windows-2022 deliberately. windows-latest has rolled to + # VS 18 / VS 2026, whose product graph only exposes Win11 SDK 26100 + # (Microsoft.VisualStudio.Component.Windows10SDK.19041 etc. are + # silently no-op'd by the VS Installer there). Win10 SDK 26100 uses + # SSE intrinsics in that v140 (which Python 3.10/3.11 + # PCbuild pin to) chokes on, breaking the build. windows-2022 still + # ships VS 2022 plus multiple older SDKs that pair cleanly with v140. + runs-on: windows-2022 strategy: fail-fast: false matrix: diff --git a/.github/workflows/verify-build-action.yml b/.github/workflows/verify-build-action.yml index 558ac7ff..df8c9552 100644 --- a/.github/workflows/verify-build-action.yml +++ b/.github/workflows/verify-build-action.yml @@ -212,7 +212,9 @@ jobs: test_windows: name: "Verify Windows" - runs-on: windows-latest + # Match the build runner (windows-2022) — see build-native-action.yml + # for why we don't use windows-latest. + runs-on: windows-2022 strategy: fail-fast: false From 252e133552b2c7e1ff9654350048634b9da359dd Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Tue, 23 Jun 2026 14:35:09 -0700 Subject: [PATCH 7/8] Send relenv runtime debug/warning output to stderr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several build tools — maturin's pep517 driver, pyo3's metadata fetcher, rust-openssl-sys's build script — invoke the relenv-built Python as a subprocess and parse the FIRST line of stdout as JSON. Whenever RELENV_DEBUG is set, relenv.runtime.debug() printed those messages to stdout, and the tool's JSON parser blew up with "expected value at line 1 column 1". CI hit this on macOS `Verify MacOS` while pip-installing salt, which pulls in cryptography>=48 (Rust + maturin), and that's exactly how Salt onedir packaging would fail too — not a flake, a real interpreter bug. debug() and the only other direct print() in runtime.py now write to stderr. No behaviour change when RELENV_DEBUG is unset. --- relenv/runtime.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/relenv/runtime.py b/relenv/runtime.py index a756b5dd..57b18ca1 100644 --- a/relenv/runtime.py +++ b/relenv/runtime.py @@ -139,12 +139,18 @@ def debug(string: str) -> None: """ Prints the provided message if RELENV_DEBUG is truthy in the environment. + Writes to stderr so this output does not contaminate stdout when relenv + Python is invoked as a helper that emits structured data (JSON/TOML) + on stdout — tools like ``maturin pep517 write-dist-info`` and several + pyo3 / rust-openssl-sys build scripts parse the first line of stdout + and fail with "expected value at line 1 column 1" otherwise. + :param string: The message to print :type string: str """ if os.environ.get("RELENV_DEBUG"): - print(string) - sys.stdout.flush() + print(string, file=sys.stderr) + sys.stderr.flush() def relenv_root() -> pathlib.Path: @@ -647,7 +653,12 @@ def set_env_if_not_set(name: str, value: str) -> None: user. """ if name in os.environ and os.environ[name] != value: - print(f"Warning: {name} environment not set to relenv's root!\nexpected: {value}\ncurrent: {os.environ[name]}") + # Stderr keeps this warning out of stdout, which several build + # tools parse as structured data — see debug() above. + print( + f"Warning: {name} environment not set to relenv's root!\nexpected: {value}\ncurrent: {os.environ[name]}", + file=sys.stderr, + ) else: debug(f"Relenv set {name}") os.environ[name] = value From b7de0739267357e66533c576dac46236ef26260a Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Tue, 23 Jun 2026 14:46:27 -0700 Subject: [PATCH 8/8] Update test_runtime to assert debug/warning land on stderr Mirror of the previous commit that moved relenv.runtime.debug() and the set_env_if_not_set warning to sys.stderr (so they don't contaminate stdout for maturin/pyo3 subprocess consumers). test_debug_print and test_set_env_if_not_set now read captured.err and also assert captured.out is empty. --- tests/test_runtime.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index f3800162..1fb3b86f 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -38,8 +38,11 @@ def test_path_import_success(tmp_path: pathlib.Path) -> None: def test_debug_print(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: monkeypatch.setenv("RELENV_DEBUG", "1") relenv.runtime.debug("hello") - out = capsys.readouterr().out - assert "hello" in out + captured = capsys.readouterr() + # debug() must write to stderr so it doesn't pollute stdout for + # tools that parse subprocess stdout as JSON (maturin, pyo3, etc). + assert "hello" in captured.err + assert captured.out == "" monkeypatch.delenv("RELENV_DEBUG", raising=False) @@ -96,7 +99,10 @@ def test_set_env_if_not_set(monkeypatch: pytest.MonkeyPatch, capsys: pytest.Capt monkeypatch.setenv(env_name, "other") relenv.runtime.set_env_if_not_set(env_name, "value") captured = capsys.readouterr() - assert "Warning:" in captured.out + # Warning is routed to stderr for the same reason as debug() — keep + # stdout clean for JSON-emitting subprocess consumers. + assert "Warning:" in captured.err + assert captured.out == "" def test_get_config_var_wrapper_with_env(monkeypatch: pytest.MonkeyPatch) -> None: