#!/bin/sh # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Install the OpenShell CLI binary. # # Usage: # curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh # # Or run directly: # ./install.sh # # Environment variables: # 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 # --------------------------------------------------------------------------- info() { printf '%s: %s\n' "$APP_NAME" "$*" >&2 } warn() { printf '%s: warning: %s\n' "$APP_NAME" "$*" >&2 } error() { printf '%s: error: %s\n' "$APP_NAME" "$*" >&2 exit 1 } # --------------------------------------------------------------------------- # Usage # --------------------------------------------------------------------------- usage() { cat </dev/null 2>&1 } check_downloader() { if has_cmd curl; then return 0 elif has_cmd wget; then return 0 else error "either 'curl' or 'wget' is required to download files" fi } # Download a URL to a file. Outputs nothing on success. download() { _url="$1" _output="$2" if has_cmd curl; then curl -fLsS --retry 3 -o "$_output" "$_url" elif has_cmd wget; then wget -q --tries=3 -O "$_output" "$_url" fi } # Follow a URL and print the final resolved URL (for detecting redirect targets). resolve_redirect() { _url="$1" 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 fi } # --------------------------------------------------------------------------- # Platform detection # --------------------------------------------------------------------------- get_os() { case "$(uname -s)" in Darwin) echo "apple-darwin" ;; Linux) echo "unknown-linux-musl" ;; *) error "unsupported OS: $(uname -s)" ;; esac } get_arch() { case "$(uname -m)" in x86_64|amd64) echo "x86_64" ;; aarch64|arm64) echo "aarch64" ;; *) error "unsupported architecture: $(uname -m)" ;; esac } get_target() { _arch="$(get_arch)" _os="$(get_os)" _target="${_arch}-${_os}" # Only these targets have published binaries. case "$_target" in x86_64-unknown-linux-musl|aarch64-unknown-linux-musl|aarch64-apple-darwin) ;; x86_64-apple-darwin) error "macOS x86_64 is not supported; use Apple Silicon (aarch64) or Rosetta 2" ;; *) error "no prebuilt binary for $_target" ;; esac echo "$_target" } # --------------------------------------------------------------------------- # Version resolution # --------------------------------------------------------------------------- resolve_version() { if [ -n "${OPENSHELL_VERSION:-}" ]; then echo "$OPENSHELL_VERSION" return 0 fi # Resolve "latest" by following the GitHub releases/latest redirect. # GitHub redirects /releases/latest -> /releases/tag/ info "resolving latest version..." _latest_url="${GITHUB_URL}/releases/latest" _resolved="$(resolve_redirect "$_latest_url")" || error "failed to resolve latest release from ${_latest_url}" # Extract the tag from the resolved URL: .../releases/tag/v0.0.4 -> v0.0.4 _version="${_resolved##*/}" if [ -z "$_version" ] || [ "$_version" = "latest" ]; then error "could not determine latest release version (resolved URL: ${_resolved})" fi echo "$_version" } # --------------------------------------------------------------------------- # Checksum verification # --------------------------------------------------------------------------- verify_checksum() { _archive="$1" _checksums="$2" _filename="$3" _expected="$(grep "$_filename" "$_checksums" | awk '{print $1}')" if [ -z "$_expected" ]; then warn "no checksum found for $_filename, skipping verification" return 0 fi if has_cmd shasum; then echo "$_expected $_archive" | shasum -a 256 -c --quiet 2>/dev/null elif has_cmd sha256sum; then echo "$_expected $_archive" | sha256sum -c --quiet 2>/dev/null else warn "sha256sum/shasum not found, skipping checksum verification" return 0 fi } # --------------------------------------------------------------------------- # Install location and PATH management # --------------------------------------------------------------------------- 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" else _home="$(get_home)" echo "${_home}/.local/bin" fi } # Check if a directory is already on PATH. is_on_path() { _dir="$1" case ":${PATH}:" in *":${_dir}:"*) return 0 ;; *) return 1 ;; esac } # Write a small env script that conditionally prepends the install dir to PATH. write_env_script_sh() { _install_dir_expr="$1" _env_script="$2" cat < "$_env_script" #!/bin/sh # Add OpenShell to PATH if not already present case ":\${PATH}:" in *:"${_install_dir_expr}":*) ;; *) export PATH="${_install_dir_expr}:\$PATH" ;; esac ENVEOF } write_env_script_fish() { _install_dir_expr="$1" _env_script="$2" cat < "$_env_script" # Add OpenShell to PATH if not already present if not contains "${_install_dir_expr}" \$PATH set -gx PATH "${_install_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" if [ "$_shell_type" = "fish" ]; then _line="source \"${_env_script_path}\"" else _line=". \"${_env_script_path}\"" fi # Check if line already exists if [ -f "$_rcfile" ] && grep -qF "$_line" "$_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" 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 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 fi } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- main() { # Parse CLI flags for arg in "$@"; do case "$arg" in --help) usage exit 0 ;; --no-modify-path) NO_MODIFY_PATH=1 ;; *) error "unknown option: $arg" ;; esac done check_downloader _version="$(resolve_version)" _target="$(get_target)" _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 info "downloading ${APP_NAME} ${_version} (${_target})..." _tmpdir="$(mktemp -d)" trap 'rm -rf "$_tmpdir"' EXIT if ! download "$_download_url" "${_tmpdir}/${_filename}"; then error "failed to download ${_download_url}" fi # Verify checksum info "verifying checksum..." if download "$_checksums_url" "${_tmpdir}/checksums.txt"; then if ! verify_checksum "${_tmpdir}/${_filename}" "${_tmpdir}/checksums.txt" "$_filename"; then error "checksum verification failed for ${_filename}" fi else warn "could not download checksums file, skipping verification" fi # Extract info "extracting..." tar -xzf "${_tmpdir}/${_filename}" -C "${_tmpdir}" # Install mkdir -p "$_install_dir" 2>/dev/null || true if [ -w "$_install_dir" ] || mkdir -p "$_install_dir" 2>/dev/null; then install -m 755 "${_tmpdir}/${APP_NAME}" "${_install_dir}/${APP_NAME}" else info "elevated permissions required to install to ${_install_dir}" sudo mkdir -p "$_install_dir" sudo install -m 755 "${_tmpdir}/${APP_NAME}" "${_install_dir}/${APP_NAME}" fi _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 fi } main "$@"