feat(install): versioned installs and a real uninstall — box#79's layout, ported
install.sh now lands every version at <root>/versions/<v> (each tree carrying its own VERSION + INSTALLED_FROM), tracks the default through an atomically-flipped 'current' symlink, and converges instead of clobbering: a same-version re-run is a no-op that says so, RIG_REINSTALL=1 replaces that version's tree by two renames (delete last), and a new version installs side by side. A pre-versioning flat tree is migrated in place — two renames, preserved bit for bit, VERSION-less trees as 0.0.0-unknown. bin/rig grows the table verbs: 'rig versions' (current + running marked), 'rig use <v>' (atomic flip, asserted effective through the PATH chain), 'rig uninstall [<v>|--all]' — which ENDS with an absence assert: every removed path re-checked, survivors exit 1 as 'uninstall INCOMPLETE' by name. One strict valid_version gate guards every place a version string becomes a path (byte-identical copies in bin/rig and install.sh, diffed by the suite so they cannot drift). Plus the VERSION file and 'rig --version' (rig#32's first item, folded in minimally — rig main had neither). The flip gate is rig's own shape, deliberately: box refuses flips under existing boxes; rig's stake is the converged host, so a flip (upgrade, 'rig use', full uninstall) on a host where /etc/rig/role exists WARNS and proceeds — no user state to strand, and upgrading a bootstrapped host is the normal case. The suite drives REAL installer runs (RIG_INSTALL_SOURCE against throwaway RIG_HOME/RIG_BIN roots): fresh install, converge, reinstall, side-by-side upgrade, use/rollback, both migrations, hostile flat VERSION, wedged- symlink healing, the marker warn gate (RIG_ROLE_MARKER fixtures), both uninstalls and the INCOMPLETE scream — driven, not grepped. Closes #35. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7646346311
commit
f1ada543dc
5 changed files with 765 additions and 29 deletions
58
README.md
58
README.md
|
|
@ -15,8 +15,56 @@ takes arguments, does its work, and stores no credential, ever.
|
||||||
curl -fsSL https://raw.githubusercontent.com/heavy-duty/rig/main/install.sh | bash
|
curl -fsSL https://raw.githubusercontent.com/heavy-duty/rig/main/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Installs the tree to `~/.local/share/rig` and links `rig` onto your
|
The layout, under the install root (`~/.local/share/rig`):
|
||||||
PATH (`/usr/local/bin` when root). Re-run any time to upgrade.
|
|
||||||
|
```
|
||||||
|
versions/<version>/ one full tree per installed version
|
||||||
|
current -> versions/<v> the tracked default
|
||||||
|
$BINDIR/rig -> current/bin/rig the PATH entry, riding the chain
|
||||||
|
```
|
||||||
|
|
||||||
|
`rig` lands on your PATH via `~/.local/bin` (`/usr/local/bin` when root).
|
||||||
|
|
||||||
|
**Re-running is a safe converge.** Installing a version you already have
|
||||||
|
changes nothing and says so (`RIG_REINSTALL=1` replaces that version's
|
||||||
|
tree); a **new** version installs side by side and becomes the default — so
|
||||||
|
"re-run any time to upgrade" stays true, and every version you had stays
|
||||||
|
installed as the way back:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rig versions # what is installed, which is current, which is running
|
||||||
|
rig use <version> # flip the default (atomic; asserts the flip took)
|
||||||
|
```
|
||||||
|
|
||||||
|
On a **bootstrapped host** (one where `/etc/rig/role` exists) switching the
|
||||||
|
default — by upgrade or by `rig use` — prints a WARNING, because a
|
||||||
|
different rig under a converged host changes what a re-converge
|
||||||
|
(`rig bootstrap`, `rig users apply`) would do. It warns rather than
|
||||||
|
refuses: unlike box (which protects live boxes), rig holds no user state a
|
||||||
|
flip can strand, and upgrading a bootstrapped host is the normal case.
|
||||||
|
|
||||||
|
A pre-versioning flat install is migrated into `versions/` automatically on
|
||||||
|
the next installer run — the tree is moved, not re-downloaded, and
|
||||||
|
preserved bit for bit. For scripting: `RIG_HOME`/`RIG_BIN` override the
|
||||||
|
install root and bin dir, `RIG_INSTALL_SOURCE=<dir-or-tarball>` installs
|
||||||
|
from a local tree instead of downloading (how the test suite proves the
|
||||||
|
installer under review), and `RIG_YES=1` answers `rig uninstall`'s prompt
|
||||||
|
in automation.
|
||||||
|
|
||||||
|
### Uninstall
|
||||||
|
|
||||||
|
`rig uninstall` is the real uninstall — no more "rm -rf two paths" prose —
|
||||||
|
and it **ends with an absence assert**: every path it removed is
|
||||||
|
re-checked, and any survivor makes it exit 1 naming the leftovers instead
|
||||||
|
of reporting a clean uninstall that wasn't.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rig uninstall <version> # one non-current version (side-by-side cleanup)
|
||||||
|
rig uninstall --all # everything: every version, current, the PATH symlinks
|
||||||
|
```
|
||||||
|
|
||||||
|
Asks before removing; `--force` or `RIG_YES=1` skips the prompt. The host
|
||||||
|
itself is untouched — what bootstrap converged stays converged.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
|
@ -717,6 +765,12 @@ to do" and exits 0.
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
`bash test/cli.sh` (dependency-free assertions) + shellcheck run in CI. The
|
`bash test/cli.sh` (dependency-free assertions) + shellcheck run in CI. The
|
||||||
|
versioned install is proven by REAL installer runs: `RIG_INSTALL_SOURCE`
|
||||||
|
points install.sh at the tree under review and the harness drives it against
|
||||||
|
throwaway `RIG_HOME`/`RIG_BIN` roots — fresh install, converge, reinstall,
|
||||||
|
side-by-side upgrade, `use`/rollback, flat-tree migration, symlink healing,
|
||||||
|
the bootstrapped-host warning (via `RIG_ROLE_MARKER` fixtures), and both
|
||||||
|
uninstalls with their absence asserts. The
|
||||||
`rig users` family is covered the same way: the harness drives its refusal
|
`rig users` family is covered the same way: the harness drives its refusal
|
||||||
matrix — users-file parsing, the marker gates, the lexical drop-in-name
|
matrix — users-file parsing, the marker gates, the lexical drop-in-name
|
||||||
assertion, the validate-then-apply ordering — through the sourced lib
|
assertion, the validate-then-apply ordering — through the sourced lib
|
||||||
|
|
|
||||||
1
VERSION
Normal file
1
VERSION
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
0.1.0-dev
|
||||||
268
bin/rig
268
bin/rig
|
|
@ -3,6 +3,11 @@ set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
|
ROOT="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
|
||||||
|
|
||||||
|
log() { printf 'rig: %s\n' "$*"; }
|
||||||
|
warn() { printf 'rig: WARNING: %s\n' "$*" >&2; }
|
||||||
|
die() { printf 'rig: ERROR: %s\n' "$*" >&2; exit 1; }
|
||||||
|
version() { echo "rig $(cat "$ROOT/VERSION" 2>/dev/null || echo unknown) ($ROOT)"; }
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
usage: rig <command> [args]
|
usage: rig <command> [args]
|
||||||
|
|
@ -59,12 +64,259 @@ commands:
|
||||||
Shut root SSH on a class=human box once an admin key works. Refuses
|
Shut root SSH on a class=human box once an admin key works. Refuses
|
||||||
on class=server — root there is the control plane's automation door —
|
on class=server — root there is the control plane's automation door —
|
||||||
and while no admin holds a key. Run as root.
|
and while no admin holds a key. Run as root.
|
||||||
|
versions
|
||||||
|
List the installed rig versions — install.sh lands each one side by
|
||||||
|
side at <root>/versions/<v>, a 'current' symlink tracks the default
|
||||||
|
(what the rig on your PATH runs). The default is marked (current);
|
||||||
|
the tree answering this command is marked (running).
|
||||||
|
use <version>
|
||||||
|
Switch the default rig version — repoint 'current' (and the PATH
|
||||||
|
symlink riding it) at an installed version, atomically, then assert
|
||||||
|
the flip took. WARNS on a bootstrapped host (/etc/rig/role exists):
|
||||||
|
switching the rig under a converged host changes what a re-converge
|
||||||
|
would do.
|
||||||
|
uninstall [<version>|--all] [--force]
|
||||||
|
Remove one NON-current installed version, or --all: every version,
|
||||||
|
'current', and the PATH symlinks. Asks first (--force or RIG_YES=1
|
||||||
|
skips the prompt) and ENDS with an absence assert — every removed
|
||||||
|
path is re-checked, and any survivor makes it exit 1 naming the
|
||||||
|
leftovers instead of reporting a clean uninstall that wasn't.
|
||||||
|
--version
|
||||||
|
Print the running rig's version (its tree's own VERSION file) and
|
||||||
|
where it runs from.
|
||||||
|
|
||||||
install/upgrade:
|
install/upgrade:
|
||||||
curl -fsSL https://raw.githubusercontent.com/heavy-duty/rig/main/install.sh | bash
|
curl -fsSL https://raw.githubusercontent.com/heavy-duty/rig/main/install.sh | bash
|
||||||
|
Re-run any time: an installed version converges (no-op), a new one
|
||||||
|
installs side by side at <root>/versions/<v> and becomes the default.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- the versioned install (box#79's layout, ported — #35) -------------------
|
||||||
|
# install.sh lands each version at <install-root>/versions/<v>, with a
|
||||||
|
# 'current' symlink naming the default and $BINDIR/rig pointing through it.
|
||||||
|
# $ROOT (readlink -f, line 4) already resolved the whole chain, so a versioned
|
||||||
|
# install always runs from .../versions/<v> — and a git checkout does not,
|
||||||
|
# which is how these verbs know to refuse instead of uninstalling somebody's
|
||||||
|
# working copy.
|
||||||
|
install_root() {
|
||||||
|
local vdir; vdir="$(dirname "$ROOT")"
|
||||||
|
[ "$(basename "$vdir")" = versions ] || return 1
|
||||||
|
dirname "$vdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# A version is a DIRECTORY NAME under versions/ — nothing else. One strict
|
||||||
|
# gate for every caller that builds a path from one (the installer's new_ver,
|
||||||
|
# migration's flat_ver, and bin/rig's 'use'/single-version uninstall): only
|
||||||
|
# [A-Za-z0-9._+-], no leading '.' or '-'. That forbids '/', '..'-escapes,
|
||||||
|
# spaces and option-lookalikes by construction — a crafted version dies HERE,
|
||||||
|
# never in an rm -rf or an ln. install.sh carries a byte-identical copy;
|
||||||
|
# test/cli.sh diffs the two so the gates cannot drift.
|
||||||
|
valid_version() {
|
||||||
|
case "$1" in
|
||||||
|
''|.*|-*) return 1 ;;
|
||||||
|
*[!A-Za-z0-9._+-]*) return 1 ;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# The flip gate, rig's shape (#35): box refuses version flips under existing
|
||||||
|
# boxes; rig's stake is the converged HOST — /etc/rig/role marks a box that
|
||||||
|
# bootstrap has made into something. Switching the default rig under it
|
||||||
|
# changes what a re-converge would do, which is worth a warning, not a
|
||||||
|
# refusal: there is no user state a flip can strand, and flipping versions on
|
||||||
|
# a bootstrapped host is the normal upgrade. RIG_ROLE_MARKER overrides the
|
||||||
|
# path so tests point it at fixtures (repo precedent: the coolify marker
|
||||||
|
# gate). install.sh carries a byte-identical copy; test/cli.sh diffs the two.
|
||||||
|
warn_bootstrapped() { # $1 = what is about to happen
|
||||||
|
local marker="${RIG_ROLE_MARKER:-/etc/rig/role}"
|
||||||
|
[ -e "$marker" ] || return 0
|
||||||
|
warn "this host is bootstrapped ($(head -n1 "$marker" 2>/dev/null || echo "role marker at $marker"))"
|
||||||
|
warn "$1 changes what a re-converge (rig bootstrap, users apply) would do — proceeding."
|
||||||
|
}
|
||||||
|
|
||||||
|
# The PATH symlinks that could ride this install: the one this invocation came
|
||||||
|
# in on, RIG_BIN's, and the tier default's. Candidates only — every consumer
|
||||||
|
# checks where a link actually points before touching it, so a symlink that is
|
||||||
|
# somebody else's (another install root, a hand-rolled wrapper) is never moved.
|
||||||
|
bin_links() {
|
||||||
|
local c=()
|
||||||
|
[ -L "${BASH_SOURCE[0]}" ] && c+=("${BASH_SOURCE[0]}")
|
||||||
|
[ -n "${RIG_BIN:-}" ] && c+=("$RIG_BIN/rig")
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then c+=(/usr/local/bin/rig); else c+=("$HOME/.local/bin/rig"); fi
|
||||||
|
printf '%s\n' "${c[@]}" | awk '!seen[$0]++'
|
||||||
|
}
|
||||||
|
|
||||||
|
converge_bin_links() { # $1 = install root: point our PATH symlinks through current
|
||||||
|
local ir="$1" p t
|
||||||
|
while IFS= read -r p; do
|
||||||
|
[ -L "$p" ] || continue
|
||||||
|
t="$(readlink -f "$p" 2>/dev/null || true)"
|
||||||
|
[ -n "$t" ] || t="$(readlink "$p" 2>/dev/null || true)"
|
||||||
|
case "$t" in
|
||||||
|
"$ir"/*) ln -sfn "$ir/current/bin/rig" "$p" ;;
|
||||||
|
esac
|
||||||
|
done < <(bin_links)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_versions() {
|
||||||
|
local ir cur d v mark
|
||||||
|
ir="$(install_root)" || die "this rig runs from a working tree ($ROOT), not a versioned install — nothing to list"
|
||||||
|
cur="$(readlink -f "$ir/current" 2>/dev/null || true)"
|
||||||
|
echo "VERSIONS ($ir)"
|
||||||
|
for d in "$ir/versions"/*/; do
|
||||||
|
[ -d "$d" ] || continue
|
||||||
|
v="$(basename "$d")"
|
||||||
|
mark=""
|
||||||
|
[ "$(readlink -f "$d")" = "$cur" ] && mark=" (current)"
|
||||||
|
[ "$(readlink -f "$d")" = "$ROOT" ] && mark="$mark (running)"
|
||||||
|
printf ' %s%s\n' "$v" "$mark"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
echo "switch the default: rig use <version>"
|
||||||
|
echo "install another: re-run install.sh (versions land side by side)"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_use() {
|
||||||
|
local v="${1:-}" ir eff expect out
|
||||||
|
if [ -z "$v" ]; then
|
||||||
|
printf 'rig: use needs a version (see: rig versions)\n' >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
ir="$(install_root)" || die "this rig runs from a working tree ($ROOT), not a versioned install — nothing to switch"
|
||||||
|
valid_version "$v" || die "not a sane version name: '$v' (a version is a directory name under versions/ — see 'rig versions')"
|
||||||
|
[ -d "$ir/versions/$v" ] || die "no such version: $v (see 'rig versions')"
|
||||||
|
warn_bootstrapped "switching the default rig version to $v"
|
||||||
|
# An atomic flip, not unlink+create: ln -sfn leaves a window where current
|
||||||
|
# is missing; a rename over it does not.
|
||||||
|
ln -sfn "versions/$v" "$ir/current.new.$$" && mv -Tf "$ir/current.new.$$" "$ir/current"
|
||||||
|
converge_bin_links "$ir"
|
||||||
|
# Assert the EFFECTIVE result, not the intent: current must resolve to the
|
||||||
|
# version asked for, and the chain's own binary must answer that version —
|
||||||
|
# a flip that "worked" while the operator's rig still runs the old tree is
|
||||||
|
# exactly the flakiness this verb exists to end.
|
||||||
|
eff="$(basename "$(readlink -f "$ir/current" 2>/dev/null || true)")"
|
||||||
|
[ "$eff" = "$v" ] || die "the flip did not take — current resolves to '${eff:-nothing}', not $v"
|
||||||
|
expect="$(cat "$ir/versions/$v/VERSION" 2>/dev/null || true)"
|
||||||
|
if [ -n "$expect" ]; then
|
||||||
|
out="$("$ir/current/bin/rig" --version 2>&1 || true)"
|
||||||
|
case "$out" in
|
||||||
|
*"$expect"*) : ;;
|
||||||
|
*) die "current/bin/rig answers '$out', not version $expect — the symlink chain is broken" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
log "switched to $v (current -> versions/$v)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The uninstall's own confirmation: --force, or RIG_YES=1, or a TTY. RIG_YES
|
||||||
|
# is the installer-family consent contract — how automation says yes without
|
||||||
|
# a terminal; without any of the three we refuse rather than assume consent.
|
||||||
|
uninstall_confirm() { # $1 = question
|
||||||
|
[ "$force" -eq 1 ] && return 0
|
||||||
|
[ -n "${RIG_YES:-}" ] && return 0
|
||||||
|
if [ ! -t 0 ]; then
|
||||||
|
printf 'rig: refusing to %s without --force (no terminal to confirm on; RIG_YES=1 also means yes)\n' "$1" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
local reply
|
||||||
|
printf 'rig: %s? [y/N] ' "$1"
|
||||||
|
read -r reply
|
||||||
|
case "$reply" in y|Y|yes|YES|Yes) return 0 ;; *) die "aborted." ;; esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# 'rig uninstall' — the real uninstall, replacing the undocumented rm -rf
|
||||||
|
# prose. Trees and symlinks, and it ENDS by PROVING the absence — the last
|
||||||
|
# word is a re-check, not a hope.
|
||||||
|
cmd_uninstall() {
|
||||||
|
local ir a ver="" all=0 force=0 cur p t leftover=""
|
||||||
|
local targets=()
|
||||||
|
for a in "$@"; do
|
||||||
|
case "$a" in
|
||||||
|
--all) all=1 ;;
|
||||||
|
--force) force=1 ;;
|
||||||
|
-*)
|
||||||
|
printf 'rig: unknown option: %s\n' "$a" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ -n "$ver" ]; then
|
||||||
|
printf 'rig: uninstall takes one version, or --all\n' >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
ver="$a"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
ir="$(install_root)" || die "this rig runs from a working tree ($ROOT), not a versioned install — nothing to uninstall (a checkout is removed with plain rm)"
|
||||||
|
[ -w "$ir" ] || die "cannot write $ir — uninstall as the user that installed it (or root: sudo rig uninstall)"
|
||||||
|
|
||||||
|
# -- one version -----------------------------------------------------------
|
||||||
|
if [ -n "$ver" ] && [ "$all" -eq 0 ]; then
|
||||||
|
valid_version "$ver" || die "not a sane version name: '$ver' (a version is a directory name under versions/ — see 'rig versions')"
|
||||||
|
[ -d "$ir/versions/$ver" ] || die "no such version: $ver (see 'rig versions')"
|
||||||
|
cur="$(basename "$(readlink -f "$ir/current" 2>/dev/null || true)")"
|
||||||
|
# A broken current makes the CURRENT guard below unfireable (cur empty
|
||||||
|
# when the link is missing; cur naming a non-directory when it dangles —
|
||||||
|
# readlink -f resolves a link whose last component does not exist). Heal
|
||||||
|
# first, then decide; never delete around a broken default.
|
||||||
|
{ [ -n "$cur" ] && [ -d "$ir/versions/$cur" ]; } \
|
||||||
|
|| die "current is dangling — 'rig use <version>' to repoint the default first (refusing to remove versions while it is broken)"
|
||||||
|
[ "$ver" != "$cur" ] || die "$ver is the CURRENT version — 'rig use <other>' first, or 'rig uninstall --all' for everything"
|
||||||
|
uninstall_confirm "remove rig version $ver from $ir"
|
||||||
|
# rm's exit code is not the verdict — the absence re-check below is (a
|
||||||
|
# half-removed tree must be reported as INCOMPLETE, not as a crash).
|
||||||
|
rm -rf "${ir:?}/versions/$ver" || true
|
||||||
|
if [ -e "$ir/versions/$ver" ] || [ -L "$ir/versions/$ver" ]; then
|
||||||
|
echo "rig: uninstall INCOMPLETE — still present: $ir/versions/$ver" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "removed version $ver (the default stays $cur)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ -n "$ver" ]; then
|
||||||
|
printf 'rig: a version and --all together is ambiguous\n' >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- everything (bare 'rig uninstall' and '--all' both mean all of it) -----
|
||||||
|
warn_bootstrapped "removing rig entirely"
|
||||||
|
uninstall_confirm "remove the ENTIRE rig install at $ir (every version)"
|
||||||
|
|
||||||
|
# The removal set, gathered BEFORE anything is deleted, so the absence
|
||||||
|
# assert below re-checks exactly what was promised gone. PATH symlinks are
|
||||||
|
# removed only when they resolve into (or dangle at) THIS install root.
|
||||||
|
targets+=("$ir")
|
||||||
|
while IFS= read -r p; do
|
||||||
|
[ -L "$p" ] || continue
|
||||||
|
t="$(readlink -f "$p" 2>/dev/null || true)"
|
||||||
|
[ -n "$t" ] || t="$(readlink "$p" 2>/dev/null || true)"
|
||||||
|
case "$t" in "$ir"/*) targets+=("$p") ;; esac
|
||||||
|
done < <(bin_links)
|
||||||
|
mapfile -t targets < <(printf '%s\n' "${targets[@]}" | awk '!seen[$0]++')
|
||||||
|
|
||||||
|
# rm's exit code is not the verdict — the absence assert below is (a
|
||||||
|
# half-removed tree must be reported as INCOMPLETE by name, not as a crash).
|
||||||
|
for p in "${targets[@]}"; do rm -rf "$p" || true; done
|
||||||
|
|
||||||
|
# END WITH THE ABSENCE ASSERT: every path re-checked — file, dir OR symlink.
|
||||||
|
# A leftover makes this exit 1 by name; "uninstalled" is a claim, and claims
|
||||||
|
# get verified.
|
||||||
|
for p in "${targets[@]}"; do
|
||||||
|
if [ -e "$p" ] || [ -L "$p" ]; then leftover="$leftover $p"; fi
|
||||||
|
done
|
||||||
|
if [ -n "$leftover" ]; then
|
||||||
|
echo "rig: uninstall INCOMPLETE — still present:$leftover" >&2
|
||||||
|
echo "rig: remove them by hand, and re-check each path is really gone." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "rig: uninstalled — removed:"
|
||||||
|
for p in "${targets[@]}"; do echo "rig: · $p"; done
|
||||||
|
}
|
||||||
|
|
||||||
cmd="${1:-}"
|
cmd="${1:-}"
|
||||||
case "$cmd" in
|
case "$cmd" in
|
||||||
bootstrap)
|
bootstrap)
|
||||||
|
|
@ -154,6 +406,22 @@ case "$cmd" in
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
|
versions)
|
||||||
|
shift
|
||||||
|
cmd_versions "$@"
|
||||||
|
;;
|
||||||
|
use)
|
||||||
|
shift
|
||||||
|
cmd_use "$@"
|
||||||
|
;;
|
||||||
|
uninstall)
|
||||||
|
shift
|
||||||
|
cmd_uninstall "$@"
|
||||||
|
;;
|
||||||
|
-V|--version)
|
||||||
|
version
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
-h|--help|help)
|
-h|--help|help)
|
||||||
usage
|
usage
|
||||||
exit 0
|
exit 0
|
||||||
|
|
|
||||||
237
install.sh
237
install.sh
|
|
@ -3,9 +3,26 @@ set -euo pipefail
|
||||||
|
|
||||||
# rig installer — intended for: curl -fsSL .../install.sh | bash
|
# rig installer — intended for: curl -fsSL .../install.sh | bash
|
||||||
#
|
#
|
||||||
# Downloads the rig repo tarball, installs the whole tree under $DEST,
|
# Downloads the rig repo tarball and installs it into the VERSIONED layout
|
||||||
# and puts a `rig` symlink on PATH via $BINDIR. Re-run any time to
|
# under $DEST (box#79's layout, ported — heavy-duty/rig#35):
|
||||||
# upgrade.
|
#
|
||||||
|
# $DEST/versions/<version>/ one full tree per installed version
|
||||||
|
# $DEST/current -> versions/<version> the default version
|
||||||
|
# $BINDIR/rig -> $DEST/current/bin/rig the PATH entry
|
||||||
|
#
|
||||||
|
# Versions install side by side: `rig versions` lists them, `rig use <v>`
|
||||||
|
# switches the default, `rig uninstall` removes them. Re-running with an
|
||||||
|
# already-installed version is a converging no-op (RIG_REINSTALL=1 replaces
|
||||||
|
# that version's tree); a NEW version installs beside the old one and becomes
|
||||||
|
# the default — with a WARNING when this host is bootstrapped (/etc/rig/role
|
||||||
|
# exists), because switching the rig under a converged host changes what a
|
||||||
|
# re-converge would do. rig warns where box refuses: rig has no boxes to
|
||||||
|
# protect, and the operator flipping versions on purpose is the common case.
|
||||||
|
# A pre-versioning flat tree is migrated in place, so upgrading is seamless.
|
||||||
|
#
|
||||||
|
# RIG_INSTALL_SOURCE=<dir-or-tarball> installs from a local tree instead of
|
||||||
|
# downloading — for CI and the test suite, so what lands is the code under
|
||||||
|
# review.
|
||||||
|
|
||||||
REPO="${RIG_REPO:-heavy-duty/rig}"
|
REPO="${RIG_REPO:-heavy-duty/rig}"
|
||||||
REF="${RIG_REF:-main}"
|
REF="${RIG_REF:-main}"
|
||||||
|
|
@ -20,45 +37,211 @@ log() { printf 'rig-install: %s\n' "$*"; }
|
||||||
warn() { printf 'rig-install: WARNING: %s\n' "$*" >&2; }
|
warn() { printf 'rig-install: WARNING: %s\n' "$*" >&2; }
|
||||||
die() { printf 'rig-install: ERROR: %s\n' "$*" >&2; exit 1; }
|
die() { printf 'rig-install: ERROR: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# A version is a DIRECTORY NAME under versions/ — nothing else. One strict
|
||||||
|
# gate for every caller that builds a path from one (the installer's new_ver,
|
||||||
|
# migration's flat_ver, and bin/rig's 'use'/single-version uninstall): only
|
||||||
|
# [A-Za-z0-9._+-], no leading '.' or '-'. That forbids '/', '..'-escapes,
|
||||||
|
# spaces and option-lookalikes by construction — a crafted version dies HERE,
|
||||||
|
# never in an rm -rf or an ln. bin/rig carries a byte-identical copy;
|
||||||
|
# test/cli.sh diffs the two so the gates cannot drift.
|
||||||
|
valid_version() {
|
||||||
|
case "$1" in
|
||||||
|
''|.*|-*) return 1 ;;
|
||||||
|
*[!A-Za-z0-9._+-]*) return 1 ;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# The flip gate, rig's shape (#35): box refuses version flips under existing
|
||||||
|
# boxes; rig's stake is the converged HOST — /etc/rig/role marks a box that
|
||||||
|
# bootstrap has made into something. Switching the default rig under it
|
||||||
|
# changes what a re-converge would do, which is worth a warning, not a
|
||||||
|
# refusal: there is no user state a flip can strand, and flipping versions on
|
||||||
|
# a bootstrapped host is the normal upgrade. RIG_ROLE_MARKER overrides the
|
||||||
|
# path so tests point it at fixtures (repo precedent: the coolify marker
|
||||||
|
# gate). bin/rig carries a byte-identical copy; test/cli.sh diffs the two.
|
||||||
|
warn_bootstrapped() { # $1 = what is about to happen
|
||||||
|
local marker="${RIG_ROLE_MARKER:-/etc/rig/role}"
|
||||||
|
[ -e "$marker" ] || return 0
|
||||||
|
warn "this host is bootstrapped ($(head -n1 "$marker" 2>/dev/null || echo "role marker at $marker"))"
|
||||||
|
warn "$1 changes what a re-converge (rig bootstrap, users apply) would do — proceeding."
|
||||||
|
}
|
||||||
|
|
||||||
# --- prerequisites -----------------------------------------------------------
|
# --- prerequisites -----------------------------------------------------------
|
||||||
command -v curl >/dev/null 2>&1 || die "curl is required but was not found."
|
# curl only when something must be downloaded — a local RIG_INSTALL_SOURCE
|
||||||
|
# needs none, which is what lets test/cli.sh drive REAL installs offline.
|
||||||
|
if [ -z "${RIG_INSTALL_SOURCE:-}" ]; then
|
||||||
|
command -v curl >/dev/null 2>&1 || die "curl is required but was not found."
|
||||||
|
fi
|
||||||
command -v tar >/dev/null 2>&1 || die "tar is required but was not found."
|
command -v tar >/dev/null 2>&1 || die "tar is required but was not found."
|
||||||
|
|
||||||
|
if [ -n "${RIG_INSTALL_SOURCE:-}" ]; then
|
||||||
|
SRCDESC="local source $RIG_INSTALL_SOURCE"
|
||||||
|
else
|
||||||
|
SRCDESC="$REPO@$REF"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Flip $DEST/current to versions/<v> atomically: build the new link beside it,
|
||||||
|
# rename over. Plain ln -sfn is unlink+create — a window where current names
|
||||||
|
# nothing and a concurrent 'rig' invocation dies mid-chain. bin/rig's cmd_use
|
||||||
|
# flips with the same pattern.
|
||||||
|
flip_current() {
|
||||||
|
ln -sfn "versions/$1" "$DEST/current.new.$$"
|
||||||
|
mv -Tf "$DEST/current.new.$$" "$DEST/current"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- migrate a pre-versioning flat install -----------------------------------
|
||||||
|
# The old installer put the tree FLAT at $DEST (bin/rig directly under it).
|
||||||
|
# Move such a tree to versions/<its-VERSION> BEFORE anything else, so the
|
||||||
|
# upgrade is seamless and the version comparison below sees the truth. The
|
||||||
|
# move is two renames inside one parent directory — no copying, no window with
|
||||||
|
# no install — and the operator's tree is preserved bit for bit. Pre-VERSION
|
||||||
|
# trees (every flat rig install, rig#32) migrate as 0.0.0-unknown.
|
||||||
|
if [ -e "$DEST/bin/rig" ] && [ ! -d "$DEST/versions" ]; then
|
||||||
|
flat_ver="$(cat "$DEST/VERSION" 2>/dev/null || echo 0.0.0-unknown)"
|
||||||
|
# The flat tree's VERSION is data from disk, not from this installer — the
|
||||||
|
# same trust boundary as the new_ver check, so the same gate: a corrupted
|
||||||
|
# (or hostile) VERSION must not steer the mv/ln below out of versions/.
|
||||||
|
valid_version "$flat_ver" || die "the flat install's VERSION is not a sane directory name: '$flat_ver' — fix $DEST/VERSION (one line, e.g. 0.1.0), then re-run"
|
||||||
|
log "found a pre-versioning flat install at $DEST (version $flat_ver) — migrating it into the versioned layout"
|
||||||
|
staging="$DEST.migrating.$$"
|
||||||
|
mv "$DEST" "$staging"
|
||||||
|
mkdir -p "$DEST/versions"
|
||||||
|
mv "$staging" "$DEST/versions/$flat_ver"
|
||||||
|
flip_current "$flat_ver"
|
||||||
|
mkdir -p "$BINDIR"
|
||||||
|
ln -sfn "$DEST/current/bin/rig" "$BINDIR/rig"
|
||||||
|
log "migrated: it now lives at $DEST/versions/$flat_ver (still current)"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- temp workspace ----------------------------------------------------------
|
# --- temp workspace ----------------------------------------------------------
|
||||||
TMPDIR="$(mktemp -d)"
|
TMPDIR="$(mktemp -d)"
|
||||||
cleanup() { rm -rf "$TMPDIR"; }
|
cleanup() { rm -rf "$TMPDIR"; }
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
URL="https://github.com/$REPO/archive/refs/heads/$REF.tar.gz"
|
# --- acquire the tree --------------------------------------------------------
|
||||||
|
if [ -n "${RIG_INSTALL_SOURCE:-}" ]; then
|
||||||
|
SRC="$RIG_INSTALL_SOURCE"
|
||||||
|
INSTALLED_FROM="local:$SRC"
|
||||||
|
if [ -d "$SRC" ]; then
|
||||||
|
log "copying local tree $SRC"
|
||||||
|
mkdir -p "$TMPDIR/tree"
|
||||||
|
# tar, not cp -a: --exclude=.git, so a working checkout never carries its
|
||||||
|
# VCS state (or its size) into the install tree.
|
||||||
|
tar -C "$SRC" --exclude=.git -cf - . | tar -xf - -C "$TMPDIR/tree"
|
||||||
|
EXTRACTED="$TMPDIR/tree"
|
||||||
|
elif [ -f "$SRC" ]; then
|
||||||
|
log "extracting local tarball $SRC"
|
||||||
|
tar -xzf "$SRC" -C "$TMPDIR" || die "failed to extract $SRC"
|
||||||
|
EXTRACTED="$(find "$TMPDIR" -mindepth 1 -maxdepth 1 -type d | head -n1)"
|
||||||
|
else
|
||||||
|
die "RIG_INSTALL_SOURCE is set but is neither a directory nor a tarball: $SRC"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
INSTALLED_FROM="$REPO@$REF"
|
||||||
|
URL="https://github.com/$REPO/archive/refs/heads/$REF.tar.gz"
|
||||||
|
log "installing rig ($REPO@$REF)"
|
||||||
|
log "downloading $URL"
|
||||||
|
curl -fsSL "$URL" -o "$TMPDIR/rig.tar.gz" \
|
||||||
|
|| die "failed to download $URL"
|
||||||
|
|
||||||
log "installing rig ($REPO@$REF)"
|
log "extracting archive"
|
||||||
log "downloading $URL"
|
tar -xzf "$TMPDIR/rig.tar.gz" -C "$TMPDIR" \
|
||||||
curl -fsSL "$URL" -o "$TMPDIR/rig.tar.gz" \
|
|| die "failed to extract archive"
|
||||||
|| die "failed to download $URL"
|
|
||||||
|
|
||||||
log "extracting archive"
|
# GitHub names the archive's top dir <repo>-<ref> — deriving that name is
|
||||||
tar -xzf "$TMPDIR/rig.tar.gz" -C "$TMPDIR" \
|
# guesswork (it broke for real at box's repo rename). The tarball has
|
||||||
|| die "failed to extract archive"
|
# exactly ONE top-level directory: take the directory, whatever it is
|
||||||
|
# called, and let the bin/rig check below judge whether it is the right tree.
|
||||||
|
EXTRACTED="$(find "$TMPDIR" -mindepth 1 -maxdepth 1 -type d | head -n1)"
|
||||||
|
fi
|
||||||
|
[ -n "${EXTRACTED:-}" ] || die "could not find the source tree in $SRCDESC"
|
||||||
|
[ -f "$EXTRACTED/bin/rig" ] || die "source does not contain bin/rig — is $SRCDESC correct?"
|
||||||
|
|
||||||
# GitHub archives extract to a single top-level dir like rig-<ref>/
|
# The tree's own VERSION file names the directory it lands in — the version IS
|
||||||
EXTRACTED="$(find "$TMPDIR" -maxdepth 1 -type d -name 'rig-*' | head -n1)"
|
# the identity of what is being installed, and 'rig versions' lists these names.
|
||||||
[ -n "$EXTRACTED" ] || die "could not find extracted rig-* directory in archive"
|
new_ver="$(cat "$EXTRACTED/VERSION" 2>/dev/null || true)"
|
||||||
[ -f "$EXTRACTED/bin/rig" ] || die "archive does not contain bin/rig — is $REPO@$REF correct?"
|
[ -n "$new_ver" ] || die "source has no VERSION file — cannot install it as a version"
|
||||||
|
valid_version "$new_ver" || die "the source's VERSION is not a sane directory name: '$new_ver'"
|
||||||
|
|
||||||
# --- atomically replace $DEST --------------------------------------------------
|
set_exec() { # $1 = a rig tree: the executable bits install.sh owns
|
||||||
log "installing into $DEST"
|
chmod +x "$1/bin/rig"
|
||||||
rm -rf "$DEST"
|
if [ -d "$1/commands" ]; then
|
||||||
mkdir -p "$(dirname "$DEST")"
|
find "$1/commands" -name '*.sh' -exec chmod +x {} +
|
||||||
mv "$EXTRACTED" "$DEST"
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
chmod +x "$DEST/bin/rig" "$DEST"/commands/*.sh
|
# --- install into $DEST/versions/<version> -----------------------------------
|
||||||
|
VDIR="$DEST/versions/$new_ver"
|
||||||
|
newly_installed=0
|
||||||
|
if [ -d "$VDIR" ]; then
|
||||||
|
if [ -n "${RIG_REINSTALL:-}" ]; then
|
||||||
|
# Replace THIS version's tree, as atomically as two renames allow — never
|
||||||
|
# a partial overlay of new files onto an old tree.
|
||||||
|
log "RIG_REINSTALL=1 — replacing the installed $new_ver tree"
|
||||||
|
stage="$VDIR.new.$$"; old="$VDIR.old.$$"
|
||||||
|
rm -rf "$stage" "$old"
|
||||||
|
set_exec "$EXTRACTED"
|
||||||
|
mv "$EXTRACTED" "$stage"
|
||||||
|
# Swap by renames, delete LAST: rm-then-move leaves a hole the whole
|
||||||
|
# length of the delete where current -> this version resolves to nothing.
|
||||||
|
mv "$VDIR" "$old"
|
||||||
|
mv "$stage" "$VDIR"
|
||||||
|
rm -rf "$old"
|
||||||
|
printf '%s\n' "$INSTALLED_FROM" > "$VDIR/INSTALLED_FROM"
|
||||||
|
log "reinstalled $new_ver"
|
||||||
|
else
|
||||||
|
cur_from="$(cat "$VDIR/INSTALLED_FROM" 2>/dev/null || echo '<unknown source>')"
|
||||||
|
log "rig $new_ver is already installed ($cur_from) — nothing to do."
|
||||||
|
log "(RIG_REINSTALL=1 replaces this version's tree; 'rig versions' lists what is installed.)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "installing $new_ver into $VDIR"
|
||||||
|
mkdir -p "$DEST/versions"
|
||||||
|
set_exec "$EXTRACTED"
|
||||||
|
mv "$EXTRACTED" "$VDIR"
|
||||||
|
newly_installed=1
|
||||||
|
# Record WHAT was installed, so a caller can assert it got what it asked
|
||||||
|
# for — an installer invoked with stale env vars silently falls back to the
|
||||||
|
# defaults, and INSTALLED_FROM is how that lie gets caught.
|
||||||
|
printf '%s\n' "$INSTALLED_FROM" > "$VDIR/INSTALLED_FROM"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- put rig on PATH ------------------------------------------------------
|
# --- which version is the default? -------------------------------------------
|
||||||
|
# 'current' is the tracked default; flipping it is the ONLY step that changes
|
||||||
|
# what an operator's `rig` runs. A fresh host (or a dangling current) is
|
||||||
|
# claimed outright; an upgrade flips — with the bootstrapped-host warning —
|
||||||
|
# because a re-run that silently left you on the old version would make
|
||||||
|
# "re-run any time to upgrade" a lie. Judged from versions/<v> itself
|
||||||
|
# (readlink -f), never from what a wedged current claims.
|
||||||
|
cur="$(readlink -f "$DEST/current" 2>/dev/null || true)"
|
||||||
|
want="$(readlink -f "$VDIR")"
|
||||||
|
if [ -z "$cur" ] || [ ! -d "$cur" ]; then
|
||||||
|
flip_current "$new_ver"
|
||||||
|
log "default version: $new_ver"
|
||||||
|
elif [ "$cur" = "$want" ]; then
|
||||||
|
: # already the default — nothing to flip
|
||||||
|
elif [ "$newly_installed" -eq 0 ]; then
|
||||||
|
# A converge/no-op (or RIG_REINSTALL) of a version that is NOT the default
|
||||||
|
# never moves the default — a re-run must change nothing; switching is
|
||||||
|
# 'rig use', a deliberate act.
|
||||||
|
log "the default stays $(basename "$cur") — 'rig use $new_ver' switches."
|
||||||
|
else
|
||||||
|
old_ver="$(basename "$cur")"
|
||||||
|
warn_bootstrapped "switching the default rig version ($old_ver -> $new_ver)"
|
||||||
|
flip_current "$new_ver"
|
||||||
|
log "default version switched: $old_ver -> $new_ver ('rig use $old_ver' switches back)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- put rig on PATH ---------------------------------------------------------
|
||||||
|
# ln -sfn converges, and that includes HEALING: a stale or dangling
|
||||||
|
# $BINDIR/rig (say, its tree half-removed by hand) must never block or wedge
|
||||||
|
# an install — it gets repointed at the current chain, whatever it said before.
|
||||||
mkdir -p "$BINDIR"
|
mkdir -p "$BINDIR"
|
||||||
ln -sf "$DEST/bin/rig" "$BINDIR/rig"
|
ln -sfn "$DEST/current/bin/rig" "$BINDIR/rig"
|
||||||
log "linked $BINDIR/rig -> $DEST/bin/rig"
|
log "linked $BINDIR/rig -> $DEST/current/bin/rig"
|
||||||
|
|
||||||
# --- PATH check ----------------------------------------------------------------
|
# --- PATH check --------------------------------------------------------------
|
||||||
case ":$PATH:" in
|
case ":$PATH:" in
|
||||||
*":$BINDIR:"*) : ;;
|
*":$BINDIR:"*) : ;;
|
||||||
*)
|
*)
|
||||||
|
|
@ -67,4 +250,4 @@ case ":$PATH:" in
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
log "done — try: rig --help"
|
log "done ($SRCDESC, version $new_ver) — try: rig --help"
|
||||||
|
|
|
||||||
230
test/cli.sh
230
test/cli.sh
|
|
@ -774,6 +774,236 @@ rm -f "$DUMP_TMP"
|
||||||
check "no main-shell os-release sourcing" 1 "" \
|
check "no main-shell os-release sourcing" 1 "" \
|
||||||
grep -rnE '^[[:space:]]*\.[[:space:]]+/etc/os-release' "$ROOT/commands"
|
grep -rnE '^[[:space:]]*\.[[:space:]]+/etc/os-release' "$ROOT/commands"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# The versioned install (box#79's layout, ported — #35). RIG_INSTALL_SOURCE
|
||||||
|
# bypasses the network, so these are REAL runs of install.sh against throwaway
|
||||||
|
# RIG_HOME/RIG_BIN roots — layout, symlink chain, flat-tree migration, symlink
|
||||||
|
# healing, use and uninstall are all DRIVEN, not grepped. The bootstrapped-host
|
||||||
|
# flip gate (rig's analog of box's #66 refusal: WARN, never refuse) is driven
|
||||||
|
# too, through RIG_ROLE_MARKER fixtures — no root, no network, no real marker.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
check "install.sh is valid bash" 0 "" bash -n "$ROOT/install.sh"
|
||||||
|
VER="$(cat "$ROOT/VERSION")"
|
||||||
|
check "--version answers the tree's own VERSION" 0 "rig $VER" "$ROOT/bin/rig" --version
|
||||||
|
check "-V is --version" 0 "rig $VER" "$ROOT/bin/rig" -V
|
||||||
|
check "help lists the versioned verbs" 0 "uninstall" "$ROOT/bin/rig" --help
|
||||||
|
|
||||||
|
WORK="$(mktemp -d)"
|
||||||
|
FAKEHOME="$WORK/home"; mkdir -p "$FAKEHOME"
|
||||||
|
|
||||||
|
# A fabricated "newer release": the same CLI, a different VERSION — what an
|
||||||
|
# upgrade actually is, from the installer's point of view.
|
||||||
|
SRC9="$WORK/src-9.9.9"; mkdir -p "$SRC9/bin"
|
||||||
|
cp "$ROOT/bin/rig" "$SRC9/bin/rig"; chmod +x "$SRC9/bin/rig"
|
||||||
|
echo "9.9.9-drill" > "$SRC9/VERSION"
|
||||||
|
SRC8="$WORK/src-8.8.8"; mkdir -p "$SRC8/bin"
|
||||||
|
cp "$ROOT/bin/rig" "$SRC8/bin/rig"; chmod +x "$SRC8/bin/rig"
|
||||||
|
echo "8.8.8-drill" > "$SRC8/VERSION"
|
||||||
|
|
||||||
|
inst() { # inst <rig_home> <rig_bin> [VAR=val ...] — run install.sh for real
|
||||||
|
local h="$1" b="$2"; shift 2
|
||||||
|
env HOME="$FAKEHOME" RIG_ROLE_MARKER="$WORK/no-marker" \
|
||||||
|
RIG_HOME="$h" RIG_BIN="$b" \
|
||||||
|
RIG_INSTALL_SOURCE="$ROOT" "$@" bash "$ROOT/install.sh"
|
||||||
|
}
|
||||||
|
irig() { # irig [VAR=val ...] <cmd...> — run an installed rig, marker-free
|
||||||
|
env HOME="$FAKEHOME" RIG_ROLE_MARKER="$WORK/no-marker" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- fresh install: the layout and the chain --------------------------------
|
||||||
|
H1="$WORK/h1"; B1="$WORK/b1"
|
||||||
|
check "install: a fresh install runs clean" 0 "done" inst "$H1" "$B1"
|
||||||
|
check "install: the tree lands in versions/<v>" 0 "" test -x "$H1/versions/$VER/bin/rig"
|
||||||
|
check "install: 'current' points at versions/<v>" 0 "versions/$VER" readlink "$H1/current"
|
||||||
|
check "install: the PATH symlink rides the chain" 0 "$H1/current/bin/rig" readlink "$B1/rig"
|
||||||
|
check "install: rig --version answers through the whole chain" 0 "rig $VER" irig "$B1/rig" --version
|
||||||
|
check "install: INSTALLED_FROM records the local source" 0 "local:" cat "$H1/versions/$VER/INSTALLED_FROM"
|
||||||
|
|
||||||
|
# --- converge, don't clobber ------------------------------------------------
|
||||||
|
touch "$H1/versions/$VER/CANARY"
|
||||||
|
check "install: a same-version re-run is a no-op that says so" 0 "already installed" inst "$H1" "$B1"
|
||||||
|
check "install: the no-op left the tree untouched" 0 "" test -e "$H1/versions/$VER/CANARY"
|
||||||
|
check "install: RIG_REINSTALL=1 replaces that version's tree" 0 "reinstalled" inst "$H1" "$B1" RIG_REINSTALL=1
|
||||||
|
check "install: the reinstall really replaced it (canary gone)" 1 "" test -e "$H1/versions/$VER/CANARY"
|
||||||
|
|
||||||
|
# --- a second version: side-by-side, and the flip ---------------------------
|
||||||
|
check "install: a second version installs side-by-side" 0 "" inst "$H1" "$B1" RIG_INSTALL_SOURCE="$SRC9"
|
||||||
|
check "install: ...into its own versions dir" 0 "" test -x "$H1/versions/9.9.9-drill/bin/rig"
|
||||||
|
check "install: ...and the old version stays" 0 "" test -d "$H1/versions/$VER"
|
||||||
|
check "install: the default flips to the new version" 0 "rig 9.9.9-drill" irig "$B1/rig" --version
|
||||||
|
|
||||||
|
# --- rig versions -----------------------------------------------------------
|
||||||
|
check "versions: lists the installed versions" 0 "$VER" irig "$B1/rig" versions
|
||||||
|
check "versions: marks the current default" 0 "(current)" irig "$B1/rig" versions
|
||||||
|
check "versions: marks the running one" 0 "(running)" irig "$B1/rig" versions
|
||||||
|
|
||||||
|
# --- rig use ----------------------------------------------------------------
|
||||||
|
check "use: no argument is a usage error" 2 "use needs a version" irig "$B1/rig" use
|
||||||
|
check "use: an unknown version is refused by name" 1 "no such version" irig "$B1/rig" use 1.2.3
|
||||||
|
# A version is a directory NAME — a crafted one must die at the gate, never
|
||||||
|
# reach the ln (current pointing outside the root) or an rm -rf.
|
||||||
|
check "use: a path-traversal version dies at the gate" 1 "not a sane version name" \
|
||||||
|
irig "$B1/rig" use '../../tmp/evil'
|
||||||
|
check "use: flips the default" 0 "switched to $VER" irig "$B1/rig" use "$VER"
|
||||||
|
check "use: the flip is effective through the PATH chain" 0 "rig $VER" irig "$B1/rig" --version
|
||||||
|
check "install: an installed-but-not-current version is a no-op too" 0 "already installed" \
|
||||||
|
inst "$H1" "$B1" RIG_INSTALL_SOURCE="$SRC9"
|
||||||
|
check "install: ...and does not move the default" 0 "rig $VER" irig "$B1/rig" --version
|
||||||
|
|
||||||
|
# --- the flip gate: a bootstrapped host WARNS, never refuses (#35) ----------
|
||||||
|
# box refuses version flips under existing boxes; rig's stake is the converged
|
||||||
|
# host itself — /etc/rig/role. The deliberate decision: warn and proceed.
|
||||||
|
# Driven against a fixture marker; counting fires proves silence too.
|
||||||
|
MARK="$WORK/role-marker"
|
||||||
|
printf 'role=workload class=server host=no join=authkey\n' > "$MARK"
|
||||||
|
H2="$WORK/h2"; B2="$WORK/b2"
|
||||||
|
check "flip gate: baseline install" 0 "done" inst "$H2" "$B2"
|
||||||
|
check "flip gate: an upgrade on a bootstrapped host WARNS" 0 "this host is bootstrapped" \
|
||||||
|
inst "$H2" "$B2" RIG_INSTALL_SOURCE="$SRC9" RIG_ROLE_MARKER="$MARK"
|
||||||
|
check "flip gate: ...and still flips (warn, not refuse)" 0 "rig 9.9.9-drill" irig "$B2/rig" --version
|
||||||
|
check "flip gate: 'rig use' on a bootstrapped host WARNS" 0 "this host is bootstrapped" \
|
||||||
|
irig RIG_ROLE_MARKER="$MARK" "$B2/rig" use "$VER"
|
||||||
|
check "flip gate: ...and still flips" 0 "rig $VER" irig "$B2/rig" --version
|
||||||
|
# Silence when no marker: warning every un-bootstrapped host would train
|
||||||
|
# operators to ignore it.
|
||||||
|
flip_warns() { # flip_warns <cmd...> — how many bootstrapped warnings fired
|
||||||
|
"$@" 2>&1 | grep -c "this host is bootstrapped" || true
|
||||||
|
}
|
||||||
|
check "flip gate: no marker, no warning (installer)" 0 "0" \
|
||||||
|
flip_warns inst "$H2" "$B2" RIG_INSTALL_SOURCE="$SRC9"
|
||||||
|
check "flip gate: no marker, no warning (rig use)" 0 "0" \
|
||||||
|
flip_warns irig "$B2/rig" use "$VER"
|
||||||
|
check "flip gate: a fresh install never warns (nothing changes under the host)" 0 "0" \
|
||||||
|
flip_warns inst "$WORK/h2f" "$WORK/b2f" RIG_ROLE_MARKER="$MARK"
|
||||||
|
|
||||||
|
# --- migration: a flat pre-versioning tree becomes a versioned one ----------
|
||||||
|
H3="$WORK/h3"; B3="$WORK/b3"; mkdir -p "$H3/bin" "$B3"
|
||||||
|
cp "$ROOT/bin/rig" "$H3/bin/rig"; chmod +x "$H3/bin/rig"
|
||||||
|
cp "$ROOT/VERSION" "$H3/VERSION"
|
||||||
|
echo "test@flat" > "$H3/INSTALLED_FROM"
|
||||||
|
ln -s "$H3/bin/rig" "$B3/rig"
|
||||||
|
check "migrate: a flat tree is moved into versions/" 0 "migrating" inst "$H3" "$B3"
|
||||||
|
check "migrate: the OPERATOR'S tree moved (not a fresh copy)" 0 "test@flat" \
|
||||||
|
cat "$H3/versions/$VER/INSTALLED_FROM"
|
||||||
|
check "migrate: nothing flat remains at the root" 1 "" test -e "$H3/bin"
|
||||||
|
check "migrate: current points at the migrated version" 0 "versions/$VER" readlink "$H3/current"
|
||||||
|
check "migrate: the PATH symlink was re-pointed through current" 0 "$H3/current/bin/rig" readlink "$B3/rig"
|
||||||
|
check "migrate: the migrated install answers --version" 0 "rig $VER" irig "$B3/rig" --version
|
||||||
|
|
||||||
|
# ...and the seamless upgrade every REAL flat rig install takes: no VERSION
|
||||||
|
# file at all (pre-rig#32), so it migrates as 0.0.0-unknown and the new
|
||||||
|
# version lands beside it and becomes the default.
|
||||||
|
H4="$WORK/h4"; B4="$WORK/b4"; mkdir -p "$H4/bin" "$B4"
|
||||||
|
cp "$ROOT/bin/rig" "$H4/bin/rig"; chmod +x "$H4/bin/rig"
|
||||||
|
ln -s "$H4/bin/rig" "$B4/rig"
|
||||||
|
check "migrate: a VERSION-less flat tree migrates as 0.0.0-unknown" 0 "0.0.0-unknown" \
|
||||||
|
inst "$H4" "$B4" RIG_INSTALL_SOURCE="$SRC9"
|
||||||
|
check "migrate+upgrade: both versions present" 0 "" \
|
||||||
|
bash -c "[ -d '$H4/versions/0.0.0-unknown' ] && [ -d '$H4/versions/9.9.9-drill' ]"
|
||||||
|
check "migrate+upgrade: the new version is the default" 0 "rig 9.9.9-drill" \
|
||||||
|
irig "$B4/rig" --version
|
||||||
|
|
||||||
|
# A broken current must halt the single-version uninstall BEFORE any decision:
|
||||||
|
# the CURRENT guard keys off what current resolves to, and a dangling link
|
||||||
|
# makes that answer a lie. Drive the version tree's own binary — the current
|
||||||
|
# chain is exactly what is broken. Heal current afterwards.
|
||||||
|
ln -sfn "versions/gone" "$H4/current"
|
||||||
|
check "uninstall: refuses while current is dangling (heal before delete)" 1 "dangling" \
|
||||||
|
irig "$H4/versions/9.9.9-drill/bin/rig" uninstall 0.0.0-unknown --force
|
||||||
|
check "uninstall: ...and both version trees survived the refusal" 0 "" \
|
||||||
|
bash -c "[ -d '$H4/versions/0.0.0-unknown' ] && [ -d '$H4/versions/9.9.9-drill' ]"
|
||||||
|
ln -sfn "versions/9.9.9-drill" "$H4/current"
|
||||||
|
|
||||||
|
# The migration reads VERSION off the old tree — disk data, not installer
|
||||||
|
# data. A hostile value must refuse BEFORE the tree moves anywhere.
|
||||||
|
H9="$WORK/h9"; B9="$WORK/b9"; mkdir -p "$H9/bin" "$B9"
|
||||||
|
cp "$ROOT/bin/rig" "$H9/bin/rig"; chmod +x "$H9/bin/rig"
|
||||||
|
printf '%s\n' '../pwn' > "$H9/VERSION"
|
||||||
|
check "migrate: a hostile flat VERSION refuses to migrate" 1 "not a sane directory name" \
|
||||||
|
inst "$H9" "$B9"
|
||||||
|
check "migrate: ...with the flat tree untouched where it was" 0 "" test -x "$H9/bin/rig"
|
||||||
|
|
||||||
|
# --- healing: a wedged $BINDIR/rig must never block an install --------------
|
||||||
|
H5="$WORK/h5"; B5="$WORK/b5"; mkdir -p "$B5"
|
||||||
|
ln -s "$WORK/nowhere/rig" "$B5/rig" # dangling
|
||||||
|
check "heal: a DANGLING \$BINDIR/rig does not wedge the install" 0 "done" inst "$H5" "$B5"
|
||||||
|
check "heal: ...and got repointed" 0 "rig $VER" irig "$B5/rig" --version
|
||||||
|
H6="$WORK/h6"; B6="$WORK/b6"; mkdir -p "$B6"
|
||||||
|
ln -s /bin/true "$B6/rig" # stale, but resolvable
|
||||||
|
check "heal: a STALE \$BINDIR/rig with no tree does not fake 'installed'" 0 "installing $VER" \
|
||||||
|
inst "$H6" "$B6"
|
||||||
|
check "heal: ...the install is real and answers" 0 "rig $VER" irig "$B6/rig" --version
|
||||||
|
|
||||||
|
# --- rig uninstall: one version ---------------------------------------------
|
||||||
|
check "uninstall: refuses to remove the CURRENT version" 1 "CURRENT" \
|
||||||
|
irig "$B1/rig" uninstall "$VER" --force
|
||||||
|
check "uninstall: an unknown version is refused by name" 1 "no such version" \
|
||||||
|
irig "$B1/rig" uninstall 5.5.5 --force
|
||||||
|
check "uninstall: a path-traversal version dies at the gate (never an rm -rf)" 1 "not a sane version name" \
|
||||||
|
irig "$B1/rig" uninstall '../../../../etc' --force
|
||||||
|
check "uninstall: a version plus --all is ambiguous (usage error)" 2 "ambiguous" \
|
||||||
|
irig "$B1/rig" uninstall 9.9.9-drill --all --force
|
||||||
|
check "uninstall: an unknown flag is refused" 2 "unknown option" \
|
||||||
|
irig "$B1/rig" uninstall --nope
|
||||||
|
check "uninstall: removes a non-current version" 0 "removed version" \
|
||||||
|
irig "$B1/rig" uninstall 9.9.9-drill --force
|
||||||
|
check "uninstall: that version dir is gone" 1 "" test -e "$H1/versions/9.9.9-drill"
|
||||||
|
check "uninstall: the current version still answers" 0 "rig $VER" irig "$B1/rig" --version
|
||||||
|
|
||||||
|
# --- rig uninstall: everything ----------------------------------------------
|
||||||
|
check "uninstall: refuses without --force when no terminal" 2 "refusing" \
|
||||||
|
irig bash -c "'$B1/rig' uninstall --all </dev/null"
|
||||||
|
check "uninstall --all: warns on a bootstrapped host (never refuses)" 0 "this host is bootstrapped" \
|
||||||
|
irig RIG_ROLE_MARKER="$MARK" "$B1/rig" uninstall --all --force
|
||||||
|
check "uninstall --all: removed the whole install" 0 "" bash -c "
|
||||||
|
[ ! -e '$H1' ] && [ ! -L '$H1' ] &&
|
||||||
|
[ ! -e '$B1/rig' ] && [ ! -L '$B1/rig' ]"
|
||||||
|
# ...and RIG_YES=1 is the installer-family consent (no --force, no tty).
|
||||||
|
check "uninstall --all: RIG_YES=1 is consent without a terminal" 0 "uninstalled" \
|
||||||
|
irig bash -c "RIG_YES=1 '$B2/rig' uninstall --all </dev/null"
|
||||||
|
check "uninstall --all: ZERO residue — root and symlinks" 0 "" bash -c "
|
||||||
|
[ ! -e '$H2' ] && [ ! -L '$H2' ] &&
|
||||||
|
[ ! -e '$B2/rig' ] && [ ! -L '$B2/rig' ]"
|
||||||
|
# The last word is a re-check: a survivor must turn into a loud INCOMPLETE,
|
||||||
|
# never a cheerful "uninstalled". (Root ignores file modes, so this drill is
|
||||||
|
# meaningful — and runnable — for a non-root runner only.)
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
H7="$WORK/h7"; B7="$WORK/b7"
|
||||||
|
inst "$H7" "$B7" >/dev/null 2>&1
|
||||||
|
mkdir -p "$H7/versions/$VER/stuck"; touch "$H7/versions/$VER/stuck/pin"
|
||||||
|
chmod 555 "$H7/versions/$VER/stuck"
|
||||||
|
check "uninstall: a survivor makes it scream INCOMPLETE (exit 1)" 1 "INCOMPLETE" \
|
||||||
|
irig "$B7/rig" uninstall --all --force
|
||||||
|
chmod -R u+w "$H7" 2>/dev/null
|
||||||
|
else
|
||||||
|
echo "skip: uninstall INCOMPLETE drill (root ignores file modes)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- the versioned verbs from a working tree: refuse, don't guess -----------
|
||||||
|
check "uninstall: refuses from a working tree" 1 "not a versioned install" "$ROOT/bin/rig" uninstall --all --force
|
||||||
|
check "versions: refuses from a working tree" 1 "not a versioned install" "$ROOT/bin/rig" versions
|
||||||
|
check "use: refuses from a working tree" 1 "not a versioned install" "$ROOT/bin/rig" use 1.0.0
|
||||||
|
|
||||||
|
# The version-name gate must be ONE decision: install.sh and bin/rig carry
|
||||||
|
# byte-identical copies (the installer runs before any tree exists), and a
|
||||||
|
# drifted copy is two gates pretending to be one — a version install.sh would
|
||||||
|
# refuse must not be one 'rig use' accepts.
|
||||||
|
VVBIN="$(mktemp)"; VVINST="$(mktemp)"
|
||||||
|
awk '/^valid_version\(\) \{/,/^\}/' "$ROOT/bin/rig" > "$VVBIN"
|
||||||
|
awk '/^valid_version\(\) \{/,/^\}/' "$ROOT/install.sh" > "$VVINST"
|
||||||
|
check "valid_version: extracted from bin/rig (guards the awk)" 0 "A-Za-z0-9" cat "$VVBIN"
|
||||||
|
check "valid_version: bin/rig and install.sh copies are byte-identical" 0 "" diff "$VVBIN" "$VVINST"
|
||||||
|
rm -f "$VVBIN" "$VVINST"
|
||||||
|
# Same discipline for the flip gate: one bootstrapped-host stance, two copies.
|
||||||
|
WBBIN="$(mktemp)"; WBINST="$(mktemp)"
|
||||||
|
awk '/^warn_bootstrapped\(\) \{/,/^\}/' "$ROOT/bin/rig" > "$WBBIN"
|
||||||
|
awk '/^warn_bootstrapped\(\) \{/,/^\}/' "$ROOT/install.sh" > "$WBINST"
|
||||||
|
check "warn_bootstrapped: extracted from bin/rig (guards the awk)" 0 "RIG_ROLE_MARKER" cat "$WBBIN"
|
||||||
|
check "warn_bootstrapped: bin/rig and install.sh copies are byte-identical" 0 "" diff "$WBBIN" "$WBINST"
|
||||||
|
rm -f "$WBBIN" "$WBINST"
|
||||||
|
|
||||||
|
rm -rf "$WORK"
|
||||||
|
|
||||||
echo "---"
|
echo "---"
|
||||||
echo "$PASS passed, $FAIL failed"
|
echo "$PASS passed, $FAIL failed"
|
||||||
[ "$FAIL" -eq 0 ]
|
[ "$FAIL" -eq 0 ]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue