From 6daa1630a61d0479ff689c6241beb1f9e5a33376 Mon Sep 17 00:00:00 2001 From: NopeCharon <115794254+NopeCharon@users.noreply.github.com> Date: Fri, 6 Nov 2026 23:30:56 +0530 Subject: [PATCH] Warn about other rec commands instead of refusing to install The installer stopped whenever any other `rec` was on PATH. On macOS that name also belongs to the recorder in SoX, which Homebrew links at /opt/homebrew/bin/rec, so anyone with SoX installed could not install rec without deleting Homebrew's symlink, and `brew reinstall sox` restored the problem. The check also ignored PATH order, and blocked `rec update` on an existing install once another `rec` appeared. The installer never writes outside ~/.local/bin, so another `rec` is no reason to stop. Install as usual, then list the other commands and say how to reach this one. Listing them last keeps the warning in view above the shell prompt, and only reports it for an install that succeeded. The warning does not say which command wins. The current shell and a new terminal can disagree, because the installer's shell configuration line puts ~/.local/bin first for new terminals only. Compare with `-ef` rather than by name, so that PATH spellings such as ~/.local/bin/ are not reported as a conflict with this install. A ~/.local/bin/rec that rec does not manage still stops the install, since replacing it would overwrite another program. --- README.md | 4 ++++ install.sh | 13 ++++++++----- tests/installer_test.py | 36 +++++++++++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 6 deletions(-) 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" )