Skip to content

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

# Inside the lbug crate (upstream Cargo.toml excerpt)
cxx = "=1.0.138"

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:

name = "cxx"
version = "1.0.138"

name = "cxx-build"
version = "1.0.138"

This lock was established via:

cargo update -p cxx-build --precise 1.0.138

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:

# crates/amplihack-cli/Cargo.toml
[build-dependencies]
cxx-build = { workspace = true }

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:

cargo test cargo_lock_cxx_consistency
# test cargo_lock_cxx_consistency_test ... ok

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.