diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ceb73e02..71f6c983 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,29 +103,6 @@ jobs: - name: FFI cdylib build (release-ffi profile) run: cargo build --locked --profile release-ffi -p agent-desktop-ffi - - name: FFI header drift check - run: | - # 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 - echo "FAIL: crates/ffi/include/agent_desktop.h is out of date" - echo "Run 'scripts/update-ffi-header.sh' locally and commit the regenerated header." - exit 1 - fi - echo "OK: FFI header is in sync with source" - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: '24' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dfab8721..f0f95798 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -180,7 +180,7 @@ jobs: # agent-desktop FFI cdylib — v${VERSION} (${{ matrix.target }}) - \`lib/${{ matrix.lib_name }}\` — shared library - - \`include/agent_desktop.h\` — cbindgen header + - \`include/agent_desktop.h\` — committed C ABI header - \`LICENSE\` — Apache-2.0 Docs: https://github.com/lahfir/agent-desktop/tree/main/skills/agent-desktop-ffi diff --git a/CLAUDE.md b/CLAUDE.md index 2da53440..50971e5d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,7 +74,7 @@ agent-desktop/ │ ├── macos/ # agent-desktop-macos (Phase 1) │ ├── windows/ # agent-desktop-windows (stub → Phase 2) │ ├── linux/ # agent-desktop-linux (stub → Phase 2) -│ └── ffi/ # agent-desktop-ffi (cdylib + cbindgen C ABI) +│ └── ffi/ # agent-desktop-ffi (cdylib + committed C ABI header) ├── src/ # agent-desktop binary (entry point) │ ├── main.rs # entry point, permission check, JSON envelope │ ├── cli.rs # clap derive enum (Commands) diff --git a/Cargo.lock b/Cargo.lock index c353476f..af0c0c53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,6 @@ dependencies = [ "agent-desktop-linux", "agent-desktop-macos", "agent-desktop-windows", - "cbindgen", "libc", "smallvec", ] @@ -147,12 +146,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - [[package]] name = "base64" version = "0.22.1" @@ -165,25 +158,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" -[[package]] -name = "cbindgen" -version = "0.29.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "befbfd072a8e81c02f8c507aefce431fe5e7d051f83d48a23ffc9b9fe5a11799" -dependencies = [ - "clap", - "heck", - "indexmap", - "log", - "proc-macro2", - "quote", - "serde", - "serde_json", - "syn", - "tempfile", - "toml", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -276,34 +250,6 @@ dependencies = [ "libc", ] -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foreign-types" version = "0.5.0" @@ -331,58 +277,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" - [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.0", - "serde", - "serde_core", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -401,24 +301,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - [[package]] name = "log" version = "0.4.29" @@ -467,16 +355,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -495,12 +373,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "regex-automata" version = "0.4.14" @@ -524,25 +396,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - [[package]] name = "serde" version = "1.0.228" @@ -586,15 +439,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -630,19 +474,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix", - "windows-sys", -] - [[package]] name = "thiserror" version = "2.0.18" @@ -672,45 +503,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow 0.7.15", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow 1.0.3", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - [[package]] name = "tracing" version = "0.1.44" @@ -778,12 +570,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "utf8parse" version = "0.2.2" @@ -796,58 +582,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "windows-link" version = "0.2.1" @@ -863,106 +597,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - -[[package]] -name = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "zmij" version = "1.0.21" diff --git a/crates/ffi/Cargo.toml b/crates/ffi/Cargo.toml index a0f46eea..397d6dbc 100644 --- a/crates/ffi/Cargo.toml +++ b/crates/ffi/Cargo.toml @@ -22,8 +22,5 @@ agent-desktop-windows = { path = "../windows" } [target.'cfg(target_os = "linux")'.dependencies] agent-desktop-linux = { path = "../linux" } -[build-dependencies] -cbindgen = "= 0.29.2" - [lints] workspace = true diff --git a/crates/ffi/build.rs b/crates/ffi/build.rs index 687d6eca..25fd7629 100644 --- a/crates/ffi/build.rs +++ b/crates/ffi/build.rs @@ -1,81 +1,8 @@ -use std::env; -use std::path::{Path, PathBuf}; - fn main() { - println!("cargo:rerun-if-changed=cbindgen.toml"); - println!("cargo:rerun-if-changed=src/"); - // Cargo's default install_name is the absolute build-machine path, // which breaks linking consumers (Swift/SPM, clang) once the dylib // is extracted from a release tarball. @rpath is the portable form. if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") { println!("cargo:rustc-cdylib-link-arg=-Wl,-install_name,@rpath/libagent_desktop_ffi.dylib"); } - - let crate_dir = match env::var("CARGO_MANIFEST_DIR") { - Ok(d) => d, - Err(_) => { - println!("cargo:warning=CARGO_MANIFEST_DIR not set; skipping cbindgen"); - return; - } - }; - let out_dir = match env::var("OUT_DIR") { - Ok(d) => d, - Err(_) => { - println!("cargo:warning=OUT_DIR not set; skipping cbindgen"); - return; - } - }; - - let config = match cbindgen::Config::from_file("cbindgen.toml") { - Ok(c) => c, - Err(err) => panic!("cbindgen.toml parse error: {}", err), - }; - - let out_path = Path::new(&out_dir).join("agent_desktop.h"); - - let bindings = match cbindgen::Builder::new() - .with_crate(&crate_dir) - .with_config(config) - .generate() - { - Ok(b) => b, - Err(err) => panic!("cbindgen generation failed: {}", err), - }; - - bindings.write_to_file(&out_path); - if !out_path.exists() { - panic!("cbindgen produced no header at {:?}", out_path); - } - - // Deliberately NOT copying into crates/ffi/include/ — the committed - // header is the ABI contract. Auto-copy would self-heal CI drift checks. - // Developers refresh it via scripts/update-ffi-header.sh. - - // Stamp the absolute path of the generated header at a stable location - // so CI and scripts don't have to `find target | head -1` (ambiguous - // under warm caches with multiple pkg-hash dirs). - 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/crates/ffi/tests/c_header_compile.rs b/crates/ffi/tests/c_header_compile.rs index 028bdc09..5749d061 100644 --- a/crates/ffi/tests/c_header_compile.rs +++ b/crates/ffi/tests/c_header_compile.rs @@ -1,8 +1,8 @@ //! Verifies the committed FFI header compiles as C and that every named //! enum discriminant documented in the header is usable from C code. This -//! guards against cbindgen configuration drift that would silently drop -//! the `AdActionKind` / `AdDirection` / etc. enum blocks, forcing C -//! consumers to hard-code numeric literals instead of AD_* constants. +//! guards against header drift that would silently drop the `AdActionKind` / +//! `AdDirection` / etc. enum blocks, forcing C consumers to hard-code numeric +//! literals instead of AD_* constants. //! //! The test shells out to the system `cc`; it skips on platforms where //! that binary is not on PATH so cargo test still passes on bare CI @@ -43,10 +43,8 @@ fn committed_header_compiles_with_every_public_enum_constant() { let tmp = std::env::temp_dir().join("agent_desktop_header_abi_test.c"); let obj = std::env::temp_dir().join("agent_desktop_header_abi_test.o"); - // Touch every named-constant family so a missing enum block in the - // header fails compilation with "undeclared identifier". Keeping the - // list close to cbindgen.toml's [export].include ensures they stay - // in sync: add an entry there, add its representative here. + // Touch every named-constant family so a missing enum block in the header + // fails compilation with "undeclared identifier". let src = r#" #include #include @@ -88,6 +86,6 @@ int main(void) { assert!( status.success(), "C compile of agent_desktop.h failed — a named enum constant is missing. \ - Check crates/ffi/cbindgen.toml [export].include." + Check crates/ffi/include/agent_desktop.h." ); } diff --git a/deny.toml b/deny.toml index dd15514f..91f3b644 100644 --- a/deny.toml +++ b/deny.toml @@ -18,6 +18,9 @@ allow = [ multiple-versions = "allow" wildcards = "allow" highlight = "all" +deny = [ + { name = "cbindgen", reason = "Keep header generation out of the default Cargo build graph; use the explicit maintainer script only." }, +] [sources] unknown-registry = "deny" diff --git a/docs/phases.md b/docs/phases.md index fcd6efe2..3ffa1298 100644 --- a/docs/phases.md +++ b/docs/phases.md @@ -467,7 +467,7 @@ Current `.github/workflows/ci.yml` on every PR: - `cargo test -p agent-desktop-ffi --tests` (c_abi_harness + c_header_compile + error_lifetime integration suites) - `cargo build --profile ci` (fast CLI binary) + 15 MB size check - `cargo build --profile release-ffi -p agent-desktop-ffi` (the shipped cdylib profile) - - FFI header drift check — diffs `crates/ffi/include/agent_desktop.h` against the build-stamped `target/ffi-header-path.txt` + - FFI header contract check — compiles `crates/ffi/include/agent_desktop.h` from C tests and keeps header regeneration out of the default build graph ### Dependencies @@ -504,7 +504,7 @@ Phase 1.5 ships `crates/ffi/` as a first-class distribution target. The CLI stay | ID | Objective | Metric | |----|-----------|--------| -| P1.5-O1 | Stable C-ABI surface | `crates/ffi/include/agent_desktop.h` drift-checked in CI via a deterministic `ffi-header-path.txt` stamp | +| P1.5-O1 | Stable C-ABI surface | `crates/ffi/include/agent_desktop.h` compiled in CI as the committed ABI contract | | P1.5-O2 | 5-platform release | Tarballs for aarch64/x86_64 apple-darwin, aarch64/x86_64 unknown-linux-gnu, and x86_64 pc-windows-msvc on every tagged release | | P1.5-O3 | Panic safety | Dedicated `release-ffi` profile overrides `panic = "abort"` → `"unwind"`; `catch_unwind` wraps every `extern "C"` boundary via `trap_panic` / `trap_panic_ptr` / `trap_panic_const_ptr` / `trap_panic_void` | | P1.5-O4 | Main-thread safety (macOS) | `require_main_thread()` guard in every build profile; worker-thread call returns `AD_RESULT_ERR_INTERNAL` with a static `'static CStr` message | @@ -519,8 +519,8 @@ Phase 1.5 ships `crates/ffi/` as a first-class distribution target. The CLI stay ``` crates/ffi/ ├── Cargo.toml # crate-type = ["cdylib", "rlib"] -├── cbindgen.toml # [export].include forces emission of AdActionKind / AdDirection / AdModifier / AdMouseButton / AdMouseEventKind / AdScreenshotKind / AdSnapshotSurface / AdWindowOpKind even though the public ABI stores raw i32 -├── build.rs # runs cbindgen into $OUT_DIR, stamps target/ffi-header-path.txt, bakes install_name = @rpath/libagent_desktop_ffi.dylib on macOS +├── cbindgen.toml # maintainer-only header regeneration config +├── build.rs # bakes install_name = @rpath/libagent_desktop_ffi.dylib on macOS ├── include/ │ └── agent_desktop.h # committed, drift-checked against the OUT_DIR output ├── src/ # ad_* extern "C" entrypoints, organized by domain @@ -569,13 +569,13 @@ Regular `release` profile keeps `panic = "abort"` for the CLI binary, so a panic - `cargo build --profile release-ffi -p agent-desktop-ffi` on every PR - `cargo test -p agent-desktop-ffi --tests` runs the 3 integration suites -- FFI header drift check diffs the committed header against the OUT_DIR output discovered via `target/ffi-header-path.txt` (deterministic even with warm caches and multiple `agent-desktop-ffi-/` directories) +- FFI header contract check compiles the committed header from C tests. Header regeneration is an explicit maintainer action via `scripts/update-ffi-header.sh`, not part of ordinary builds. ### New Dependencies | Crate | Version | Scope | Purpose | |-------|---------|-------|---------| -| `cbindgen` | = 0.27.0 (pinned) | `crates/ffi` build-dep | C header generation | +| `cbindgen` | maintainer-installed tool, denied in Cargo graph | `scripts/update-ffi-header.sh` only | C header regeneration | | `libc` | 0.2+ | `crates/ffi` macOS target | `pthread_main_np` for main-thread check | ### Forward Compatibility @@ -1892,7 +1892,7 @@ All runners enforce: `cargo clippy --all-targets -- -D warnings`, `cargo test -- | `clap` 4.x, `serde` 1.x, `thiserror` 2.x, `tracing` 0.1+, `base64` 0.22+ | Phase 1 | Core: CLI, JSON, errors, logging, encoding | | `tracing-subscriber` 0.3, `rustc-hash` 2.1 | Phase 1 | Log formatter + fast hashing | | `accessibility-sys` 0.1+, `core-foundation` 0.10+, `core-graphics` 0.24+ | Phase 1 | macOS AX API FFI | -| `cbindgen` = 0.27.0 (pinned), `libc` 0.2+ | Phase 1.5 | C header generation + macOS `pthread_main_np` for FFI main-thread guard | +| `cbindgen` maintainer tool, `libc` 0.2+ | Phase 1.5 | explicit C header regeneration + macOS `pthread_main_np` for FFI main-thread guard | | `uiautomation` 0.24+ | Phase 2 | Windows UIA wrapper | | `windows` 0.62.2 | Phase 2 | Win32 / WinRT bindings (pinned to match `windows-capture 1.5` pin) | | `windows-capture` 1.5.4 | Phase 2 | Modern `Windows.Graphics.Capture` screenshot | diff --git a/docs/solutions/best-practices/deterministic-build-artifact-marker-2026-04-16.md b/docs/solutions/best-practices/deterministic-build-artifact-marker-2026-04-16.md index 5be8316d..332e7c6f 100644 --- a/docs/solutions/best-practices/deterministic-build-artifact-marker-2026-04-16.md +++ b/docs/solutions/best-practices/deterministic-build-artifact-marker-2026-04-16.md @@ -6,6 +6,7 @@ module: crates/ffi problem_type: best_practice component: tooling severity: medium +status: superseded_for_agent_desktop applies_when: - A build script (cbindgen, bindgen, codegen) writes an artifact to $OUT_DIR whose path includes a cargo-generated hash - CI or a developer script needs to locate that artifact after the build finishes @@ -23,6 +24,12 @@ tags: ## Problem +Update, 2026-05-19: `agent-desktop` no longer uses this pattern for the FFI +header. `cbindgen` is intentionally denied from the Cargo dependency graph, and +`crates/ffi/include/agent_desktop.h` is treated as the committed C ABI +contract. The pattern below still applies to projects that deliberately run a +build-script generator in the normal Cargo build graph. + Cargo puts build-script artifacts under a hash-randomized directory: ``` @@ -122,8 +129,6 @@ explicit developer action (dedicated script) or CI-only step. ## References -- `crates/ffi/build.rs` — the stamping logic -- `.github/workflows/ci.yml` — "FFI header drift check" step -- `scripts/update-ffi-header.sh` — developer-facing refresh script +- `scripts/update-ffi-header.sh` — explicit maintainer refresh script - Todo `006-ready-p2-deterministic-ffi-header-drift-path` — the bug this pattern resolves diff --git a/scripts/update-ffi-header.sh b/scripts/update-ffi-header.sh index 299e4817..9987b7cf 100755 --- a/scripts/update-ffi-header.sh +++ b/scripts/update-ffi-header.sh @@ -1,30 +1,19 @@ #!/usr/bin/env bash 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. 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. +# Regenerate the committed FFI header from source. This script is deliberately +# outside the normal Cargo build graph so cbindgen never executes during +# ordinary builds, tests, CI, or release packaging. ROOT=$(git rev-parse --show-toplevel) cd "$ROOT" -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 +if ! command -v cbindgen >/dev/null 2>&1; then + echo "ERROR: cbindgen is not installed. Install and audit it explicitly before regenerating the FFI header." >&2 exit 1 fi -GENERATED=$(cat "$STAMP") -if [ ! -f "$GENERATED" ]; then - echo "ERROR: stamped header path does not exist: $GENERATED" >&2 - exit 1 -fi - -cp "$GENERATED" crates/ffi/include/agent_desktop.h +cbindgen crates/ffi \ + --config crates/ffi/cbindgen.toml \ + --output crates/ffi/include/agent_desktop.h echo "Updated crates/ffi/include/agent_desktop.h"