diff --git a/.github/actions/rust-build-release/src/runtime.py b/.github/actions/rust-build-release/src/runtime.py index cfaafc36..801874ac 100644 --- a/.github/actions/rust-build-release/src/runtime.py +++ b/.github/actions/rust-build-release/src/runtime.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import os import shutil import subprocess import typing as typ @@ -15,6 +16,7 @@ CROSS_CONTAINER_ERROR_CODES = {125, 126, 127} DEFAULT_HOST_TARGET = "x86_64-unknown-linux-gnu" +PROBE_TIMEOUT = int(os.environ.get("RUNTIME_PROBE_TIMEOUT", "10")) def runtime_available(name: str, *, cwd: str | Path | None = None) -> bool: @@ -33,7 +35,7 @@ def runtime_available(name: str, *, cwd: str | Path | None = None) -> bool: allowed_names=(name, f"{name}.exe"), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - timeout=10, + timeout=PROBE_TIMEOUT, cwd=cwd, ) except (OSError, subprocess.TimeoutExpired): @@ -51,7 +53,7 @@ def runtime_available(name: str, *, cwd: str | Path | None = None) -> bool: capture_output=True, text=True, check=True, - timeout=10, + timeout=PROBE_TIMEOUT, cwd=cwd, ) except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired): @@ -102,8 +104,13 @@ def detect_host_target( capture_output=True, text=True, check=True, + timeout=PROBE_TIMEOUT, ) - except (FileNotFoundError, subprocess.CalledProcessError, OSError): + except ( + subprocess.CalledProcessError, + subprocess.TimeoutExpired, + OSError, + ): return default triple = next( diff --git a/.github/actions/rust-build-release/tests/test_runtime.py b/.github/actions/rust-build-release/tests/test_runtime.py index 079c963f..965cd929 100644 --- a/.github/actions/rust-build-release/tests/test_runtime.py +++ b/.github/actions/rust-build-release/tests/test_runtime.py @@ -52,7 +52,7 @@ def fake_run( ) -> subprocess.CompletedProcess[str]: _ = allowed_names cmd = [executable, *args] - raise subprocess.TimeoutExpired(cmd, 10) + raise subprocess.TimeoutExpired(cmd, runtime_module.PROBE_TIMEOUT) harness.monkeypatch.setattr(runtime_module, "run_validated", fake_run) @@ -146,7 +146,7 @@ def fake_run( _ = (allowed_names, capture_output, check, text) cmd = [executable, *args] if "--format" in args: - raise subprocess.TimeoutExpired(cmd, 10) + raise subprocess.TimeoutExpired(cmd, runtime_module.PROBE_TIMEOUT) return subprocess.CompletedProcess(cmd, 0, stdout="") harness.monkeypatch.setattr(runtime_module, "run_validated", fake_run) @@ -189,3 +189,69 @@ def fake_run( harness.monkeypatch.setattr(runtime_module, "run_validated", fake_run) assert runtime_module.detect_host_target() == "custom-triple" + + +def test_detect_host_target_returns_default_on_timeout( + runtime_module: ModuleType, module_harness: HarnessFactory +) -> None: + """Falls back to the default triple when rustc probing times out.""" + harness = module_harness(runtime_module) + harness.patch_shutil_which( + lambda name: "/usr/bin/rustc" if name == "rustc" else None + ) + harness.patch_attr("ensure_allowed_executable", lambda path, allowed: path) + + def fake_run( + executable: str, + args: list[str], + *, + allowed_names: tuple[str, ...], + **_: object, + ) -> subprocess.CompletedProcess[str]: + _ = (executable, args, allowed_names) + raise subprocess.TimeoutExpired( + [executable, *args], runtime_module.PROBE_TIMEOUT + ) + + harness.monkeypatch.setattr(runtime_module, "run_validated", fake_run) + + assert ( + runtime_module.detect_host_target(default="fallback-triple") + == "fallback-triple" + ) + + +def test_detect_host_target_passes_timeout_to_run_validated( + runtime_module: ModuleType, module_harness: HarnessFactory +) -> None: + """Ensures rustc probing is bounded via the timeout parameter.""" + harness = module_harness(runtime_module) + harness.patch_shutil_which( + lambda name: "/usr/bin/rustc" if name == "rustc" else None + ) + harness.patch_attr("ensure_allowed_executable", lambda path, allowed: path) + + call_kwargs: dict[str, object] = {} + + def fake_run( + executable: str, + args: list[str], + *, + allowed_names: tuple[str, ...], + **kwargs: object, + ) -> subprocess.CompletedProcess[str]: + _ = (executable, args) + call_kwargs.update(kwargs) + call_kwargs["allowed_names"] = allowed_names + return subprocess.CompletedProcess( + [executable, *args], 0, stdout="host: bounded\n" + ) + + harness.monkeypatch.setattr(runtime_module, "run_validated", fake_run) + + assert runtime_module.detect_host_target() == "bounded" + assert call_kwargs.get("timeout") == runtime_module.PROBE_TIMEOUT + assert call_kwargs.get("capture_output") is True + assert call_kwargs.get("text") is True + assert call_kwargs.get("check") is True + assert call_kwargs.get("allowed_names") == ("rustc", "rustc.exe") diff --git a/.github/actions/setup-windows-gnu/action.yml b/.github/actions/setup-windows-gnu/action.yml index 3d3a82bb..3fed254f 100644 --- a/.github/actions/setup-windows-gnu/action.yml +++ b/.github/actions/setup-windows-gnu/action.yml @@ -54,7 +54,8 @@ runs: Remove-Item $destination -Recurse -Force } Expand-Archive $archive -DestinationPath $destination -Force - $binPath = Join-Path $destination "llvm-mingw-$version-ucrt-x86_64\bin" + $toolRoot = Join-Path $destination "llvm-mingw-$version-ucrt-x86_64" + $binPath = Join-Path $toolRoot "bin" if (-not (Test-Path $binPath)) { throw "llvm-mingw bin directory not found at $binPath" }