diff --git a/.github/workflows/verify-windows-shim.yml b/.github/workflows/verify-windows-shim.yml new file mode 100644 index 0000000..47600c1 --- /dev/null +++ b/.github/workflows/verify-windows-shim.yml @@ -0,0 +1,71 @@ +name: verify windows shim + +# Rebuilds the Windows shim from dotslash_windows_shim.rs and fails if the +# checked-in .exe does not match, so the source cannot change without the +# regenerated binary. The build is byte-for-byte reproducible (rust-lld with +# /Brepro, plus the pinned toolchain in windows_shim/rust-toolchain.toml), so a +# plain `git diff` is a reliable check. Each architecture is built on its own +# native runner to avoid cross-linking. +# +# The freshly built binary is uploaded as an artifact before the diff check, so +# when the check fails (e.g. the committed binaries are out of date) you can +# download the correct binary from the run's "Artifacts" section and commit it +# without needing a local Windows machine. + +on: + push: + branches: [main] + paths: + - windows_shim/** + - .github/workflows/verify-windows-shim.yml + pull_request: + paths: + - windows_shim/** + - .github/workflows/verify-windows-shim.yml + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify-windows-shim: + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + target: x86_64-pc-windows-msvc + arch: x86_64 + - os: windows-11-arm + target: aarch64-pc-windows-msvc + arch: aarch64 + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + defaults: + run: + shell: bash + working-directory: windows_shim + steps: + - uses: actions/checkout@v6 + - name: Provision the pinned toolchain and target + # `cargo --version` auto-installs the nightly pinned by + # rust-toolchain.toml; the target then provisions its precompiled std. + run: | + cargo --version + rustup target add "${{ matrix.target }}" + - name: Rebuild the shim + run: python release.py "${{ matrix.target }}" + - name: Upload the rebuilt shim + # Runs before the diff check so the binary is downloadable even when the + # committed copy is out of date and the job ultimately fails. + uses: actions/upload-artifact@v4 + with: + name: dotslash_windows_shim-${{ matrix.arch }} + path: windows_shim/dotslash_windows_shim-${{ matrix.arch }}.exe + if-no-files-found: error + - name: Verify the checked-in binary is up to date + run: | + if ! git diff --exit-code -- 'dotslash_windows_shim-*.exe'; then + echo "::error::Checked-in shim binary is out of date. Download the 'dotslash_windows_shim-${{ matrix.arch }}' artifact from this run and commit it (or run 'py release.py' locally)." + exit 1 + fi diff --git a/website/docs/windows.md b/website/docs/windows.md index 6a676db..7c2d48b 100644 --- a/website/docs/windows.md +++ b/website/docs/windows.md @@ -69,8 +69,9 @@ of this documentation. ### DotSlash Windows Shim -**This is the preferred method.** The _DotSlash Windows Shim_ is a tiny 4KB -`.exe` executable that is placed next to the DotSlash file that performs the +**This is the preferred method.** The _DotSlash Windows Shim_ is a tiny +(a few kilobytes) `.exe` executable that is placed next to the DotSlash file +that performs the same function as the [batch script](#sibling-batch-script) above, but is a native executable rather than a batch script. This is the _ideal_ method that allows for easy execution without any of the drawbacks of batch scripts. But diff --git a/windows_shim/README.md b/windows_shim/README.md index 7b502cd..8f9bd13 100644 --- a/windows_shim/README.md +++ b/windows_shim/README.md @@ -25,14 +25,45 @@ The _DotSlash Windows Shim_ does this: ## Binary size _DotSlash Windows Shim_ builds without a standard library and only uses Windows -APIs. Release binaries are around ~5KB. +APIs to stay small. It is meant to be checked into source control next to every +DotSlash file that needs to run on Windows, so the release binaries are only a +few kilobytes. ## Release +The checked-in `dotslash_windows_shim-x86_64.exe` and +`dotslash_windows_shim-aarch64.exe` are built from `dotslash_windows_shim.rs`. +Regenerate them on Windows with: + ```shell py release.py ``` +Building both architectures requires their targets to be installed +(`rustup target add x86_64-pc-windows-msvc aarch64-pc-windows-msvc`). Pass a +single target triple to build just one architecture: + +```shell +py release.py aarch64-pc-windows-msvc +``` + +The build is byte-for-byte reproducible. It links with the Rust-bundled +`rust-lld` — which, unlike MSVC's `link.exe`, embeds no toolchain-specific +"Rich" header — and passes `/Brepro` so timestamps are content hashes rather +than wall-clock time. The output therefore depends only on the toolchain pinned +in `rust-toolchain.toml`. The `verify windows shim` GitHub Actions workflow +rebuilds the shim whenever anything under `windows_shim/` changes and fails if +the committed binaries are stale, so regenerate and commit them in the same +change as any edit to the source or a bump of the pinned toolchain. + +If you do not have a Windows machine, let CI build the binaries for you: push +your change (or trigger the workflow manually), then download the +`dotslash_windows_shim-x86_64` and `dotslash_windows_shim-aarch64` artifacts +from the workflow run — each contains the freshly built `.exe`. Because the +build is reproducible, those artifacts are exactly what a local `py release.py` +would produce; commit them into `windows_shim/` and re-run the workflow to +confirm it passes. + ## Testing ```shell diff --git a/windows_shim/dotslash_windows_shim-aarch64.exe b/windows_shim/dotslash_windows_shim-aarch64.exe index 4f33d81..09d2946 100644 Binary files a/windows_shim/dotslash_windows_shim-aarch64.exe and b/windows_shim/dotslash_windows_shim-aarch64.exe differ diff --git a/windows_shim/dotslash_windows_shim-x86_64.exe b/windows_shim/dotslash_windows_shim-x86_64.exe index 730f8d7..5a989e4 100644 Binary files a/windows_shim/dotslash_windows_shim-x86_64.exe and b/windows_shim/dotslash_windows_shim-x86_64.exe differ diff --git a/windows_shim/release.py b/windows_shim/release.py index a5663b0..c4c4d56 100644 --- a/windows_shim/release.py +++ b/windows_shim/release.py @@ -11,6 +11,7 @@ import os import shutil import subprocess +import sys from pathlib import Path IS_WINDOWS: bool = os.name == "nt" @@ -18,10 +19,19 @@ target_triplets: list[str] = ["x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"] -def main() -> None: +def main(targets: list[str] | None = None) -> None: if not IS_WINDOWS: raise Exception("Only Windows is supported.") + # Default to all targets; a caller (e.g. CI) may pass a subset to build just + # the target that matches the current runner's architecture. + if targets: + unknown = [t for t in targets if t not in target_triplets] + if unknown: + raise SystemExit(f"Unknown target(s): {', '.join(unknown)}") + else: + targets = target_triplets + dotslash_windows_shim_root = Path(os.path.realpath(__file__)).parent target_dir = ( @@ -30,7 +40,36 @@ def main() -> None: else None ) - for triplet in target_triplets: + # Link with the linker bundled in the active Rust toolchain (rust-lld) + # instead of the MSVC link.exe. lld-link emits no "Rich" header, so the + # output depends only on the pinned toolchain (see rust-toolchain.toml) and + # not on whichever Visual Studio version happens to be installed. Together + # with /Brepro below this keeps the checked-in binaries byte-for-byte + # reproducible, which the verify-windows-shim CI job relies on. + target_libdir = Path( + subprocess.check_output(["rustc", "--print", "target-libdir"], text=True).strip() + ) + rust_lld = target_libdir.parent / "bin" / "rust-lld.exe" + if not rust_lld.is_file(): + raise FileNotFoundError(f"Rust's bundled linker was not found: {rust_lld}") + + rustflags = [ + f"-Clinker={rust_lld}", + "-Clinker-flavor=lld-link", + "-Clink-arg=/DEBUG:NONE", # Avoid an embedded PDB path. + "-Clink-arg=/NODEFAULTLIB:msvcrt", # The shim does not use the CRT. + "-Clink-arg=/Brepro", # Hash-based timestamps instead of wall-clock time. + ] + + # Ambient RUSTFLAGS could change the measured release layout and break + # reproducibility. Encoded flags also preserve the linker path as a single + # argument when the workspace path contains spaces. + build_env = {**os.environ} + build_env.pop("RUSTFLAGS", None) + build_env["RUSTC_BOOTSTRAP"] = "1" # Required by no_std language items. + build_env["CARGO_ENCODED_RUSTFLAGS"] = "\x1f".join(rustflags) + + for triplet in targets: subprocess.run( [ "cargo", @@ -43,11 +82,7 @@ def main() -> None: f"--target={triplet}", ], check=True, - env={ - **os.environ, - "RUSTC_BOOTSTRAP": "1", - "RUSTFLAGS": "-Clink-arg=/DEBUG:NONE", # Avoid embedded pdb path - }, + env=build_env, ) src = ( @@ -64,4 +99,4 @@ def main() -> None: if __name__ == "__main__": - main() + main(sys.argv[1:]) diff --git a/windows_shim/rust-toolchain.toml b/windows_shim/rust-toolchain.toml index 5d56faf..bcd545e 100644 --- a/windows_shim/rust-toolchain.toml +++ b/windows_shim/rust-toolchain.toml @@ -1,2 +1,6 @@ [toolchain] -channel = "nightly" +# Pinned so the checked-in shim binaries stay byte-for-byte reproducible: the +# CI job in .github/workflows/verify-windows-shim.yml rebuilds the shim and +# fails if the result differs from what is committed. Bump this and regenerate +# the binaries (`py release.py`) in the same change. +channel = "nightly-2026-07-28"