Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/rust-target-per-checkout.md
Original file line number Diff line number Diff line change
@@ -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-<name>-<checksum>`), 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.
62 changes: 60 additions & 2 deletions scripts/rust.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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-<checkout>-<checksum>,
# 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

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
Loading