Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


Expand Down
252 changes: 251 additions & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -27,13 +28,79 @@ 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 ;;
*) return 1 ;;
esac
}

have_tty() {
[ -r /dev/tty ] && [ -w /dev/tty ]
}

run_as_root() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
Expand Down Expand Up @@ -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"

Expand All @@ -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/tty || die "Could not read overwrite confirmation from /dev/tty."

case "$overwrite_reply" in
y|Y|yes|YES)
;;
*)
die "Installation aborted. Existing file left unchanged at ${destination_path}."
;;
esac
}

install_binary() {
source_path="$1"
destination_path="$2"
Expand Down Expand Up @@ -329,6 +562,7 @@ build_asset_url() {
}

main() {
parse_args "$@"
detect_platform
detect_arch
ensure_linux_libc
Expand All @@ -354,6 +588,7 @@ main() {
die "Failed to download ${asset_name}. Check that version ${SAFEPAW_VERSION} exists for ${PLATFORM}/${ARCH}."
fi

confirm_overwrite "$destination_path"
install_binary "$release_path" "$destination_path"

if ! "$destination_path" --help >/dev/null 2>&1; then
Expand All @@ -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"
Expand Down
Loading