From f56b90c1c399ae8e10297112f3648c80ef0107b8 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Tue, 17 Mar 2026 10:58:00 -0700 Subject: [PATCH 1/4] fix: show shell-appropriate PATH guidance in install script Closes #394 The install script showed both fish and POSIX source commands regardless of the user's shell. On systems with fish config present (e.g. DGX Spark), this confused bash users by suggesting they source the fish env script. Now detects the current shell via $SHELL and shows only the relevant command. Also fixes the wget fallback in resolve_redirect to use portable sed instead of grep -oP (Perl regex) which is unavailable on some systems. --- install.sh | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/install.sh b/install.sh index d945cde7..bf6fe590 100755 --- a/install.sh +++ b/install.sh @@ -111,8 +111,8 @@ resolve_redirect() { if has_cmd curl; then curl -fLsS -o /dev/null -w '%{url_effective}' "$_url" elif has_cmd wget; then - # wget --spider follows redirects and prints the final URL - wget --spider -q --max-redirect=10 "$_url" 2>&1 | grep -oP 'Location: \K\S+' | tail -1 + # wget --spider follows redirects; capture the final Location from stderr + wget --spider --max-redirect=10 "$_url" 2>&1 | sed -n 's/^.*Location: \([^ ]*\).*/\1/p' | tail -1 fi } @@ -351,13 +351,20 @@ setup_path() { fi if [ "$_needs_source" = "1" ] || ! is_on_path "$_install_dir"; then + # Detect the user's current shell to show the right source command + _current_shell="$(basename "${SHELL:-sh}" 2>/dev/null || echo "sh")" + echo "" info "to add ${APP_NAME} to your PATH, restart your shell or run:" info "" - info " source \"${_env_script}\" (sh, bash, zsh)" - if [ -d "${_home}/.config/fish" ]; then - info " source \"${_fish_env_script}\" (fish)" - fi + case "$_current_shell" in + fish) + info " source \"${_fish_env_script}\"" + ;; + *) + info " . \"${_env_script}\"" + ;; + esac fi } From c364cf6fb6ddbc57fd8336cfc3ab6ee9865c4189 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Tue, 17 Mar 2026 11:42:09 -0700 Subject: [PATCH 2/4] fix: prevent POSIX shell variable clobbering in install script helpers Functions write_env_script_fish, add_source_line, and verify_checksum used variable names that collided with their callers in setup_path and main. Since POSIX sh has no local scope, write_env_script_fish overwrote _env_script, causing the fish env path to be sourced in .bashrc/.profile instead of the POSIX env script. Use function-prefixed variable names to avoid all cross-function clobbering. --- install.sh | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/install.sh b/install.sh index bf6fe590..9ce9a390 100755 --- a/install.sh +++ b/install.sh @@ -182,21 +182,21 @@ resolve_version() { # --------------------------------------------------------------------------- verify_checksum() { - _archive="$1" - _checksums="$2" - _filename="$3" + _vc_archive="$1" + _vc_checksums="$2" + _vc_filename="$3" - _expected="$(grep "$_filename" "$_checksums" | awk '{print $1}')" + _vc_expected="$(grep "$_vc_filename" "$_vc_checksums" | awk '{print $1}')" - if [ -z "$_expected" ]; then - warn "no checksum found for $_filename, skipping verification" + if [ -z "$_vc_expected" ]; then + warn "no checksum found for $_vc_filename, skipping verification" return 0 fi if has_cmd shasum; then - echo "$_expected $_archive" | shasum -a 256 -c --quiet 2>/dev/null + echo "$_vc_expected $_vc_archive" | shasum -a 256 -c --quiet 2>/dev/null elif has_cmd sha256sum; then - echo "$_expected $_archive" | sha256sum -c --quiet 2>/dev/null + echo "$_vc_expected $_vc_archive" | sha256sum -c --quiet 2>/dev/null else warn "sha256sum/shasum not found, skipping checksum verification" return 0 @@ -237,53 +237,53 @@ is_on_path() { # Write a small env script that conditionally prepends the install dir to PATH. write_env_script_sh() { - _install_dir_expr="$1" - _env_script="$2" + _wes_dir_expr="$1" + _wes_out="$2" - cat < "$_env_script" + cat < "$_wes_out" #!/bin/sh # Add OpenShell to PATH if not already present case ":\${PATH}:" in - *:"${_install_dir_expr}":*) + *:"${_wes_dir_expr}":*) ;; *) - export PATH="${_install_dir_expr}:\$PATH" + export PATH="${_wes_dir_expr}:\$PATH" ;; esac ENVEOF } write_env_script_fish() { - _install_dir_expr="$1" - _env_script="$2" + _wef_dir_expr="$1" + _wef_out="$2" - cat < "$_env_script" + cat < "$_wef_out" # Add OpenShell to PATH if not already present -if not contains "${_install_dir_expr}" \$PATH - set -gx PATH "${_install_dir_expr}" \$PATH +if not contains "${_wef_dir_expr}" \$PATH + set -gx PATH "${_wef_dir_expr}" \$PATH end ENVEOF } # Add a `. /path/to/env` line to a shell rc file if not already present. add_source_line() { - _env_script_path="$1" - _rcfile="$2" - _shell_type="$3" + _asl_script="$1" + _asl_rcfile="$2" + _asl_shell="$3" - if [ "$_shell_type" = "fish" ]; then - _line="source \"${_env_script_path}\"" + if [ "$_asl_shell" = "fish" ]; then + _asl_line="source \"${_asl_script}\"" else - _line=". \"${_env_script_path}\"" + _asl_line=". \"${_asl_script}\"" fi # Check if line already exists - if [ -f "$_rcfile" ] && grep -qF "$_line" "$_rcfile" 2>/dev/null; then + if [ -f "$_asl_rcfile" ] && grep -qF "$_asl_line" "$_asl_rcfile" 2>/dev/null; then return 0 fi # Append with a leading newline in case the file doesn't end with one - printf '\n%s\n' "$_line" >> "$_rcfile" + printf '\n%s\n' "$_asl_line" >> "$_asl_rcfile" return 1 } From 88f4fef272cab8a8fd5a78589f3e433c5b748b72 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Tue, 17 Mar 2026 11:56:59 -0700 Subject: [PATCH 3/4] test: add CI job for install.sh PATH setup validation Adds e2e/test-install.sh with 12 test cases covering: - POSIX and fish env script syntax correctness - rc files (.bashrc, .profile, .zshrc) source the correct env script - fish conf.d sources the fish env script - existing rc file content is preserved (no overwrites) - no duplicate source lines on re-runs - shell-aware guidance output (POSIX vs fish) - variable clobbering regression (the #394 root cause) - .profile creation when no rc files exist Adds .github/workflows/test-install.yml that runs on changes to install.sh or the test script. --- .github/workflows/test-install.yml | 28 +++ e2e/test-install.sh | 380 +++++++++++++++++++++++++++++ 2 files changed, 408 insertions(+) create mode 100644 .github/workflows/test-install.yml create mode 100755 e2e/test-install.sh diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml new file mode 100644 index 00000000..dd8fb7be --- /dev/null +++ b/.github/workflows/test-install.yml @@ -0,0 +1,28 @@ +name: Test Install Script + +on: + pull_request: + paths: + - 'install.sh' + - 'e2e/test-install.sh' + - '.github/workflows/test-install.yml' + push: + branches: [main] + paths: + - 'install.sh' + - 'e2e/test-install.sh' + - '.github/workflows/test-install.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + test-install: + name: Install script tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run install.sh tests + run: sh e2e/test-install.sh diff --git a/e2e/test-install.sh b/e2e/test-install.sh new file mode 100755 index 00000000..fa342f6c --- /dev/null +++ b/e2e/test-install.sh @@ -0,0 +1,380 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Tests for install.sh PATH setup logic. +# +# Validates that: +# - env scripts contain correct shell-specific syntax +# - POSIX rc files source the POSIX env script (not fish) +# - fish conf.d sources the fish env script +# - existing rc file content is preserved (no overwrites) +# - duplicate source lines are not appended on re-runs +# - user-facing guidance matches the detected shell +# +# Usage: +# ./tests/test-install.sh +# +set -eu + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +INSTALL_SCRIPT="$REPO_ROOT/install.sh" + +PASS=0 +FAIL=0 + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +pass() { + PASS=$((PASS + 1)) + printf ' PASS: %s\n' "$1" +} + +fail() { + FAIL=$((FAIL + 1)) + printf ' FAIL: %s\n' "$1" >&2 + if [ -n "${2:-}" ]; then + printf ' %s\n' "$2" >&2 + fi +} + +assert_file_contains() { + _afc_file="$1" + _afc_pattern="$2" + _afc_label="$3" + + if grep -qF "$_afc_pattern" "$_afc_file" 2>/dev/null; then + pass "$_afc_label" + else + fail "$_afc_label" "expected '$_afc_pattern' in $_afc_file" + fi +} + +assert_file_not_contains() { + _afnc_file="$1" + _afnc_pattern="$2" + _afnc_label="$3" + + if grep -qF "$_afnc_pattern" "$_afnc_file" 2>/dev/null; then + fail "$_afnc_label" "unexpected '$_afnc_pattern' found in $_afnc_file" + else + pass "$_afnc_label" + fi +} + +count_occurrences() { + _co_file="$1" + _co_pattern="$2" + grep -cF "$_co_pattern" "$_co_file" 2>/dev/null || echo "0" +} + +# Create a fresh temporary HOME for each test. +make_test_home() { + _mth_dir="$(mktemp -d)" + echo "$_mth_dir" +} + +cleanup_test_home() { + rm -rf "$1" +} + +# Source the install script functions without running main. +# We do this by extracting everything except the final `main "$@"` line. +prepare_install_functions() { + _pif_tmpscript="$(mktemp)" + sed '$d' "$INSTALL_SCRIPT" > "$_pif_tmpscript" + echo "$_pif_tmpscript" +} + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + +test_env_script_posix_syntax() { + printf 'TEST: env script contains POSIX syntax\n' + _test_home="$(make_test_home)" + _install_dir="$_test_home/.local/bin" + mkdir -p "$_install_dir" + + _funcs="$(prepare_install_functions)" + HOME="$_test_home" sh -c ". '$_funcs' && write_env_script_sh '\$HOME/.local/bin' '$_install_dir/env'" + rm -f "$_funcs" + + assert_file_contains "$_install_dir/env" 'export PATH=' "env has 'export PATH='" + assert_file_contains "$_install_dir/env" 'case ":${PATH}:"' "env uses POSIX case syntax" + assert_file_not_contains "$_install_dir/env" 'set -gx' "env does not contain fish syntax" + assert_file_not_contains "$_install_dir/env" 'not contains' "env does not contain fish keywords" + + cleanup_test_home "$_test_home" +} + +test_env_script_fish_syntax() { + printf 'TEST: env.fish script contains fish syntax\n' + _test_home="$(make_test_home)" + _install_dir="$_test_home/.local/bin" + mkdir -p "$_install_dir" + + _funcs="$(prepare_install_functions)" + HOME="$_test_home" sh -c ". '$_funcs' && write_env_script_fish '\$HOME/.local/bin' '$_install_dir/env.fish'" + rm -f "$_funcs" + + assert_file_contains "$_install_dir/env.fish" 'set -gx PATH' "env.fish has fish PATH syntax" + assert_file_contains "$_install_dir/env.fish" 'not contains' "env.fish uses fish conditionals" + assert_file_not_contains "$_install_dir/env.fish" 'export PATH=' "env.fish does not contain POSIX export" + + cleanup_test_home "$_test_home" +} + +test_bashrc_sources_posix_env() { + printf 'TEST: .bashrc sources POSIX env script (not env.fish)\n' + _test_home="$(make_test_home)" + _install_dir="$_test_home/.local/bin" + mkdir -p "$_install_dir" + + # Pre-create .bashrc with existing content + echo '# existing bashrc content' > "$_test_home/.bashrc" + + _funcs="$(prepare_install_functions)" + HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" + rm -f "$_funcs" + + assert_file_contains "$_test_home/.bashrc" ". \"$_install_dir/env\"" ".bashrc sources env" + assert_file_not_contains "$_test_home/.bashrc" "env.fish" ".bashrc does not reference env.fish" + + cleanup_test_home "$_test_home" +} + +test_profile_sources_posix_env() { + printf 'TEST: .profile sources POSIX env script (not env.fish)\n' + _test_home="$(make_test_home)" + _install_dir="$_test_home/.local/bin" + mkdir -p "$_install_dir" + + # Pre-create .profile with existing content + echo '# existing profile content' > "$_test_home/.profile" + + _funcs="$(prepare_install_functions)" + HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" + rm -f "$_funcs" + + assert_file_contains "$_test_home/.profile" ". \"$_install_dir/env\"" ".profile sources env" + assert_file_not_contains "$_test_home/.profile" "env.fish" ".profile does not reference env.fish" + + cleanup_test_home "$_test_home" +} + +test_fish_conf_sources_fish_env() { + printf 'TEST: fish conf.d sources fish env script\n' + _test_home="$(make_test_home)" + _install_dir="$_test_home/.local/bin" + mkdir -p "$_install_dir" + mkdir -p "$_test_home/.config/fish" + + _funcs="$(prepare_install_functions)" + HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" + rm -f "$_funcs" + + _fish_conf="$_test_home/.config/fish/conf.d/openshell.env.fish" + assert_file_contains "$_fish_conf" "env.fish" "fish conf.d sources env.fish" + assert_file_not_contains "$_fish_conf" ". \"" "fish conf.d does not use POSIX dot-source" + + cleanup_test_home "$_test_home" +} + +test_existing_content_preserved() { + printf 'TEST: existing rc file content is preserved\n' + _test_home="$(make_test_home)" + _install_dir="$_test_home/.local/bin" + mkdir -p "$_install_dir" + + # Write distinctive content to rc files + printf '# my custom bashrc aliases\nalias ll="ls -la"\nexport MY_VAR=hello\n' > "$_test_home/.bashrc" + printf '# my custom profile\nexport EDITOR=vim\n' > "$_test_home/.profile" + + _funcs="$(prepare_install_functions)" + HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" + rm -f "$_funcs" + + assert_file_contains "$_test_home/.bashrc" 'alias ll="ls -la"' ".bashrc alias preserved" + assert_file_contains "$_test_home/.bashrc" 'export MY_VAR=hello' ".bashrc export preserved" + assert_file_contains "$_test_home/.profile" 'export EDITOR=vim' ".profile export preserved" + + cleanup_test_home "$_test_home" +} + +test_no_duplicate_source_lines() { + printf 'TEST: running setup_path twice does not duplicate source lines\n' + _test_home="$(make_test_home)" + _install_dir="$_test_home/.local/bin" + mkdir -p "$_install_dir" + + echo '# bashrc' > "$_test_home/.bashrc" + + _funcs="$(prepare_install_functions)" + HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" + # Run again + HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" + rm -f "$_funcs" + + _count="$(count_occurrences "$_test_home/.bashrc" ". \"$_install_dir/env\"")" + if [ "$_count" = "1" ]; then + pass "source line appears exactly once in .bashrc" + else + fail "source line appears exactly once in .bashrc" "found $_count occurrences" + fi + + cleanup_test_home "$_test_home" +} + +test_guidance_shows_posix_for_bash() { + printf 'TEST: guidance shows POSIX source for bash users\n' + _test_home="$(make_test_home)" + _install_dir="$_test_home/.local/bin" + mkdir -p "$_install_dir" + + echo '# bashrc' > "$_test_home/.bashrc" + + _funcs="$(prepare_install_functions)" + _output="$(HOME="$_test_home" SHELL="/bin/bash" PATH="/usr/bin:/bin:/usr/sbin:/sbin" sh -c ". '$_funcs' && setup_path '$_install_dir'" 2>&1)" + rm -f "$_funcs" + + if echo "$_output" | grep -qF '. "'; then + pass "bash user sees POSIX dot-source command" + else + fail "bash user sees POSIX dot-source command" "output: $_output" + fi + + if echo "$_output" | grep -qF 'env.fish'; then + fail "bash user does not see env.fish hint" "output: $_output" + else + pass "bash user does not see env.fish hint" + fi + + cleanup_test_home "$_test_home" +} + +test_guidance_shows_fish_for_fish() { + printf 'TEST: guidance shows fish source for fish users\n' + _test_home="$(make_test_home)" + _install_dir="$_test_home/.local/bin" + mkdir -p "$_install_dir" + + echo '# bashrc' > "$_test_home/.bashrc" + + _funcs="$(prepare_install_functions)" + _output="$(HOME="$_test_home" SHELL="/usr/bin/fish" PATH="/usr/bin:/bin:/usr/sbin:/sbin" sh -c ". '$_funcs' && setup_path '$_install_dir'" 2>&1)" + rm -f "$_funcs" + + if echo "$_output" | grep -qF 'env.fish'; then + pass "fish user sees env.fish source command" + else + fail "fish user sees env.fish source command" "output: $_output" + fi + + cleanup_test_home "$_test_home" +} + +test_no_variable_clobbering() { + printf 'TEST: helper functions do not clobber caller variables\n' + _test_home="$(make_test_home)" + _install_dir="$_test_home/.local/bin" + mkdir -p "$_install_dir" + + echo '# bashrc' > "$_test_home/.bashrc" + + # This test verifies the core bug from #394: write_env_script_fish must not + # clobber _env_script. We call setup_path and check that .bashrc does NOT + # get env.fish sourced (which was the symptom of the clobbering bug). + _funcs="$(prepare_install_functions)" + HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" + rm -f "$_funcs" + + # The POSIX env script should be sourced, not env.fish + assert_file_not_contains "$_test_home/.bashrc" "env.fish" ".bashrc does not source env.fish (no variable clobbering)" + assert_file_contains "$_test_home/.bashrc" ". \"$_install_dir/env\"" ".bashrc sources the correct POSIX env script" + + cleanup_test_home "$_test_home" +} + +test_creates_profile_when_no_rc_files() { + printf 'TEST: creates .profile when no rc files exist\n' + _test_home="$(make_test_home)" + _install_dir="$_test_home/.local/bin" + mkdir -p "$_install_dir" + + # Don't create any rc files — empty home + _funcs="$(prepare_install_functions)" + HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" + rm -f "$_funcs" + + if [ -f "$_test_home/.profile" ]; then + pass ".profile was created" + assert_file_contains "$_test_home/.profile" ". \"$_install_dir/env\"" ".profile sources POSIX env" + assert_file_not_contains "$_test_home/.profile" "env.fish" ".profile does not reference env.fish" + else + fail ".profile was created" "file does not exist" + fi + + cleanup_test_home "$_test_home" +} + +test_zshrc_sources_posix_env() { + printf 'TEST: .zshrc sources POSIX env script\n' + _test_home="$(make_test_home)" + _install_dir="$_test_home/.local/bin" + mkdir -p "$_install_dir" + + echo '# existing zshrc' > "$_test_home/.zshrc" + + _funcs="$(prepare_install_functions)" + HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" + rm -f "$_funcs" + + assert_file_contains "$_test_home/.zshrc" ". \"$_install_dir/env\"" ".zshrc sources env" + assert_file_not_contains "$_test_home/.zshrc" "env.fish" ".zshrc does not reference env.fish" + + cleanup_test_home "$_test_home" +} + +# --------------------------------------------------------------------------- +# Runner +# --------------------------------------------------------------------------- + +main() { + printf '=== install.sh PATH setup tests ===\n\n' + + test_env_script_posix_syntax + echo "" + test_env_script_fish_syntax + echo "" + test_bashrc_sources_posix_env + echo "" + test_profile_sources_posix_env + echo "" + test_fish_conf_sources_fish_env + echo "" + test_existing_content_preserved + echo "" + test_no_duplicate_source_lines + echo "" + test_guidance_shows_posix_for_bash + echo "" + test_guidance_shows_fish_for_fish + echo "" + test_no_variable_clobbering + echo "" + test_creates_profile_when_no_rc_files + echo "" + test_zshrc_sources_posix_env + + printf '\n=== Results: %d passed, %d failed ===\n' "$PASS" "$FAIL" + + if [ "$FAIL" -gt 0 ]; then + exit 1 + fi +} + +main From e626614c181dae8f6763df0007b6b94d23730e73 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Tue, 17 Mar 2026 12:12:26 -0700 Subject: [PATCH 4/4] refactor: simplify install.sh to print PATH guidance instead of modifying rc files Remove all rc file modification logic (env scripts, source lines, fish conf.d setup). The installer now prints shell-appropriate instructions when ~/.local/bin is not on PATH, leaving dotfile changes to the user. Rewrite e2e tests as true end-to-end tests that download the latest release, verify the binary is installed correctly, and check PATH guidance output per shell. --- .github/workflows/test-install.yml | 33 ++- e2e/install/bash_test.sh | 80 ++++++ e2e/install/fish_test.fish | 152 ++++++++++++ e2e/install/helpers.sh | 100 ++++++++ e2e/install/sh_test.sh | 105 ++++++++ e2e/install/zsh_test.sh | 80 ++++++ e2e/test-install.sh | 380 ----------------------------- install.sh | 201 +++------------ 8 files changed, 573 insertions(+), 558 deletions(-) create mode 100755 e2e/install/bash_test.sh create mode 100755 e2e/install/fish_test.fish create mode 100644 e2e/install/helpers.sh create mode 100755 e2e/install/sh_test.sh create mode 100755 e2e/install/zsh_test.sh delete mode 100755 e2e/test-install.sh diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml index dd8fb7be..416adef6 100644 --- a/.github/workflows/test-install.yml +++ b/.github/workflows/test-install.yml @@ -4,13 +4,13 @@ on: pull_request: paths: - 'install.sh' - - 'e2e/test-install.sh' + - 'e2e/install/**' - '.github/workflows/test-install.yml' push: branches: [main] paths: - 'install.sh' - - 'e2e/test-install.sh' + - 'e2e/install/**' - '.github/workflows/test-install.yml' workflow_dispatch: @@ -19,10 +19,33 @@ permissions: jobs: test-install: - name: Install script tests + name: install.sh (${{ matrix.shell }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - shell: sh + test: e2e/install/sh_test.sh + run: sh e2e/install/sh_test.sh + - shell: bash + test: e2e/install/bash_test.sh + run: bash e2e/install/bash_test.sh + - shell: zsh + test: e2e/install/zsh_test.sh + run: zsh e2e/install/zsh_test.sh + install: zsh + - shell: fish + test: e2e/install/fish_test.fish + run: fish e2e/install/fish_test.fish + install: fish + steps: - uses: actions/checkout@v4 - - name: Run install.sh tests - run: sh e2e/test-install.sh + - name: Install ${{ matrix.shell }} + if: matrix.install + run: sudo apt-get update && sudo apt-get install -y ${{ matrix.install }} + + - name: Run tests (${{ matrix.shell }}) + run: ${{ matrix.run }} diff --git a/e2e/install/bash_test.sh b/e2e/install/bash_test.sh new file mode 100755 index 00000000..2b4db1ca --- /dev/null +++ b/e2e/install/bash_test.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Bash e2e tests for install.sh. +# +# Downloads the latest release for real and validates: +# - Binary is installed to the correct directory +# - Binary is executable and runs +# - PATH guidance shows the correct export command for bash +# +set -euo pipefail + +. "$(dirname "$0")/helpers.sh" + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + +test_binary_installed() { + printf 'TEST: binary exists in install directory\n' + + if [ -f "$INSTALL_DIR/openshell" ]; then + pass "openshell binary exists at $INSTALL_DIR/openshell" + else + fail "openshell binary exists" "not found at $INSTALL_DIR/openshell" + fi +} + +test_binary_executable() { + printf 'TEST: binary is executable\n' + + if [ -x "$INSTALL_DIR/openshell" ]; then + pass "openshell binary is executable" + else + fail "openshell binary is executable" "$INSTALL_DIR/openshell is not executable" + fi +} + +test_binary_runs() { + printf 'TEST: binary runs successfully\n' + + if _version="$("$INSTALL_DIR/openshell" --version 2>/dev/null)"; then + pass "openshell --version succeeds: $_version" + else + fail "openshell --version succeeds" "exit code: $?" + fi +} + +test_guidance_shows_export_path() { + printf 'TEST: guidance shows export PATH for bash users\n' + + assert_output_contains "$INSTALL_OUTPUT" 'export PATH="' "shows export PATH command" + assert_output_not_contains "$INSTALL_OUTPUT" "fish_add_path" "does not show fish command" +} + +test_guidance_mentions_not_on_path() { + printf 'TEST: guidance mentions install dir is not on PATH\n' + + assert_output_contains "$INSTALL_OUTPUT" "is not on your PATH" "mentions PATH issue" + assert_output_contains "$INSTALL_OUTPUT" "$INSTALL_DIR" "includes install dir in guidance" +} + +# --------------------------------------------------------------------------- +# Runner +# --------------------------------------------------------------------------- + +printf '=== install.sh e2e tests: bash ===\n\n' + +printf 'Installing openshell...\n' +SHELL="/bin/bash" run_install +printf 'Done.\n\n' + +test_binary_installed; echo "" +test_binary_executable; echo "" +test_binary_runs; echo "" +test_guidance_shows_export_path; echo "" +test_guidance_mentions_not_on_path + +print_summary diff --git a/e2e/install/fish_test.fish b/e2e/install/fish_test.fish new file mode 100755 index 00000000..10176071 --- /dev/null +++ b/e2e/install/fish_test.fish @@ -0,0 +1,152 @@ +#!/usr/bin/env fish +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Fish e2e tests for install.sh. +# +# Downloads the latest release for real and validates: +# - Binary is installed to the correct directory +# - Binary is executable and runs +# - PATH guidance shows fish_add_path (not export PATH) + +set -g PASS 0 +set -g FAIL 0 + +# Resolve paths relative to this script +set -g SCRIPT_DIR (builtin cd (dirname (status filename)) && pwd) +set -g REPO_ROOT (builtin cd "$SCRIPT_DIR/../.." && pwd) +set -g INSTALL_SCRIPT "$REPO_ROOT/install.sh" + +# Set by run_install +set -g INSTALL_DIR "" +set -g INSTALL_OUTPUT "" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +function pass + set -g PASS (math $PASS + 1) + printf ' PASS: %s\n' $argv[1] +end + +function fail + set -g FAIL (math $FAIL + 1) + printf ' FAIL: %s\n' $argv[1] >&2 + if test (count $argv) -gt 1 + printf ' %s\n' $argv[2] >&2 + end +end + +function assert_output_contains + set -l output $argv[1] + set -l pattern $argv[2] + set -l label $argv[3] + + if string match -q -- "*$pattern*" "$output" + pass "$label" + else + fail "$label" "expected '$pattern' in output" + end +end + +function assert_output_not_contains + set -l output $argv[1] + set -l pattern $argv[2] + set -l label $argv[3] + + if string match -q -- "*$pattern*" "$output" + fail "$label" "unexpected '$pattern' found in output" + else + pass "$label" + end +end + +function run_install + set -g INSTALL_DIR (mktemp -d)/bin + + set -g INSTALL_OUTPUT (OPENSHELL_INSTALL_DIR="$INSTALL_DIR" \ + SHELL="/usr/bin/fish" \ + PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ + sh "$INSTALL_SCRIPT" 2>&1) + + if test $status -ne 0 + printf 'install.sh failed:\n%s\n' "$INSTALL_OUTPUT" >&2 + return 1 + end +end + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + +function test_binary_installed + printf 'TEST: binary exists in install directory\n' + + if test -f "$INSTALL_DIR/openshell" + pass "openshell binary exists at $INSTALL_DIR/openshell" + else + fail "openshell binary exists" "not found at $INSTALL_DIR/openshell" + end +end + +function test_binary_executable + printf 'TEST: binary is executable\n' + + if test -x "$INSTALL_DIR/openshell" + pass "openshell binary is executable" + else + fail "openshell binary is executable" "$INSTALL_DIR/openshell is not executable" + end +end + +function test_binary_runs + printf 'TEST: binary runs successfully\n' + + set -l version_output ("$INSTALL_DIR/openshell" --version 2>/dev/null) + if test $status -eq 0 + pass "openshell --version succeeds: $version_output" + else + fail "openshell --version succeeds" "exit code: $status" + end +end + +function test_guidance_shows_fish_add_path + printf 'TEST: guidance shows fish_add_path for fish users\n' + + assert_output_contains "$INSTALL_OUTPUT" "fish_add_path" "shows fish_add_path command" + assert_output_not_contains "$INSTALL_OUTPUT" 'export PATH="' "does not show POSIX export" +end + +function test_guidance_mentions_not_on_path + printf 'TEST: guidance mentions install dir is not on PATH\n' + + assert_output_contains "$INSTALL_OUTPUT" "is not on your PATH" "mentions PATH issue" + assert_output_contains "$INSTALL_OUTPUT" "$INSTALL_DIR" "includes install dir in guidance" +end + +# --------------------------------------------------------------------------- +# Runner +# --------------------------------------------------------------------------- + +printf '=== install.sh e2e tests: fish ===\n\n' + +printf 'Installing openshell...\n' +run_install +printf 'Done.\n\n' + +test_binary_installed +echo "" +test_binary_executable +echo "" +test_binary_runs +echo "" +test_guidance_shows_fish_add_path +echo "" +test_guidance_mentions_not_on_path + +printf '\n=== Results: %d passed, %d failed ===\n' $PASS $FAIL + +if test $FAIL -gt 0 + exit 1 +end diff --git a/e2e/install/helpers.sh b/e2e/install/helpers.sh new file mode 100644 index 00000000..ff5f6637 --- /dev/null +++ b/e2e/install/helpers.sh @@ -0,0 +1,100 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Shared test helpers for install.sh e2e tests. +# Sourced by each per-shell test file (except fish, which has its own helpers). +# +# Provides: +# - pass / fail / print_summary +# - assert_output_contains / assert_output_not_contains +# - run_install (runs the real install.sh to a temp dir, captures output) +# - REPO_ROOT / INSTALL_SCRIPT paths +# - INSTALL_DIR / INSTALL_OUTPUT (set after run_install) + +HELPERS_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$HELPERS_DIR/../.." && pwd)" +INSTALL_SCRIPT="$REPO_ROOT/install.sh" + +_PASS=0 +_FAIL=0 + +# Set by run_install +INSTALL_DIR="" +INSTALL_OUTPUT="" + +# --------------------------------------------------------------------------- +# Assertions +# --------------------------------------------------------------------------- + +pass() { + _PASS=$((_PASS + 1)) + printf ' PASS: %s\n' "$1" +} + +fail() { + _FAIL=$((_FAIL + 1)) + printf ' FAIL: %s\n' "$1" >&2 + if [ -n "${2:-}" ]; then + printf ' %s\n' "$2" >&2 + fi +} + +assert_output_contains() { + _aoc_output="$1" + _aoc_pattern="$2" + _aoc_label="$3" + + if printf '%s' "$_aoc_output" | grep -qF "$_aoc_pattern"; then + pass "$_aoc_label" + else + fail "$_aoc_label" "expected '$_aoc_pattern' in output" + fi +} + +assert_output_not_contains() { + _aonc_output="$1" + _aonc_pattern="$2" + _aonc_label="$3" + + if printf '%s' "$_aonc_output" | grep -qF "$_aonc_pattern"; then + fail "$_aonc_label" "unexpected '$_aonc_pattern' found in output" + else + pass "$_aonc_label" + fi +} + +# --------------------------------------------------------------------------- +# Install runner +# --------------------------------------------------------------------------- + +# Run the real install.sh, installing to a temp directory with the install +# dir removed from PATH so we always get PATH guidance output. +# +# Sets INSTALL_DIR and INSTALL_OUTPUT for subsequent assertions. +# The SHELL variable is passed through so tests can control which shell +# guidance is shown. +# +# Usage: +# SHELL="/bin/bash" run_install +run_install() { + INSTALL_DIR="$(mktemp -d)/bin" + + # Remove the install dir from PATH (it won't be there, but be explicit). + # Keep a minimal PATH so curl/tar/install are available. + INSTALL_OUTPUT="$(OPENSHELL_INSTALL_DIR="$INSTALL_DIR" \ + PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ + sh "$INSTALL_SCRIPT" 2>&1)" || { + printf 'install.sh failed:\n%s\n' "$INSTALL_OUTPUT" >&2 + return 1 + } +} + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- + +print_summary() { + printf '\n=== Results: %d passed, %d failed ===\n' "$_PASS" "$_FAIL" + [ "$_FAIL" -eq 0 ] +} diff --git a/e2e/install/sh_test.sh b/e2e/install/sh_test.sh new file mode 100755 index 00000000..320c00ef --- /dev/null +++ b/e2e/install/sh_test.sh @@ -0,0 +1,105 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# POSIX sh e2e tests for install.sh. +# +# Downloads the latest release for real and validates: +# - Binary is installed to the correct directory +# - Binary is executable and runs +# - PATH guidance shows the correct export command for sh +# - No rc files or env scripts are created +# +set -eu + +. "$(dirname "$0")/helpers.sh" + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + +test_binary_installed() { + printf 'TEST: binary exists in install directory\n' + + if [ -f "$INSTALL_DIR/openshell" ]; then + pass "openshell binary exists at $INSTALL_DIR/openshell" + else + fail "openshell binary exists" "not found at $INSTALL_DIR/openshell" + fi +} + +test_binary_executable() { + printf 'TEST: binary is executable\n' + + if [ -x "$INSTALL_DIR/openshell" ]; then + pass "openshell binary is executable" + else + fail "openshell binary is executable" "$INSTALL_DIR/openshell is not executable" + fi +} + +test_binary_runs() { + printf 'TEST: binary runs successfully\n' + + if _version="$("$INSTALL_DIR/openshell" --version 2>/dev/null)"; then + pass "openshell --version succeeds: $_version" + else + fail "openshell --version succeeds" "exit code: $?" + fi +} + +test_guidance_shows_export_path() { + printf 'TEST: guidance shows export PATH for sh users\n' + + assert_output_contains "$INSTALL_OUTPUT" 'export PATH="' "shows export PATH command" + assert_output_not_contains "$INSTALL_OUTPUT" "fish_add_path" "does not show fish command" +} + +test_guidance_mentions_not_on_path() { + printf 'TEST: guidance mentions install dir is not on PATH\n' + + assert_output_contains "$INSTALL_OUTPUT" "is not on your PATH" "mentions PATH issue" + assert_output_contains "$INSTALL_OUTPUT" "$INSTALL_DIR" "includes install dir in guidance" +} + +test_guidance_mentions_restart() { + printf 'TEST: guidance tells user to restart shell\n' + + assert_output_contains "$INSTALL_OUTPUT" "restart your shell" "mentions shell restart" +} + +test_no_env_scripts_created() { + printf 'TEST: no env scripts are created in install dir\n' + + if [ -f "$INSTALL_DIR/env" ]; then + fail "no env script created" "found $INSTALL_DIR/env" + else + pass "no env script created" + fi + + if [ -f "$INSTALL_DIR/env.fish" ]; then + fail "no env.fish script created" "found $INSTALL_DIR/env.fish" + else + pass "no env.fish script created" + fi +} + +# --------------------------------------------------------------------------- +# Runner +# --------------------------------------------------------------------------- + +printf '=== install.sh e2e tests: sh ===\n\n' + +printf 'Installing openshell...\n' +SHELL="/bin/sh" run_install +printf 'Done.\n\n' + +test_binary_installed; echo "" +test_binary_executable; echo "" +test_binary_runs; echo "" +test_guidance_shows_export_path; echo "" +test_guidance_mentions_not_on_path; echo "" +test_guidance_mentions_restart; echo "" +test_no_env_scripts_created + +print_summary diff --git a/e2e/install/zsh_test.sh b/e2e/install/zsh_test.sh new file mode 100755 index 00000000..621d35f8 --- /dev/null +++ b/e2e/install/zsh_test.sh @@ -0,0 +1,80 @@ +#!/bin/zsh +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Zsh e2e tests for install.sh. +# +# Downloads the latest release for real and validates: +# - Binary is installed to the correct directory +# - Binary is executable and runs +# - PATH guidance shows the correct export command for zsh +# +set -eu + +. "$(dirname "$0")/helpers.sh" + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + +test_binary_installed() { + printf 'TEST: binary exists in install directory\n' + + if [ -f "$INSTALL_DIR/openshell" ]; then + pass "openshell binary exists at $INSTALL_DIR/openshell" + else + fail "openshell binary exists" "not found at $INSTALL_DIR/openshell" + fi +} + +test_binary_executable() { + printf 'TEST: binary is executable\n' + + if [ -x "$INSTALL_DIR/openshell" ]; then + pass "openshell binary is executable" + else + fail "openshell binary is executable" "$INSTALL_DIR/openshell is not executable" + fi +} + +test_binary_runs() { + printf 'TEST: binary runs successfully\n' + + if _version="$("$INSTALL_DIR/openshell" --version 2>/dev/null)"; then + pass "openshell --version succeeds: $_version" + else + fail "openshell --version succeeds" "exit code: $?" + fi +} + +test_guidance_shows_export_path() { + printf 'TEST: guidance shows export PATH for zsh users\n' + + assert_output_contains "$INSTALL_OUTPUT" 'export PATH="' "shows export PATH command" + assert_output_not_contains "$INSTALL_OUTPUT" "fish_add_path" "does not show fish command" +} + +test_guidance_mentions_not_on_path() { + printf 'TEST: guidance mentions install dir is not on PATH\n' + + assert_output_contains "$INSTALL_OUTPUT" "is not on your PATH" "mentions PATH issue" + assert_output_contains "$INSTALL_OUTPUT" "$INSTALL_DIR" "includes install dir in guidance" +} + +# --------------------------------------------------------------------------- +# Runner +# --------------------------------------------------------------------------- + +printf '=== install.sh e2e tests: zsh ===\n\n' + +printf 'Installing openshell...\n' +SHELL="/bin/zsh" run_install +printf 'Done.\n\n' + +test_binary_installed; echo "" +test_binary_executable; echo "" +test_binary_runs; echo "" +test_guidance_shows_export_path; echo "" +test_guidance_mentions_not_on_path + +print_summary diff --git a/e2e/test-install.sh b/e2e/test-install.sh deleted file mode 100755 index fa342f6c..00000000 --- a/e2e/test-install.sh +++ /dev/null @@ -1,380 +0,0 @@ -#!/bin/sh -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Tests for install.sh PATH setup logic. -# -# Validates that: -# - env scripts contain correct shell-specific syntax -# - POSIX rc files source the POSIX env script (not fish) -# - fish conf.d sources the fish env script -# - existing rc file content is preserved (no overwrites) -# - duplicate source lines are not appended on re-runs -# - user-facing guidance matches the detected shell -# -# Usage: -# ./tests/test-install.sh -# -set -eu - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -INSTALL_SCRIPT="$REPO_ROOT/install.sh" - -PASS=0 -FAIL=0 - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -pass() { - PASS=$((PASS + 1)) - printf ' PASS: %s\n' "$1" -} - -fail() { - FAIL=$((FAIL + 1)) - printf ' FAIL: %s\n' "$1" >&2 - if [ -n "${2:-}" ]; then - printf ' %s\n' "$2" >&2 - fi -} - -assert_file_contains() { - _afc_file="$1" - _afc_pattern="$2" - _afc_label="$3" - - if grep -qF "$_afc_pattern" "$_afc_file" 2>/dev/null; then - pass "$_afc_label" - else - fail "$_afc_label" "expected '$_afc_pattern' in $_afc_file" - fi -} - -assert_file_not_contains() { - _afnc_file="$1" - _afnc_pattern="$2" - _afnc_label="$3" - - if grep -qF "$_afnc_pattern" "$_afnc_file" 2>/dev/null; then - fail "$_afnc_label" "unexpected '$_afnc_pattern' found in $_afnc_file" - else - pass "$_afnc_label" - fi -} - -count_occurrences() { - _co_file="$1" - _co_pattern="$2" - grep -cF "$_co_pattern" "$_co_file" 2>/dev/null || echo "0" -} - -# Create a fresh temporary HOME for each test. -make_test_home() { - _mth_dir="$(mktemp -d)" - echo "$_mth_dir" -} - -cleanup_test_home() { - rm -rf "$1" -} - -# Source the install script functions without running main. -# We do this by extracting everything except the final `main "$@"` line. -prepare_install_functions() { - _pif_tmpscript="$(mktemp)" - sed '$d' "$INSTALL_SCRIPT" > "$_pif_tmpscript" - echo "$_pif_tmpscript" -} - -# --------------------------------------------------------------------------- -# Tests -# --------------------------------------------------------------------------- - -test_env_script_posix_syntax() { - printf 'TEST: env script contains POSIX syntax\n' - _test_home="$(make_test_home)" - _install_dir="$_test_home/.local/bin" - mkdir -p "$_install_dir" - - _funcs="$(prepare_install_functions)" - HOME="$_test_home" sh -c ". '$_funcs' && write_env_script_sh '\$HOME/.local/bin' '$_install_dir/env'" - rm -f "$_funcs" - - assert_file_contains "$_install_dir/env" 'export PATH=' "env has 'export PATH='" - assert_file_contains "$_install_dir/env" 'case ":${PATH}:"' "env uses POSIX case syntax" - assert_file_not_contains "$_install_dir/env" 'set -gx' "env does not contain fish syntax" - assert_file_not_contains "$_install_dir/env" 'not contains' "env does not contain fish keywords" - - cleanup_test_home "$_test_home" -} - -test_env_script_fish_syntax() { - printf 'TEST: env.fish script contains fish syntax\n' - _test_home="$(make_test_home)" - _install_dir="$_test_home/.local/bin" - mkdir -p "$_install_dir" - - _funcs="$(prepare_install_functions)" - HOME="$_test_home" sh -c ". '$_funcs' && write_env_script_fish '\$HOME/.local/bin' '$_install_dir/env.fish'" - rm -f "$_funcs" - - assert_file_contains "$_install_dir/env.fish" 'set -gx PATH' "env.fish has fish PATH syntax" - assert_file_contains "$_install_dir/env.fish" 'not contains' "env.fish uses fish conditionals" - assert_file_not_contains "$_install_dir/env.fish" 'export PATH=' "env.fish does not contain POSIX export" - - cleanup_test_home "$_test_home" -} - -test_bashrc_sources_posix_env() { - printf 'TEST: .bashrc sources POSIX env script (not env.fish)\n' - _test_home="$(make_test_home)" - _install_dir="$_test_home/.local/bin" - mkdir -p "$_install_dir" - - # Pre-create .bashrc with existing content - echo '# existing bashrc content' > "$_test_home/.bashrc" - - _funcs="$(prepare_install_functions)" - HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" - rm -f "$_funcs" - - assert_file_contains "$_test_home/.bashrc" ". \"$_install_dir/env\"" ".bashrc sources env" - assert_file_not_contains "$_test_home/.bashrc" "env.fish" ".bashrc does not reference env.fish" - - cleanup_test_home "$_test_home" -} - -test_profile_sources_posix_env() { - printf 'TEST: .profile sources POSIX env script (not env.fish)\n' - _test_home="$(make_test_home)" - _install_dir="$_test_home/.local/bin" - mkdir -p "$_install_dir" - - # Pre-create .profile with existing content - echo '# existing profile content' > "$_test_home/.profile" - - _funcs="$(prepare_install_functions)" - HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" - rm -f "$_funcs" - - assert_file_contains "$_test_home/.profile" ". \"$_install_dir/env\"" ".profile sources env" - assert_file_not_contains "$_test_home/.profile" "env.fish" ".profile does not reference env.fish" - - cleanup_test_home "$_test_home" -} - -test_fish_conf_sources_fish_env() { - printf 'TEST: fish conf.d sources fish env script\n' - _test_home="$(make_test_home)" - _install_dir="$_test_home/.local/bin" - mkdir -p "$_install_dir" - mkdir -p "$_test_home/.config/fish" - - _funcs="$(prepare_install_functions)" - HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" - rm -f "$_funcs" - - _fish_conf="$_test_home/.config/fish/conf.d/openshell.env.fish" - assert_file_contains "$_fish_conf" "env.fish" "fish conf.d sources env.fish" - assert_file_not_contains "$_fish_conf" ". \"" "fish conf.d does not use POSIX dot-source" - - cleanup_test_home "$_test_home" -} - -test_existing_content_preserved() { - printf 'TEST: existing rc file content is preserved\n' - _test_home="$(make_test_home)" - _install_dir="$_test_home/.local/bin" - mkdir -p "$_install_dir" - - # Write distinctive content to rc files - printf '# my custom bashrc aliases\nalias ll="ls -la"\nexport MY_VAR=hello\n' > "$_test_home/.bashrc" - printf '# my custom profile\nexport EDITOR=vim\n' > "$_test_home/.profile" - - _funcs="$(prepare_install_functions)" - HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" - rm -f "$_funcs" - - assert_file_contains "$_test_home/.bashrc" 'alias ll="ls -la"' ".bashrc alias preserved" - assert_file_contains "$_test_home/.bashrc" 'export MY_VAR=hello' ".bashrc export preserved" - assert_file_contains "$_test_home/.profile" 'export EDITOR=vim' ".profile export preserved" - - cleanup_test_home "$_test_home" -} - -test_no_duplicate_source_lines() { - printf 'TEST: running setup_path twice does not duplicate source lines\n' - _test_home="$(make_test_home)" - _install_dir="$_test_home/.local/bin" - mkdir -p "$_install_dir" - - echo '# bashrc' > "$_test_home/.bashrc" - - _funcs="$(prepare_install_functions)" - HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" - # Run again - HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" - rm -f "$_funcs" - - _count="$(count_occurrences "$_test_home/.bashrc" ". \"$_install_dir/env\"")" - if [ "$_count" = "1" ]; then - pass "source line appears exactly once in .bashrc" - else - fail "source line appears exactly once in .bashrc" "found $_count occurrences" - fi - - cleanup_test_home "$_test_home" -} - -test_guidance_shows_posix_for_bash() { - printf 'TEST: guidance shows POSIX source for bash users\n' - _test_home="$(make_test_home)" - _install_dir="$_test_home/.local/bin" - mkdir -p "$_install_dir" - - echo '# bashrc' > "$_test_home/.bashrc" - - _funcs="$(prepare_install_functions)" - _output="$(HOME="$_test_home" SHELL="/bin/bash" PATH="/usr/bin:/bin:/usr/sbin:/sbin" sh -c ". '$_funcs' && setup_path '$_install_dir'" 2>&1)" - rm -f "$_funcs" - - if echo "$_output" | grep -qF '. "'; then - pass "bash user sees POSIX dot-source command" - else - fail "bash user sees POSIX dot-source command" "output: $_output" - fi - - if echo "$_output" | grep -qF 'env.fish'; then - fail "bash user does not see env.fish hint" "output: $_output" - else - pass "bash user does not see env.fish hint" - fi - - cleanup_test_home "$_test_home" -} - -test_guidance_shows_fish_for_fish() { - printf 'TEST: guidance shows fish source for fish users\n' - _test_home="$(make_test_home)" - _install_dir="$_test_home/.local/bin" - mkdir -p "$_install_dir" - - echo '# bashrc' > "$_test_home/.bashrc" - - _funcs="$(prepare_install_functions)" - _output="$(HOME="$_test_home" SHELL="/usr/bin/fish" PATH="/usr/bin:/bin:/usr/sbin:/sbin" sh -c ". '$_funcs' && setup_path '$_install_dir'" 2>&1)" - rm -f "$_funcs" - - if echo "$_output" | grep -qF 'env.fish'; then - pass "fish user sees env.fish source command" - else - fail "fish user sees env.fish source command" "output: $_output" - fi - - cleanup_test_home "$_test_home" -} - -test_no_variable_clobbering() { - printf 'TEST: helper functions do not clobber caller variables\n' - _test_home="$(make_test_home)" - _install_dir="$_test_home/.local/bin" - mkdir -p "$_install_dir" - - echo '# bashrc' > "$_test_home/.bashrc" - - # This test verifies the core bug from #394: write_env_script_fish must not - # clobber _env_script. We call setup_path and check that .bashrc does NOT - # get env.fish sourced (which was the symptom of the clobbering bug). - _funcs="$(prepare_install_functions)" - HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" - rm -f "$_funcs" - - # The POSIX env script should be sourced, not env.fish - assert_file_not_contains "$_test_home/.bashrc" "env.fish" ".bashrc does not source env.fish (no variable clobbering)" - assert_file_contains "$_test_home/.bashrc" ". \"$_install_dir/env\"" ".bashrc sources the correct POSIX env script" - - cleanup_test_home "$_test_home" -} - -test_creates_profile_when_no_rc_files() { - printf 'TEST: creates .profile when no rc files exist\n' - _test_home="$(make_test_home)" - _install_dir="$_test_home/.local/bin" - mkdir -p "$_install_dir" - - # Don't create any rc files — empty home - _funcs="$(prepare_install_functions)" - HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" - rm -f "$_funcs" - - if [ -f "$_test_home/.profile" ]; then - pass ".profile was created" - assert_file_contains "$_test_home/.profile" ". \"$_install_dir/env\"" ".profile sources POSIX env" - assert_file_not_contains "$_test_home/.profile" "env.fish" ".profile does not reference env.fish" - else - fail ".profile was created" "file does not exist" - fi - - cleanup_test_home "$_test_home" -} - -test_zshrc_sources_posix_env() { - printf 'TEST: .zshrc sources POSIX env script\n' - _test_home="$(make_test_home)" - _install_dir="$_test_home/.local/bin" - mkdir -p "$_install_dir" - - echo '# existing zshrc' > "$_test_home/.zshrc" - - _funcs="$(prepare_install_functions)" - HOME="$_test_home" sh -c ". '$_funcs' && setup_path '$_install_dir'" - rm -f "$_funcs" - - assert_file_contains "$_test_home/.zshrc" ". \"$_install_dir/env\"" ".zshrc sources env" - assert_file_not_contains "$_test_home/.zshrc" "env.fish" ".zshrc does not reference env.fish" - - cleanup_test_home "$_test_home" -} - -# --------------------------------------------------------------------------- -# Runner -# --------------------------------------------------------------------------- - -main() { - printf '=== install.sh PATH setup tests ===\n\n' - - test_env_script_posix_syntax - echo "" - test_env_script_fish_syntax - echo "" - test_bashrc_sources_posix_env - echo "" - test_profile_sources_posix_env - echo "" - test_fish_conf_sources_fish_env - echo "" - test_existing_content_preserved - echo "" - test_no_duplicate_source_lines - echo "" - test_guidance_shows_posix_for_bash - echo "" - test_guidance_shows_fish_for_fish - echo "" - test_no_variable_clobbering - echo "" - test_creates_profile_when_no_rc_files - echo "" - test_zshrc_sources_posix_env - - printf '\n=== Results: %d passed, %d failed ===\n' "$PASS" "$FAIL" - - if [ "$FAIL" -gt 0 ]; then - exit 1 - fi -} - -main diff --git a/install.sh b/install.sh index 9ce9a390..faec3bb2 100755 --- a/install.sh +++ b/install.sh @@ -14,16 +14,11 @@ # OPENSHELL_VERSION - Release tag to install (default: latest tagged release) # OPENSHELL_INSTALL_DIR - Directory to install into (default: ~/.local/bin) # -# CLI flags: -# --help - Print usage information -# --no-modify-path - Skip PATH modification in shell profiles -# set -eu APP_NAME="openshell" REPO="NVIDIA/OpenShell" GITHUB_URL="https://github.com/${REPO}" -NO_MODIFY_PATH=0 # --------------------------------------------------------------------------- # Logging @@ -55,8 +50,7 @@ USAGE: ./install.sh [OPTIONS] OPTIONS: - --no-modify-path Don't add the install directory to PATH - --help Print this help message + --help Print this help message ENVIRONMENT VARIABLES: OPENSHELL_VERSION Release tag to install (default: latest tagged release) @@ -204,25 +198,14 @@ verify_checksum() { } # --------------------------------------------------------------------------- -# Install location and PATH management +# Install location # --------------------------------------------------------------------------- -get_home() { - if [ -n "${HOME:-}" ]; then - echo "$HOME" - elif [ -n "${USER:-}" ]; then - getent passwd "$USER" | cut -d: -f6 - else - getent passwd "$(id -un)" | cut -d: -f6 - fi -} - -get_default_install_dir() { - if [ -n "${XDG_BIN_HOME:-}" ]; then - echo "$XDG_BIN_HOME" +get_install_dir() { + if [ -n "${OPENSHELL_INSTALL_DIR:-}" ]; then + echo "$OPENSHELL_INSTALL_DIR" else - _home="$(get_home)" - echo "${_home}/.local/bin" + echo "${HOME}/.local/bin" fi } @@ -235,139 +218,6 @@ is_on_path() { esac } -# Write a small env script that conditionally prepends the install dir to PATH. -write_env_script_sh() { - _wes_dir_expr="$1" - _wes_out="$2" - - cat < "$_wes_out" -#!/bin/sh -# Add OpenShell to PATH if not already present -case ":\${PATH}:" in - *:"${_wes_dir_expr}":*) - ;; - *) - export PATH="${_wes_dir_expr}:\$PATH" - ;; -esac -ENVEOF -} - -write_env_script_fish() { - _wef_dir_expr="$1" - _wef_out="$2" - - cat < "$_wef_out" -# Add OpenShell to PATH if not already present -if not contains "${_wef_dir_expr}" \$PATH - set -gx PATH "${_wef_dir_expr}" \$PATH -end -ENVEOF -} - -# Add a `. /path/to/env` line to a shell rc file if not already present. -add_source_line() { - _asl_script="$1" - _asl_rcfile="$2" - _asl_shell="$3" - - if [ "$_asl_shell" = "fish" ]; then - _asl_line="source \"${_asl_script}\"" - else - _asl_line=". \"${_asl_script}\"" - fi - - # Check if line already exists - if [ -f "$_asl_rcfile" ] && grep -qF "$_asl_line" "$_asl_rcfile" 2>/dev/null; then - return 0 - fi - - # Append with a leading newline in case the file doesn't end with one - printf '\n%s\n' "$_asl_line" >> "$_asl_rcfile" - return 1 -} - -# Set up PATH modification in common shell rc files. -setup_path() { - _install_dir="$1" - _home="$(get_home)" - _env_script="${_install_dir}/env" - _fish_env_script="${_install_dir}/env.fish" - _needs_source=0 - - # Replace $HOME in the expression for late-bound references in rc files - if [ -n "${HOME:-}" ]; then - # shellcheck disable=SC2016 - _install_dir_expr='$HOME'"${_install_dir#"$_home"}" - else - _install_dir_expr="$_install_dir" - fi - - # Write the env scripts - write_env_script_sh "$_install_dir_expr" "$_env_script" - write_env_script_fish "$_install_dir_expr" "$_fish_env_script" - - # POSIX shells: .profile, .bashrc, .bash_profile, .zshrc, .zshenv - for _rcfile_rel in .profile .bashrc .bash_profile .zshrc .zshenv; do - _rcdir="$_home" - # zsh respects ZDOTDIR - case "$_rcfile_rel" in - .zsh*) _rcdir="${ZDOTDIR:-$_home}" ;; - esac - _rcfile="${_rcdir}/${_rcfile_rel}" - if [ -f "$_rcfile" ]; then - if ! add_source_line "$_env_script" "$_rcfile" "sh"; then - _needs_source=1 - fi - fi - done - - # If none of the above existed, create .profile - if [ "$_needs_source" = "0" ]; then - _found_any=0 - for _rcfile_rel in .profile .bashrc .bash_profile .zshrc .zshenv; do - if [ -f "${_home}/${_rcfile_rel}" ]; then - _found_any=1 - break - fi - done - if [ "$_found_any" = "0" ]; then - if ! add_source_line "$_env_script" "${_home}/.profile" "sh"; then - _needs_source=1 - fi - fi - fi - - # Fish shell - _fish_conf_dir="${_home}/.config/fish/conf.d" - if [ -d "${_home}/.config/fish" ]; then - mkdir -p "$_fish_conf_dir" - add_source_line "$_fish_env_script" "${_fish_conf_dir}/${APP_NAME}.env.fish" "fish" || true - fi - - # GitHub Actions: write to GITHUB_PATH for CI environments - if [ -n "${GITHUB_PATH:-}" ]; then - echo "$_install_dir" >> "$GITHUB_PATH" - fi - - if [ "$_needs_source" = "1" ] || ! is_on_path "$_install_dir"; then - # Detect the user's current shell to show the right source command - _current_shell="$(basename "${SHELL:-sh}" 2>/dev/null || echo "sh")" - - echo "" - info "to add ${APP_NAME} to your PATH, restart your shell or run:" - info "" - case "$_current_shell" in - fish) - info " source \"${_fish_env_script}\"" - ;; - *) - info " . \"${_env_script}\"" - ;; - esac - fi -} - # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- @@ -380,9 +230,6 @@ main() { usage exit 0 ;; - --no-modify-path) - NO_MODIFY_PATH=1 - ;; *) error "unknown option: $arg" ;; @@ -396,15 +243,7 @@ main() { _filename="${APP_NAME}-${_target}.tar.gz" _download_url="${GITHUB_URL}/releases/download/${_version}/${_filename}" _checksums_url="${GITHUB_URL}/releases/download/${_version}/${APP_NAME}-checksums-sha256.txt" - - # Determine install directory - _using_default_dir=0 - if [ -n "${OPENSHELL_INSTALL_DIR:-}" ]; then - _install_dir="$OPENSHELL_INSTALL_DIR" - else - _install_dir="$(get_default_install_dir)" - _using_default_dir=1 - fi + _install_dir="$(get_install_dir)" info "downloading ${APP_NAME} ${_version} (${_target})..." @@ -443,11 +282,27 @@ main() { _installed_version="$("${_install_dir}/${APP_NAME}" --version 2>/dev/null || echo "${_version}")" info "installed ${APP_NAME} ${_installed_version} to ${_install_dir}/${APP_NAME}" - # Set up PATH for default install location - if [ "$_using_default_dir" = "1" ] && [ "$NO_MODIFY_PATH" = "0" ]; then - if ! is_on_path "$_install_dir"; then - setup_path "$_install_dir" - fi + # If the install directory isn't on PATH, print instructions + if ! is_on_path "$_install_dir"; then + echo "" + info "${_install_dir} is not on your PATH." + info "" + info "Add it by appending the following to your shell configuration file" + info "(e.g. ~/.bashrc, ~/.zshrc, or ~/.config/fish/config.fish):" + info "" + + _current_shell="$(basename "${SHELL:-sh}" 2>/dev/null || echo "sh")" + case "$_current_shell" in + fish) + info " fish_add_path ${_install_dir}" + ;; + *) + info " export PATH=\"${_install_dir}:\$PATH\"" + ;; + esac + + info "" + info "Then restart your shell or run the command above in your current session." fi }