diff --git a/.changeset/rust-target-per-checkout.md b/.changeset/rust-target-per-checkout.md new file mode 100644 index 00000000..518ffec1 --- /dev/null +++ b/.changeset/rust-target-per-checkout.md @@ -0,0 +1,13 @@ +--- +--- + +fix(tooling): key the Rust build directory to the checkout + +`scripts/rust.sh` mounted one `cache/target` at `/target` for every source tree. Every +checkout is mounted at the same container path, `/work/nostream`, so cargo could not tell +one tree's artifacts from another's: a scratch build left a test binary compiled from +reverted sources, the next run in a worktree executed it without recompiling, and only +touching the sources cleared it. The target directory is now derived from the checkout's +basename and a checksum of its full path (`/target--`), and +`CARGO_TARGET_DIR` still overrides it. The toolchain and registry under the cache root stay +shared; the price is one cold build per checkout. diff --git a/scripts/rust.sh b/scripts/rust.sh index 45263fa8..5eff13a2 100755 --- a/scripts/rust.sh +++ b/scripts/rust.sh @@ -15,6 +15,12 @@ # NOSTREAM_HOST_DIR source tree as the Docker daemon sees it. Default: this script's # repository root, translated /workspace -> /home/cameri/Workspace. # RUST_CACHE_HOST host cache root (default /home/cameri/Workspace/containers/rust-builder/cache) +# CARGO_TARGET_DIR in-container build directory. Default: /target--, +# one directory per source path, mounted under RUST_CACHE_HOST. Set it +# only to override that keying; a shared path reintroduces #906. +# RUST_CACHE_VISIBLE where this shell sees RUST_CACHE_HOST, when that differs (a cache kept +# on a remote daemon host). Only used to create the per-checkout target +# directory with the right owner. # RUST_BUILDER_IMAGE image (default containers-rust-builder:latest) set -eu @@ -51,6 +57,58 @@ if [ ! -f "$checked_root/Cargo.toml" ] && [ ! -f "$local_root/Cargo.toml" ]; the exit 66 fi +# One target directory per checkout. Every checkout is mounted at the same container path, +# /work/nostream, so cargo fingerprints a build of a scratch copy identically to a build of +# this tree. A single shared /target therefore reuses artifacts compiled from other sources: +# a revert in one tree surfaces as a failed test in another, and a revert-then-fix pair can +# only be made green by touching the sources, which is the symptom, not the fix (#906). +# The key is the checkout's basename and a checksum of its full path: readable, and two +# trees sharing a name stay apart. The toolchain and registry under RUST_CACHE_HOST stay +# shared; the price is a cold build the first time each checkout is used. +if [ -n "${CARGO_TARGET_DIR:-}" ]; then + target_dir=$CARGO_TARGET_DIR + case "$target_dir" in + /*) ;; + *) echo "scripts/rust.sh: CARGO_TARGET_DIR must be absolute, got: $target_dir" >&2; exit 64 ;; + esac +else + target_key=$(printf '%s' "$src_host" | cksum | cut -d' ' -f1) + target_dir="/target-$(basename "$src_host")-$target_key" +fi +target_host="$cache_host$target_dir" + +# The daemon creates a missing bind source as an empty directory owned by root, which uid +# 1000 then cannot write, so create the target directory here when this shell can see it. +# The cache root is a path on the daemon's host: the sandbox sees the same tree at +# /workspace, and a remote daemon may own the cache entirely on its side. Pick the spelling +# this shell can see; when neither exists, leave the directory to the daemon, which is how +# a daemon-only cache already works, rather than refusing a cache this side cannot check. +if [ -n "${RUST_CACHE_VISIBLE:-}" ]; then + if [ ! -d "$RUST_CACHE_VISIBLE" ]; then + echo "scripts/rust.sh: RUST_CACHE_VISIBLE=$RUST_CACHE_VISIBLE is not a directory" >&2 + exit 66 + fi + cache_visible=$RUST_CACHE_VISIBLE +elif [ -d "$cache_host" ]; then + cache_visible=$cache_host +else + case "$cache_host" in + /home/cameri/Workspace/*) cache_visible="/workspace/${cache_host#/home/cameri/Workspace/}" ;; + *) cache_visible= ;; + esac +fi +if [ -n "$cache_visible" ] && [ -d "$cache_visible" ]; then + if ! mkdir -p "$cache_visible$target_dir" 2>/dev/null || [ ! -w "$cache_visible$target_dir" ]; then + echo "scripts/rust.sh: cannot write $cache_visible$target_dir (it must be writable by uid 1000; set CARGO_TARGET_DIR, RUST_CACHE_HOST or RUST_CACHE_VISIBLE)" >&2 + exit 66 + fi +else + # The cache root is not visible from this shell, so this side cannot create the directory; + # the daemon would create a missing bind source as root, which uid 1000 cannot write. + # Say so once rather than letting cargo surface it as a permission error. + echo "scripts/rust.sh: $target_host is not visible here; it must exist on the Docker host and be writable by uid 1000 (set RUST_CACHE_VISIBLE to where this shell sees the cache)" >&2 +fi + name="rust-run-$$-$(date +%s)" cleanup() { docker rm -f "$name" >/dev/null 2>&1 || true @@ -60,12 +118,12 @@ trap cleanup EXIT INT HUP TERM docker create --name "$name" --label org.testcontainers=true --user 1000:1000 \ -e CARGO_HOME=/home/node/.cargo \ -e RUSTUP_HOME=/home/node/.rustup \ - -e CARGO_TARGET_DIR=/target \ + -e CARGO_TARGET_DIR="$target_dir" \ -e CARGO_TERM_COLOR=never \ -e HOME=/home/node \ -v "$cache_host/cargo:/home/node/.cargo" \ -v "$cache_host/rustup:/home/node/.rustup" \ - -v "$cache_host/target:/target" \ + -v "$target_host:$target_dir" \ -v "$src_host:/work/nostream" \ "$image" bash -lc 'cd /work/nostream && exec "$@"' rust-sh "$@" >/dev/null