Files
Codewhale 853bbb2513 chore(scripts): dev-cache warns before it fills a disk
The #5465 wedge was dev-cache.sh putting ~6 GB build dirs on a volume with
6.5 GiB free and running it to ENOSPC. Add a free-space probe: warn when the
volume holding the cache root has less than CODEWHALE_DEV_CACHE_MIN_FREE_GIB
free, name the cold-build cost (~6 GB), and point at the escape hatches
(CODEWHALE_CACHE_ROOT, the volume-preference envs). Dev tooling only; no
product behavior changes.
2026-08-18 19:22:35 -07:00

474 lines
17 KiB
Bash
Executable File

#!/bin/sh
# Portable opt-in Cargo cache topology for Codewhale worktrees.
#
# Isolated per-workspace intermediates (Cargo build-dir + {workspace-path-hash})
# plus optional shared sccache. Never hard-codes a machine path. Does not
# change product behavior. This file is safe to source from other /bin/sh
# helpers; the CLI path is the only one that calls `set -eu` and `exit`.
#
# Usage:
# . scripts/dev-cache.sh && codewhale_dev_cache_apply
# scripts/dev-cache.sh --status
# scripts/dev-cache.sh --self-check
# scripts/dev-cache.sh --print-exports
# scripts/dev-cache.sh --help
#
# Environment:
# CODEWHALE_CACHE_ROOT cache root (default:
# ${XDG_CACHE_HOME:-$HOME/.cache}/codewhale)
# CODEWHALE_DEV_CACHE auto|1|force|local|0 (default auto)
# auto/1/force = isolated build-dir (still
# respects an already-set
# CARGO_TARGET_DIR / CARGO_BUILD_BUILD_DIR)
# local = keep compiling into ./target
# 0 = do nothing
# CODEWHALE_SCCACHE auto|1|0 (default auto)
# auto = wrap rustc only when incremental is
# already off and sccache is on PATH
# 1 = request CARGO_INCREMENTAL=0 (unless
# the caller already set it to on) and
# wrap if sccache exists
# 0 = never wrap
# CODEWHALE_DEV_CACHE_QUIET 1 to suppress the one-line status
# CODEWHALE_DEV_CACHE_REPO_ROOT
# workspace root used for ./target detection
# (default: $PWD)
# CODEWHALE_RUSTC_COMMIT override the rustc commit used to namespace
# SCCACHE_DIR (tests / offline hosts)
#
# sccache cannot cache incremental units. This helper never enables the
# wrapper while CARGO_INCREMENTAL is on, and it never turns incremental off
# unless CODEWHALE_SCCACHE=1. Everyday edits stay on cargo's incremental
# default. New-worktree / CI-like rebuilds that already set
# CARGO_INCREMENTAL=0 get the measured shared-compiler-output path.
codewhale_dev_cache_truthy() {
case ${1:-} in
1|true|yes|on|force) return 0 ;;
*) return 1 ;;
esac
}
codewhale_dev_cache_falsey() {
case ${1:-} in
0|false|no|off|never|disabled) return 0 ;;
*) return 1 ;;
esac
}
codewhale_dev_cache_root() {
if [ -n "${CODEWHALE_CACHE_ROOT:-}" ]; then
printf '%s\n' "$CODEWHALE_CACHE_ROOT"
return
fi
if [ -n "${XDG_CACHE_HOME:-}" ]; then
printf '%s\n' "$XDG_CACHE_HOME/codewhale"
return
fi
if [ -n "${HOME:-}" ]; then
printf '%s\n' "$HOME/.cache/codewhale"
return
fi
printf '%s\n' "${TMPDIR:-/tmp}/codewhale-cache"
}
codewhale_dev_cache_repo_root() {
if [ -n "${CODEWHALE_DEV_CACHE_REPO_ROOT:-}" ]; then
printf '%s\n' "$CODEWHALE_DEV_CACHE_REPO_ROOT"
return
fi
pwd
}
codewhale_dev_cache_rustc_commit() {
if [ -n "${CODEWHALE_RUSTC_COMMIT:-}" ]; then
printf '%s\n' "$CODEWHALE_RUSTC_COMMIT"
return
fi
# rustc -vV is cheap and does not start a compile.
_cw_out=$(rustc -vV 2>/dev/null || true)
_cw_commit=$(printf '%s\n' "$_cw_out" | awk '/^commit-hash:/{print $2; exit}')
if [ -n "$_cw_commit" ]; then
printf '%s\n' "$_cw_commit"
else
printf '%s\n' "unknown"
fi
}
codewhale_dev_cache_cargo_version_mm() {
# cargo 1.97.0 -> 1097. Empty if cargo is missing or unparsable.
cargo --version 2>/dev/null | awk '{
split($2, a, ".")
if (a[1] ~ /^[0-9]+$/ && a[2] ~ /^[0-9]+$/)
printf "%d\n", a[1]*1000+a[2]
}'
}
# Stable 16-hex-char fingerprint of a path. Used only for the pre-1.91
# CARGO_TARGET_DIR fallback; Cargo 1.91+ expands {workspace-path-hash}.
codewhale_dev_cache_path_fingerprint() {
if command -v sha256sum >/dev/null 2>&1; then
printf '%s' "$1" | sha256sum | awk '{print substr($1,1,16)}'
elif command -v shasum >/dev/null 2>&1; then
printf '%s' "$1" | shasum -a 256 | awk '{print substr($1,1,16)}'
elif command -v openssl >/dev/null 2>&1; then
printf '%s' "$1" | openssl dgst -sha256 | awk '{print substr($NF,1,16)}'
else
printf '%s' "$1" | cksum | awk '{printf "%s%08s\n", $1, $2}' | tr ' ' '0'
fi
}
codewhale_dev_cache_incremental_off() {
case ${CARGO_INCREMENTAL:-} in
0|false|no|off) return 0 ;;
*) return 1 ;;
esac
}
codewhale_dev_cache_incremental_on() {
case ${CARGO_INCREMENTAL:-} in
1|true|yes|on) return 0 ;;
*) return 1 ;;
esac
}
codewhale_dev_cache_sccache_bin() {
command -v sccache 2>/dev/null || true
}
codewhale_dev_cache_log() {
if [ "${CODEWHALE_DEV_CACHE_QUIET:-}" = 1 ]; then
return 0
fi
printf 'dev-cache: %s\n' "$*" >&2
}
# Free space (in GiB, integer, truncated) on the filesystem holding $1, or
# empty when it cannot be determined. `df -Pk` is the POSIX-portable form and
# behaves the same on macOS and Linux.
codewhale_dev_cache_free_gib() {
_cw_probe=$1
# Walk up to the nearest existing ancestor: the cache root usually does not
# exist yet on a first run, and df needs a real path.
while [ -n "$_cw_probe" ] && [ ! -d "$_cw_probe" ]; do
_cw_parent=$(dirname -- "$_cw_probe")
[ "$_cw_parent" = "$_cw_probe" ] && break
_cw_probe=$_cw_parent
done
[ -d "$_cw_probe" ] || return 0
df -Pk -- "$_cw_probe" 2>/dev/null | awk 'NR==2 { print int($4 / 1048576) }'
}
# A cold workspace build-dir is ~6 GB, and cargo writes it incrementally, so a
# volume that runs out mid-build leaves a half-written cache and a failed run.
# Worse, on the host where this was added the low volume was also $TMPDIR's:
# the shell tool's spill `tempfile` fails on a full disk, which is the exact
# wedge #5465 was filed for. Warn before the build starts rather than after it
# dies, and name the override that fixes it.
CODEWHALE_DEV_CACHE_MIN_FREE_GIB=${CODEWHALE_DEV_CACHE_MIN_FREE_GIB:-15}
codewhale_dev_cache_warn_low_space() {
_cw_root=$1
_cw_free=$(codewhale_dev_cache_free_gib "$_cw_root")
[ -n "$_cw_free" ] || return 0
[ "$_cw_free" -lt "$CODEWHALE_DEV_CACHE_MIN_FREE_GIB" ] || return 0
codewhale_dev_cache_log \
"WARNING: only ${_cw_free}GiB free on the volume holding ${_cw_root}; a cold build-dir is ~6GB."
codewhale_dev_cache_log \
" Set CODEWHALE_CACHE_ROOT=<dir on a roomier volume> (threshold: CODEWHALE_DEV_CACHE_MIN_FREE_GIB)."
}
# Apply the topology to the current shell. Idempotent. Never overrides an
# already-set CARGO_TARGET_DIR, CARGO_BUILD_BUILD_DIR, RUSTC_WRAPPER, or
# SCCACHE_DIR.
codewhale_dev_cache_apply() {
CODEWHALE_DEV_CACHE_MODE=${CODEWHALE_DEV_CACHE_MODE:-}
# Re-entry after a previous apply in this shell: keep the first decision
# unless the caller unset the mode (tests do that between cases).
if [ -n "${CODEWHALE_DEV_CACHE_APPLIED:-}" ] && [ -n "${CODEWHALE_DEV_CACHE_MODE:-}" ]; then
return 0
fi
_cw_want=${CODEWHALE_DEV_CACHE:-auto}
_cw_root=$(codewhale_dev_cache_root)
_cw_sccache_want=${CODEWHALE_SCCACHE:-auto}
_cw_commit=$(codewhale_dev_cache_rustc_commit)
CODEWHALE_DEV_CACHE_SCCACHE_DIR=${SCCACHE_DIR:-${_cw_root}/sccache/${_cw_commit}}
CODEWHALE_DEV_CACHE_BUILD_DIR=${_cw_root}/build/'{workspace-path-hash}'
CODEWHALE_DEV_CACHE_LEGACY_TARGET_DIR=${_cw_root}/target/$(codewhale_dev_cache_path_fingerprint "$(codewhale_dev_cache_repo_root)")
if codewhale_dev_cache_falsey "$_cw_want"; then
CODEWHALE_DEV_CACHE_MODE=disabled
CODEWHALE_DEV_CACHE_SCCACHE=disabled
CODEWHALE_DEV_CACHE_APPLIED=1
export CODEWHALE_DEV_CACHE_MODE CODEWHALE_DEV_CACHE_SCCACHE
codewhale_dev_cache_log "disabled (CODEWHALE_DEV_CACHE=${_cw_want})"
return 0
fi
if [ -n "${CARGO_TARGET_DIR:-}" ]; then
CODEWHALE_DEV_CACHE_MODE=inherited-target-dir
elif [ -n "${CARGO_BUILD_BUILD_DIR:-}" ]; then
CODEWHALE_DEV_CACHE_MODE=inherited-build-dir
elif [ "$_cw_want" = local ]; then
# Explicit stay-in-./target. A stub target/ created by a previous
# isolated build-dir run is not a reason to abandon isolation: Cargo
# still writes CACHEDIR.TAG (and sometimes finals) under ./target
# when build-dir is split, and treating that as "warm" made the
# second command recompile everything into a second tree.
CODEWHALE_DEV_CACHE_MODE=existing-target
else
CODEWHALE_DEV_CACHE_MODE=isolated-build-dir
fi
case $CODEWHALE_DEV_CACHE_MODE in
force-isolated|isolated-build-dir)
_cw_mm=$(codewhale_dev_cache_cargo_version_mm)
if [ -n "$_cw_mm" ] && [ "$_cw_mm" -lt 1091 ]; then
# Cargo 1.91 introduced build.build-dir. Older cargo only isolates
# via CARGO_TARGET_DIR; keep that fallback per-workspace.
if [ -z "${CARGO_TARGET_DIR:-}" ]; then
CARGO_TARGET_DIR=$CODEWHALE_DEV_CACHE_LEGACY_TARGET_DIR
export CARGO_TARGET_DIR
fi
CODEWHALE_DEV_CACHE_MODE=${CODEWHALE_DEV_CACHE_MODE}-legacy-target-dir
else
CARGO_BUILD_BUILD_DIR=$CODEWHALE_DEV_CACHE_BUILD_DIR
export CARGO_BUILD_BUILD_DIR
mkdir -p "$_cw_root/build" 2>/dev/null || true
fi
;;
esac
# sccache: never wrap incremental rustc. Missing binary is a fallback,
# not an error.
if codewhale_dev_cache_falsey "$_cw_sccache_want"; then
CODEWHALE_DEV_CACHE_SCCACHE=disabled
elif [ -n "${RUSTC_WRAPPER:-}" ]; then
case $RUSTC_WRAPPER in
*sccache*) CODEWHALE_DEV_CACHE_SCCACHE=external-sccache ;;
*) CODEWHALE_DEV_CACHE_SCCACHE=external-wrapper ;;
esac
else
if codewhale_dev_cache_truthy "$_cw_sccache_want" && ! codewhale_dev_cache_incremental_on; then
# Request the measured new-worktree / CI-like topology.
if [ -z "${CARGO_INCREMENTAL:-}" ]; then
CARGO_INCREMENTAL=0
export CARGO_INCREMENTAL
fi
fi
if ! codewhale_dev_cache_incremental_off; then
CODEWHALE_DEV_CACHE_SCCACHE=skipped-incremental
else
_cw_bin=$(codewhale_dev_cache_sccache_bin)
if [ -z "$_cw_bin" ]; then
CODEWHALE_DEV_CACHE_SCCACHE=not-found
else
RUSTC_WRAPPER=$_cw_bin
export RUSTC_WRAPPER
if [ -z "${SCCACHE_DIR:-}" ]; then
SCCACHE_DIR=$CODEWHALE_DEV_CACHE_SCCACHE_DIR
export SCCACHE_DIR
fi
mkdir -p "$SCCACHE_DIR" 2>/dev/null || true
CODEWHALE_DEV_CACHE_SCCACHE=enabled
fi
fi
fi
CODEWHALE_DEV_CACHE_APPLIED=1
export CODEWHALE_DEV_CACHE_MODE CODEWHALE_DEV_CACHE_SCCACHE
export CODEWHALE_DEV_CACHE_SCCACHE_DIR
_cw_line="mode=${CODEWHALE_DEV_CACHE_MODE} sccache=${CODEWHALE_DEV_CACHE_SCCACHE}"
case $CODEWHALE_DEV_CACHE_MODE in
isolated-build-dir*|force-isolated*)
_cw_line="${_cw_line} build-dir=${CARGO_BUILD_BUILD_DIR:-${CARGO_TARGET_DIR:-unset}}"
;;
inherited-target-dir)
_cw_line="${_cw_line} CARGO_TARGET_DIR=${CARGO_TARGET_DIR}"
;;
inherited-build-dir)
_cw_line="${_cw_line} CARGO_BUILD_BUILD_DIR=${CARGO_BUILD_BUILD_DIR}"
;;
existing-target)
_cw_line="${_cw_line} using ./target (CODEWHALE_DEV_CACHE=local)"
;;
esac
if [ "${CODEWHALE_DEV_CACHE_SCCACHE}" = enabled ]; then
_cw_line="${_cw_line} SCCACHE_DIR=${SCCACHE_DIR} rustc=${_cw_commit}"
fi
codewhale_dev_cache_log "$_cw_line"
case $CODEWHALE_DEV_CACHE_MODE in
disabled|existing-target) ;;
*) codewhale_dev_cache_warn_low_space "$(codewhale_dev_cache_root)" ;;
esac
}
codewhale_dev_cache_status() {
_cw_tmpl=${CODEWHALE_DEV_CACHE_BUILD_DIR:-}
if [ -z "$_cw_tmpl" ]; then
_cw_tmpl=$(codewhale_dev_cache_root)/build/'{workspace-path-hash}'
fi
printf '%s\n' \
"cache_root=$(codewhale_dev_cache_root)" \
"repo_root=$(codewhale_dev_cache_repo_root)" \
"mode=${CODEWHALE_DEV_CACHE_MODE:-unset}" \
"sccache=${CODEWHALE_DEV_CACHE_SCCACHE:-unset}" \
"build_dir_template=${_cw_tmpl}" \
"CARGO_BUILD_BUILD_DIR=${CARGO_BUILD_BUILD_DIR:-<unset>}" \
"CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-<unset>}" \
"CARGO_INCREMENTAL=${CARGO_INCREMENTAL:-<unset>}" \
"RUSTC_WRAPPER=${RUSTC_WRAPPER:-<unset>}" \
"SCCACHE_DIR=${SCCACHE_DIR:-<unset>}" \
"rustc_commit=$(codewhale_dev_cache_rustc_commit)" \
"sccache_bin=$(codewhale_dev_cache_sccache_bin)"
}
# Extra cargo CLI flags that force template expansion of build-dir.
# CARGO_BUILD_BUILD_DIR is still exported for nested tools; --config is
# what Cargo documents as supporting {workspace-path-hash}.
codewhale_dev_cache_exec_cargo() {
case ${CODEWHALE_DEV_CACHE_MODE:-} in
isolated-build-dir|force-isolated)
if [ -n "${CARGO_BUILD_BUILD_DIR:-}" ]; then
exec cargo --config "build.build-dir = \"${CARGO_BUILD_BUILD_DIR}\"" "$@"
fi
;;
esac
exec cargo "$@"
}
codewhale_dev_cache_print_exports() {
[ -n "${CARGO_BUILD_BUILD_DIR:-}" ] && printf 'export CARGO_BUILD_BUILD_DIR=%s\n' "$CARGO_BUILD_BUILD_DIR"
[ -n "${CARGO_TARGET_DIR:-}" ] && printf 'export CARGO_TARGET_DIR=%s\n' "$CARGO_TARGET_DIR"
[ -n "${CARGO_INCREMENTAL:-}" ] && printf 'export CARGO_INCREMENTAL=%s\n' "$CARGO_INCREMENTAL"
[ -n "${RUSTC_WRAPPER:-}" ] && printf 'export RUSTC_WRAPPER=%s\n' "$RUSTC_WRAPPER"
[ -n "${SCCACHE_DIR:-}" ] && printf 'export SCCACHE_DIR=%s\n' "$SCCACHE_DIR"
[ -n "${CODEWHALE_DEV_CACHE_MODE:-}" ] && printf 'export CODEWHALE_DEV_CACHE_MODE=%s\n' "$CODEWHALE_DEV_CACHE_MODE"
[ -n "${CODEWHALE_DEV_CACHE_SCCACHE:-}" ] && printf 'export CODEWHALE_DEV_CACHE_SCCACHE=%s\n' "$CODEWHALE_DEV_CACHE_SCCACHE"
}
codewhale_dev_cache_self_check() {
_cw_fail=0
_cw_root=$(codewhale_dev_cache_root)
_cw_home=${HOME:-}
_cw_xdg=${XDG_CACHE_HOME:-}
if [ -z "${CODEWHALE_CACHE_ROOT:-}" ]; then
if [ -n "$_cw_xdg" ]; then
case $_cw_root in
"$_cw_xdg"/codewhale) ;;
*)
printf 'self-check: expected XDG default %s/codewhale, got %s\n' "$_cw_xdg" "$_cw_root" >&2
_cw_fail=1
;;
esac
elif [ -n "$_cw_home" ]; then
case $_cw_root in
"$_cw_home"/.cache/codewhale) ;;
*)
printf 'self-check: expected HOME default %s/.cache/codewhale, got %s\n' "$_cw_home" "$_cw_root" >&2
_cw_fail=1
;;
esac
fi
fi
case ${CARGO_BUILD_BUILD_DIR:-} in
*'{workspace-path-hash}'*)
;;
'')
case ${CODEWHALE_DEV_CACHE_MODE:-} in
isolated-build-dir|force-isolated)
printf 'self-check: isolated mode did not set CARGO_BUILD_BUILD_DIR\n' >&2
_cw_fail=1
;;
esac
;;
*)
case ${CODEWHALE_DEV_CACHE_MODE:-} in
isolated-build-dir|force-isolated)
printf 'self-check: isolated build-dir lacks {workspace-path-hash}: %s\n' "$CARGO_BUILD_BUILD_DIR" >&2
_cw_fail=1
;;
esac
;;
esac
if [ "${CODEWHALE_DEV_CACHE_SCCACHE:-}" = enabled ]; then
if ! codewhale_dev_cache_incremental_off; then
printf 'self-check: sccache enabled while incremental is not off\n' >&2
_cw_fail=1
fi
case ${RUSTC_WRAPPER:-} in
*sccache*) ;;
*)
printf 'self-check: sccache enabled but RUSTC_WRAPPER=%s\n' "${RUSTC_WRAPPER:-<unset>}" >&2
_cw_fail=1
;;
esac
fi
if [ "${CODEWHALE_DEV_CACHE_SCCACHE:-}" = not-found ]; then
if [ -n "${RUSTC_WRAPPER:-}" ]; then
printf 'self-check: missing sccache must not set RUSTC_WRAPPER (%s)\n' "$RUSTC_WRAPPER" >&2
_cw_fail=1
fi
fi
if [ "$_cw_fail" -eq 0 ]; then
printf 'dev-cache: self-check ok\n'
return 0
fi
printf 'dev-cache: self-check FAILED\n' >&2
return 1
}
codewhale_dev_cache_usage() {
cat <<'EOF'
usage: scripts/dev-cache.sh --status|--self-check|--print-exports|--help
Portable opt-in Cargo cache topology. Source this file and call
codewhale_dev_cache_apply from scripts/dev-test.sh / scripts/dev-cargo.sh.
--status apply (if needed) and print key=value status
--self-check apply and check portable defaults / sccache fallback
--print-exports apply and print export statements for eval
--help this message
Environment overrides are documented at the top of this file. Defaults
never contain a machine-specific absolute path.
EOF
}
# CLI only when this file is the executed program, not when sourced.
case ${0##*/} in
dev-cache.sh)
set -eu
_cw_cmd=${1:---status}
case $_cw_cmd in
--status|-s)
codewhale_dev_cache_apply
codewhale_dev_cache_status
;;
--self-check)
codewhale_dev_cache_apply
codewhale_dev_cache_status
codewhale_dev_cache_self_check
;;
--print-exports)
codewhale_dev_cache_apply
codewhale_dev_cache_print_exports
;;
--help|-h)
codewhale_dev_cache_usage
;;
*)
codewhale_dev_cache_usage >&2
exit 2
;;
esac
;;
esac