chore(build): disable incremental compilation, add just target-sweep

Cargo has no garbage collector. It deletes a crate's previous incremental
session only when the build that replaced it succeeds, so every killed or
interrupted build orphans its session permanently. Parallel agents share
these target dirs and routinely kill each other's test runs, so the orphans
reached 1,423 sessions / 38 GB in target/ plus 174 / 19 GB in the desktop
tree -- 42% of all Rust build output, for a cache that kept getting
invalidated anyway.

Set incremental = false in .cargo/config.toml to close the class, and add
scripts/target-sweep.sh (just target-sweep) to reclaim what already
accumulated. The sweep refuses to run while any cargo/rustc is live: these
trees are shared, and deleting a session dir under a running rustc fails
that build and reads as a compiler bug rather than as our doing.

deps/ is deliberately untouched -- it is the warm cache that keeps a rebuild
cheap, so clearing it is a cold build and stays just clean.

Signed-off-by: Joshua Belke <joshua@innovationhub-act.org>
This commit is contained in:
Josh Belke 2026-08-08 16:10:40 -04:00
commit 94ab49bfd0
5 changed files with 132 additions and 1 deletions

View file

@ -1,3 +1,12 @@
[build]
# Cargo deletes a crate's previous incremental session only when the build
# that replaced it *succeeds*. Parallel agents kill each other's test runs, so
# the interrupted sessions orphan permanently and are never reclaimed — this
# tree reached 1,423 session dirs / 35 GB, half of `target/debug`, for a cache
# that kept getting invalidated anyway. Want it back for a tight single-crate
# edit loop? Override per-shell: CARGO_INCREMENTAL=1
incremental = false
[profile.dev]
# Full file/line backtraces, no per-variable debug info. ~20-30% faster
# builds and much faster macOS link. Need lldb locals? Override per-shell:

View file

@ -360,6 +360,26 @@ commands to compensate for an unconfigured shell `PATH`.
**Commit with `git commit -s`.** The required **DCO Check** fails any PR with a commit missing a `Signed-off-by` trailer, and `just hooks` installs a `commit-msg` hook that adds it to commits you create locally (`git rebase` and `git cherry-pick` still need `--signoff`) — if you build commit commands programmatically, include `-s` every time. To repair a branch that already has unsigned commits: `git rebase --signoff main`, then force-push.
**Incremental compilation is off, and `just target-sweep` reclaims what it
left behind.** Cargo has no garbage collector: it deletes a crate's previous
incremental session only when the build that replaced it *succeeds*, so every
killed or interrupted build orphans its session permanently. Parallel agents
share these target dirs and routinely kill each other's test runs, so the
orphans grew to 1,423 sessions / 38 GB in `target/` plus 174 / 19 GB in the
desktop tree — 42% of all build output, for a cache that kept getting
invalidated anyway. `incremental = false` in `.cargo/config.toml` closes that
class; set `CARGO_INCREMENTAL=1` per-shell if you want it back for a tight
single-crate edit loop. `just target-sweep` clears existing caches from both
trees and refuses to run while a build is in flight, because deleting a session
dir under a live `rustc` fails that build and reads as a compiler bug.
`deps/` is a separate decision. It is the warm cache that keeps a rebuild
cheap, and it is also where the other accumulation lives: each `cargo test -p
<crate>` resolves a different feature union than the full workspace, minting a
fresh `-C metadata` hash and a complete new artifact set that is then kept
forever (62 cached builds of `libsqlx_postgres` here). Clearing it means a cold
build — that is `just clean`, never folded into a sweep.
Additional rules:
- No `unsafe` code

View file

@ -864,7 +864,12 @@ test-db-clean:
# `-run` half is what CI invokes, so the selection lives in ONE place and a CI
# job and a developer's box cannot drift apart.
# The 46 `#[ignore]`d meridian-relay tests, which are the row's PRIMARY crate.
# The 51 `#[ignore]`d meridian-relay tests, which are the row's PRIMARY crate.
# Re-derive this rather than trusting it: three authors wrote 46, 47 and 49
# beside the same selection while the tree held 50. `grep -rc '#\[ignore'
# crates/meridian-relay/src/` overcounts — nine are the token inside comment
# prose — so count what the binary lists:
# cargo test -p meridian-relay --lib -- --ignored --list | grep -c ': test'
#
# Before this recipe, 27 of the crate's 48 ran in NO recipe and NO CI job: CI
# reached only `api::invites`, `api::media` and `api::admin` through two
@ -1510,6 +1515,10 @@ clean:
cargo clean
cargo clean --manifest-path desktop/src-tauri/Cargo.toml
# Reclaim incremental-cache disk in both target trees without a cold rebuild
target-sweep *ARGS:
./scripts/target-sweep.sh {{ARGS}}
# Check the Rust workspace compiles without producing binaries
check-compile:
cargo check --workspace --all-targets

View file

@ -19,6 +19,7 @@ Grouped by job:
| PR / repo hygiene | `post-screenshots.sh`, `check-pr-image-urls.sh`, `check-branch-skew.sh`, `release-rulesets.sh`; `git-commit-scoped.sh` — commits exactly the paths it is given on a branch shared by parallel agents. The root contract forbids branching so agents interleave on `main`, which means they share one working tree and **one git index**, and a bare `git commit -m` takes that whole index (finding F077: commit `2c2eb9153` carried seven of another agent's files despite explicit staging). Two mechanisms, two halves, neither sufficient alone — a pathspec excludes foreign index entries, and a post-commit assertion catches files a `stage_fixed` hook adds mid-commit, which a pathspec provably cannot exclude. It never rewrites history and never unstages another agent's work |
| Release | `mobile-release.sh`, `publish-mobile-release-candidate.sh`, `verify-release-ref.sh`, `build-meridian-harness.sh`, `bundle-sidecars.sh` |
| Worktrees | `mobile-worktree-overrides.sh`, `mobile-worktree-clean.sh` |
| Build caches | `target-sweep.sh` — run by `just target-sweep`, drops `incremental/` from **both** target trees (the desktop crate is outside the root workspace, so it carries its own) and reports what it reclaimed; `--dry-run` reports without deleting. It sweeps only `incremental/`: `deps/` is the warm cache that keeps a rebuild cheap, so clearing it is a cold build and stays `just clean`. The load-bearing part is the guard — these trees are shared by every agent in the repo, and deleting a session dir under a live `rustc` fails that build and reads as a compiler bug rather than as our doing, so the script refuses while any `cargo`/`rustc` is running and prints the offending PIDs |
| Seeding / local state | `seed-local-community.sh`, `seed-admin-dashboard.sh`, `setup-desktop-test-data.sh`, `reset-desktop-dev-state.sh`, `reset-desktop-standalone-state.sh`, `cleanup-instance-agents.sh`, `grab-emoji.sh` |
| Operator SQL | `attach-schema-partitions.sql`, `backfill-d-tag.sql`, `cutover/` (`1321_backfill_default_community.sql`, `nip_rs_search_allowlist.sql`, `README.md`), `maintenance/` |
| Brand generators | `generate-globe-geometry.mjs` — simplifies Natural Earth 1:110m land (`.settings/brand/`) into the desktop `landOutline.ts` every globe resolves to; **one target on purpose** (web imports it via its `@brand` alias, the control plane inlines a bundle built from it), and the output needs a Biome pass after writing. `generate-control-plane-brand.mjs` — bundles the desktop globe + particle-wordmark renderers into `crates/meridian-control-plane/assets/brand.{js,css}`, which the Rust pages inline at compile time so they stay one self-contained document; `--check` (wired into `just check-brand`) fails on drift. Both write generated artefacts: re-run the script, never hand-edit the output |

92
scripts/target-sweep.sh Executable file
View file

@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Reclaims Rust build-cache disk without forcing a cold rebuild.
#
# Cargo has no garbage collector. It deletes a crate's previous incremental
# session only when the build that replaced it *succeeds*, so every killed or
# interrupted build orphans its session permanently. Parallel agents share
# these target dirs and routinely kill each other's test runs, so the orphans
# accumulate without bound — this tree reached 1,423 session dirs / 38 GB in
# `target/` plus 174 / 19 GB in the desktop tree, 42% of all build output, for
# a cache that kept getting invalidated anyway.
#
# Only `incremental/` is swept. `deps/` is left alone: it is the warm cache
# that makes a rebuild cheap, so clearing it means a cold build — that is
# `just clean`, deliberately a separate decision.
#
# Run `just target-sweep`, or this script directly; pass --dry-run to report
# what would be reclaimed without deleting.
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"
dry_run=0
if [[ "${1:-}" == "--dry-run" ]]; then
dry_run=1
fi
# The desktop crate is excluded from the root workspace, so it carries its own
# target dir and its own copy of this problem.
target_roots=(
"target"
"desktop/src-tauri/target"
)
# ── Refuse to sweep under a live build ───────────────────────────────────────
#
# These trees are shared by every agent working in the repo. Deleting a session
# dir that a running rustc is writing fails that build, and the failure reads
# as a compiler bug rather than as our doing.
builders="$(pgrep -f 'rustup/toolchains/[^ ]*/bin/(cargo|rustc)' 2>/dev/null || true)"
if [[ -n "$builders" ]]; then
echo "target-sweep: a Rust build is in flight — refusing to sweep." >&2
echo "" >&2
pgrep -lf 'rustup/toolchains/[^ ]*/bin/(cargo|rustc)' 2>/dev/null \
| cut -c1-120 \
| sed 's/^/ /' >&2
echo "" >&2
echo "Wait for it to finish, then re-run. Sweeping now would fail that build." >&2
exit 1
fi
# ── Sweep ────────────────────────────────────────────────────────────────────
swept_kb=0
swept_dirs=0
for root in "${target_roots[@]}"; do
for profile_dir in "$root"/*/; do
incremental="${profile_dir}incremental"
[[ -d "$incremental" ]] || continue
sessions="$(find "$incremental" -maxdepth 1 -mindepth 1 -type d | wc -l | tr -d ' ')"
[[ "$sessions" -gt 0 ]] || continue
size_kb="$(du -sk "$incremental" | cut -f1)"
printf '%-52s %8s %5s sessions\n' \
"$incremental" "$(du -sh "$incremental" | cut -f1)" "$sessions"
if [[ "$dry_run" -eq 0 ]]; then
rm -rf "$incremental"
fi
swept_kb=$((swept_kb + size_kb))
swept_dirs=$((swept_dirs + sessions))
done
done
if [[ "$swept_dirs" -eq 0 ]]; then
echo "target-sweep: nothing to reclaim."
exit 0
fi
verb="reclaimed"
[[ "$dry_run" -eq 1 ]] && verb="would reclaim"
awk -v kb="$swept_kb" -v d="$swept_dirs" -v v="$verb" \
'BEGIN { printf "\ntarget-sweep: %s %.1f GB across %d sessions\n", v, kb/1024/1024, d }'
if [[ "$dry_run" -eq 0 ]]; then
echo "The next build of each crate is non-incremental once; deps/ stays warm."
fi