From e442c31a5e8012613e065ecfafc3e1e748f77d79 Mon Sep 17 00:00:00 2001 From: lambiengcode <60530946+lambiengcode@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:34:54 +0700 Subject: [PATCH 1/4] ci: test on the platforms releases actually ship MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Releases ship Windows and macOS binaries. Neither had ever been executed by a job: CI ran on Linux only, so a portability break would have reached users before it reached us, and the Windows binary added last week was shipped on the strength of a type-check alone. A new `platform` job runs the suite and an end-to-end CLI smoke on windows-latest and macos-latest. `check` stays on Linux and keeps everything that does not vary by platform — formatting, clippy, and the network-egress gate, which needs iptables. Duplicating those three times would cost minutes per run and catch nothing. Two platform details are handled rather than discovered later. Windows runners convert LF to CRLF on checkout, which would test fixtures no user's repository actually contains and shift every byte offset the index records. `core.autocrlf false` is set in a step *before* checkout, because configuring it afterwards is too late. The smoke step runs under bash rather than the Windows default. PowerShell propagates only the last command's exit code, so a failing `init` followed by a passing `context` would have left the step green — a CI step that cannot fail is worse than no step. `fail-fast: false`, so a Windows failure does not hide a macOS one. --- .github/workflows/ci.yml | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4dfe9f..cd1922b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,48 @@ jobs: cargo test --all --offline sudo iptables -F OUTPUT || true + # Every platform a release ships a binary for is tested here. + # + # `check` above stays on Linux and owns the things that do not vary by platform — + # formatting, clippy, and the network-egress gate, which needs iptables. This job + # owns what does vary: path handling, the temporary directory, line endings, and + # whether the CLI actually runs. Releases shipped Windows and macOS binaries that no + # job had ever executed; a portability break would have reached users first. + platform: + name: test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest] + steps: + # Before checkout, deliberately: Windows runners convert LF to CRLF on the way + # in, which would mean testing fixtures no user's repository actually contains + # and shifting every byte offset the index records. Configuring git afterwards + # would be too late. + - name: Check out with the line endings as committed + if: runner.os == 'Windows' + run: git config --global core.autocrlf false + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + + - name: Test + run: cargo test --all + + # The unit tests can pass while the binary cannot start. This is the same + # end-to-end path `bench-smoke` runs on Linux, reduced to what proves the CLI + # works on this platform at all. + - name: The CLI runs end to end + # bash, not the Windows default: PowerShell only propagates the *last* + # command's exit code, so a failing `init` here would pass the step silently. + shell: bash + run: | + set -euo pipefail + cargo run --release -p reify-cli -- -C fixtures/minierp init + cargo run --release -p reify-cli -- -C fixtures/minierp index + cargo run --release -p reify-cli -- -C fixtures/minierp --json context "strategic account discount" + deny: runs-on: ubuntu-latest steps: From d399a9cf86a848e9faa86b23a3b8024748453b09 Mon Sep 17 00:00:00 2001 From: lambiengcode <60530946+lambiengcode@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:39:30 +0700 Subject: [PATCH 2/4] fix(lockfile): the index lock did nothing on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `process_is_alive` was implemented for unix and stubbed to `false` everywhere else. The comment called that the safer failure — erring toward reclaiming a lock rather than deadlocking a repository — but for the *current* process it means every lock looks stale, so the lock reclaims itself and stops excluding anything. Two `reify index` runs on Windows would both proceed against the same store. Windows CI found it in its first run, by failing to recognise its own process as alive. The Windows binary shipped last week has carried this since; it was added on the strength of a type-check, which is exactly what a type-check cannot catch. Implemented with `OpenProcess` + `WaitForSingleObject(handle, 0)`, declared by hand for one question asked once, the same way `kill` already is rather than taking a libc dependency. `WaitForSingleObject` is used in preference to `GetExitCodeProcess`, which reports the sentinel 259 for a running process and cannot distinguish it from one that genuinely exited with 259. `QUERY_LIMITED_INFORMATION` is the narrowest right that answers the question and is granted where `PROCESS_QUERY_INFORMATION` is not. The remaining `not(any(unix, windows))` arm now returns `true` rather than `false`. Without a liveness check the lock cannot be trusted, and refusing to reclaim a lock is a worse outcome for one user than letting two indexers share a store is for everyone. --- crates/reify/src/lockfile.rs | 56 ++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/crates/reify/src/lockfile.rs b/crates/reify/src/lockfile.rs index d27d9c8..ef32cd9 100644 --- a/crates/reify/src/lockfile.rs +++ b/crates/reify/src/lockfile.rs @@ -96,9 +96,11 @@ impl Drop for IndexLock { /// Is a process with this id running? /// -/// `kill(pid, 0)` is the portable POSIX existence check. On other platforms this -/// returns `false`, which errs toward reclaiming a lock rather than deadlocking a -/// repository — the safer failure for an advisory lock. +/// The lock is only as good as this answer. A liveness check that always says "no" +/// does not err on the safe side — it makes every lock look stale, so the lock stops +/// excluding anything and two indexers write the same store. That is what the +/// `not(unix)` stub used to do, and CI on Windows found it by failing to recognise +/// its own process as alive. #[cfg(unix)] fn process_is_alive(pid: u32) -> bool { // SAFETY: `kill` with signal 0 performs no action; it only reports whether the @@ -112,9 +114,53 @@ extern "C" { fn libc_kill(pid: i32, sig: i32) -> i32; } -#[cfg(not(unix))] +/// Win32's answer to `kill(pid, 0)`. +/// +/// Declared by hand rather than pulling in a Windows crate, for one question asked +/// once — the same reason `kill` is declared above rather than taking a libc +/// dependency. +#[cfg(windows)] +mod win32 { + pub type Handle = *mut core::ffi::c_void; + extern "system" { + pub fn OpenProcess(access: u32, inherit: i32, pid: u32) -> Handle; + pub fn WaitForSingleObject(handle: Handle, millis: u32) -> u32; + pub fn CloseHandle(handle: Handle) -> i32; + } +} + +#[cfg(windows)] +fn process_is_alive(pid: u32) -> bool { + /// The narrowest right to ask "does this exist"; granted across integrity levels + /// where `PROCESS_QUERY_INFORMATION` is not. + const QUERY_LIMITED_INFORMATION: u32 = 0x1000; + /// The handle is not signalled, so the process has not exited. + const WAIT_TIMEOUT: u32 = 258; + + // SAFETY: `OpenProcess` returns null rather than an invalid handle on failure, and + // the handle is closed on every path that obtained one. + unsafe { + let handle = win32::OpenProcess(QUERY_LIMITED_INFORMATION, 0, pid); + if handle.is_null() { + // No such process, or one this user may not query. Either way, treating + // the lock as reclaimable is the behaviour a dead owner should get. + return false; + } + // Waiting zero milliseconds asks the question without blocking. Preferred over + // `GetExitCodeProcess`, which reports the sentinel 259 for a running process + // and cannot distinguish it from one that genuinely exited with 259. + let state = win32::WaitForSingleObject(handle, 0); + win32::CloseHandle(handle); + state == WAIT_TIMEOUT + } +} + +/// Any other platform. Deliberately pessimistic: without a liveness check the lock +/// cannot be trusted, so it refuses to reclaim rather than silently allowing two +/// indexers to share a store. +#[cfg(not(any(unix, windows)))] fn process_is_alive(_pid: u32) -> bool { - false + true } #[cfg(test)] From 07cc9318b6e1a3ff23bacc2260169229e260ac63 Mon Sep 17 00:00:00 2001 From: lambiengcode <60530946+lambiengcode@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:43:14 +0700 Subject: [PATCH 3/4] fix(lockfile): request SYNCHRONIZE, or the Windows wait cannot run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first attempt opened the process with `QUERY_LIMITED_INFORMATION` alone. `WaitForSingleObject` requires `SYNCHRONIZE`, so the wait did not return `WAIT_TIMEOUT` for a running process — it returned `WAIT_FAILED`, which the comparison read as "not running", restoring exactly the bug the function was written to fix. Windows CI caught it a second time, on the same assertion. Worth the comment it now carries: omitting an access right does not make the check stricter, it makes the call fail, and a failed liveness check that reads as "dead" is indistinguishable from the stub this replaced. --- crates/reify/src/lockfile.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/reify/src/lockfile.rs b/crates/reify/src/lockfile.rs index ef32cd9..152e042 100644 --- a/crates/reify/src/lockfile.rs +++ b/crates/reify/src/lockfile.rs @@ -134,13 +134,17 @@ fn process_is_alive(pid: u32) -> bool { /// The narrowest right to ask "does this exist"; granted across integrity levels /// where `PROCESS_QUERY_INFORMATION` is not. const QUERY_LIMITED_INFORMATION: u32 = 0x1000; + /// Required to wait on the handle at all. Omitting it does not make the wait + /// stricter — it makes it fail with `WAIT_FAILED`, which reads as "not running" + /// and silently restores the bug this function exists to fix. + const SYNCHRONIZE: u32 = 0x0010_0000; /// The handle is not signalled, so the process has not exited. const WAIT_TIMEOUT: u32 = 258; // SAFETY: `OpenProcess` returns null rather than an invalid handle on failure, and // the handle is closed on every path that obtained one. unsafe { - let handle = win32::OpenProcess(QUERY_LIMITED_INFORMATION, 0, pid); + let handle = win32::OpenProcess(SYNCHRONIZE | QUERY_LIMITED_INFORMATION, 0, pid); if handle.is_null() { // No such process, or one this user may not query. Either way, treating // the lock as reclaimable is the behaviour a dead owner should get. From 3948224b81c6bfe81569ff6a46f2ba4e86fc9286 Mon Sep 17 00:00:00 2001 From: lambiengcode <60530946+lambiengcode@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:50:59 +0700 Subject: [PATCH 4/4] docs: say Windows is supported, now that it is tested MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install section listed a Windows binary; nothing else did. The two translated READMEs never mentioned Windows at all — they still offered macOS and Linux only — and the docs site said the same. A reader on Windows had no way to tell the tool was for them. All three READMEs and the site now carry a platform badge and name Windows alongside macOS and Linux, with what to do from PowerShell, where a `curl | sh` line is no help: take the msvc archive, verify its checksum, put `reify.exe` on PATH. The claim is only made because it is now true. Every listed platform runs the full suite in CI and has the CLI exercised end to end — the sentence saying so is in the README because it is the difference between this and the previous release, which published a Windows binary whose index lock did nothing. --- README.md | 12 ++++++++++-- README.vi.md | 12 +++++++++++- README.zh.md | 10 +++++++++- site/docs.html | 9 ++++++++- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a7f1afd..7fa0459 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Apache-2.0 SWE-bench retrieval 87.0% network calls: 0 + platforms: macOS, Linux, Windows

@@ -382,8 +383,15 @@ Three things that only break once you leave Latin script, each of which broke he curl -fsSL https://raw.githubusercontent.com/lambiengcode/reify/main/install.sh | sh ``` -Prebuilt binaries for macOS (Apple Silicon and Intel), Linux (x86_64 and aarch64) -and Windows (x86_64). +Prebuilt binaries for **macOS** (Apple Silicon and Intel), **Linux** (x86_64 and +aarch64) and **Windows** (x86_64). On Windows the line above works as written in Git +Bash, MSYS2 or WSL; from PowerShell, take the `x86_64-pc-windows-msvc` archive from +[the latest release](https://github.com/lambiengcode/reify/releases/latest), verify its +`.sha256`, and put `reify.exe` somewhere on your `PATH`. + +Every one of those platforms runs the full test suite in CI, and the CLI is exercised +end to end on each — a binary is not published for a platform nothing has executed. + Or build from source: ```bash diff --git a/README.vi.md b/README.vi.md index 3f68ff7..f4a742f 100644 --- a/README.vi.md +++ b/README.vi.md @@ -20,6 +20,7 @@ Apache-2.0 SWE-bench retrieval 87.0% network calls: 0 + platforms: macOS, Linux, Windows

@@ -403,7 +404,16 @@ Ba thứ chỉ vỡ khi bạn rời khỏi hệ chữ Latinh, và cả ba đều curl -fsSL https://raw.githubusercontent.com/lambiengcode/reify/main/install.sh | sh ``` -Có sẵn binary dựng trước cho macOS (Apple Silicon và Intel) và Linux (x86_64 và aarch64). +Có sẵn binary dựng trước cho **macOS** (Apple Silicon và Intel), **Linux** (x86_64 và +aarch64) và **Windows** (x86_64). Trên Windows, dòng lệnh trên chạy được nguyên vẹn +trong Git Bash, MSYS2 hoặc WSL; từ PowerShell, hãy tải archive `x86_64-pc-windows-msvc` +ở [bản phát hành mới nhất](https://github.com/lambiengcode/reify/releases/latest), kiểm +tra `.sha256` của nó, rồi đặt `reify.exe` vào một thư mục nằm trong `PATH`. + +Mọi nền tảng trong số đó đều chạy toàn bộ test suite trên CI, và CLI được chạy thử +đầu-cuối trên từng nền tảng — không phát hành binary cho một nền tảng mà chưa có gì +từng chạy trên đó. + Hoặc build từ mã nguồn: ```bash diff --git a/README.zh.md b/README.zh.md index 14382a1..d4134f7 100644 --- a/README.zh.md +++ b/README.zh.md @@ -20,6 +20,7 @@ Apache-2.0 SWE-bench retrieval 87.0% network calls: 0 + platforms: macOS, Linux, Windows

@@ -390,7 +391,14 @@ C/C++、Kotlin,外加 SQL。每一种都有一个测试断言它能产出容 curl -fsSL https://raw.githubusercontent.com/lambiengcode/reify/main/install.sh | sh ``` -提供 macOS(Apple Silicon 与 Intel)和 Linux(x86_64 与 aarch64)的预编译二进制。 +提供 **macOS**(Apple Silicon 与 Intel)、**Linux**(x86_64 与 aarch64)和 **Windows** +(x86_64)的预编译二进制。在 Windows 上,上面这行命令在 Git Bash、MSYS2 或 WSL 中可以 +原样运行;若使用 PowerShell,请从[最新发布](https://github.com/lambiengcode/reify/releases/latest) +下载 `x86_64-pc-windows-msvc` 压缩包,校验其 `.sha256`,再把 `reify.exe` 放到 `PATH` 中。 + +以上每个平台都会在 CI 上跑完整的测试套件,并逐一做端到端的 CLI 验证 —— 不会为一个从未 +被执行过的平台发布二进制。 + 也可以从源码构建: ```bash diff --git a/site/docs.html b/site/docs.html index 3944d1e..6ff273b 100644 --- a/site/docs.html +++ b/site/docs.html @@ -43,7 +43,14 @@

Install

-

Prebuilt binaries for macOS (Apple Silicon and Intel) and Linux (x86_64 and aarch64).

+

+ Prebuilt binaries for macOS (Apple Silicon and Intel), Linux (x86_64 and aarch64) + and Windows (x86_64). On Windows the command above works in Git Bash, MSYS2 or + WSL; from PowerShell, take the x86_64-pc-windows-msvc archive from + the latest release, verify its .sha256, and put + reify.exe on your PATH. Every platform runs the full + test suite in CI. +

 curl -fsSL https://raw.githubusercontent.com/lambiengcode/reify/main/install.sh | sh