The cxx/cxx-build Version Contract¶
amplihack-rs uses lbug 0.15.3, the LadybugDB (formerly Kuzu) C++ graph database, via the cxx FFI bridge. This document explains why cxx and cxx-build must stay on the exact same version, and what breaks when they don't.
Contents¶
- Why cxx needs two crates
- The version contract
- Version compatibility table
- How the mismatch happens
- How Cargo.lock prevents the mismatch
- How the workspace pin prevents drift
- Verifying the pin is in effect
- How CI enforces the pin
- Related
Why cxx needs two crates¶
The cxx crate splits its work across two packages:
| Crate | Role | Appears in |
|---|---|---|
cxx |
Runtime bridge — CxxString, CxxVector, UniquePtr |
Compiled binary |
cxx-build |
Build-time code generator — emits the C++ glue from .rs bridge declarations |
build.rs only |
The code generator (cxx-build) emits C++ that calls into the runtime (cxx). The two halves communicate through symbol names that include the minor version — for example cxxbridge$1$0$138$.... If the generator uses version 1.0.194 but the runtime is 1.0.138, the symbols don't match and the linker fails with undefined reference errors.
The version contract¶
Rule: cxx and cxx-build must always be on the same 1.0.x minor version.
This is not just a convention — the symbol names embed the minor version token. A mismatch produces linker errors, not a compile error, making the problem hard to diagnose without knowing the contract.
lbug 0.15.3 pins cxx exactly:
This means lbug's runtime expects symbols generated by cxx-build 1.0.138.
Version compatibility table¶
| lbug version | cxx (runtime) |
cxx-build (generator) |
|---|---|---|
| 0.15.3 | 1.0.138 (exact pin) |
must be 1.0.138 |
How the mismatch happens¶
Cargo's dependency resolver satisfies cxx = "=1.0.138" (exact pin) but resolves cxx-build = "^1.0" (semver range) independently. If another crate in the dependency graph requests a newer cxx-build, Cargo may select cxx-build 1.0.194 alongside cxx 1.0.138:
cxx = 1.0.138 ← exact pin, lbug requires this
cxx-build = 1.0.194 ← semver drift, different minor token
↑ symbols don't match → linker error
The symptom is cargo build failing with errors such as:
error: linking with `cc` failed: exit status: 1
= note: /usr/bin/ld: /tmp/kuzu-build/.../cxxbridge.o: undefined reference
to `cxxbridge1$box$kuzu$Database$alloc'
Builds pass in CI when the resolver happens to pick 1.0.138 for cxx-build (either due to a clean lockfile or a compatible resolution order), and fail locally when the lockfile drifts.
How Cargo.lock prevents the mismatch¶
Cargo.lock records exact resolved versions for every transitive dependency. Once cxx-build is pinned in the lockfile, every build — local or CI — uses the same version.
In amplihack-rs the lockfile pins both to 1.0.138:
This lock was established via:
If you regenerate Cargo.lock (e.g. cargo update or rm Cargo.lock) without re-pinning, the mismatch can recur. See Resolve LadybugDB linker errors for recovery steps.
How the workspace pin prevents drift¶
Cargo.lock only protects against the mismatch on machines that already have the lock. On a clean checkout — or after cargo update — the resolver is free to pick any cxx-build version that satisfies the lbug crate's open-range constraint ^1.0, potentially bumping past 1.0.138 and reintroducing the linker error.
To prevent this, amplihack-rs declares an exact workspace-level pin for cxx-build:
# Cargo.toml (workspace root)
[workspace.dependencies]
# SEC: Exact pin prevents cxx/cxx-build version skew that breaks LadybugDB FFI.
# Do not change without verifying lbug crate compatibility.
# See docs/concepts/cxx-version-contract.md.
cxx-build = "=1.0.138"
The amplihack-cli crate references this pin explicitly as a build dependency:
This explicit reference forces the Cargo resolver to apply the =1.0.138 exact constraint to the entire dependency graph, including lbug's transitive pull. Without an explicit reference in at least one crate, a workspace declaration has no effect on purely transitive dependencies.
A no-op build.rs in amplihack-cli activates the [build-dependencies] section (Cargo requires a build.rs to process build dependencies):
// crates/amplihack-cli/build.rs
// Intentionally empty — activates [build-dependencies] for cxx-build workspace pin.
// SEC: Do not add build logic here without a security review.
fn main() {}
With this in place, cargo update will not bump cxx-build past 1.0.138 because the = prefix denotes an exact version, not a semver range.
Verifying the pin is in effect¶
After any cargo update, confirm both versions are still pinned:
grep -A1 'name = "cxx"' Cargo.lock
# name = "cxx"
# version = "1.0.138"
grep -A1 'name = "cxx-build"' Cargo.lock
# name = "cxx-build"
# version = "1.0.138"
The cargo_lock_cxx_consistency_test integration test enforces this automatically at test time:
How CI enforces the pin¶
The check CI job runs a shell step before any Rust toolchain setup that reads
Cargo.lock directly and fails the build if cxx-build is not exactly
1.0.138:
- name: Verify cxx-build pin
run: |
version=$(grep -A1 'name = "cxx-build"' Cargo.lock | grep version | head -1 | sed 's/.*"\(.*\)".*//')
if [ "$version" != "1.0.138" ]; then
echo "ERROR: cxx-build must be pinned to 1.0.138, found $version"
echo "Run: cargo update -p cxx-build --precise 1.0.138"
exit 1
fi
Because test and cross-compile both declare needs: check, a failed pin
check blocks the entire pipeline before any compilation begins.
If CI fails on this step, follow the Fix the cxx-build Pin CI Failure guide to restore the correct lockfile entry.
Related¶
- Resolve LadybugDB linker errors — Step-by-step fix when the mismatch occurs
- Fix the cxx-build Pin CI Failure — Restore the Cargo.lock pin after a failed CI check
- cxx documentation — Official cxx bridge reference
- lbug Rust crate — LadybugDB 0.15.3 API reference
- GitHub Issue #35 — Original linker error report