aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-09-11 15:35:35 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-09-11 15:35:35 +0300
commit2778411b6cc31ff804655753c2dee3efe78ad413 (patch)
tree9955c3b5d7ff40cd552e0b2b84d402aceaa3539c
parent03457913816b0e22e5fb7de5c83d199ee5ae7c51 (diff)
chore: add one-time windows cross-check setup
-rw-r--r--AGENTS.md14
-rw-r--r--CONTRIBUTING.md17
-rw-r--r--justfile10
-rw-r--r--scripts/test_windows_cross.py82
-rw-r--r--scripts/windows_cross.py106
5 files changed, 222 insertions, 7 deletions
diff --git a/AGENTS.md b/AGENTS.md
index acec3c03..92b29da9 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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/`.
diff --git a/justfile b/justfile
index 0dcce374..74e17848 100644
--- a/justfile
+++ b/justfile
@@ -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())