From 4f1b4de1c92aaea9b1fdcc06049ea825112fac8e Mon Sep 17 00:00:00 2001 From: ysyneu Date: Thu, 15 Oct 2026 20:23:23 -0700 Subject: [PATCH] fix(install): replace the flashduty already on PATH by default Without FLASHDUTY_INSTALL_DIR the installer always targeted /usr/local/bin. When the active flashduty lives elsewhere (for example ~/.local/bin from the installer's own non-interactive fallback, or ~/go/bin), an upgrade installed a second copy that the earlier PATH entry kept shadowing, so `flashduty version` still reported the old release. `flashduty update` from releases before v1.5.11 hits the same path, since those releases do not pass the running binary's location to the installer. - Default the install directory to that of the flashduty found on PATH, falling back to /usr/local/bin for fresh installs. - After installing, warn when the shell still resolves a different copy. Directories are compared, so dash's "//flashduty" output for a trailing-slash PATH entry does not raise a false warning. - Refuse to run when FLASHDUTY_MANAGED_BY_RUNNER=1, matching `flashduty update`: flashduty-runner ships and verifies its own copy. --- README.md | 2 +- README_zh.md | 2 +- install.sh | 34 +++++++++++++---- tests/install_sh_sudo_test.sh | 71 ++++++++++++++++++++++++++++++++++- 4 files changed, 98 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0ab5bb6..a6e0473 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Download the archive for your platform from [GitHub Releases](https://github.com | Variable | Description | Default | |----------|-------------|---------| | `FLASHDUTY_VERSION` | Install a specific version (e.g. `v1.5.12`) | latest | -| `FLASHDUTY_INSTALL_DIR` | Install directory | `/usr/local/bin` (shell), `~\.flashduty\bin` (PowerShell) | +| `FLASHDUTY_INSTALL_DIR` | Install directory | shell: the directory of the `flashduty` already on `PATH`, else `/usr/local/bin`; PowerShell: `~\.flashduty\bin` | | `MIRROR_URL` | Release asset mirror (must be `https://`) | `https://static.flashcat.cloud/flashduty-cli` | ## Quick start diff --git a/README_zh.md b/README_zh.md index 40e7359..7e7afb1 100644 --- a/README_zh.md +++ b/README_zh.md @@ -41,7 +41,7 @@ irm https://static.flashcat.cloud/flashduty-cli/install.ps1 | iex | 变量 | 说明 | 默认值 | |------|------|--------| | `FLASHDUTY_VERSION` | 安装指定版本(如 `v1.5.12`) | 最新版 | -| `FLASHDUTY_INSTALL_DIR` | 安装目录 | `/usr/local/bin`(shell),`~\.flashduty\bin`(PowerShell) | +| `FLASHDUTY_INSTALL_DIR` | 安装目录 | shell:`PATH` 上已有 `flashduty` 所在目录,没有则 `/usr/local/bin`;PowerShell:`~\.flashduty\bin` | | `MIRROR_URL` | 下载镜像地址(必须是 `https://`) | `https://static.flashcat.cloud/flashduty-cli` | ## 快速上手 diff --git a/install.sh b/install.sh index ee7527e..5d2e0ed 100755 --- a/install.sh +++ b/install.sh @@ -4,7 +4,8 @@ # # Environment: # FLASHDUTY_VERSION Install a specific version (e.g. v0.1.2). Default: latest. -# FLASHDUTY_INSTALL_DIR Install directory. Default: /usr/local/bin. +# FLASHDUTY_INSTALL_DIR Install directory. Default: the directory of the +# flashduty already on PATH, else /usr/local/bin. # INSTALLED_NAME Installed command name. Default: flashduty. # MIRROR_URL Fetch release assets from this https mirror prefix. # Default: https://static.flashcat.cloud/flashduty-cli. @@ -18,7 +19,15 @@ set -e REPO="flashcatcloud/flashduty-cli" BINARY="flashduty-cli" INSTALLED_NAME="${INSTALLED_NAME:-flashduty}" -INSTALL_DIR="${FLASHDUTY_INSTALL_DIR:-/usr/local/bin}" +# Default to the directory of the copy the shell already runs, so an upgrade +# replaces it instead of landing behind it on PATH. Fresh installs use +# /usr/local/bin. +existing=$(command -v "${INSTALLED_NAME}" 2>/dev/null || true) +case "${existing}" in + /*) default_dir=$(dirname -- "${existing}") ;; + *) default_dir="/usr/local/bin" ;; +esac +INSTALL_DIR="${FLASHDUTY_INSTALL_DIR:-${default_dir}}" # By default release downloads are fetched from the Flashcat CDN. Set MIRROR_URL # to another prefix to override, or to an empty string to force GitHub fallback. @@ -230,6 +239,12 @@ EOF # --- main --- main() { + # flashduty-runner stages its own bundled copy; replacing it would desync the + # runner from the CLI it was built and verified against. + if [ "${FLASHDUTY_MANAGED_BY_RUNNER:-}" = "1" ]; then + fail "flashduty is managed by flashduty-runner; upgrade the runner instead" + fi + need_cmd uname need_cmd tar need_cmd curl @@ -316,12 +331,15 @@ main() { info "Installed to ${INSTALL_DIR}/${INSTALLED_NAME}" - # Warn if install dir is not in PATH - case ":${PATH}:" in - *":${INSTALL_DIR}:"*) ;; - *) info "WARNING: ${INSTALL_DIR} is not in your PATH. Add it with:" - info " export PATH=\"${INSTALL_DIR}:\$PATH\"" ;; - esac + # Warn unless the shell now runs the binary just installed. + active=$(command -v "${INSTALLED_NAME}" 2>/dev/null || true) + if [ -z "${active}" ]; then + info "WARNING: ${INSTALL_DIR} is not in your PATH. Add it with:" + info " export PATH=\"${INSTALL_DIR}:\$PATH\"" + elif [ "$(dirname -- "${active}")" != "${INSTALL_DIR%/}" ]; then + info "WARNING: your shell runs ${active}, not this install." + info " Remove that copy, or put ${INSTALL_DIR} ahead of it on PATH." + fi # Best-effort shell completion; never fail the install over it. BIN="${INSTALL_DIR}/${INSTALLED_NAME}" diff --git a/tests/install_sh_sudo_test.sh b/tests/install_sh_sudo_test.sh index b1a0428..8710ceb 100644 --- a/tests/install_sh_sudo_test.sh +++ b/tests/install_sh_sudo_test.sh @@ -93,7 +93,7 @@ run_install() { home_dir="$1" install_dir="$2" out_file="$3" - env PATH="${FAKE_BIN}:$PATH" \ + env PATH="${FAKE_BIN}:${EXTRA_PATH:+${EXTRA_PATH}:}$PATH" \ HOME="${home_dir}" \ SHELL=/bin/false \ FLASHDUTY_VERSION=v9.9.9 \ @@ -186,7 +186,76 @@ test_tty_prompts_for_interactive_sudo() { assert_file_missing "${home_dir}/.local/bin/flashduty" } +# A stale copy earlier on PATH: with no FLASHDUTY_INSTALL_DIR the upgrade must +# replace it in place rather than install a shadowed copy elsewhere. +make_existing_copy() { + mkdir -p "$1" + printf '#!/bin/sh\necho "old flashduty"\n' > "$1/flashduty" + chmod +x "$1/flashduty" +} + +test_replaces_copy_already_on_path() { + SUDO_LOG="${TMP_DIR}/sudo-existing.log" + export SUDO_LOG + : > "${SUDO_LOG}" + home_dir="${TMP_DIR}/home-existing" + existing_dir="${TMP_DIR}/existing-bin" + out_file="${TMP_DIR}/existing.out" + mkdir -p "${home_dir}" + make_existing_copy "${existing_dir}" + + # Trailing slash: dash's `command -v` then reports "//flashduty". + EXTRA_PATH="${existing_dir}/" run_install "${home_dir}" "" "${out_file}" + + assert_contains "${out_file}" "Installed to ${existing_dir}/flashduty" + assert_contains "${existing_dir}/flashduty" "fake flashduty" + assert_file_missing "${home_dir}/.local/bin/flashduty" + if grep -Fq "WARNING" "${out_file}"; then + echo "unexpected warning:" >&2 + cat "${out_file}" >&2 + exit 1 + fi +} + +test_warns_when_copy_on_path_shadows_install() { + SUDO_LOG="${TMP_DIR}/sudo-shadow.log" + export SUDO_LOG + : > "${SUDO_LOG}" + home_dir="${TMP_DIR}/home-shadow" + existing_dir="${TMP_DIR}/shadow-bin" + out_file="${TMP_DIR}/shadow.out" + mkdir -p "${home_dir}" + make_existing_copy "${existing_dir}" + chmod 555 "${existing_dir}" + + EXTRA_PATH="${existing_dir}" run_install "${home_dir}" "" "${out_file}" + + assert_file_exists "${home_dir}/.local/bin/flashduty" + assert_contains "${existing_dir}/flashduty" "old flashduty" + assert_contains "${out_file}" "WARNING: your shell runs ${existing_dir}/flashduty, not this install." +} + +test_refuses_runner_managed_copy() { + SUDO_LOG="${TMP_DIR}/sudo-runner.log" + export SUDO_LOG + : > "${SUDO_LOG}" + home_dir="${TMP_DIR}/home-runner" + install_dir="${TMP_DIR}/runner-bin" + out_file="${TMP_DIR}/runner.out" + mkdir -p "${home_dir}" "${install_dir}" + + if FLASHDUTY_MANAGED_BY_RUNNER=1 run_install "${home_dir}" "${install_dir}" "${out_file}"; then + echo "expected install to fail when managed by flashduty-runner" >&2 + exit 1 + fi + assert_contains "${out_file}" "flashduty is managed by flashduty-runner; upgrade the runner instead" + assert_file_missing "${install_dir}/flashduty" +} + make_fixtures make_fake_commands test_non_tty_falls_back_to_user_bin test_tty_prompts_for_interactive_sudo +test_replaces_copy_already_on_path +test_warns_when_copy_on_path_shadows_install +test_refuses_runner_managed_copy