diff --git a/.github/workflows/build-native-action.yml b/.github/workflows/build-native-action.yml index aefe1eed..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: @@ -317,6 +324,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/.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 diff --git a/relenv/_scripts/install_vc_build.ps1 b/relenv/_scripts/install_vc_build.ps1 index 5528ca7e..eb4256f2 100644 --- a/relenv/_scripts/install_vc_build.ps1 +++ b/relenv/_scripts/install_vc_build.ps1 @@ -109,6 +109,55 @@ 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 + } + # 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. + # `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. + # 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 ` + -ArgumentList $installer_cmdline ` + -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 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) 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 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: