874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
134 lines
7.9 KiB
Bash
Executable File
134 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Run a command on a Linux build host (default playhorizon) at a worktree's commit, and get its output back.
|
|
#
|
|
# tools/remote.sh [--host H] [--name N] [--cpus LIST] [--exclusive] [--jobs J] <worktree> <command...>
|
|
# tools/remote.sh .claude/worktrees/my-fix cargo test -p horizon-world --lib
|
|
# tools/remote.sh . python3 tools/parity.py food light
|
|
# tools/remote.sh --cpus 0-3 --exclusive . python3 tools/perf_bench.py # a benchmark: CPUs 0-3 to itself
|
|
# tools/remote.sh --list # checkouts on the host, their size and last use
|
|
# tools/remote.sh --gc [DAYS] # remove checkouts unused for DAYS (default 2)
|
|
#
|
|
# The host fetches the commit from GitHub itself: commit and push first (uncommitted changes are not sent, and
|
|
# nothing else leaves this machine: no files, no credentials, no environment). Each name (default: the worktree's
|
|
# folder) keeps one checkout there, ~/remote-work/<name>, with its own target/: the next run checks out the new
|
|
# commit over it, so only what changed rebuilds (sccache shares the registry crates between names). One run per
|
|
# name at a time. The command runs in bash at the checkout's root, niced in the host's CI slice, with the host's
|
|
# toolchains, a virtual screen and scratch homes for the game and launcher. .local/ links to the host's vanilla
|
|
# data; a command that runs harnesses (it names parity, lan_e2e or horizon-prepare) also gets .local/horizon-client,
|
|
# prepared as CI prepares it (tools/ci/vanilla-data.sh). Output streams back and is kept in ~/remote-work/<name>.logs/.
|
|
# What the command leaves running is stopped when it ends; stopping this script stops the command on the host.
|
|
# The exit status is the command's.
|
|
#
|
|
# CPUs: playhorizon's CI runner has CPUs 4-7 (a systemd drop-in pins it, docs/release-flow.md), so runs here get
|
|
# 0-3 by default and never slow a pull request's parity job. A run holds its CPU set's lock shared; --exclusive (a
|
|
# benchmark: name it with --cpus and say so in its pull request) holds it alone, so it starts only when no other run
|
|
# uses those CPUs, and the others wait for it to end. --cpus 4-7 or all compete with CI: only when it is idle.
|
|
#
|
|
# The host is lent by a partner who keeps hypervisor access: never send secrets, tokens or credentials there,
|
|
# and never treat a result from it as a CI result.
|
|
set -euo pipefail
|
|
|
|
host=playhorizon name="" jobs="" cpus=0-3 exclusive=0
|
|
usage() { sed -n '2,9s/^# \{0,1\}//p' "$0" >&2; exit 2; }
|
|
while [ $# -gt 0 ]; do
|
|
case $1 in
|
|
--host) host=$2; shift 2;;
|
|
--name) name=$2; shift 2;;
|
|
--jobs) jobs=$2; shift 2;;
|
|
--cpus) cpus=$2; shift 2;;
|
|
--exclusive) exclusive=1; shift;;
|
|
--list) exec ssh "ludovic@$host" 'cd ~/remote-work 2>/dev/null && for d in */; do d=${d%/};
|
|
printf "%-32s %8s last used %s\n" "$d" "$(du -sh "$d" 2>/dev/null | cut -f1)" "$(stat -c %y "$d.logs" 2>/dev/null | cut -c1-16)"; done';;
|
|
--gc) days=${2:-2}; exec ssh "ludovic@$host" "cd ~/remote-work 2>/dev/null || exit 0
|
|
find . -maxdepth 1 -name '*.logs' -mtime +$days | while read -r logs; do d=\${logs%.logs};
|
|
flock -n \"\$d.lock\" rm -rf \"\$d\" \"\$logs\" \"\$d.lock\" && echo \"removed \${d#./}\"; done";;
|
|
-h|--help) usage;;
|
|
*) break;;
|
|
esac
|
|
done
|
|
[ $# -ge 2 ] || usage
|
|
tree=$(cd "$1" && git rev-parse --show-toplevel); shift
|
|
sha=$(git -C "$tree" rev-parse HEAD)
|
|
name=${name:-$(basename "$tree")}
|
|
[[ $name =~ ^[A-Za-z0-9._-]+$ ]] || { echo "remote.sh: name '$name' must be letters, digits, . _ -" >&2; exit 2; }
|
|
if ! git -C "$tree" diff --quiet HEAD -- 2>/dev/null; then
|
|
echo "remote.sh: uncommitted changes in $tree are not sent: the host runs ${sha:0:9}" >&2
|
|
fi
|
|
if [ -z "$(git -C "$tree" branch -r --contains "$sha" 2>/dev/null)" ]; then
|
|
git -C "$tree" fetch -q origin 2>/dev/null || true
|
|
[ -n "$(git -C "$tree" branch -r --contains "$sha" 2>/dev/null)" ] ||
|
|
{ echo "remote.sh: ${sha:0:9} is not on GitHub yet: push it first (the host fetches from GitHub)" >&2; exit 2; }
|
|
fi
|
|
command=$(printf '%q ' "$@")
|
|
|
|
# The host side reads its script and these four values on stdin (nothing is re-split by a remote shell); its
|
|
# own environment comes from the runner's files there.
|
|
{
|
|
[[ $cpus =~ ^[0-9,-]+$|^all$ ]] || { echo "remote.sh: --cpus takes a list such as 0-3, or all" >&2; exit 2; }
|
|
printf 'name=%q sha=%q jobs=%q cpus=%q exclusive=%q command=%q\n' "$name" "$sha" "$jobs" "$cpus" "$exclusive" "$command"
|
|
cat <<'REMOTE'
|
|
set -euo pipefail
|
|
if [ "$cpus" = all ]; then pin=(); jobs=${jobs:-$(nproc)}; else pin=(taskset -c "$cpus"); jobs=${jobs:-$(taskset -c "$cpus" nproc)}; fi
|
|
runner=~/.actions-runners/horizon
|
|
if [ -e "$runner/paused" ]; then echo "remote.sh: this host is paused ($runner/paused)" >&2; exit 75; fi
|
|
set -a; . "$runner/.env"; set +a
|
|
export PATH="$(cat "$runner/.path")" DISPLAY=":$(cat "$runner/display")" RUSTC_WRAPPER="$(command -v sccache || true)"
|
|
export CARGO_BUILD_JOBS=$jobs FLEET_LOAD_JOBS=$jobs PARITY_LOCAL_JOBS=$(( jobs > 1 ? jobs / 2 : 1 ))
|
|
unset WAYLAND_DISPLAY
|
|
mkdir -p ~/remote-work && cd ~/remote-work
|
|
exec 9> "$name.lock"
|
|
flock -n 9 || { echo "remote.sh: $name is busy with another run" >&2; exit 75; }
|
|
# The CPU set's lock: shared by ordinary runs, held alone by an --exclusive one (a benchmark).
|
|
exec 8> ".cpus-$cpus.lock"
|
|
if [ "$exclusive" = 1 ]; then
|
|
flock -x -n 8 || { echo "remote.sh: other runs use CPUs $cpus; try again when they are done (--list)" >&2; exit 75; }
|
|
elif ! flock -s -n 8; then
|
|
echo "remote.sh: an exclusive run (a benchmark) has CPUs $cpus; waiting for it to end" >&2
|
|
flock -s 8
|
|
fi
|
|
if [ ! -d "$name/.git" ]; then
|
|
git clone -q --filter=blob:none --no-checkout https://github.com/horizon-rust/horizon "$name" 8>&- 9>&-
|
|
fi
|
|
cd "$name"
|
|
# The lock's descriptor stays out of everything started here (8>&- 9>&-): a daemon one of them starts (sccache's server,
|
|
# from the preparer's build or the command's) would hold the lock after the run.
|
|
git fetch -q --no-tags origin "$sha" 8>&- 9>&-
|
|
git checkout -q --force --detach "$sha"
|
|
git clean -qfd -e /target -e /.local
|
|
# The host's vanilla data, linked as CI's runner links it; for harnesses also .local/horizon-client, prepared by
|
|
# this commit's own preparer (once per preparer version, kept beside the data and shared by every name).
|
|
if [[ $command =~ parity|lan_e2e|horizon-prepare ]]; then
|
|
RUNNER_WORKSPACE=$(dirname "$HORIZON_SHARED_SOURCE") RUNNER_TEMP=$(mktemp -d) bash tools/ci/vanilla-data.sh >&2 8>&- 9>&-
|
|
else
|
|
mkdir -p .local
|
|
for d in minecraft tools datagen; do
|
|
if [ -e "$HORIZON_SHARED_SOURCE/$d" ]; then ln -sfn "$HORIZON_SHARED_SOURCE/$d" ".local/$d"; fi
|
|
done
|
|
fi
|
|
scratch=$(mktemp -d)
|
|
trap 'rm -rf "$scratch"' EXIT
|
|
export HORIZON_HOME=$scratch/game HORIZON_LAUNCHER_HOME=$scratch/launcher XDG_DATA_HOME=$scratch/data \
|
|
XDG_CONFIG_HOME=$scratch/config HORIZON_NO_AUDIO=1 HORIZON_LAUNCHER_NO_SELF_UPDATE=1 HORIZON_TELEMETRY_URL=off
|
|
mkdir -p "$HORIZON_HOME" "$HORIZON_LAUNCHER_HOME" "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" ../"$name.logs"
|
|
log=../"$name.logs/$(date -u +%Y%m%dT%H%M%SZ)-${sha:0:9}.log"
|
|
echo "remote.sh: $(hostname) at ${sha:0:9} in ~/remote-work/$name, CPUs $cpus, -j $jobs; log ~/remote-work/${log#../}" >&2
|
|
# The command runs in a scope of its own, stopped with everything in it once the command is done (a game or a
|
|
# daemon it left behind), when the caller goes away first (the ssh session ends), and before the next run here.
|
|
unit=remote-$name.scope
|
|
systemctl --user stop "$unit" 2>/dev/null || true
|
|
session=$PPID
|
|
( while kill -0 "$session" 2>/dev/null; do sleep 2; done; systemctl --user stop "$unit" 2>/dev/null ) \
|
|
< /dev/null > /dev/null 2>&1 8>&- 9>&- &
|
|
watcher=$!
|
|
status=0
|
|
systemd-run --user --scope --quiet --slice=horizon-ci.slice --unit="$unit" \
|
|
"${pin[@]}" nice -n 10 bash -c "$command" 2>&1 < /dev/null 8>&- 9>&- | tee "$log" || status=${PIPESTATUS[0]}
|
|
kill "$watcher" 2>/dev/null || true
|
|
systemctl --user stop "$unit" 2>/dev/null || true
|
|
exit "$status"
|
|
REMOTE
|
|
} | ssh -o BatchMode=yes "ludovic@$host" bash -s &
|
|
# Stopping this script ends the session, and the host then stops the command (above).
|
|
trap 'kill $! 2>/dev/null' INT TERM HUP
|
|
wait $!
|