#!/usr/bin/env bash # Run a command against this repository inside the capped CI container. # # docker/ci/run cargo check --workspace --all-targets # docker/ci/run cargo clippy --workspace --all-targets --keep-going -- -D warnings # docker/ci/run bash -c 'cargo fmt --all; cargo fmt --all -- --check | grep -c "^Diff in"' # # 🔴 EVERY HEAVY COMMAND GOES THROUGH HERE. A bare `cargo build` on the host is # unbounded: `CARGO_BUILD_JOBS` caps codegen units, not rustc's own threads, not # the linker, not the test harness. A full-parallel build has OOM-crashed this # box, and unbounded host runs have frozen it since. The container is the cap # that actually holds. # # Half the machine, no swap. `--memory-swap` equal to `--memory` means a build # that would swap is killed instead, which is a fast failure rather than an # hour of thrashing that takes the desktop with it. set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" IMAGE="${SYLPH_CI_IMAGE:-sylph-ci:local}" CPUS="${SYLPH_CI_CPUS:-6}" MEM_GB="${SYLPH_CI_MEM_GB:-7}" CARGO_VOL="${SYLPH_CI_CARGO_VOL:-sylph-ci-cargo}" TARGET_VOL="${SYLPH_CI_TARGET_VOL:-sylph-ci-target}" # 🔴 Docker on the dev boxes is ROOTFUL, so without `--user` every byte the build # writes into the bind-mounted repo is owned by root — and the user then needs # `sudo` to delete their own artifacts. The regen command in the workspace # `CLAUDE.md` writes `sylpheed.db` straight into /work, so it lands root-owned, # and a stray root-owned file is exactly what survived the last cleanup and had # to be sudo'd away. # # The catch is that the daemon creates a named volume root-owned, so a `--user` # container cannot write /cargo or /target at all. Take ownership once — and # only when it is actually wrong, because a recursive chown across a ~36 GB # target volume is not something to repeat on every invocation. RUN_UID="$(id -u)" RUN_GID="$(id -g)" docker volume create "$CARGO_VOL" >/dev/null docker volume create "$TARGET_VOL" >/dev/null # Both volumes, not just one: they are chowned together but can drift apart if an # older root-owned run created only one of them. vol_owner="$(docker run --rm -v "$CARGO_VOL:/cargo" -v "$TARGET_VOL:/target" "$IMAGE" \ stat -c %u /cargo /target 2>/dev/null | sort -u | tr '\n' ' ' || echo unknown)" if [ "$vol_owner" != "$RUN_UID " ]; then echo "docker/ci/run: chowning the cargo/target volumes to $RUN_UID:$RUN_GID (one-off)" >&2 docker run --rm \ -v "$CARGO_VOL:/cargo" -v "$TARGET_VOL:/target" \ "$IMAGE" chown -R "$RUN_UID:$RUN_GID" /cargo /target fi args=( --rm --cpus "$CPUS" --memory "${MEM_GB}g" --memory-swap "${MEM_GB}g" --pids-limit 2048 # Run as the invoking user so build output in /work is owned by them, not root. --user "$RUN_UID:$RUN_GID" -v "$REPO:/work" # Named volumes, not bind mounts: the host tree keeps a 32 GB `target/` from # earlier host-side builds, and mixing the two produces rebuilds that look # like cache misses and are actually two toolchains fighting over one directory. -v "$CARGO_VOL:/cargo" -e CARGO_HOME=/cargo -v "$TARGET_VOL:/target" -e CARGO_TARGET_DIR=/target -w /work ) # The corpora, read-only, when a disc-backed test or the exporter needs them. # # All three, not just the disc: a suite whose corpus is absent self-skips and # still counts as passed, so mounting one of three made an in-container run look # like a full one while `res3d` and `iso` suites silently sat out (#16). Each is # mounted only when it exists, and `target/sylpheed-corpus-report.txt` says which # ones the run actually had. DISC="${SYLPHEED_DISC:-$REPO/../sylph_extract}" [ -d "$DISC" ] && args+=(-v "$DISC:/disc:ro" -e SYLPHEED_DISC=/disc) RES3D="${SYLPHEED_RES3D:-}" [ -n "$RES3D" ] && [ -d "$RES3D" ] && args+=(-v "$RES3D:/res3d:ro" -e SYLPHEED_RES3D=/res3d) ISO="${SYLPHEED_ISO:-}" [ -n "$ISO" ] && [ -f "$ISO" ] && args+=(-v "$ISO:/disc.iso:ro" -e SYLPHEED_ISO=/disc.iso) exec docker run "${args[@]}" "$IMAGE" "$@"