Skip to content

Commit aaa265b

Browse files
authored
Fix Windows linker discovery command invocation (#161)
* Fix Windows linker discovery command invocation * Fix rust build release windows linker tests * Normalize Windows command expectations in tests * Add coverage for Windows rustup discovery errors
1 parent 9da83a4 commit aaa265b

File tree

3 files changed

+215
-37
lines changed

3 files changed

+215
-37
lines changed

.github/actions/release-to-pypi-uv/tests/test_publish_release.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
from ._helpers import SCRIPTS_DIR, load_script_module
1616

1717

18+
def _normalize_invocation(formulated: list[str]) -> list[str]:
19+
"""Return *formulated* with a platform-neutral executable name."""
20+
normalized = list(formulated)
21+
normalized[0] = Path(normalized[0]).stem
22+
return normalized
23+
24+
1825
@pytest.fixture(name="publish_module")
1926
def fixture_publish_module() -> ModuleType:
2027
"""Load the ``publish_release`` script module with repository paths set."""
@@ -49,8 +56,7 @@ def test_publish_index_behaviour(
4956

5057
def fake_run_cmd(cmd: object, **_: object) -> None:
5158
formulated = list(cmd.formulate())
52-
formulated[0] = Path(formulated[0]).name
53-
calls.append(formulated)
59+
calls.append(_normalize_invocation(formulated))
5460

5561
monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd)
5662

@@ -120,8 +126,7 @@ def test_cli_runner_default_index(
120126

121127
def fake_run_cmd(cmd: object, **_: object) -> None:
122128
formulated = list(cmd.formulate())
123-
formulated[0] = Path(formulated[0]).name
124-
calls.append(formulated)
129+
calls.append(_normalize_invocation(formulated))
125130

126131
monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd)
127132

@@ -143,8 +148,7 @@ def test_cli_runner_respects_env_index(
143148

144149
def fake_run_cmd(cmd: object, **_: object) -> None:
145150
formulated = list(cmd.formulate())
146-
formulated[0] = Path(formulated[0]).name
147-
calls.append(formulated)
151+
calls.append(_normalize_invocation(formulated))
148152

149153
monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd)
150154

.github/actions/rust-build-release/src/toolchain.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,25 @@ def configure_windows_linkers(toolchain_name: str, target: str, rustup: str) ->
3232
rustup_exec = ensure_allowed_executable(rustup, ("rustup", "rustup.exe"))
3333
triple = toolchain_triple(toolchain_name)
3434
if triple:
35+
rustup_args = ["which", "rustc", "--toolchain", toolchain_name]
36+
rustup_cmd = [rustup_exec, *rustup_args]
3537
try:
3638
rustc_path_result = run_validated(
3739
rustup_exec,
38-
["which", "rustc", "--toolchain", toolchain_name],
40+
rustup_args,
3941
allowed_names=("rustup", "rustup.exe"),
40-
capture_output=True,
41-
text=True,
42-
check=True,
42+
method="run",
4343
)
44-
except (FileNotFoundError, subprocess.CalledProcessError):
44+
except FileNotFoundError:
4545
pass
4646
else:
47+
if rustc_path_result.returncode != 0:
48+
raise subprocess.CalledProcessError(
49+
rustc_path_result.returncode,
50+
rustup_cmd,
51+
output=rustc_path_result.stdout,
52+
stderr=rustc_path_result.stderr,
53+
)
4754
if rustc_stdout := rustc_path_result.stdout.strip():
4855
toolchain_root = Path(rustc_stdout).resolve().parent.parent
4956
linker_path = (

.github/actions/rust-build-release/tests/test_target_install.py

Lines changed: 193 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import subprocess
77
import typing as typ
8+
from pathlib import Path
89

910
import pytest
1011
from shared_actions_conftest import (
@@ -15,8 +16,9 @@
1516
_register_rustup_toolchain_stub,
1617
)
1718

19+
from cmd_utils import RunResult
20+
1821
if typ.TYPE_CHECKING:
19-
from pathlib import Path
2022
from types import ModuleType
2123

2224
from shared_actions_conftest import CmdMox
@@ -871,12 +873,12 @@ def timeout_runtime(_name: str, *, cwd: object | None = None) -> bool:
871873

872874

873875
def test_configure_windows_linkers_prefers_toolchain_gcc(
874-
main_module: ModuleType,
876+
toolchain_module: ModuleType,
875877
module_harness: HarnessFactory,
876878
tmp_path: Path,
877879
) -> None:
878880
"""Toolchain-provided GCC is preferred for Windows host builds."""
879-
harness = module_harness(main_module)
881+
harness = module_harness(toolchain_module)
880882
harness.patch_platform("win32")
881883
toolchain_name = "1.89.0-x86_64-pc-windows-gnu"
882884
host_triple = "x86_64-pc-windows-gnu"
@@ -895,23 +897,24 @@ def fake_run(
895897
args: list[str],
896898
*,
897899
allowed_names: tuple[str, ...],
898-
capture_output: bool = False,
899-
text: bool = False,
900-
check: bool = False,
901-
**_: object,
902-
) -> subprocess.CompletedProcess[str]:
900+
method: str = "run",
901+
**run_kwargs: object,
902+
) -> RunResult:
903903
_ = allowed_names
904904
cmd = [executable, *args]
905-
assert cmd[:2] == [rustup_path, "which"]
906-
return subprocess.CompletedProcess(cmd, 0, stdout=str(rustc_path))
907-
908-
harness.monkeypatch.setattr(main_module, "run_validated", fake_run)
909-
harness.monkeypatch.setattr(main_module.shutil, "which", lambda name: None)
905+
assert Path(cmd[0]) == Path(rustup_path)
906+
assert cmd[1] == "which"
907+
assert method == "run"
908+
assert not run_kwargs
909+
return RunResult(0, str(rustc_path), "")
910+
911+
harness.monkeypatch.setattr(toolchain_module, "run_validated", fake_run)
912+
harness.monkeypatch.setattr(toolchain_module.shutil, "which", lambda name: None)
910913
harness.monkeypatch.delenv(
911914
"CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER", raising=False
912915
)
913916

914-
main_module.configure_windows_linkers(toolchain_name, host_triple, rustup_path)
917+
toolchain_module.configure_windows_linkers(toolchain_name, host_triple, rustup_path)
915918

916919
expected = str(host_linker)
917920
assert os.environ["CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER"] == expected
@@ -925,17 +928,18 @@ def fake_run(
925928
],
926929
)
927930
def test_configure_windows_linkers_sets_cross_linker(
928-
main_module: ModuleType,
931+
toolchain_module: ModuleType,
929932
module_harness: HarnessFactory,
930933
tmp_path: Path,
931934
linker_name: str,
932935
) -> None:
933936
"""Cross linkers discovered on PATH are exported for non-host targets."""
934-
harness = module_harness(main_module)
937+
harness = module_harness(toolchain_module)
935938
harness.patch_platform("win32")
936939
toolchain_name = "1.89.0-x86_64-pc-windows-gnu"
937940
rustup_path = "/usr/bin/rustup"
938941
host_triple = "x86_64-pc-windows-gnu"
942+
target_triple = "aarch64-pc-windows-gnu"
939943

940944
toolchain_root = tmp_path / "toolchain"
941945
rustc_path = toolchain_root / "bin" / "rustc.exe"
@@ -952,32 +956,195 @@ def fake_run(
952956
args: list[str],
953957
*,
954958
allowed_names: tuple[str, ...],
955-
capture_output: bool = False,
956-
text: bool = False,
957-
check: bool = False,
958-
**_: object,
959-
) -> subprocess.CompletedProcess[str]:
959+
method: str = "run",
960+
**run_kwargs: object,
961+
) -> RunResult:
960962
_ = allowed_names
961963
cmd = [executable, *args]
962-
assert cmd[:2] == [rustup_path, "which"]
963-
return subprocess.CompletedProcess(cmd, 0, stdout=str(rustc_path))
964+
assert Path(cmd[0]) == Path(rustup_path)
965+
assert cmd[1] == "which"
966+
assert method == "run"
967+
assert not run_kwargs
968+
return RunResult(0, str(rustc_path), "")
964969

965-
harness.monkeypatch.setattr(main_module, "run_validated", fake_run)
970+
harness.monkeypatch.setattr(toolchain_module, "run_validated", fake_run)
966971

967972
def fake_which(name: str) -> str | None:
968973
return str(cross_linker) if name == linker_name else None
969974

970-
harness.monkeypatch.setattr(main_module.shutil, "which", fake_which)
975+
harness.monkeypatch.setattr(toolchain_module.shutil, "which", fake_which)
971976
harness.monkeypatch.delenv(
972977
"CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER", raising=False
973978
)
974979
harness.monkeypatch.delenv(
975980
"CARGO_TARGET_AARCH64_PC_WINDOWS_GNU_LINKER", raising=False
976981
)
977982

978-
main_module.configure_windows_linkers(toolchain_name, host_triple, rustup_path)
983+
toolchain_module.configure_windows_linkers(
984+
toolchain_name, target_triple, rustup_path
985+
)
979986

980987
host_env = os.environ["CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER"]
981988
cross_env = os.environ["CARGO_TARGET_AARCH64_PC_WINDOWS_GNU_LINKER"]
982989
assert host_env == str(host_linker)
983990
assert cross_env == str(cross_linker)
991+
992+
993+
def test_configure_windows_linkers_raises_on_rustup_failure(
994+
toolchain_module: ModuleType,
995+
module_harness: HarnessFactory,
996+
) -> None:
997+
"""Rustup discovery failures propagate as CalledProcessError."""
998+
harness = module_harness(toolchain_module)
999+
harness.patch_platform("win32")
1000+
toolchain_name = "1.89.0-x86_64-pc-windows-gnu"
1001+
host_triple = "x86_64-pc-windows-gnu"
1002+
rustup_path = "/usr/bin/rustup"
1003+
1004+
def fake_run(
1005+
executable: str,
1006+
args: list[str],
1007+
*,
1008+
allowed_names: tuple[str, ...],
1009+
method: str = "run",
1010+
**run_kwargs: object,
1011+
) -> RunResult:
1012+
_ = allowed_names
1013+
assert method == "run"
1014+
assert not run_kwargs
1015+
assert Path(executable) == Path(rustup_path)
1016+
assert args
1017+
assert args[0] == "which"
1018+
return RunResult(9, "", "rustup error")
1019+
1020+
harness.monkeypatch.setattr(toolchain_module, "run_validated", fake_run)
1021+
harness.monkeypatch.setattr(toolchain_module.shutil, "which", lambda name: None)
1022+
harness.monkeypatch.delenv(
1023+
"CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER", raising=False
1024+
)
1025+
1026+
with pytest.raises(subprocess.CalledProcessError) as excinfo:
1027+
toolchain_module.configure_windows_linkers(
1028+
toolchain_name, host_triple, rustup_path
1029+
)
1030+
1031+
exc = excinfo.value
1032+
assert exc.returncode == 9
1033+
assert Path(exc.cmd[0]) == Path(rustup_path)
1034+
assert exc.cmd[1] == "which"
1035+
assert exc.stderr == "rustup error"
1036+
assert "CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER" not in os.environ
1037+
1038+
1039+
def test_configure_windows_linkers_ignores_missing_rustup(
1040+
toolchain_module: ModuleType,
1041+
module_harness: HarnessFactory,
1042+
) -> None:
1043+
"""Missing rustup executables do not raise during linker configuration."""
1044+
harness = module_harness(toolchain_module)
1045+
harness.patch_platform("win32")
1046+
toolchain_name = "1.89.0-x86_64-pc-windows-gnu"
1047+
host_triple = "x86_64-pc-windows-gnu"
1048+
rustup_path = "/usr/bin/rustup"
1049+
1050+
def fake_run(
1051+
executable: str,
1052+
args: list[str],
1053+
*,
1054+
allowed_names: tuple[str, ...],
1055+
method: str = "run",
1056+
**run_kwargs: object,
1057+
) -> RunResult:
1058+
_ = allowed_names
1059+
assert method == "run"
1060+
assert not run_kwargs
1061+
assert Path(executable) == Path(rustup_path)
1062+
assert args[0] == "which"
1063+
message = "rustup not found"
1064+
raise FileNotFoundError(message)
1065+
1066+
harness.monkeypatch.setattr(toolchain_module, "run_validated", fake_run)
1067+
harness.monkeypatch.setattr(toolchain_module.shutil, "which", lambda _: None)
1068+
harness.monkeypatch.delenv(
1069+
"CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER", raising=False
1070+
)
1071+
1072+
toolchain_module.configure_windows_linkers(toolchain_name, host_triple, rustup_path)
1073+
1074+
assert "CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER" not in os.environ
1075+
1076+
1077+
@pytest.fixture
1078+
def ensure_packaging_version() -> None:
1079+
"""Ensure packaging.version is importable for dependency guards."""
1080+
import packaging.version # noqa: F401
1081+
1082+
1083+
def test_main_propagates_windows_linker_rustup_failure(
1084+
ensure_packaging_version: None,
1085+
main_module: ModuleType,
1086+
toolchain_module: ModuleType,
1087+
module_harness: HarnessFactory,
1088+
) -> None:
1089+
"""Behavioural coverage for rustup discovery failures in the action entrypoint."""
1090+
harness = module_harness(main_module)
1091+
harness.patch_platform("win32")
1092+
toolchain_name = "1.89.0-x86_64-pc-windows-gnu"
1093+
rustup_path = "/usr/bin/rustup"
1094+
target = "x86_64-pc-windows-gnu"
1095+
1096+
def fake_run(
1097+
executable: str,
1098+
args: list[str],
1099+
*,
1100+
allowed_names: tuple[str, ...],
1101+
method: str = "run",
1102+
**run_kwargs: object,
1103+
) -> RunResult:
1104+
_ = allowed_names
1105+
assert method == "run"
1106+
assert not run_kwargs
1107+
assert Path(executable) == Path(rustup_path)
1108+
assert args[0] == "which"
1109+
return RunResult(9, "", "rustup error")
1110+
1111+
harness.monkeypatch.setattr(toolchain_module, "run_validated", fake_run)
1112+
harness.patch_attr(
1113+
"configure_windows_linkers", toolchain_module.configure_windows_linkers
1114+
)
1115+
harness.patch_attr("_resolve_target_argument", lambda value: value)
1116+
harness.patch_attr("_ensure_rustup_exec", lambda: rustup_path)
1117+
harness.patch_attr(
1118+
"_resolve_toolchain",
1119+
lambda *_: (toolchain_name, [toolchain_name]),
1120+
)
1121+
harness.patch_attr("_ensure_target_installed", lambda *_: True)
1122+
harness.patch_attr(
1123+
"_decide_cross_usage",
1124+
lambda *_args, **_kwargs: main_module._CrossDecision(
1125+
cross_path=None,
1126+
cross_version=None,
1127+
use_cross=False,
1128+
cross_toolchain_spec=None,
1129+
cargo_toolchain_spec=None,
1130+
use_cross_local_backend=False,
1131+
docker_present=False,
1132+
podman_present=False,
1133+
has_container=False,
1134+
container_engine=None,
1135+
requires_cross_container=False,
1136+
),
1137+
)
1138+
harness.monkeypatch.delenv(
1139+
"CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER", raising=False
1140+
)
1141+
1142+
with pytest.raises(subprocess.CalledProcessError) as excinfo:
1143+
main_module.main(target, toolchain_name)
1144+
1145+
exc = excinfo.value
1146+
assert exc.returncode == 9
1147+
assert Path(exc.cmd[0]) == Path(rustup_path)
1148+
assert exc.cmd[1] == "which"
1149+
assert exc.stderr == "rustup error"
1150+
assert "CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER" not in os.environ

0 commit comments

Comments
 (0)