diff --git a/README.md b/README.md index 0e08a2a..6ca954e 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,10 @@ curl -fsSL https://raw.githubusercontent.com/recursive-oss/rec/main/install.sh | This installs `rec` in `~/.local/bin` and adds it to your PATH for new terminals. All other data lives under `~/.local/share/rec`. +The installer leaves any other program named `rec`, such as the recorder in +SoX, untouched and lists its path. Whichever comes first on your PATH runs as +`rec`, and `~/.local/bin/rec` always runs this one. + ## Usage diff --git a/install.sh b/install.sh index 35c27f7..2738cb5 100644 --- a/install.sh +++ b/install.sh @@ -3,11 +3,11 @@ rec_install() ( set -euo pipefail umask 077 - bold='' faint='' green='' red='' blue='' reset='' erase='' + bold='' faint='' green='' red='' yellow='' blue='' reset='' erase='' if [ -t 1 ] && [ "${TERM:-dumb}" != dumb ]; then erase=$'\r\033[K' if [ -z "${NO_COLOR:-}" ]; then - bold=$'\033[1m' faint=$'\033[2m' green=$'\033[32m' red=$'\033[31m' blue=$'\033[38;5;68m' reset=$'\033[0m' + bold=$'\033[1m' faint=$'\033[2m' green=$'\033[32m' red=$'\033[31m' yellow=$'\033[33m' blue=$'\033[38;5;68m' reset=$'\033[0m' fi fi fail() { printf '%s' "$erase"; printf '%srec install:%s %s\n' "$red$bold" "$reset" "$*" >&2; exit 1; } @@ -44,15 +44,12 @@ rec_install() ( mkdir "$lock_dir" 2>/dev/null || fail "another install is active; if interrupted, remove $lock_dir" tmp=$(mktemp -d "$data/.install.XXXXXX") trap 'rm -rf "$tmp"; rmdir "$lock_dir" 2>/dev/null || true' EXIT - existing=$(command -v rec || true) if [ -e "$bin/rec" ] || [ -L "$bin/rec" ]; then [ -L "$bin/rec" ] || fail "$bin/rec exists and is not a managed symlink" old=$(readlink "$bin/rec") case "$old" in "$data"/releases/*/rec) [ -f "$(dirname "$old")/.managed" ] || fail 'existing rec is not managed';; *) fail 'existing rec belongs to another program';; esac - [ -z "$existing" ] || [ "$existing" = "$bin/rec" ] || fail "rec already resolves to $existing" else old='' - [ -z "$existing" ] || fail "rec already resolves to $existing" fi printf '%sInstalling rec%s\n\n' "$bold" "$reset" begin "Downloading rec ${version:-(latest release)}" @@ -129,6 +126,12 @@ rec_install() ( *) printf '\n%sOpen a new terminal, or run: export PATH="$HOME/.local/bin:$PATH"%s\n' "$faint" "$reset";; esac fi + # Other programs can be called rec too, such as the recorder in SoX. They stay untouched. + others=$(type -aP rec | while IFS= read -r other; do [ "$other" -ef "$bin/rec" ] || printf ' %s\n' "$other"; done | awk '!seen[$0]++') || true + if [ -n "$others" ]; then + printf '\n%sOther rec commands on your PATH:%s\n%s\nWhichever comes first on PATH runs as rec. This install is always at %s/rec.\n' \ + "$yellow$bold" "$reset" "$others" "$bin" >&2 + fi if [ "$serve_after" = 1 ]; then rm -rf "$tmp" rmdir "$lock_dir" diff --git a/tests/installer_test.py b/tests/installer_test.py index 8d22788..c20a6ed 100644 --- a/tests/installer_test.py +++ b/tests/installer_test.py @@ -154,10 +154,44 @@ def installed(home): (home / ".local/bin/rec").write_text("unrelated program\n") install(env, ok=False) assert (home / ".local/bin/rec").read_text() == "unrelated program\n" + + other = root / "other program" + other.mkdir() + (other / "rec").write_text('#!/bin/sh\necho "$*" >> "$HOME/other.log"\n') + (other / "rec").chmod(0o755) + notice = "Other rec commands" + listed = f" {other}/rec\n" + + # A conflict ahead of the install warns on the first install and on later updates. + home, env = new_home() + env["PATH"] = f"{other}:{env['PATH']}" + for _ in range(2): + r = install(env) + assert notice in r.stderr and listed in r.stderr, r.stderr + assert installed(home).parent.name == "0.3.0" + install(env, "--serve") + assert (home / "serve.log").read_text() == "serve\n" + assert not (home / "other.log").exists(), "installer ran the other program" + + # A conflict behind the install is still listed, once per path, without the install itself. + home, env = new_home() + own = home / ".local/bin" + env["PATH"] = f"{own}/:{other}:{other}:{env['PATH']}" + r = install(env) + assert r.stderr.count(listed) == 1, r.stderr + assert f" {own}/" not in r.stderr, r.stderr + + # The install alone is not a conflict, however PATH spells its directory. + home, env = new_home() + assert notice not in install(env).stderr + own = home / ".local/bin" + for entry in [f"{own}", f"{own}/", f"{own}/."]: + r = install(dict(env, PATH=f"{entry}:{env['PATH']}")) + assert notice not in r.stderr, (entry, r.stderr) server.shutdown() server.server_close() print( - "Installer: public default URL, pins, idempotence, checksum/version/device rejection, running-server stop, --serve, platform checks and collision preservation passed" + "Installer: public default URL, pins, idempotence, checksum/version/device rejection, running-server stop, --serve, platform checks, collision preservation and other rec warnings passed" )