diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-09-11 15:35:35 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-09-11 15:35:35 +0300 |
| commit | 2778411b6cc31ff804655753c2dee3efe78ad413 (patch) | |
| tree | 9955c3b5d7ff40cd552e0b2b84d402aceaa3539c | |
| parent | 03457913816b0e22e5fb7de5c83d199ee5ae7c51 (diff) | |
chore: add one-time windows cross-check setup
| -rw-r--r-- | AGENTS.md | 14 | ||||
| -rw-r--r-- | CONTRIBUTING.md | 17 | ||||
| -rw-r--r-- | justfile | 10 | ||||
| -rw-r--r-- | scripts/test_windows_cross.py | 82 | ||||
| -rw-r--r-- | scripts/windows_cross.py | 106 |
5 files changed, 222 insertions, 7 deletions
@@ -141,10 +141,16 @@ just check # formatting check + cargo nextest + maintenance script Run `just check` before committing unless Can explicitly accepts narrower validation. Do not bypass failing checks; fix the failure or explain exactly why a narrower check is enough. Windows MSVC cross-compilation from Unix requires SDK/CRT headers and libraries. -Set `LIBGHOSTTY_VT_WINDOWS_LIBC` to a Zig libc configuration file whose directories -point to that SDK (the format is shown by `zig libc` on Windows). `just windows-lint` -and the Windows stage of `just check` use this configuration. Native Windows builds -auto-detect their installed SDK and do not require the override. +Install `xwin` with `cargo install xwin --locked`, then run +`just setup-windows-cross` once and accept Microsoft's SDK license when prompted. +This downloads the SDK directly from Microsoft; no Windows machine is required. +The SDK and Zig libc configuration live at `~/.local/share/herdr/windows-cross/`, +shared by worktrees. `just windows-lint` and the Windows stage of `just check` +use this configuration automatically. To use another SDK, set +`LIBGHOSTTY_VT_WINDOWS_LIBC` to its Zig libc configuration file. +Setup accepts `--accept-license` for explicit noninteractive license acceptance; +normal checks never download the SDK. Native Windows builds auto-detect their +installed SDK. Native Linux/macOS builds do not need the Windows SDK. Unit tests live next to the code (`#[cfg(test)] mod tests`). New `AppState` or `Workspace` behavior should be testable with `AppState::test_new()` and `Workspace::test_new()` without PTYs. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc772265..4be44fed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -119,6 +119,23 @@ just ci The checks must pass. Make sure the tests exercise the reported failure and would fail without the fix. +Maintainers also run `just check`, which includes Windows cross-compilation on +Linux/macOS. Set up that target once: + +```bash +cargo install xwin --locked +just setup-windows-cross +``` + +`xwin` downloads Microsoft's Windows SDK and CRT directly and prompts for license +acceptance. No Windows machine is needed. For noninteractive setup, pass +`just setup-windows-cross --accept-license` only after accepting that license. +The SDK is stored in `~/.local/share/herdr/windows-cross/` and reused across +worktrees; subsequent `just check` and `just windows-lint` runs use it automatically. +An existing SDK can be selected with `LIBGHOSTTY_VT_WINDOWS_LIBC`, pointing to its +Zig libc configuration file. Ordinary native Linux/macOS builds do not need this +setup. Native Windows builds use the SDK installed with Visual Studio Build Tools. + ### Handle documentation correctly For normal code changes, do not edit the root `README.md`, root `CHANGELOG.md`, `docs/preview/`, `docs/versions/`, or release payloads under `distribution/`. @@ -13,7 +13,7 @@ test: # Run repository maintenance contract tests maintenance-test: - {{python}} -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_config_reference_check scripts.test_docs_translation_parity scripts.test_hermes_integration_asset scripts.test_package_windows_conpty scripts.test_preview scripts.test_unix_installer scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty + {{python}} -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_config_reference_check scripts.test_docs_translation_parity scripts.test_hermes_integration_asset scripts.test_package_windows_conpty scripts.test_preview scripts.test_unix_installer scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty scripts.test_windows_cross # Run one nextest filter, e.g. `just test-one codex_stale_working` test-one filter: @@ -41,11 +41,15 @@ ci filter='all()': lint just ui-hot-path-architecture-test just integration-assets-test +# Download the Windows SDK once (requires xwin; prompts for Microsoft's SDK license) +[unix] +setup-windows-cross *args: + {{python}} scripts/windows_cross.py setup {{args}} + # Run Windows target lint from Unix/macOS to catch cfg(windows) compile and clippy failures before CI [unix] windows-lint: - rustup target add x86_64-pc-windows-msvc - LIBGHOSTTY_VT_SIMD=false cargo clippy --bin herdr --locked --target x86_64-pc-windows-msvc -- -D warnings + {{python}} scripts/windows_cross.py lint # Check formatting + run unit tests + Windows target lint + documentation contract tests [unix] diff --git a/scripts/test_windows_cross.py b/scripts/test_windows_cross.py new file mode 100644 index 00000000..4f8f0fe4 --- /dev/null +++ b/scripts/test_windows_cross.py @@ -0,0 +1,82 @@ +import os +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from scripts import windows_cross + + +class WindowsCrossTests(unittest.TestCase): + def test_libc_configuration_matches_xwin_layout(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for relative in ( + "sdk/include/ucrt/stdlib.h", "crt/include/vcruntime.h", + "sdk/lib/ucrt/x86_64/ucrt.lib", "crt/lib/x86_64/vcruntime.lib", + "sdk/lib/um/x86_64/kernel32.lib", + ): + path = root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() + contents = windows_cross.libc_contents(root) + self.assertIn(f"include_dir={root}/sdk/include/ucrt\n", contents) + self.assertIn(f"msvc_lib_dir={root}/crt/lib/x86_64\n", contents) + self.assertTrue(contents.endswith("gcc_dir=\n")) + (root / "crt/include/vcruntime.h").unlink() + with self.assertRaisesRegex(ValueError, "missing .*vcruntime.h"): + windows_cross.libc_contents(root) + + def test_persistent_configuration_and_explicit_override(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + default = root / "libc.txt" + override = root / "custom.txt" + default.touch() + override.touch() + with patch.object(windows_cross, "SDK_ROOT", root), patch.dict(os.environ, {}, clear=True): + self.assertEqual(windows_cross.libc_path(), default) + with patch.dict(os.environ, {windows_cross.LIBC_ENV: str(override)}): + self.assertEqual(windows_cross.libc_path(), override) + override.unlink() + with self.assertRaisesRegex(ValueError, "just setup-windows-cross"): + windows_cross.libc_path() + + def test_missing_setup_does_not_run_build_or_download(self): + with tempfile.TemporaryDirectory() as directory: + with patch.object(windows_cross, "SDK_ROOT", Path(directory)), \ + patch.dict(os.environ, {}, clear=True), \ + patch.object(windows_cross.subprocess, "run") as run: + with self.assertRaisesRegex(ValueError, "just setup-windows-cross"): + windows_cross.lint() + run.assert_not_called() + + def test_lint_passes_sdk_to_cargo_without_changing_parent_environment(self): + with patch.object(windows_cross, "libc_path", return_value=Path("/sdk/libc.txt")), \ + patch.dict(os.environ, {"KEEP_ME": "yes"}, clear=True), \ + patch.object(windows_cross.subprocess, "run") as run: + windows_cross.lint() + self.assertEqual(run.call_count, 2) + cargo = run.call_args + self.assertEqual(cargo.args[0][:2], ["cargo", "clippy"]) + self.assertEqual(cargo.kwargs["env"][windows_cross.LIBC_ENV], "/sdk/libc.txt") + self.assertEqual(cargo.kwargs["env"]["KEEP_ME"], "yes") + self.assertNotIn(windows_cross.LIBC_ENV, os.environ) + + def test_license_acceptance_is_only_forwarded_when_explicit(self): + with tempfile.TemporaryDirectory() as directory: + with patch.object(windows_cross, "SDK_ROOT", Path(directory)), \ + patch.object(windows_cross.shutil, "which", return_value="tool"), \ + patch.object(windows_cross, "libc_contents", return_value="configuration"), \ + patch.object(windows_cross.subprocess, "run") as run: + for accepted in (False, True): + run.reset_mock() + windows_cross.setup(accepted) + command = run.call_args_list[0].args[0] + self.assertEqual("--accept-license" in command, accepted) + self.assertEqual(command[0], "xwin") + self.assertIn("--copy", command) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/windows_cross.py b/scripts/windows_cross.py new file mode 100644 index 00000000..55795d35 --- /dev/null +++ b/scripts/windows_cross.py @@ -0,0 +1,106 @@ +"""One-time Windows SDK setup and Windows target linting from Unix hosts.""" + +import argparse +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile + + +SDK_ROOT = Path.home() / ".local/share/herdr/windows-cross" +LIBC_ENV = "LIBGHOSTTY_VT_WINDOWS_LIBC" +TARGET = "x86_64-pc-windows-msvc" + + +def libc_contents(root: Path) -> str: + paths = { + "include_dir": root / "sdk/include/ucrt", + "sys_include_dir": root / "crt/include", + "crt_dir": root / "sdk/lib/ucrt/x86_64", + "msvc_lib_dir": root / "crt/lib/x86_64", + "kernel32_lib_dir": root / "sdk/lib/um/x86_64", + } + required = { + "include_dir": "stdlib.h", + "sys_include_dir": "vcruntime.h", + "crt_dir": "ucrt.lib", + "msvc_lib_dir": "vcruntime.lib", + "kernel32_lib_dir": "kernel32.lib", + } + for key, filename in required.items(): + if not (paths[key] / filename).is_file(): + raise ValueError(f"Windows SDK is incomplete: missing {paths[key] / filename}") + return "".join(f"{key}={value}\n" for key, value in paths.items()) + "gcc_dir=\n" + + +def libc_path() -> Path: + override = os.environ.get(LIBC_ENV) + path = Path(override).expanduser() if override else SDK_ROOT / "libc.txt" + if not path.is_file(): + raise ValueError( + f"Windows cross-check needs SDK configuration at {path}.\n" + "Run `just setup-windows-cross` once, or set " + f"{LIBC_ENV} to an existing Zig libc configuration." + ) + return path.resolve() + + +def setup(accept_license: bool) -> None: + if not shutil.which("xwin"): + raise ValueError("Install xwin first: cargo install xwin --locked") + if not shutil.which(os.environ.get("ZIG", "zig")): + raise ValueError("Install Zig 0.16.0 first, or set ZIG to its executable.") + SDK_ROOT.mkdir(parents=True, exist_ok=True) + # SDK downloads can be large; /tmp is often RAM-backed on Linux. + temp_parent = "/var/tmp" if sys.platform.startswith("linux") else None + with tempfile.TemporaryDirectory(prefix="herdr-windows-sdk-", dir=temp_parent) as cache: + command = ["xwin", "--arch", "x86_64", "--cache-dir", cache] + if accept_license: + command.append("--accept-license") + subprocess.run(command + ["splat", "--copy", "--output", str(SDK_ROOT)], check=True) + config = SDK_ROOT / "libc.txt" + config.write_text(libc_contents(SDK_ROOT)) + subprocess.run( + [os.environ.get("ZIG", "zig"), "libc", "-target", "x86_64-windows-msvc", str(config)], + check=True, + ) + print(f"Windows SDK configured at {config}. Run `just windows-lint` or `just check`.") + + +def lint() -> None: + env = {**os.environ, LIBC_ENV: str(libc_path()), "LIBGHOSTTY_VT_SIMD": "false"} + subprocess.run(["rustup", "target", "add", TARGET], check=True) + subprocess.run( + ["cargo", "clippy", "--bin", "herdr", "--locked", "--target", TARGET, "--", "-D", "warnings"], + env=env, + check=True, + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + setup_parser = commands.add_parser("setup", help="Download Microsoft's SDK using xwin") + setup_parser.add_argument( + "--accept-license", action="store_true", + help="Explicitly accept Microsoft's SDK license instead of xwin's interactive prompt", + ) + commands.add_parser("lint", help="Run Windows clippy with the configured SDK") + args = parser.parse_args() + try: + if args.command == "setup": + setup(args.accept_license) + else: + lint() + except (ValueError, OSError) as error: + print(error, file=sys.stderr) + return 1 + except subprocess.CalledProcessError as error: + return error.returncode + return 0 + + +if __name__ == "__main__": + sys.exit(main()) |
