From 321b0695ba837ecf52c9689f20cd0b5b2d7c6d5d Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Fri, 21 Feb 2025 23:12:25 -0600 Subject: [PATCH 1/5] `echo` with a `-e` flag does not work on macos. --- install.sh | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/install.sh b/install.sh index 1acb8c7f..e9ce1965 100755 --- a/install.sh +++ b/install.sh @@ -78,13 +78,13 @@ EOF say() { if [ "1" = "$PRINT_QUIET" ]; then - echo -e "$1" + echo "$1" fi } say_verbose() { if [ "1" = "$PRINT_VERBOSE" ]; then - echo -e "[DEBUG] $1" + echo "[DEBUG] $1" fi } @@ -119,10 +119,14 @@ err() { if [ "1" = "$PRINT_QUIET" ]; then local _red=$(tput setaf 1 2>/dev/null || echo '') local _reset=$(tput sgr0 2>/dev/null || echo '') - say "\n${_red}[ERROR]${_reset}: $1" >&2 - say "\nRun with --verbose for more details." - say "\nIf you need help, please feel free to open an issue:" - say " $REPO_URL/issues\n" + say "" + say "${_red}[ERROR]${_reset}: $1" >&2 + say "" + say "Run with --verbose for more details." + say "" + say "If you need help, please feel free to open an issue:" + say " $REPO_URL/issues" + say "" fi exit 1 } @@ -141,7 +145,8 @@ err_missing_cmd() { elif [ $_platform == "macos" ]; then _help_text="Hint: brew install $_cmd_name" fi - err "A required dependency is missing. Please install: $*\n$_help_text" + err "A required dependency is missing. Please install: $1 +$_help_text" } # Check if a command exists @@ -434,7 +439,8 @@ update_path() { for config_file in "${config_files[@]}"; do if [ -f "$config_file" ]; then if ! grep -E "^[^#]*export[[:space:]]+PATH=.*(:$new_path|$new_path:|$new_path\$)" "$config_file" >/dev/null 2>&1; then - echo -e "\nexport PATH=\"$new_path:\$PATH\"" >> "$config_file" + echo "" >> "$config_file" # newline + echo "export PATH=\"$new_path:\$PATH\"" >> "$config_file" say_verbose "Added PATH $new_path to $config_file" else say_verbose "PATH $new_path already in $config_file" @@ -618,16 +624,21 @@ parse_args() { main() { parse_args "$@" - say "$LOGO\n" + say "$LOGO" + say "" if [ $DO_UNINSTALL -eq 1 ]; then uninstall - say "\n$MSG_UNINSTALL\n" + say "" + say "$MSG_UNINSTALL" + say "" exit 0 fi if check_cmd $APP_NAME; then - say "\n$MSG_ALREADY_INSTALLED\n" + say "" + say "$MSG_ALREADY_INSTALLED" + say "" exit 0 fi @@ -646,7 +657,9 @@ main() { exit 0 fi - say "\n$MSG_SUCCESS\n" + say "" + say "$MSG_SUCCESS" + say "" exit 0 } From f9a34411dae0e89e857fdb4892e9ab881baf23cc Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Fri, 21 Feb 2025 23:23:57 -0600 Subject: [PATCH 2/5] More robust update feature; uninstall and reinstall. --- install.sh | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/install.sh b/install.sh index e9ce1965..c9e04861 100755 --- a/install.sh +++ b/install.sh @@ -41,8 +41,7 @@ EOF ) MSG_ALREADY_INSTALLED=$(cat </dev/null)" || { PYTHON_BIN_PATH="" } + if [ -x "$PYTHON_BIN_PATH" ]; then + err "Failed to find Python" + fi # uninstall the app local _packages_dir="$($PYTHON_BIN_PATH -m site --user-site 2>/dev/null)" || { - err "Failed to find user site packages directory" + say "Failed to find user site packages directory" } - say_verbose "Uninstalling from $_packages_dir" - local _uninstall_cmd="uv pip uninstall --python="$PYTHON_BIN_PATH" --target="$_packages_dir" $APP_NAME" - say_verbose "$_uninstall_cmd" - local _uninstall_out="$(eval "$_uninstall_cmd" 2>&1)" - say_verbose "$_uninstall_out" - if [ $? -ne 0 ] || echo "$_uninstall_out" | grep -qi "error\|failed\|exception"; then - err "Failed to uninstall $APP_NAME." + if [ -d "$_packages_dir" ]; then + say_verbose "Uninstalling from $_packages_dir" + local _uninstall_cmd="uv pip uninstall --python="$PYTHON_BIN_PATH" --target="$_packages_dir" $APP_NAME" + say_verbose "$_uninstall_cmd" + local _uninstall_out="$(eval "$_uninstall_cmd" 2>&1)" + say_verbose "$_uninstall_out" + if [ $? -ne 0 ] || echo "$_uninstall_out" | grep -qi "error\|failed\|exception"; then + err "Failed to uninstall $APP_NAME." + fi fi # remove the bin file - rm -f "$HOME/.local/bin/$APP_NAME" + rm -f "$(which $APP_NAME 2>/dev/null)" || { + say "Failed to find bin file" + } end_activity } @@ -628,18 +634,16 @@ main() { say "" if [ $DO_UNINSTALL -eq 1 ]; then + # uninstall requested, uninstall and exit uninstall say "" say "$MSG_UNINSTALL" say "" exit 0 - fi - - if check_cmd $APP_NAME; then - say "" + elif check_cmd $APP_NAME; then + # app is already installed, uninstall and proceed to install say "$MSG_ALREADY_INSTALLED" - say "" - exit 0 + uninstall fi say "Starting installation..." From d60da403b218df85f4b7bcc278a87de5c2640089 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Sat, 22 Feb 2025 11:05:51 -0600 Subject: [PATCH 3/5] Handle un-writeable uv cache directories. --- install.sh | 91 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/install.sh b/install.sh index c9e04861..0b292bfa 100755 --- a/install.sh +++ b/install.sh @@ -20,6 +20,7 @@ RELEASE_PATH_URL="$REPO_URL/archive/refs/tags" CHECKSUM_URL="" # TODO PYTHON_VERSION=">=3.10,<3.13" UV_INSTALLER_URL="https://astral.sh/uv/install.sh" +CACHE_DIR="$HOME/.cache" PYTHON_BIN_PATH="" # set after a verified install is found DEV_BRANCH="" # set by --dev-branch flag DO_UNINSTALL=0 # set by uninstall flag @@ -96,7 +97,7 @@ _show_activity() { } show_activity() { - if [ "1" = "$PRINT_QUIET" ] || [ "1" = "$PRINT_VERBOSE" ]; then + if [ "0" = "$PRINT_QUIET" ] || [ "1" = "$PRINT_VERBOSE" ]; then return 0 fi _show_activity & @@ -237,8 +238,6 @@ install_uv() { err "uv installation failed: $_output" fi - update_path "$HOME/.local/bin" - # verify uv installation local _uv_version _uv_version="$(uv --version 2>/dev/null)" || { @@ -263,9 +262,9 @@ setup_python() { say "Python $_python_version is available." return 0 else - show_activity - say "Installing Python $PYTHON_VERSION..." + show_activity + uv python install "$PYTHON_VERSION" --preview 2>/dev/null || { err "Failed to install Python" } @@ -399,7 +398,6 @@ setup_app() { fi make_python_bin "$HOME/.local/bin/$APP_NAME" - update_path "$HOME/.local/bin" say_verbose "Added bin to ~/.local/bin/$APP_NAME" # verify installation @@ -422,28 +420,22 @@ init_project() { # Update PATH in shell config files update_path() { - local new_path="$1" - - # update for current session if not already set - if ! echo $PATH | grep -q "$new_path"; then - export PATH="$new_path:$PATH" - fi - + local _new_path="$1" + # update for each shell - local config_files=( + local _config_files=( "$HOME/.bashrc" # bash - "$HOME/.zshrc" # ssh + "$HOME/.zshrc" # zsh "$HOME/.profile" # POSIX fallback (sh, ksh, etc.) ) - for config_file in "${config_files[@]}"; do - if [ -f "$config_file" ]; then - if ! grep -E "^[^#]*export[[:space:]]+PATH=.*(:$new_path|$new_path:|$new_path\$)" "$config_file" >/dev/null 2>&1; then - echo "" >> "$config_file" # newline - echo "export PATH=\"$new_path:\$PATH\"" >> "$config_file" - say_verbose "Added PATH $new_path to $config_file" - else - say_verbose "PATH $new_path already in $config_file" - fi + for _config_file in "${_config_files[@]}"; do + say_verbose "looking for PATH in $_config_file" + if ! grep -E "^[^#]*export[[:space:]]+PATH=.*(:$_new_path|$_new_path:|$_new_path\$)" "$_config_file" >/dev/null 2>&1; then + echo "" >> "$_config_file" # newline + echo "export PATH=\"$_new_path:\$PATH\"" >> "$_config_file" + say_verbose "Added PATH $_new_path to $_config_file" + else + say_verbose "PATH $_new_path already in $_config_file" fi done } @@ -472,17 +464,17 @@ uninstall() { say "Uninstalling $APP_NAME..." show_activity - update_path "$HOME/.local/bin" PYTHON_BIN_PATH="$(uv python find "$PYTHON_VERSION" 2>/dev/null)" || { PYTHON_BIN_PATH="" } - if [ -x "$PYTHON_BIN_PATH" ]; then + say_verbose $PYTHON_BIN_PATH + if [ ! -x "$PYTHON_BIN_PATH" ]; then err "Failed to find Python" fi # uninstall the app local _packages_dir="$($PYTHON_BIN_PATH -m site --user-site 2>/dev/null)" || { - say "Failed to find user site packages directory" + say_verbose "Failed to find user site packages directory" } if [ -d "$_packages_dir" ]; then say_verbose "Uninstalling from $_packages_dir" @@ -497,12 +489,45 @@ uninstall() { # remove the bin file rm -f "$(which $APP_NAME 2>/dev/null)" || { - say "Failed to find bin file" + say_verbose "Failed to find bin file" } end_activity } +# uv cache dir can be un-writeable on some systems, perhaps from a previous install +# being executed with `sudo`; use a fallback dir if we need to. +ensure_uv_cache_dir() { + say_verbose "ensuring UV_CACHE_DIR is writeable" + # if cache dir exists, check that it is writeable + if [ ! -e "$CACHE_DIR" ]; then + say_verbose "$CACHE_DIR does not exist; creating" + mkdir -p "$CACHE_DIR" + fi + if [ ! -d "$CACHE_DIR" ] || [ ! -w "$CACHE_DIR" ]; then + say_verbose "Cache directory $CACHE_DIR is not writeable" + say_verbose "Using $HOME/.agentstack-cache instead" + CACHE_DIR="$HOME/.agentstack-cache" + mkdir -p "$CACHE_DIR" + fi + + # if uv cache dir exists, check that it is writeable + UV_CACHE_DIR="$CACHE_DIR/uv" + if [ ! -e "$UV_CACHE_DIR" ]; then + say_verbose "$UV_CACHE_DIR does not exist; creating" + mkdir -p "$UV_CACHE_DIR" 2>&1 || { + say_verbose "Failed to create $UV_CACHE_DIR" + } + fi + if [ ! -d "$UV_CACHE_DIR" ] || [ ! -w "$UV_CACHE_DIR" ]; then + say_verbose "Cache directory $UV_CACHE_DIR is not writeable" + say_verbose "Using $CACHE_DIR/uv-agentstack instead" + UV_CACHE_DIR="$CACHE_DIR/uv-agentstack" + mkdir -p "$UV_CACHE_DIR" + export UV_CACHE_DIR="$UV_CACHE_DIR" + fi +} + # Download a file. Try curl first, if not installed, use wget instead. download_file() { local _url="$1" @@ -629,9 +654,16 @@ parse_args() { main() { parse_args "$@" - + say "$LOGO" say "" + + # update the path for the current session early + export PATH="$HOME/.local/bin:$PATH" + say_verbose "Session path: $PATH" + + # ensure we have a writeable cache dir for uv + ensure_uv_cache_dir if [ $DO_UNINSTALL -eq 1 ]; then # uninstall requested, uninstall and exit @@ -648,6 +680,7 @@ main() { say "Starting installation..." check_dependencies + update_path "$HOME/.local/bin" install_uv setup_python if [ -n "$DEV_BRANCH" ]; then From 325396892c889da752077f544a76e28efe00a0ad Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Sat, 22 Feb 2025 11:31:28 -0600 Subject: [PATCH 4/5] Alternate installation method message. --- install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/install.sh b/install.sh index 0b292bfa..f24253f2 100755 --- a/install.sh +++ b/install.sh @@ -127,6 +127,9 @@ err() { say "If you need help, please feel free to open an issue:" say " $REPO_URL/issues" say "" + say "Or, try an alternate installation method at:" + say " https://docs.agentstack.sh/installation" + say "" fi exit 1 } From 1c80281081544c79c6de855522266d6e4502e449 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Sat, 22 Feb 2025 11:58:16 -0600 Subject: [PATCH 5/5] `sh` doesn't support arrays. --- install.sh | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/install.sh b/install.sh index f24253f2..f6f8a8ad 100755 --- a/install.sh +++ b/install.sh @@ -421,26 +421,26 @@ init_project() { $APP_NAME init "$INIT_NAME" --template "$INIT_TEMPLATE" } +update_path_for_shell() { + local _new_path=$1 + local _config_file=$2 + say_verbose "looking for PATH in $_config_file" + if ! grep -E "^[^#]*export[[:space:]]+PATH=.*(:$_new_path|$_new_path:|$_new_path\$)" "$_config_file" >/dev/null 2>&1; then + echo "" >> "$_config_file" # newline + echo "export PATH=\"$_new_path:\$PATH\"" >> "$_config_file" + say_verbose "Added PATH $_new_path to $_config_file" + else + say_verbose "PATH $_new_path already in $_config_file" + fi +} + # Update PATH in shell config files update_path() { local _new_path="$1" - # update for each shell - local _config_files=( - "$HOME/.bashrc" # bash - "$HOME/.zshrc" # zsh - "$HOME/.profile" # POSIX fallback (sh, ksh, etc.) - ) - for _config_file in "${_config_files[@]}"; do - say_verbose "looking for PATH in $_config_file" - if ! grep -E "^[^#]*export[[:space:]]+PATH=.*(:$_new_path|$_new_path:|$_new_path\$)" "$_config_file" >/dev/null 2>&1; then - echo "" >> "$_config_file" # newline - echo "export PATH=\"$_new_path:\$PATH\"" >> "$_config_file" - say_verbose "Added PATH $_new_path to $_config_file" - else - say_verbose "PATH $_new_path already in $_config_file" - fi - done + update_path_for_shell "$_new_path" "$HOME/.bashrc" + update_path_for_shell "$_new_path" "$HOME/.zshrc" + update_path_for_shell "$_new_path" "$HOME/.profile" } # Create a bin file for the app. Assumes entrypoint is main.py:main