chore: remove cbindgen from build graph

This commit is contained in:
Lahfir 2026-05-19 15:42:38 -07:00
parent f637659dc3
commit 70bef8fe7c
11 changed files with 34 additions and 504 deletions

View file

@ -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'

View file

@ -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

View file

@ -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)

366
Cargo.lock generated
View file

@ -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"

View file

@ -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

View file

@ -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<PathBuf> {
// OUT_DIR = .../target/<profile>/build/<pkg-hash>/out
let mut current = out_dir;
for _ in 0..4 {
current = current.parent()?;
}
Some(current.to_path_buf())
}

View file

@ -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 <stdint.h>
#include <stdbool.h>
@ -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."
);
}

View file

@ -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"

View file

@ -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-<hash>/` 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 |

View file

@ -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

View file

@ -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-<hash>/` 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"