diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da5e401..fb65f45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,6 +75,13 @@ jobs: - name: Unit tests run: cargo test --lib --workspace + # The FFI crate ships integration harnesses under crates/ffi/tests/ that + # exercise the C-ABI from outside the crate (raw extern "C" decls, enum + # fuzzing, out-param zeroing, last-error lifetimes). The workspace --lib + # run skips them, so wire them in explicitly so regressions fail PR CI. + - name: FFI integration tests + run: cargo test -p agent-desktop-ffi --tests + # Use the ci profile (no LTO, opt-level 1) — fast compile, still checks the binary builds. - name: Build binary (ci profile) run: cargo build --profile ci @@ -95,9 +102,18 @@ jobs: - name: FFI header drift check run: | - GENERATED=$(find target -path '*/agent-desktop-ffi-*/out/agent_desktop.h' | head -1) - if [ -z "$GENERATED" ]; then - echo "FAIL: cbindgen did not produce a header in OUT_DIR" + # build.rs stamps target/ffi-header-path.txt with the absolute path + # to the header produced by the most recent build, so the drift + # check always compares against the just-generated artifact rather + # than an arbitrary match from `find | head -1` over a warm cache. + STAMP=target/ffi-header-path.txt + if [ ! -f "$STAMP" ]; then + echo "FAIL: $STAMP missing; build.rs did not stamp header path" + exit 1 + fi + GENERATED=$(cat "$STAMP") + if [ ! -f "$GENERATED" ]; then + echo "FAIL: stamped header path does not exist: $GENERATED" exit 1 fi if ! diff -u crates/ffi/include/agent_desktop.h "$GENERATED"; then diff --git a/crates/ffi/build.rs b/crates/ffi/build.rs index e33bec3..34ace24 100644 --- a/crates/ffi/build.rs +++ b/crates/ffi/build.rs @@ -1,5 +1,5 @@ use std::env; -use std::path::Path; +use std::path::{Path, PathBuf}; fn main() { println!("cargo:rerun-if-changed=cbindgen.toml"); @@ -51,4 +51,34 @@ fn main() { // script auto-copied it, the drift check would self-heal instead of // catching stale headers. Developers update the committed header by // running `scripts/update-ffi-header.sh`. + + // Stamp the absolute path to the just-generated header at a stable, + // deterministic location that CI and scripts can read without ever + // resorting to `find target | head -1` (which picks arbitrarily when + // multiple `agent-desktop-ffi-/` build dirs are cached). Walking + // 4 parents up from OUT_DIR yields the cargo target root: + // {target}/{profile}/build/{pkg-hash}/out → {target} + if let Some(target_root) = target_root_from_out_dir(Path::new(&out_dir)) { + let stamp = target_root.join("ffi-header-path.txt"); + if let Err(err) = std::fs::write(&stamp, out_path.to_string_lossy().as_bytes()) { + println!( + "cargo:warning=failed to stamp FFI header path at {:?}: {}", + stamp, err + ); + } + } else { + println!( + "cargo:warning=could not infer cargo target root from OUT_DIR={}; skipping ffi-header-path.txt", + out_dir + ); + } +} + +fn target_root_from_out_dir(out_dir: &Path) -> Option { + // OUT_DIR = .../target//build//out + let mut current = out_dir; + for _ in 0..4 { + current = current.parent()?; + } + Some(current.to_path_buf()) } diff --git a/scripts/update-ffi-header.sh b/scripts/update-ffi-header.sh index dfda516..299e481 100755 --- a/scripts/update-ffi-header.sh +++ b/scripts/update-ffi-header.sh @@ -2,18 +2,27 @@ set -euo pipefail # Regenerate the committed FFI header from source. -# Run this after changing any `#[no_mangle] pub extern "C"` signatures -# in crates/ffi/src/ and commit the result. +# +# Run this after changing any `#[no_mangle] pub extern "C"` signatures in +# crates/ffi/src/ and commit the result. build.rs stamps the absolute path +# to the generated header at target/ffi-header-path.txt so we never have to +# guess which of several cached `agent-desktop-ffi-/` build dirs is +# current — `find target | head -1` would pick arbitrarily. -cargo build -p agent-desktop-ffi 2>/dev/null +ROOT=$(git rev-parse --show-toplevel) +cd "$ROOT" -GENERATED=$(find target -path '*/agent-desktop-ffi-*/out/agent_desktop.h' -newer crates/ffi/include/agent_desktop.h | head -1) -if [ -z "$GENERATED" ]; then - GENERATED=$(find target -path '*/agent-desktop-ffi-*/out/agent_desktop.h' | head -1) +cargo build -p agent-desktop-ffi >/dev/null 2>&1 + +STAMP=target/ffi-header-path.txt +if [ ! -f "$STAMP" ]; then + echo "ERROR: $STAMP was not produced by build.rs. Check the build output." >&2 + exit 1 fi -if [ -z "$GENERATED" ]; then - echo "ERROR: cbindgen did not produce a header. Check build output." >&2 +GENERATED=$(cat "$STAMP") +if [ ! -f "$GENERATED" ]; then + echo "ERROR: stamped header path does not exist: $GENERATED" >&2 exit 1 fi