mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-02 07:34:45 +08:00
chore(installer): promote package install script (#1261)
This commit is contained in:
@@ -20,9 +20,13 @@ jobs:
|
||||
runs-on: macos-latest-xlarge
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Install dev and check status
|
||||
- name: Ensure VM driver
|
||||
run: |
|
||||
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/${{ github.event.workflow_run.head_sha || github.sha }}/install-dev.sh | sh
|
||||
launchctl setenv OPENSHELL_DRIVERS vm
|
||||
|
||||
- name: Install and check status
|
||||
run: |
|
||||
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/${{ github.event.workflow_run.head_sha || github.sha }}/install.sh | sh
|
||||
openshell status
|
||||
|
||||
ubuntu:
|
||||
@@ -42,9 +46,9 @@ jobs:
|
||||
printf 'OPENSHELL_DRIVERS=docker\n' > "${HOME}/.config/openshell/gateway.env"
|
||||
docker info
|
||||
|
||||
- name: Install dev and check status
|
||||
- name: Install and check status
|
||||
run: |
|
||||
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/${{ github.event.workflow_run.head_sha || github.sha }}/install-dev.sh | sh
|
||||
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/${{ github.event.workflow_run.head_sha || github.sha }}/install.sh | sh
|
||||
openshell status
|
||||
|
||||
fedora:
|
||||
@@ -63,7 +67,7 @@ jobs:
|
||||
printf 'OPENSHELL_DRIVERS=podman\n' > "${HOME}/.config/openshell/gateway.env"
|
||||
podman info
|
||||
|
||||
- name: Install dev and check status
|
||||
- name: Install and check status
|
||||
run: |
|
||||
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/${{ github.event.workflow_run.head_sha || github.sha }}/install-dev.sh | sh
|
||||
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/${{ github.event.workflow_run.head_sha || github.sha }}/install.sh | sh
|
||||
openshell status
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
name: Test Install Script
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'install.sh'
|
||||
- 'e2e/install/**'
|
||||
- '.github/workflows/test-install.yml'
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'install.sh'
|
||||
- 'e2e/install/**'
|
||||
- '.github/workflows/test-install.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-install:
|
||||
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@v6
|
||||
|
||||
- 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 }}
|
||||
@@ -180,17 +180,17 @@ The VM guest's serial console is appended to `<state-dir>/<sandbox-id>/console.l
|
||||
- runtime tarballs: the rolling `vm-runtime` release, rebuilt on demand by
|
||||
`release-vm-kernel.yml`
|
||||
|
||||
On Debian-family Linux amd64 and arm64 systems, `install-dev.sh` installs the
|
||||
On Debian-family Linux amd64 and arm64 systems, `install.sh` installs the
|
||||
Debian package from the selected `OPENSHELL_VERSION` release tag. That package
|
||||
includes `openshell-gateway` and `openshell-driver-vm`, but leaves
|
||||
`OPENSHELL_DRIVERS` unset so the gateway uses its normal runtime
|
||||
auto-detection. Set `OPENSHELL_DRIVERS=vm` to force the VM driver.
|
||||
|
||||
On RPM-family Linux x86_64 and aarch64 systems, `install-dev.sh` installs the
|
||||
On RPM-family Linux x86_64 and aarch64 systems, `install.sh` installs the
|
||||
`openshell` and `openshell-gateway` RPM packages from the selected release tag.
|
||||
The RPM gateway package is configured for the Podman driver.
|
||||
|
||||
On Apple Silicon macOS, `install-dev.sh` stages the generated `openshell.rb`
|
||||
On Apple Silicon macOS, `install.sh` stages the generated `openshell.rb`
|
||||
formula from the selected release in the `nvidia/openshell` Homebrew tap.
|
||||
Homebrew installs `openshell`, `openshell-gateway`, and
|
||||
`openshell-driver-vm`, ad-hoc signs the driver with the Hypervisor entitlement
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,152 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,100 +0,0 @@
|
||||
#!/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 ]
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/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
|
||||
-714
@@ -1,714 +0,0 @@
|
||||
#!/bin/sh
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Install the OpenShell development build from a GitHub release.
|
||||
#
|
||||
# Linux installs either the Debian or RPM packages from the selected release.
|
||||
# Apple Silicon macOS installs the generated Homebrew formula, so Homebrew owns
|
||||
# the binary layout and launchd service lifecycle.
|
||||
#
|
||||
set -e
|
||||
|
||||
APP_NAME="openshell"
|
||||
REPO="NVIDIA/OpenShell"
|
||||
GITHUB_URL="https://github.com/${REPO}"
|
||||
RELEASE_TAG="${OPENSHELL_VERSION:-dev}"
|
||||
CHECKSUMS_NAME="openshell-checksums-sha256.txt"
|
||||
LOCAL_GATEWAY_PORT="17670"
|
||||
HOMEBREW_TAP="nvidia/openshell"
|
||||
HOMEBREW_FORMULA_NAME="openshell"
|
||||
|
||||
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() {
|
||||
cat <<EOF
|
||||
install-dev.sh - Install the OpenShell development build
|
||||
|
||||
USAGE:
|
||||
curl -fsSL https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install-dev.sh -o install-dev.sh
|
||||
sh install-dev.sh
|
||||
|
||||
curl -fsSL https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install-dev.sh | sh
|
||||
|
||||
OPTIONS:
|
||||
--help Print this help message
|
||||
|
||||
ENVIRONMENT VARIABLES:
|
||||
OPENSHELL_VERSION Release tag to install (default: dev).
|
||||
|
||||
NOTES:
|
||||
This installs the selected release from:
|
||||
${GITHUB_URL}/releases/tag/${RELEASE_TAG}
|
||||
|
||||
Linux installs the Debian package on amd64/arm64 or the RPM packages on
|
||||
x86_64/aarch64, depending on the host package manager.
|
||||
macOS installs the release Homebrew formula on Apple Silicon and starts a
|
||||
brew services-backed local gateway.
|
||||
EOF
|
||||
}
|
||||
|
||||
has_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
if ! has_cmd "$1"; then
|
||||
error "'$1' is required"
|
||||
fi
|
||||
}
|
||||
|
||||
download() {
|
||||
_url="$1"
|
||||
_output="$2"
|
||||
curl -fLsS --retry 3 --max-redirs 5 -o "$_output" "$_url"
|
||||
}
|
||||
|
||||
download_release_asset() {
|
||||
_tag="$1"
|
||||
_filename="$2"
|
||||
_output="$3"
|
||||
|
||||
if curl -fLs --retry 3 --max-redirs 5 -o "$_output" \
|
||||
"${GITHUB_URL}/releases/download/${_tag}/${_filename}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# GitHub normalizes `~` to `.` in release asset names, while checksum files
|
||||
# can still record package filenames with `~dev` for correct version ordering.
|
||||
# Download the normalized asset but verify it against the checksum entry for
|
||||
# the original package filename.
|
||||
_normalized="$(printf '%s' "$_filename" | tr '~' '.')"
|
||||
if [ "$_normalized" != "$_filename" ]; then
|
||||
if download "${GITHUB_URL}/releases/download/${_tag}/${_normalized}" "$_output"; then
|
||||
info "using GitHub-normalized asset name ${_normalized}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
as_root() {
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
"$@"
|
||||
elif has_cmd sudo; then
|
||||
sudo "$@"
|
||||
else
|
||||
error "this installer needs root privileges; rerun as root or install sudo"
|
||||
fi
|
||||
}
|
||||
|
||||
target_user() {
|
||||
if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then
|
||||
echo "$SUDO_USER"
|
||||
else
|
||||
id -un
|
||||
fi
|
||||
}
|
||||
|
||||
user_home() {
|
||||
_user="$1"
|
||||
if has_cmd getent; then
|
||||
_home="$(getent passwd "$_user" | awk -F: '{ print $6 }')"
|
||||
if [ -n "$_home" ]; then
|
||||
echo "$_home"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$(uname -s)" = "Darwin" ] && has_cmd dscl; then
|
||||
_home="$(dscl . -read "/Users/${_user}" NFSHomeDirectory 2>/dev/null | awk '{ print $2 }')"
|
||||
if [ -n "$_home" ]; then
|
||||
echo "$_home"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$(id -un)" = "$_user" ]; then
|
||||
echo "${HOME:-}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
echo "/Users/${_user}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "/home/${_user}"
|
||||
}
|
||||
|
||||
as_target_user() {
|
||||
if [ "${PLATFORM:-}" = "darwin" ]; then
|
||||
if [ "$(id -u)" -eq "$TARGET_UID" ]; then
|
||||
env HOME="$TARGET_HOME" "$@"
|
||||
elif has_cmd sudo; then
|
||||
sudo -u "$TARGET_USER" env HOME="$TARGET_HOME" "$@"
|
||||
else
|
||||
error "cannot run commands as ${TARGET_USER}; install sudo or run as ${TARGET_USER}"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
_bus="unix:path=${TARGET_RUNTIME_DIR}/bus"
|
||||
if [ "$(id -u)" -eq "$TARGET_UID" ]; then
|
||||
env HOME="$TARGET_HOME" XDG_RUNTIME_DIR="$TARGET_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$_bus" "$@"
|
||||
elif has_cmd sudo; then
|
||||
sudo -u "$TARGET_USER" env HOME="$TARGET_HOME" XDG_RUNTIME_DIR="$TARGET_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$_bus" "$@"
|
||||
elif has_cmd runuser; then
|
||||
runuser -u "$TARGET_USER" -- env HOME="$TARGET_HOME" XDG_RUNTIME_DIR="$TARGET_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$_bus" "$@"
|
||||
else
|
||||
error "cannot run user service commands as ${TARGET_USER}; install sudo or run as ${TARGET_USER}"
|
||||
fi
|
||||
}
|
||||
|
||||
detect_platform() {
|
||||
case "$(uname -s)" in
|
||||
Linux)
|
||||
echo "linux"
|
||||
;;
|
||||
Darwin)
|
||||
echo "darwin"
|
||||
;;
|
||||
*)
|
||||
error "unsupported OS: $(uname -s); dev builds support Linux and macOS"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
linux_package_method() {
|
||||
if has_cmd dpkg; then
|
||||
echo "deb"
|
||||
elif has_cmd rpm; then
|
||||
echo "rpm"
|
||||
else
|
||||
error "Linux dev installs require either dpkg or rpm"
|
||||
fi
|
||||
}
|
||||
|
||||
set_linux_target_runtime_dir() {
|
||||
if [ "$(id -u)" -eq "$TARGET_UID" ] && [ -n "${XDG_RUNTIME_DIR:-}" ]; then
|
||||
TARGET_RUNTIME_DIR="$XDG_RUNTIME_DIR"
|
||||
else
|
||||
TARGET_RUNTIME_DIR="/run/user/${TARGET_UID}"
|
||||
fi
|
||||
}
|
||||
|
||||
check_linux_deb_platform() {
|
||||
require_cmd dpkg
|
||||
}
|
||||
|
||||
check_macos_platform() {
|
||||
_arch="$(uname -m)"
|
||||
|
||||
case "$_arch" in
|
||||
arm64|aarch64)
|
||||
;;
|
||||
x86_64|amd64)
|
||||
error "Intel macOS is not supported because no x86_64-apple-darwin dev assets are published"
|
||||
;;
|
||||
*)
|
||||
error "no macOS dev build is published for architecture: ${_arch}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! as_target_user brew --version >/dev/null 2>&1; then
|
||||
error "Homebrew is required for macOS dev installs; install it from https://brew.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
get_deb_arch() {
|
||||
_arch="$(dpkg --print-architecture)"
|
||||
|
||||
case "$_arch" in
|
||||
amd64|arm64)
|
||||
echo "$_arch"
|
||||
;;
|
||||
*)
|
||||
error "no dev Debian package is published for architecture: ${_arch}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_rpm_arch() {
|
||||
if has_cmd rpm; then
|
||||
_arch="$(rpm --eval '%{_arch}' 2>/dev/null || true)"
|
||||
else
|
||||
_arch=""
|
||||
fi
|
||||
|
||||
if [ -z "$_arch" ]; then
|
||||
_arch="$(uname -m)"
|
||||
fi
|
||||
|
||||
case "$_arch" in
|
||||
x86_64|amd64)
|
||||
echo "x86_64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
echo "aarch64"
|
||||
;;
|
||||
*)
|
||||
error "no dev RPM package is published for architecture: ${_arch}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
find_deb_asset() {
|
||||
_checksums="$1"
|
||||
_arch="$2"
|
||||
|
||||
awk -v arch="$_arch" '
|
||||
$2 ~ "^\\*?openshell[-_].*[-_]" arch "\\.deb$" {
|
||||
sub("^\\*", "", $2)
|
||||
print $2
|
||||
exit
|
||||
}
|
||||
' "$_checksums"
|
||||
}
|
||||
|
||||
find_rpm_asset() {
|
||||
_checksums="$1"
|
||||
_arch="$2"
|
||||
_package="$3"
|
||||
|
||||
case "$_package" in
|
||||
openshell)
|
||||
_dev_name="openshell-dev-${_arch}.rpm"
|
||||
_fallback_re="^openshell-[0-9].*\\.${_arch}\\.rpm$"
|
||||
;;
|
||||
openshell-gateway)
|
||||
_dev_name="openshell-gateway-dev-${_arch}.rpm"
|
||||
_fallback_re="^openshell-gateway-[0-9].*\\.${_arch}\\.rpm$"
|
||||
;;
|
||||
*)
|
||||
error "unknown RPM package selector: ${_package}"
|
||||
;;
|
||||
esac
|
||||
|
||||
awk -v dev_name="$_dev_name" -v fallback_re="$_fallback_re" '
|
||||
{
|
||||
name = $2
|
||||
sub("^\\*", "", name)
|
||||
|
||||
if (name == dev_name) {
|
||||
selected = name
|
||||
found = 1
|
||||
exit
|
||||
}
|
||||
|
||||
if (fallback == "" && name ~ fallback_re) {
|
||||
fallback = name
|
||||
}
|
||||
}
|
||||
END {
|
||||
if (found) {
|
||||
print selected
|
||||
} else if (fallback != "") {
|
||||
print fallback
|
||||
}
|
||||
}
|
||||
' "$_checksums"
|
||||
}
|
||||
|
||||
verify_checksum() {
|
||||
_archive="$1"
|
||||
_checksums="$2"
|
||||
_filename="$3"
|
||||
|
||||
if has_cmd sha256sum; then
|
||||
_expected="$(awk -v name="$_filename" '($2 == name || $2 == "*" name) { print $1; exit }' "$_checksums")"
|
||||
[ -n "$_expected" ] || error "no checksum entry found for ${_filename}"
|
||||
echo "$_expected $_archive" | sha256sum -c --quiet
|
||||
elif has_cmd shasum; then
|
||||
_expected="$(awk -v name="$_filename" '($2 == name || $2 == "*" name) { print $1; exit }' "$_checksums")"
|
||||
[ -n "$_expected" ] || error "no checksum entry found for ${_filename}"
|
||||
echo "$_expected $_archive" | shasum -a 256 -c --quiet
|
||||
else
|
||||
error "neither 'sha256sum' nor 'shasum' found; cannot verify download integrity"
|
||||
fi
|
||||
}
|
||||
|
||||
install_deb_package() {
|
||||
_deb_path="$1"
|
||||
|
||||
if has_cmd apt-get; then
|
||||
as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-o Dpkg::Options::=--force-confnew \
|
||||
"$_deb_path"
|
||||
elif has_cmd apt; then
|
||||
as_root env DEBIAN_FRONTEND=noninteractive apt install -y \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-o Dpkg::Options::=--force-confnew \
|
||||
"$_deb_path"
|
||||
else
|
||||
as_root dpkg --force-confdef --force-confnew -i "$_deb_path"
|
||||
fi
|
||||
}
|
||||
|
||||
install_rpm_packages() {
|
||||
if has_cmd dnf; then
|
||||
as_root dnf install -y "$@"
|
||||
elif has_cmd yum; then
|
||||
as_root yum install -y "$@"
|
||||
elif has_cmd zypper; then
|
||||
as_root zypper --non-interactive install --allow-unsigned-rpm "$@"
|
||||
elif has_cmd rpm; then
|
||||
warn "installing with rpm directly; dependencies must already be installed"
|
||||
as_root rpm -Uvh --replacepkgs "$@"
|
||||
else
|
||||
error "'dnf', 'yum', 'zypper', or 'rpm' is required to install RPM packages"
|
||||
fi
|
||||
}
|
||||
|
||||
homebrew_formula_path() {
|
||||
_tap="$1"
|
||||
_formula="$2"
|
||||
|
||||
if ! as_target_user brew tap-info "$_tap" >/dev/null 2>&1; then
|
||||
info "creating local Homebrew tap ${_tap}..."
|
||||
as_target_user brew tap-new --no-git "$_tap" >/dev/null
|
||||
fi
|
||||
|
||||
_tap_dir="$(as_target_user brew --repository "$_tap" 2>/dev/null || true)"
|
||||
[ -n "$_tap_dir" ] || error "could not locate Homebrew tap ${_tap}"
|
||||
|
||||
_formula_dir="${_tap_dir}/Formula"
|
||||
as_target_user mkdir -p "$_formula_dir"
|
||||
printf '%s/%s.rb\n' "$_formula_dir" "$_formula"
|
||||
}
|
||||
|
||||
patch_homebrew_formula() {
|
||||
_formula_file="$1"
|
||||
_patched_file="${_formula_file}.patched"
|
||||
|
||||
if grep -q 'entitlements.write <<~XML' "$_formula_file"; then
|
||||
info "patching Homebrew formula for idempotent postinstall..."
|
||||
sed 's/entitlements\.write <<~XML/entitlements.atomic_write <<~XML/' "$_formula_file" >"$_patched_file"
|
||||
mv "$_patched_file" "$_formula_file"
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
start_user_gateway() {
|
||||
info "restarting openshell-gateway user service as ${TARGET_USER}..."
|
||||
|
||||
if ! as_target_user systemctl --user daemon-reload; then
|
||||
info "could not reach the user systemd manager for ${TARGET_USER}"
|
||||
info "restart the gateway later with: systemctl --user enable openshell-gateway && systemctl --user restart openshell-gateway"
|
||||
info "then register it with: openshell gateway add https://127.0.0.1:17670 --local --name openshell"
|
||||
return 0
|
||||
fi
|
||||
|
||||
as_target_user systemctl --user enable openshell-gateway
|
||||
as_target_user systemctl --user restart openshell-gateway
|
||||
as_target_user systemctl --user is-active --quiet openshell-gateway
|
||||
|
||||
info "registering local gateway as ${TARGET_USER}..."
|
||||
register_local_gateway
|
||||
wait_for_local_gateway_listener
|
||||
wait_for_local_gateway_status
|
||||
}
|
||||
|
||||
wait_for_local_gateway_listener() {
|
||||
_timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}"
|
||||
_elapsed=0
|
||||
_last_output=""
|
||||
_probe_url="https://127.0.0.1:${LOCAL_GATEWAY_PORT}/"
|
||||
_mtls_dir="${TARGET_HOME}/.config/openshell/gateways/openshell/mtls"
|
||||
|
||||
info "waiting for local gateway listener to become reachable..."
|
||||
while [ "$_elapsed" -lt "$_timeout" ]; do
|
||||
if [ ! -f "${_mtls_dir}/ca.crt" ] || [ ! -f "${_mtls_dir}/tls.crt" ] || [ ! -f "${_mtls_dir}/tls.key" ]; then
|
||||
_last_output="mTLS client bundle is not ready under ${_mtls_dir}"
|
||||
elif _last_output="$(as_target_user curl -sS --max-time 2 --cacert "${_mtls_dir}/ca.crt" --cert "${_mtls_dir}/tls.crt" --key "${_mtls_dir}/tls.key" -o /dev/null "$_probe_url" 2>&1)"; then
|
||||
info "local gateway listener is reachable"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
_elapsed=$((_elapsed + 1))
|
||||
done
|
||||
|
||||
printf '%s\n' "$_last_output" >&2
|
||||
error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s"
|
||||
}
|
||||
|
||||
wait_for_local_gateway_status() {
|
||||
_timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}"
|
||||
_elapsed=0
|
||||
_status_output=""
|
||||
_register_bin="${OPENSHELL_REGISTER_BIN:-openshell}"
|
||||
|
||||
info "waiting for openshell status to report connected..."
|
||||
while [ "$_elapsed" -lt "$_timeout" ]; do
|
||||
if _status_output="$(as_target_user env NO_COLOR=1 "$_register_bin" status 2>&1)"; then
|
||||
case "$_status_output" in
|
||||
*"Version:"*)
|
||||
info "openshell status reports connected"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
sleep 1
|
||||
_elapsed=$((_elapsed + 1))
|
||||
done
|
||||
|
||||
printf '%s\n' "$_status_output" >&2
|
||||
error "openshell status did not report connected within ${_timeout}s"
|
||||
}
|
||||
|
||||
remove_local_gateway_registration() {
|
||||
[ -n "$TARGET_HOME" ] || error "cannot resolve home directory for ${TARGET_USER}"
|
||||
_config_dir="${TARGET_HOME}/.config/openshell"
|
||||
|
||||
# The install-dev gateway is a user service. Replace the CLI registration
|
||||
# directly instead of asking `gateway destroy` to tear down Docker resources.
|
||||
# shellcheck disable=SC2016
|
||||
as_target_user sh -c '
|
||||
config_dir=$1
|
||||
rm -rf "${config_dir}/gateways/local"
|
||||
mkdir -p "${config_dir}/gateways/openshell"
|
||||
rm -f \
|
||||
"${config_dir}/gateways/openshell/metadata.json" \
|
||||
"${config_dir}/gateways/openshell/edge_token" \
|
||||
"${config_dir}/gateways/openshell/cf_token" \
|
||||
"${config_dir}/gateways/openshell/oidc_token.json"
|
||||
active="${config_dir}/active_gateway"
|
||||
active_name="$(cat "$active" 2>/dev/null || true)"
|
||||
if [ "$active_name" = "local" ] || [ "$active_name" = "openshell" ]; then
|
||||
rm -f "$active"
|
||||
fi
|
||||
' sh "$_config_dir"
|
||||
}
|
||||
|
||||
register_local_gateway() {
|
||||
_register_bin="${OPENSHELL_REGISTER_BIN:-openshell}"
|
||||
|
||||
if _add_output="$(as_target_user "$_register_bin" gateway add "https://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name openshell 2>&1)"; then
|
||||
[ -z "$_add_output" ] || print_gateway_add_output "$_add_output"
|
||||
return 0
|
||||
else
|
||||
_add_status=$?
|
||||
fi
|
||||
|
||||
case "$_add_output" in
|
||||
*"already exists"*)
|
||||
info "local gateway already exists; removing and re-adding it..."
|
||||
remove_local_gateway_registration
|
||||
as_target_user "$_register_bin" gateway add "https://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name openshell
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$_add_output" >&2
|
||||
return "$_add_status"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
print_gateway_add_output() {
|
||||
printf '%s\n' "$1" | while IFS= read -r _line; do
|
||||
case "$_line" in
|
||||
*"Gateway is not reachable at https://127.0.0.1:${LOCAL_GATEWAY_PORT}"*) ;;
|
||||
*"Verify the gateway is running and the endpoint is correct."*) ;;
|
||||
*) printf '%s\n' "$_line" >&2 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
install_linux_deb() {
|
||||
check_linux_deb_platform
|
||||
set_linux_target_runtime_dir
|
||||
|
||||
_arch="$(get_deb_arch)"
|
||||
_tmpdir="$(mktemp -d)"
|
||||
chmod 0755 "$_tmpdir"
|
||||
trap 'rm -rf "$_tmpdir"' EXIT
|
||||
|
||||
_checksums_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/${CHECKSUMS_NAME}"
|
||||
info "downloading ${RELEASE_TAG} release checksums..."
|
||||
download "$_checksums_url" "${_tmpdir}/${CHECKSUMS_NAME}" || {
|
||||
error "failed to download ${_checksums_url}"
|
||||
}
|
||||
|
||||
_deb_file="$(find_deb_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch")"
|
||||
if [ -z "$_deb_file" ]; then
|
||||
error "no dev Debian package found for architecture: ${_arch}"
|
||||
fi
|
||||
|
||||
_deb_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/${_deb_file}"
|
||||
_deb_path="${_tmpdir}/${_deb_file}"
|
||||
|
||||
info "selected ${_deb_file}"
|
||||
|
||||
info "downloading ${_deb_file}..."
|
||||
download_release_asset "$RELEASE_TAG" "$_deb_file" "$_deb_path" || {
|
||||
error "failed to download ${_deb_url}"
|
||||
}
|
||||
chmod 0644 "$_deb_path"
|
||||
|
||||
info "verifying checksum..."
|
||||
verify_checksum "$_deb_path" "${_tmpdir}/${CHECKSUMS_NAME}" "$_deb_file"
|
||||
|
||||
info "installing ${_deb_file}..."
|
||||
install_deb_package "$_deb_path"
|
||||
info "installed ${APP_NAME} package from ${RELEASE_TAG}"
|
||||
start_user_gateway
|
||||
}
|
||||
|
||||
install_linux_rpm() {
|
||||
require_cmd rpm
|
||||
set_linux_target_runtime_dir
|
||||
|
||||
_arch="$(get_rpm_arch)"
|
||||
_tmpdir="$(mktemp -d)"
|
||||
chmod 0755 "$_tmpdir"
|
||||
trap 'rm -rf "$_tmpdir"' EXIT
|
||||
|
||||
_checksums_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/${CHECKSUMS_NAME}"
|
||||
info "downloading ${RELEASE_TAG} release checksums..."
|
||||
download "$_checksums_url" "${_tmpdir}/${CHECKSUMS_NAME}" || {
|
||||
error "failed to download ${_checksums_url}"
|
||||
}
|
||||
|
||||
_rpm_file="$(find_rpm_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch" openshell)"
|
||||
if [ -z "$_rpm_file" ]; then
|
||||
error "no dev openshell RPM package found for architecture: ${_arch}"
|
||||
fi
|
||||
|
||||
_gateway_rpm_file="$(find_rpm_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch" openshell-gateway)"
|
||||
if [ -z "$_gateway_rpm_file" ]; then
|
||||
error "no dev openshell-gateway RPM package found for architecture: ${_arch}"
|
||||
fi
|
||||
|
||||
info "selected ${_rpm_file} and ${_gateway_rpm_file}"
|
||||
|
||||
for _package_file in "$_rpm_file" "$_gateway_rpm_file"; do
|
||||
_package_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/${_package_file}"
|
||||
_package_path="${_tmpdir}/${_package_file}"
|
||||
|
||||
info "downloading ${_package_file}..."
|
||||
download_release_asset "$RELEASE_TAG" "$_package_file" "$_package_path" || {
|
||||
error "failed to download ${_package_url}"
|
||||
}
|
||||
chmod 0644 "$_package_path"
|
||||
|
||||
info "verifying checksum for ${_package_file}..."
|
||||
verify_checksum "$_package_path" "${_tmpdir}/${CHECKSUMS_NAME}" "$_package_file"
|
||||
done
|
||||
|
||||
info "installing ${_rpm_file} and ${_gateway_rpm_file}..."
|
||||
install_rpm_packages "${_tmpdir}/${_rpm_file}" "${_tmpdir}/${_gateway_rpm_file}"
|
||||
info "installed ${APP_NAME} RPM packages from ${RELEASE_TAG}"
|
||||
start_user_gateway
|
||||
}
|
||||
|
||||
install_macos_homebrew() {
|
||||
check_macos_platform
|
||||
|
||||
_tmpdir="$(mktemp -d)"
|
||||
chmod 0755 "$_tmpdir"
|
||||
trap 'rm -rf "$_tmpdir"' EXIT
|
||||
|
||||
_formula_file="${_tmpdir}/openshell.rb"
|
||||
_formula_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/openshell.rb"
|
||||
|
||||
info "downloading Homebrew formula from ${_formula_url}..."
|
||||
download_release_asset "$RELEASE_TAG" "openshell.rb" "$_formula_file" || {
|
||||
error "failed to download ${_formula_url}; the selected release may not include a Homebrew formula"
|
||||
}
|
||||
chmod 0644 "$_formula_file"
|
||||
patch_homebrew_formula "$_formula_file"
|
||||
|
||||
_tap_formula_file="$(homebrew_formula_path "$HOMEBREW_TAP" "$HOMEBREW_FORMULA_NAME")"
|
||||
info "staging Homebrew formula in tap ${HOMEBREW_TAP}..."
|
||||
cp "$_formula_file" "$_tap_formula_file"
|
||||
chmod 0644 "$_tap_formula_file"
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
chown "$TARGET_USER" "$_tap_formula_file" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
_formula_ref="${HOMEBREW_TAP}/${HOMEBREW_FORMULA_NAME}"
|
||||
|
||||
if as_target_user brew list --formula openshell >/dev/null 2>&1; then
|
||||
info "reinstalling OpenShell with Homebrew..."
|
||||
as_target_user brew reinstall --formula "$_formula_ref"
|
||||
else
|
||||
info "installing OpenShell with Homebrew..."
|
||||
as_target_user brew install --formula "$_formula_ref"
|
||||
fi
|
||||
|
||||
info "restarting OpenShell Homebrew service..."
|
||||
if ! as_target_user brew services restart "$_formula_ref"; then
|
||||
warn "could not restart the OpenShell Homebrew service"
|
||||
info "restart it later with: brew services restart ${_formula_ref}"
|
||||
info "then register it with: openshell gateway add https://127.0.0.1:${LOCAL_GATEWAY_PORT} --local --name openshell"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_brew_prefix="$(as_target_user brew --prefix 2>/dev/null || true)"
|
||||
if [ -n "$_brew_prefix" ] && [ -x "${_brew_prefix}/bin/openshell" ]; then
|
||||
OPENSHELL_REGISTER_BIN="${_brew_prefix}/bin/openshell"
|
||||
fi
|
||||
|
||||
info "registering local gateway as ${TARGET_USER}..."
|
||||
register_local_gateway
|
||||
wait_for_local_gateway_listener
|
||||
wait_for_local_gateway_status
|
||||
}
|
||||
|
||||
main() {
|
||||
if [ "$#" -gt 0 ]; then
|
||||
case "$1" in
|
||||
--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
error "unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
require_cmd curl
|
||||
PLATFORM="$(detect_platform)"
|
||||
|
||||
TARGET_USER="$(target_user)"
|
||||
TARGET_UID="$(id -u "$TARGET_USER" 2>/dev/null || true)"
|
||||
[ -n "$TARGET_UID" ] || error "cannot resolve uid for ${TARGET_USER}"
|
||||
TARGET_HOME="$(user_home "$TARGET_USER")"
|
||||
|
||||
case "$PLATFORM" in
|
||||
linux)
|
||||
case "$(linux_package_method)" in
|
||||
deb)
|
||||
install_linux_deb
|
||||
;;
|
||||
rpm)
|
||||
install_linux_rpm
|
||||
;;
|
||||
*)
|
||||
error "unsupported Linux package method"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
darwin)
|
||||
install_macos_homebrew
|
||||
;;
|
||||
*)
|
||||
error "unsupported platform: ${PLATFORM}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
+641
-217
@@ -2,27 +2,22 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Install the OpenShell CLI binary.
|
||||
# Install OpenShell from a GitHub release.
|
||||
#
|
||||
# Usage:
|
||||
# curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh
|
||||
# Linux installs either the Debian or RPM packages from the selected release.
|
||||
# Apple Silicon macOS installs the generated Homebrew formula, so Homebrew owns
|
||||
# the binary layout and launchd service lifecycle.
|
||||
#
|
||||
# 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)
|
||||
#
|
||||
set -eu
|
||||
set -e
|
||||
|
||||
APP_NAME="openshell"
|
||||
REPO="NVIDIA/OpenShell"
|
||||
GITHUB_URL="https://github.com/${REPO}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Logging
|
||||
# ---------------------------------------------------------------------------
|
||||
RELEASE_TAG="${OPENSHELL_VERSION:-}"
|
||||
CHECKSUMS_NAME="openshell-checksums-sha256.txt"
|
||||
LOCAL_GATEWAY_PORT="17670"
|
||||
HOMEBREW_TAP="nvidia/openshell"
|
||||
HOMEBREW_FORMULA_NAME="openshell"
|
||||
|
||||
info() {
|
||||
printf '%s: %s\n' "$APP_NAME" "$*" >&2
|
||||
@@ -37,134 +32,62 @@ error() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Usage
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
install.sh — Install the OpenShell CLI
|
||||
install.sh - Install OpenShell
|
||||
|
||||
USAGE:
|
||||
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh
|
||||
./install.sh [OPTIONS]
|
||||
curl -fsSL https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh -o install.sh
|
||||
sh install.sh
|
||||
|
||||
curl -fsSL https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh
|
||||
|
||||
OPTIONS:
|
||||
--help Print this help message
|
||||
--help Print this help message
|
||||
|
||||
ENVIRONMENT VARIABLES:
|
||||
OPENSHELL_VERSION Release tag to install (default: latest tagged release)
|
||||
OPENSHELL_INSTALL_DIR Directory to install into (default: ~/.local/bin)
|
||||
OPENSHELL_VERSION Release tag to install (default: latest tagged release).
|
||||
Set OPENSHELL_VERSION=dev to install the rolling dev build.
|
||||
|
||||
EXAMPLES:
|
||||
# Install latest release
|
||||
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh
|
||||
NOTES:
|
||||
When OPENSHELL_VERSION is unset, this resolves the latest tagged release
|
||||
from ${GITHUB_URL}/releases/latest.
|
||||
|
||||
# Install a specific version
|
||||
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_VERSION=v0.0.9 sh
|
||||
|
||||
# Install to /usr/local/bin
|
||||
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_INSTALL_DIR=/usr/local/bin sh
|
||||
Linux installs the Debian package on amd64/arm64 or the RPM packages on
|
||||
x86_64/aarch64, depending on the host package manager.
|
||||
macOS installs the release Homebrew formula on Apple Silicon and starts a
|
||||
brew services-backed local gateway.
|
||||
EOF
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HTTP helpers — prefer curl, fall back to wget
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
has_cmd() {
|
||||
command -v "$1" >/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"
|
||||
require_cmd() {
|
||||
if ! has_cmd "$1"; then
|
||||
error "'$1' is required"
|
||||
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 --max-redirs 5 -o "$_output" "$_url"
|
||||
elif has_cmd wget; then
|
||||
wget -q --tries=3 --max-redirect=5 -O "$_output" "$_url"
|
||||
fi
|
||||
curl -fLsS --retry 3 --max-redirs 5 -o "$_output" "$_url"
|
||||
}
|
||||
|
||||
# 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; capture the final Location from stderr
|
||||
wget --spider --max-redirect=10 "$_url" 2>&1 | sed -n 's/^.*Location: \([^ ]*\).*/\1/p' | 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() {
|
||||
resolve_release_tag() {
|
||||
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/<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}"
|
||||
_resolved="$(curl -fLsS -o /dev/null -w '%{url_effective}' "$_latest_url")" || {
|
||||
error "failed to resolve latest release from ${_latest_url}"
|
||||
}
|
||||
|
||||
# Validate that the redirect stayed on the expected GitHub origin.
|
||||
# A MITM or DNS hijack could redirect to an attacker-controlled domain,
|
||||
# which would also serve a matching checksums file (making checksum
|
||||
# verification useless). See: https://github.com/NVIDIA/OpenShell/issues/638
|
||||
case "$_resolved" in
|
||||
https://github.com/${REPO}/releases/*)
|
||||
;;
|
||||
@@ -173,9 +96,7 @@ resolve_version() {
|
||||
;;
|
||||
esac
|
||||
|
||||
# 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
|
||||
@@ -183,138 +104,641 @@ resolve_version() {
|
||||
echo "$_version"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Checksum verification
|
||||
# ---------------------------------------------------------------------------
|
||||
download_release_asset() {
|
||||
_tag="$1"
|
||||
_filename="$2"
|
||||
_output="$3"
|
||||
|
||||
verify_checksum() {
|
||||
_vc_archive="$1"
|
||||
_vc_checksums="$2"
|
||||
_vc_filename="$3"
|
||||
|
||||
if ! has_cmd shasum && ! has_cmd sha256sum; then
|
||||
error "neither 'shasum' nor 'sha256sum' found; cannot verify download integrity"
|
||||
if curl -fLs --retry 3 --max-redirs 5 -o "$_output" \
|
||||
"${GITHUB_URL}/releases/download/${_tag}/${_filename}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
_vc_expected="$(grep -F "$_vc_filename" "$_vc_checksums" | awk '{print $1}')"
|
||||
|
||||
if [ -z "$_vc_expected" ]; then
|
||||
error "no checksum entry found for $_vc_filename in checksums file"
|
||||
# GitHub normalizes `~` to `.` in release asset names, while checksum files
|
||||
# can still record package filenames with `~dev` for correct version ordering.
|
||||
# Download the normalized asset but verify it against the checksum entry for
|
||||
# the original package filename.
|
||||
_normalized="$(printf '%s' "$_filename" | tr '~' '.')"
|
||||
if [ "$_normalized" != "$_filename" ]; then
|
||||
if download "${GITHUB_URL}/releases/download/${_tag}/${_normalized}" "$_output"; then
|
||||
info "using GitHub-normalized asset name ${_normalized}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if has_cmd shasum; then
|
||||
echo "$_vc_expected $_vc_archive" | shasum -a 256 -c --quiet 2>/dev/null
|
||||
elif has_cmd sha256sum; then
|
||||
echo "$_vc_expected $_vc_archive" | sha256sum -c --quiet 2>/dev/null
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Install location
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
get_install_dir() {
|
||||
if [ -n "${OPENSHELL_INSTALL_DIR:-}" ]; then
|
||||
echo "$OPENSHELL_INSTALL_DIR"
|
||||
as_root() {
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
"$@"
|
||||
elif has_cmd sudo; then
|
||||
sudo "$@"
|
||||
else
|
||||
echo "${HOME}/.local/bin"
|
||||
error "this installer needs root privileges; rerun as root or install sudo"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if a directory is already on PATH.
|
||||
is_on_path() {
|
||||
_dir="$1"
|
||||
case ":${PATH}:" in
|
||||
*":${_dir}:"*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
target_user() {
|
||||
if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then
|
||||
echo "$SUDO_USER"
|
||||
else
|
||||
id -un
|
||||
fi
|
||||
}
|
||||
|
||||
user_home() {
|
||||
_user="$1"
|
||||
if has_cmd getent; then
|
||||
_home="$(getent passwd "$_user" | awk -F: '{ print $6 }')"
|
||||
if [ -n "$_home" ]; then
|
||||
echo "$_home"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$(uname -s)" = "Darwin" ] && has_cmd dscl; then
|
||||
_home="$(dscl . -read "/Users/${_user}" NFSHomeDirectory 2>/dev/null | awk '{ print $2 }')"
|
||||
if [ -n "$_home" ]; then
|
||||
echo "$_home"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$(id -un)" = "$_user" ]; then
|
||||
echo "${HOME:-}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
echo "/Users/${_user}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "/home/${_user}"
|
||||
}
|
||||
|
||||
as_target_user() {
|
||||
if [ "${PLATFORM:-}" = "darwin" ]; then
|
||||
if [ "$(id -u)" -eq "$TARGET_UID" ]; then
|
||||
env HOME="$TARGET_HOME" "$@"
|
||||
elif has_cmd sudo; then
|
||||
sudo -u "$TARGET_USER" env HOME="$TARGET_HOME" "$@"
|
||||
else
|
||||
error "cannot run commands as ${TARGET_USER}; install sudo or run as ${TARGET_USER}"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
_bus="unix:path=${TARGET_RUNTIME_DIR}/bus"
|
||||
if [ "$(id -u)" -eq "$TARGET_UID" ]; then
|
||||
env HOME="$TARGET_HOME" XDG_RUNTIME_DIR="$TARGET_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$_bus" "$@"
|
||||
elif has_cmd sudo; then
|
||||
sudo -u "$TARGET_USER" env HOME="$TARGET_HOME" XDG_RUNTIME_DIR="$TARGET_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$_bus" "$@"
|
||||
elif has_cmd runuser; then
|
||||
runuser -u "$TARGET_USER" -- env HOME="$TARGET_HOME" XDG_RUNTIME_DIR="$TARGET_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$_bus" "$@"
|
||||
else
|
||||
error "cannot run user service commands as ${TARGET_USER}; install sudo or run as ${TARGET_USER}"
|
||||
fi
|
||||
}
|
||||
|
||||
detect_platform() {
|
||||
case "$(uname -s)" in
|
||||
Linux)
|
||||
echo "linux"
|
||||
;;
|
||||
Darwin)
|
||||
echo "darwin"
|
||||
;;
|
||||
*)
|
||||
error "unsupported OS: $(uname -s); this installer supports Linux and macOS"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
linux_package_method() {
|
||||
if has_cmd dpkg; then
|
||||
echo "deb"
|
||||
elif has_cmd rpm; then
|
||||
echo "rpm"
|
||||
else
|
||||
error "Linux installs require either dpkg or rpm"
|
||||
fi
|
||||
}
|
||||
|
||||
set_linux_target_runtime_dir() {
|
||||
if [ "$(id -u)" -eq "$TARGET_UID" ] && [ -n "${XDG_RUNTIME_DIR:-}" ]; then
|
||||
TARGET_RUNTIME_DIR="$XDG_RUNTIME_DIR"
|
||||
else
|
||||
TARGET_RUNTIME_DIR="/run/user/${TARGET_UID}"
|
||||
fi
|
||||
}
|
||||
|
||||
check_linux_deb_platform() {
|
||||
require_cmd dpkg
|
||||
}
|
||||
|
||||
check_macos_platform() {
|
||||
_arch="$(uname -m)"
|
||||
|
||||
case "$_arch" in
|
||||
arm64|aarch64)
|
||||
;;
|
||||
x86_64|amd64)
|
||||
error "Intel macOS is not supported because no x86_64-apple-darwin release assets are published"
|
||||
;;
|
||||
*)
|
||||
error "no macOS release build is published for architecture: ${_arch}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! as_target_user brew --version >/dev/null 2>&1; then
|
||||
error "Homebrew is required for macOS installs; install it from https://brew.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
get_deb_arch() {
|
||||
_arch="$(dpkg --print-architecture)"
|
||||
|
||||
case "$_arch" in
|
||||
amd64|arm64)
|
||||
echo "$_arch"
|
||||
;;
|
||||
*)
|
||||
error "no Debian package is published for architecture: ${_arch}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_rpm_arch() {
|
||||
if has_cmd rpm; then
|
||||
_arch="$(rpm --eval '%{_arch}' 2>/dev/null || true)"
|
||||
else
|
||||
_arch=""
|
||||
fi
|
||||
|
||||
if [ -z "$_arch" ]; then
|
||||
_arch="$(uname -m)"
|
||||
fi
|
||||
|
||||
case "$_arch" in
|
||||
x86_64|amd64)
|
||||
echo "x86_64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
echo "aarch64"
|
||||
;;
|
||||
*)
|
||||
error "no RPM package is published for architecture: ${_arch}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
find_deb_asset() {
|
||||
_checksums="$1"
|
||||
_arch="$2"
|
||||
|
||||
awk -v arch="$_arch" '
|
||||
$2 ~ "^\\*?openshell[-_].*[-_]" arch "\\.deb$" {
|
||||
sub("^\\*", "", $2)
|
||||
print $2
|
||||
exit
|
||||
}
|
||||
' "$_checksums"
|
||||
}
|
||||
|
||||
find_rpm_asset() {
|
||||
_checksums="$1"
|
||||
_arch="$2"
|
||||
_package="$3"
|
||||
|
||||
case "$_package" in
|
||||
openshell)
|
||||
_dev_name="openshell-dev-${_arch}.rpm"
|
||||
_fallback_re="^openshell-[0-9].*\\.${_arch}\\.rpm$"
|
||||
;;
|
||||
openshell-gateway)
|
||||
_dev_name="openshell-gateway-dev-${_arch}.rpm"
|
||||
_fallback_re="^openshell-gateway-[0-9].*\\.${_arch}\\.rpm$"
|
||||
;;
|
||||
*)
|
||||
error "unknown RPM package selector: ${_package}"
|
||||
;;
|
||||
esac
|
||||
|
||||
awk -v dev_name="$_dev_name" -v fallback_re="$_fallback_re" '
|
||||
{
|
||||
name = $2
|
||||
sub("^\\*", "", name)
|
||||
|
||||
if (name == dev_name) {
|
||||
selected = name
|
||||
found = 1
|
||||
exit
|
||||
}
|
||||
|
||||
if (fallback == "" && name ~ fallback_re) {
|
||||
fallback = name
|
||||
}
|
||||
}
|
||||
END {
|
||||
if (found) {
|
||||
print selected
|
||||
} else if (fallback != "") {
|
||||
print fallback
|
||||
}
|
||||
}
|
||||
' "$_checksums"
|
||||
}
|
||||
|
||||
verify_checksum() {
|
||||
_archive="$1"
|
||||
_checksums="$2"
|
||||
_filename="$3"
|
||||
|
||||
if has_cmd sha256sum; then
|
||||
_expected="$(awk -v name="$_filename" '($2 == name || $2 == "*" name) { print $1; exit }' "$_checksums")"
|
||||
[ -n "$_expected" ] || error "no checksum entry found for ${_filename}"
|
||||
echo "$_expected $_archive" | sha256sum -c --quiet
|
||||
elif has_cmd shasum; then
|
||||
_expected="$(awk -v name="$_filename" '($2 == name || $2 == "*" name) { print $1; exit }' "$_checksums")"
|
||||
[ -n "$_expected" ] || error "no checksum entry found for ${_filename}"
|
||||
echo "$_expected $_archive" | shasum -a 256 -c --quiet
|
||||
else
|
||||
error "neither 'sha256sum' nor 'shasum' found; cannot verify download integrity"
|
||||
fi
|
||||
}
|
||||
|
||||
install_deb_package() {
|
||||
_deb_path="$1"
|
||||
|
||||
if has_cmd apt-get; then
|
||||
as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-o Dpkg::Options::=--force-confnew \
|
||||
"$_deb_path"
|
||||
elif has_cmd apt; then
|
||||
as_root env DEBIAN_FRONTEND=noninteractive apt install -y \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-o Dpkg::Options::=--force-confnew \
|
||||
"$_deb_path"
|
||||
else
|
||||
as_root dpkg --force-confdef --force-confnew -i "$_deb_path"
|
||||
fi
|
||||
}
|
||||
|
||||
install_rpm_packages() {
|
||||
if has_cmd dnf; then
|
||||
as_root dnf install -y "$@"
|
||||
elif has_cmd yum; then
|
||||
as_root yum install -y "$@"
|
||||
elif has_cmd zypper; then
|
||||
as_root zypper --non-interactive install --allow-unsigned-rpm "$@"
|
||||
elif has_cmd rpm; then
|
||||
warn "installing with rpm directly; dependencies must already be installed"
|
||||
as_root rpm -Uvh --replacepkgs "$@"
|
||||
else
|
||||
error "'dnf', 'yum', 'zypper', or 'rpm' is required to install RPM packages"
|
||||
fi
|
||||
}
|
||||
|
||||
homebrew_formula_path() {
|
||||
_tap="$1"
|
||||
_formula="$2"
|
||||
|
||||
if ! as_target_user brew tap-info "$_tap" >/dev/null 2>&1; then
|
||||
info "creating local Homebrew tap ${_tap}..."
|
||||
as_target_user brew tap-new --no-git "$_tap" >/dev/null
|
||||
fi
|
||||
|
||||
_tap_dir="$(as_target_user brew --repository "$_tap" 2>/dev/null || true)"
|
||||
[ -n "$_tap_dir" ] || error "could not locate Homebrew tap ${_tap}"
|
||||
|
||||
_formula_dir="${_tap_dir}/Formula"
|
||||
as_target_user mkdir -p "$_formula_dir"
|
||||
printf '%s/%s.rb\n' "$_formula_dir" "$_formula"
|
||||
}
|
||||
|
||||
patch_homebrew_formula() {
|
||||
_formula_file="$1"
|
||||
_patched_file="${_formula_file}.patched"
|
||||
|
||||
if grep -q 'entitlements.write <<~XML' "$_formula_file"; then
|
||||
info "patching Homebrew formula for idempotent postinstall..."
|
||||
sed 's/entitlements\.write <<~XML/entitlements.atomic_write <<~XML/' "$_formula_file" >"$_patched_file"
|
||||
mv "$_patched_file" "$_formula_file"
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
start_user_gateway() {
|
||||
info "restarting openshell-gateway user service as ${TARGET_USER}..."
|
||||
|
||||
if ! as_target_user systemctl --user daemon-reload; then
|
||||
info "could not reach the user systemd manager for ${TARGET_USER}"
|
||||
info "restart the gateway later with: systemctl --user enable openshell-gateway && systemctl --user restart openshell-gateway"
|
||||
info "then register it with: openshell gateway add https://127.0.0.1:17670 --local --name openshell"
|
||||
return 0
|
||||
fi
|
||||
|
||||
as_target_user systemctl --user enable openshell-gateway
|
||||
as_target_user systemctl --user restart openshell-gateway
|
||||
as_target_user systemctl --user is-active --quiet openshell-gateway
|
||||
|
||||
info "registering local gateway as ${TARGET_USER}..."
|
||||
register_local_gateway
|
||||
wait_for_local_gateway_listener
|
||||
wait_for_local_gateway_status
|
||||
}
|
||||
|
||||
wait_for_local_gateway_listener() {
|
||||
_timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}"
|
||||
_elapsed=0
|
||||
_last_output=""
|
||||
_probe_url="https://127.0.0.1:${LOCAL_GATEWAY_PORT}/"
|
||||
_mtls_dir="${TARGET_HOME}/.config/openshell/gateways/openshell/mtls"
|
||||
|
||||
info "waiting for local gateway listener to become reachable..."
|
||||
while [ "$_elapsed" -lt "$_timeout" ]; do
|
||||
if [ ! -f "${_mtls_dir}/ca.crt" ] || [ ! -f "${_mtls_dir}/tls.crt" ] || [ ! -f "${_mtls_dir}/tls.key" ]; then
|
||||
_last_output="mTLS client bundle is not ready under ${_mtls_dir}"
|
||||
elif _last_output="$(as_target_user curl -sS --max-time 2 --cacert "${_mtls_dir}/ca.crt" --cert "${_mtls_dir}/tls.crt" --key "${_mtls_dir}/tls.key" -o /dev/null "$_probe_url" 2>&1)"; then
|
||||
info "local gateway listener is reachable"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
_elapsed=$((_elapsed + 1))
|
||||
done
|
||||
|
||||
printf '%s\n' "$_last_output" >&2
|
||||
error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s"
|
||||
}
|
||||
|
||||
wait_for_local_gateway_status() {
|
||||
_timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}"
|
||||
_elapsed=0
|
||||
_status_output=""
|
||||
_register_bin="${OPENSHELL_REGISTER_BIN:-openshell}"
|
||||
|
||||
info "waiting for openshell status to report connected..."
|
||||
while [ "$_elapsed" -lt "$_timeout" ]; do
|
||||
if _status_output="$(as_target_user env NO_COLOR=1 "$_register_bin" status 2>&1)"; then
|
||||
case "$_status_output" in
|
||||
*"Version:"*)
|
||||
info "openshell status reports connected"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
sleep 1
|
||||
_elapsed=$((_elapsed + 1))
|
||||
done
|
||||
|
||||
printf '%s\n' "$_status_output" >&2
|
||||
error "openshell status did not report connected within ${_timeout}s"
|
||||
}
|
||||
|
||||
remove_local_gateway_registration() {
|
||||
[ -n "$TARGET_HOME" ] || error "cannot resolve home directory for ${TARGET_USER}"
|
||||
_config_dir="${TARGET_HOME}/.config/openshell"
|
||||
|
||||
# The install-dev gateway is a user service. Replace the CLI registration
|
||||
# directly instead of asking `gateway destroy` to tear down Docker resources.
|
||||
# shellcheck disable=SC2016
|
||||
as_target_user sh -c '
|
||||
config_dir=$1
|
||||
rm -rf "${config_dir}/gateways/local"
|
||||
mkdir -p "${config_dir}/gateways/openshell"
|
||||
rm -f \
|
||||
"${config_dir}/gateways/openshell/metadata.json" \
|
||||
"${config_dir}/gateways/openshell/edge_token" \
|
||||
"${config_dir}/gateways/openshell/cf_token" \
|
||||
"${config_dir}/gateways/openshell/oidc_token.json"
|
||||
active="${config_dir}/active_gateway"
|
||||
active_name="$(cat "$active" 2>/dev/null || true)"
|
||||
if [ "$active_name" = "local" ] || [ "$active_name" = "openshell" ]; then
|
||||
rm -f "$active"
|
||||
fi
|
||||
' sh "$_config_dir"
|
||||
}
|
||||
|
||||
register_local_gateway() {
|
||||
_register_bin="${OPENSHELL_REGISTER_BIN:-openshell}"
|
||||
|
||||
if _add_output="$(as_target_user "$_register_bin" gateway add "https://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name openshell 2>&1)"; then
|
||||
[ -z "$_add_output" ] || print_gateway_add_output "$_add_output"
|
||||
return 0
|
||||
else
|
||||
_add_status=$?
|
||||
fi
|
||||
|
||||
case "$_add_output" in
|
||||
*"already exists"*)
|
||||
info "local gateway already exists; removing and re-adding it..."
|
||||
remove_local_gateway_registration
|
||||
as_target_user "$_register_bin" gateway add "https://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name openshell
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$_add_output" >&2
|
||||
return "$_add_status"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
print_gateway_add_output() {
|
||||
printf '%s\n' "$1" | while IFS= read -r _line; do
|
||||
case "$_line" in
|
||||
*"Gateway is not reachable at https://127.0.0.1:${LOCAL_GATEWAY_PORT}"*) ;;
|
||||
*"Verify the gateway is running and the endpoint is correct."*) ;;
|
||||
*) printf '%s\n' "$_line" >&2 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
install_linux_deb() {
|
||||
check_linux_deb_platform
|
||||
set_linux_target_runtime_dir
|
||||
|
||||
_arch="$(get_deb_arch)"
|
||||
_tmpdir="$(mktemp -d)"
|
||||
chmod 0755 "$_tmpdir"
|
||||
trap 'rm -rf "$_tmpdir"' EXIT
|
||||
|
||||
_checksums_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/${CHECKSUMS_NAME}"
|
||||
info "downloading ${RELEASE_TAG} release checksums..."
|
||||
download "$_checksums_url" "${_tmpdir}/${CHECKSUMS_NAME}" || {
|
||||
error "failed to download ${_checksums_url}"
|
||||
}
|
||||
|
||||
_deb_file="$(find_deb_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch")"
|
||||
if [ -z "$_deb_file" ]; then
|
||||
error "no Debian package found for architecture: ${_arch}"
|
||||
fi
|
||||
|
||||
_deb_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/${_deb_file}"
|
||||
_deb_path="${_tmpdir}/${_deb_file}"
|
||||
|
||||
info "selected ${_deb_file}"
|
||||
|
||||
info "downloading ${_deb_file}..."
|
||||
download_release_asset "$RELEASE_TAG" "$_deb_file" "$_deb_path" || {
|
||||
error "failed to download ${_deb_url}"
|
||||
}
|
||||
chmod 0644 "$_deb_path"
|
||||
|
||||
info "verifying checksum..."
|
||||
verify_checksum "$_deb_path" "${_tmpdir}/${CHECKSUMS_NAME}" "$_deb_file"
|
||||
|
||||
info "installing ${_deb_file}..."
|
||||
install_deb_package "$_deb_path"
|
||||
info "installed ${APP_NAME} package from ${RELEASE_TAG}"
|
||||
start_user_gateway
|
||||
}
|
||||
|
||||
install_linux_rpm() {
|
||||
require_cmd rpm
|
||||
set_linux_target_runtime_dir
|
||||
|
||||
_arch="$(get_rpm_arch)"
|
||||
_tmpdir="$(mktemp -d)"
|
||||
chmod 0755 "$_tmpdir"
|
||||
trap 'rm -rf "$_tmpdir"' EXIT
|
||||
|
||||
_checksums_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/${CHECKSUMS_NAME}"
|
||||
info "downloading ${RELEASE_TAG} release checksums..."
|
||||
download "$_checksums_url" "${_tmpdir}/${CHECKSUMS_NAME}" || {
|
||||
error "failed to download ${_checksums_url}"
|
||||
}
|
||||
|
||||
_rpm_file="$(find_rpm_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch" openshell)"
|
||||
if [ -z "$_rpm_file" ]; then
|
||||
error "no openshell RPM package found for architecture: ${_arch}"
|
||||
fi
|
||||
|
||||
_gateway_rpm_file="$(find_rpm_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch" openshell-gateway)"
|
||||
if [ -z "$_gateway_rpm_file" ]; then
|
||||
error "no openshell-gateway RPM package found for architecture: ${_arch}"
|
||||
fi
|
||||
|
||||
info "selected ${_rpm_file} and ${_gateway_rpm_file}"
|
||||
|
||||
for _package_file in "$_rpm_file" "$_gateway_rpm_file"; do
|
||||
_package_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/${_package_file}"
|
||||
_package_path="${_tmpdir}/${_package_file}"
|
||||
|
||||
info "downloading ${_package_file}..."
|
||||
download_release_asset "$RELEASE_TAG" "$_package_file" "$_package_path" || {
|
||||
error "failed to download ${_package_url}"
|
||||
}
|
||||
chmod 0644 "$_package_path"
|
||||
|
||||
info "verifying checksum for ${_package_file}..."
|
||||
verify_checksum "$_package_path" "${_tmpdir}/${CHECKSUMS_NAME}" "$_package_file"
|
||||
done
|
||||
|
||||
info "installing ${_rpm_file} and ${_gateway_rpm_file}..."
|
||||
install_rpm_packages "${_tmpdir}/${_rpm_file}" "${_tmpdir}/${_gateway_rpm_file}"
|
||||
info "installed ${APP_NAME} RPM packages from ${RELEASE_TAG}"
|
||||
start_user_gateway
|
||||
}
|
||||
|
||||
install_macos_homebrew() {
|
||||
check_macos_platform
|
||||
|
||||
_tmpdir="$(mktemp -d)"
|
||||
chmod 0755 "$_tmpdir"
|
||||
trap 'rm -rf "$_tmpdir"' EXIT
|
||||
|
||||
_formula_file="${_tmpdir}/openshell.rb"
|
||||
_formula_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/openshell.rb"
|
||||
|
||||
info "downloading Homebrew formula from ${_formula_url}..."
|
||||
download_release_asset "$RELEASE_TAG" "openshell.rb" "$_formula_file" || {
|
||||
error "failed to download ${_formula_url}; the selected release may not include a Homebrew formula"
|
||||
}
|
||||
chmod 0644 "$_formula_file"
|
||||
patch_homebrew_formula "$_formula_file"
|
||||
|
||||
_tap_formula_file="$(homebrew_formula_path "$HOMEBREW_TAP" "$HOMEBREW_FORMULA_NAME")"
|
||||
info "staging Homebrew formula in tap ${HOMEBREW_TAP}..."
|
||||
cp "$_formula_file" "$_tap_formula_file"
|
||||
chmod 0644 "$_tap_formula_file"
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
chown "$TARGET_USER" "$_tap_formula_file" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
_formula_ref="${HOMEBREW_TAP}/${HOMEBREW_FORMULA_NAME}"
|
||||
|
||||
if as_target_user brew list --formula openshell >/dev/null 2>&1; then
|
||||
info "reinstalling OpenShell with Homebrew..."
|
||||
as_target_user brew reinstall --formula "$_formula_ref"
|
||||
else
|
||||
info "installing OpenShell with Homebrew..."
|
||||
as_target_user brew install --formula "$_formula_ref"
|
||||
fi
|
||||
|
||||
info "restarting OpenShell Homebrew service..."
|
||||
if ! as_target_user brew services restart "$_formula_ref"; then
|
||||
warn "could not restart the OpenShell Homebrew service"
|
||||
info "restart it later with: brew services restart ${_formula_ref}"
|
||||
info "then register it with: openshell gateway add https://127.0.0.1:${LOCAL_GATEWAY_PORT} --local --name openshell"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_brew_prefix="$(as_target_user brew --prefix 2>/dev/null || true)"
|
||||
if [ -n "$_brew_prefix" ] && [ -x "${_brew_prefix}/bin/openshell" ]; then
|
||||
OPENSHELL_REGISTER_BIN="${_brew_prefix}/bin/openshell"
|
||||
fi
|
||||
|
||||
info "registering local gateway as ${TARGET_USER}..."
|
||||
register_local_gateway
|
||||
wait_for_local_gateway_listener
|
||||
wait_for_local_gateway_status
|
||||
}
|
||||
|
||||
main() {
|
||||
# Parse CLI flags
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
if [ "$#" -gt 0 ]; then
|
||||
case "$1" in
|
||||
--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
error "unknown option: $arg"
|
||||
error "unknown option: $1"
|
||||
;;
|
||||
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"
|
||||
_install_dir="$(get_install_dir)"
|
||||
|
||||
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 (mandatory — never skip)
|
||||
info "verifying checksum..."
|
||||
if ! download "$_checksums_url" "${_tmpdir}/checksums.txt"; then
|
||||
error "failed to download checksums file from ${_checksums_url}"
|
||||
fi
|
||||
if ! verify_checksum "${_tmpdir}/${_filename}" "${_tmpdir}/checksums.txt" "$_filename"; then
|
||||
error "checksum verification failed for ${_filename}"
|
||||
fi
|
||||
require_cmd curl
|
||||
RELEASE_TAG="$(resolve_release_tag)"
|
||||
PLATFORM="$(detect_platform)"
|
||||
|
||||
# Extract
|
||||
info "extracting..."
|
||||
tar -xzf "${_tmpdir}/${_filename}" -C "${_tmpdir}" --no-same-owner --no-same-permissions "${APP_NAME}"
|
||||
TARGET_USER="$(target_user)"
|
||||
TARGET_UID="$(id -u "$TARGET_USER" 2>/dev/null || true)"
|
||||
[ -n "$TARGET_UID" ] || error "cannot resolve uid for ${TARGET_USER}"
|
||||
TARGET_HOME="$(user_home "$TARGET_USER")"
|
||||
|
||||
# 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 ${_installed_version} to ${_install_dir}/${APP_NAME}"
|
||||
|
||||
# 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
|
||||
case "$PLATFORM" in
|
||||
linux)
|
||||
case "$(linux_package_method)" in
|
||||
deb)
|
||||
install_linux_deb
|
||||
;;
|
||||
rpm)
|
||||
install_linux_rpm
|
||||
;;
|
||||
*)
|
||||
error "unsupported Linux package method"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
darwin)
|
||||
install_macos_homebrew
|
||||
;;
|
||||
*)
|
||||
error "unsupported platform: ${PLATFORM}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user