diff --git a/README.md b/README.md index 54b50d6..c46c8fd 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,25 @@ curl -fsSL https://raw.githubusercontent.com/zkonduit/SafePaw/main/install.sh | The installer defaults to the latest release on `https://github.com/zkonduit/SafePaw/releases`, installs `multipass` when it can, and then places `safepaw` in `/usr/local/bin` or `~/.local/bin`. +If the target install directory is not already on `PATH`, the installer now tries to add it to your shell profile automatically when that directory lives under your home directory. If it cannot do that safely, it prints the exact commands you need to run. + +If `safepaw` already exists at the destination path, the installer stops and asks before replacing it. For non-interactive installs, pass `--overwrite`: +```bash +curl -fsSL https://raw.githubusercontent.com/zkonduit/SafePaw/main/install.sh | bash -s -- --overwrite +``` + Pin a release or change the install location with environment variables: ```bash curl -fsSL https://raw.githubusercontent.com/zkonduit/SafePaw/main/install.sh | \ SAFEPAW_VERSION=v0.1.0 SAFEPAW_INSTALL_DIR="$HOME/.local/bin" bash ``` +The installer also accepts CLI flags: +```bash +curl -fsSL https://raw.githubusercontent.com/zkonduit/SafePaw/main/install.sh | \ + bash -s -- --version v0.1.0 --install-dir "$HOME/.local/bin" +``` + If automatic `multipass` installation is not supported on the host, the script exits with the official Multipass install guide. diff --git a/install.sh b/install.sh index d5ffd44..c9f96d3 100755 --- a/install.sh +++ b/install.sh @@ -6,6 +6,7 @@ SAFEPAW_RELEASE_REPO="${SAFEPAW_RELEASE_REPO:-SafePaw}" SAFEPAW_VERSION="${SAFEPAW_VERSION:-latest}" SAFEPAW_INSTALL_DIR="${SAFEPAW_INSTALL_DIR:-}" SAFEPAW_SKIP_MULTIPASS="${SAFEPAW_SKIP_MULTIPASS:-0}" +SAFEPAW_OVERWRITE="${SAFEPAW_OVERWRITE:-0}" MULTIPASS_INSTALL_DOCS="https://documentation.ubuntu.com/multipass/stable/how-to-guides/install-multipass/" SAFEPAW_RELEASES_BASE_URL="https://github.com/${SAFEPAW_RELEASE_OWNER}/${SAFEPAW_RELEASE_REPO}/releases" @@ -27,6 +28,68 @@ have_command() { command -v "$1" >/dev/null 2>&1 } +print_usage() { + cat <<'EOF' +Usage: install.sh [options] + +Options: + --overwrite, --force Replace an existing safepaw binary at the target path. + --install-dir DIR Install into DIR instead of the default location. + --version VERSION Install a specific release tag instead of latest. + --skip-multipass Skip automatic Multipass installation. + -h, --help Show this help text. + +Environment variables: + SAFEPAW_INSTALL_DIR + SAFEPAW_OVERWRITE=1 + SAFEPAW_SKIP_MULTIPASS=1 + SAFEPAW_VERSION=vX.Y.Z + +Examples: + curl -fsSL https://raw.githubusercontent.com/zkonduit/SafePaw/main/install.sh | bash + curl -fsSL https://raw.githubusercontent.com/zkonduit/SafePaw/main/install.sh | bash -s -- --overwrite + curl -fsSL https://raw.githubusercontent.com/zkonduit/SafePaw/main/install.sh | \ + SAFEPAW_VERSION=v0.1.0 bash -s -- --install-dir "$HOME/.local/bin" +EOF +} + +parse_args() { + while [ "$#" -gt 0 ]; do + case "$1" in + --overwrite|--force) + SAFEPAW_OVERWRITE=1 + ;; + --install-dir) + shift + [ "$#" -gt 0 ] || die "--install-dir requires a directory path." + SAFEPAW_INSTALL_DIR="$1" + ;; + --version) + shift + [ "$#" -gt 0 ] || die "--version requires a release tag." + SAFEPAW_VERSION="$1" + ;; + --skip-multipass) + SAFEPAW_SKIP_MULTIPASS=1 + ;; + -h|--help) + print_usage + exit 0 + ;; + --) + shift + break + ;; + *) + die "Unknown argument: $1. See --help for supported options." + ;; + esac + shift + done + + [ "$#" -eq 0 ] || die "Unexpected positional argument: $1" +} + path_contains() { case ":${PATH}:" in *":$1:"*) return 0 ;; @@ -34,6 +97,10 @@ path_contains() { esac } +have_tty() { + [ -r /dev/tty ] && [ -w /dev/tty ] +} + run_as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" @@ -259,6 +326,138 @@ resolve_install_dir() { printf '%s/.local/bin\n' "$HOME" } +resolve_shell_profile() { + shell_name="$(basename "${SHELL:-sh}")" + + case "$shell_name" in + zsh) + PROFILE_SHELL="zsh" + PROFILE_PATH="${HOME}/.zshrc" + ;; + bash) + PROFILE_SHELL="bash" + if [ -f "${HOME}/.bashrc" ]; then + PROFILE_PATH="${HOME}/.bashrc" + elif [ -f "${HOME}/.bash_profile" ]; then + PROFILE_PATH="${HOME}/.bash_profile" + else + PROFILE_PATH="${HOME}/.bashrc" + fi + ;; + fish) + PROFILE_SHELL="fish" + PROFILE_PATH="${HOME}/.config/fish/config.fish" + ;; + *) + PROFILE_SHELL="sh" + PROFILE_PATH="${HOME}/.profile" + ;; + esac +} + +build_persistent_path_line() { + install_dir="$1" + resolve_shell_profile + + case "$PROFILE_SHELL" in + fish) + printf 'fish_add_path -g -- "%s"\n' "$install_dir" + ;; + *) + printf 'export PATH="%s:$PATH"\n' "$install_dir" + ;; + esac +} + +build_current_shell_path_command() { + install_dir="$1" + resolve_shell_profile + + case "$PROFILE_SHELL" in + fish) + printf 'fish_add_path -- "%s"' "$install_dir" + ;; + *) + printf 'export PATH="%s:$PATH"' "$install_dir" + ;; + esac +} + +build_reload_command() { + resolve_shell_profile + + case "$PROFILE_SHELL" in + fish) + printf 'source %s' "$PROFILE_PATH" + ;; + *) + printf '. %s' "$PROFILE_PATH" + ;; + esac +} + +maybe_persist_path() { + install_dir="$1" + + case "$install_dir" in + "$HOME"|"$HOME"/*) ;; + *) return 1 ;; + esac + + resolve_shell_profile + profile_dir="$(dirname "$PROFILE_PATH")" + path_line="$(build_persistent_path_line "$install_dir")" + home_relative_install_dir="" + + case "$install_dir" in + "$HOME") + home_relative_install_dir="\$HOME" + ;; + "$HOME"/*) + home_relative_install_dir="\$HOME/${install_dir#"$HOME"/}" + ;; + esac + + if ! mkdir -p "$profile_dir" 2>/dev/null; then + return 1 + fi + + if [ -f "$PROFILE_PATH" ]; then + if grep -F "$install_dir" "$PROFILE_PATH" >/dev/null 2>&1; then + PATH_PERSIST_STATUS="already_present" + return 0 + fi + + if [ -n "$home_relative_install_dir" ] && grep -F "$home_relative_install_dir" "$PROFILE_PATH" >/dev/null 2>&1; then + PATH_PERSIST_STATUS="already_present" + return 0 + fi + fi + + if ! printf '\n%s\n' "$path_line" >>"$PROFILE_PATH" 2>/dev/null; then + return 1 + fi + + PATH_PERSIST_STATUS="added" + return 0 +} + +print_path_instructions() { + install_dir="$1" + resolve_shell_profile + current_shell_command="$(build_current_shell_path_command "$install_dir")" + reload_command="$(build_reload_command)" + path_line="$(build_persistent_path_line "$install_dir")" + + warn "${install_dir} is not currently on PATH." + log "Run this now to use safepaw in the current shell:" + printf ' %s\n' "$current_shell_command" + log "Persist it by adding this line to ${PROFILE_PATH}:" + printf ' %s\n' "$path_line" + log "Then reload your shell:" + printf ' %s\n' "$reload_command" +} + ensure_directory() { dir_path="$1" @@ -273,6 +472,40 @@ ensure_directory() { run_as_root mkdir -p "$dir_path" } +confirm_overwrite() { + destination_path="$1" + + if [ -d "$destination_path" ]; then + die "Cannot install SafePaw to ${destination_path} because it is a directory." + fi + + if [ ! -e "$destination_path" ]; then + return + fi + + if [ "$SAFEPAW_OVERWRITE" = "1" ]; then + warn "Overwriting existing file at ${destination_path} because --overwrite was supplied." + return + fi + + warn "An existing safepaw binary was found at ${destination_path}." + + if ! have_tty; then + die "Refusing to overwrite ${destination_path} without confirmation. Re-run with --overwrite to replace it." + fi + + printf 'Overwrite it? [y/N] ' >/dev/tty + read -r overwrite_reply /dev/null 2>&1; then @@ -362,7 +597,22 @@ main() { log "Installed SafePaw to ${destination_path}" if ! path_contains "$install_dir"; then - warn "${install_dir} is not currently on PATH. Add it before running safepaw." + if maybe_persist_path "$install_dir"; then + reload_command="$(build_reload_command)" + if [ "$PATH_PERSIST_STATUS" = "added" ]; then + log "Added ${install_dir} to PATH in ${PROFILE_PATH}." + else + log "${PROFILE_PATH} already contains a PATH entry for ${install_dir}." + fi + warn "Your current shell still needs to be reloaded before 'safepaw' will resolve by name." + log "Reload your shell with: ${reload_command}" + log "Next step: ${reload_command} && ${binary_name} start" + return + fi + + print_path_instructions "$install_dir" + log "Next step: $(build_current_shell_path_command "$install_dir") && ${binary_name} start" + return fi log "Next step: ${binary_name} start"