mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-29 07:23:25 +08:00
* fix(installer): resolve the gateway config dir like the CLI does install.sh hardcoded $TARGET_HOME/.config/openshell in two places: the readiness check, which reads the client certificates it expects the gateway registration to have written, and the cleanup that removes a stale openshell entry before re-adding it. The CLI instead keeps both under $XDG_CONFIG_HOME/openshell whenever that variable is set, so with the variable pointing anywhere other than ~/.config the installer and the CLI looked in different directories: the readiness check waited for certificates that were never going to appear there, and the cleanup left the real stale entry in place. Resolve the directory through as_target_user instead, which answers from the same environment the CLI is invoked in. That covers both branches without duplicating their logic: the same-user branch inherits the caller's XDG_CONFIG_HOME, while the sudo and runuser branches reset the environment and fall back to the target user's home. An empty XDG_CONFIG_HOME falls back to $HOME/.config, following the XDG base directory specification. The CLI diverges here: xdg_config_dir treats a set-but-empty value as set and joins onto it, yielding the relative path "openshell", which no installer path can usefully match. Worth fixing separately in the CLI. Signed-off-by: fede-kamel <fkamelhar@gmail.com> * fix(core): treat an empty XDG_CONFIG_HOME as unset xdg_config_dir() joined onto an empty XDG_CONFIG_HOME and resolved the config root to the relative path "openshell", while the installer falls back to $HOME/.config/openshell for the same value. Registration runs the CLI with the caller's environment, so the two could disagree on where the gateway entry lives. Treat an empty value as unset, as the XDG base directory spec requires, so the CLI and installer resolve the same directory. Add tests for a value with spaces and drive registration cleanup through the helper for the spaces and empty cases. Signed-off-by: fede-kamel <fkamelhar@gmail.com> --------- Signed-off-by: fede-kamel <fkamelhar@gmail.com>
1654 lines
51 KiB
Bash
Executable File
1654 lines
51 KiB
Bash
Executable File
#!/bin/sh
|
|
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# Install OpenShell 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:-}"
|
|
RELEASE_ASSET_DIR=""
|
|
CHECKSUMS_NAME="openshell-checksums-sha256.txt"
|
|
LOCAL_GATEWAY_PORT="17670"
|
|
HOMEBREW_TAP="nvidia/openshell"
|
|
HOMEBREW_FORMULA_NAME="openshell"
|
|
HOMEBREW_CLI_ASSET="openshell-aarch64-apple-darwin.tar.gz"
|
|
HOMEBREW_GATEWAY_ASSET="openshell-gateway-aarch64-apple-darwin.tar.gz"
|
|
HOMEBREW_DRIVER_VM_ASSET="openshell-driver-vm-aarch64-apple-darwin.tar.gz"
|
|
HOMEBREW_PROVER_ASSET="openshell-prover-aarch64-apple-darwin.tar.gz"
|
|
BREAKING_RELEASE_VERSION="0.0.37"
|
|
LINUX_PACKAGE_GLIBC_MIN_VERSION="2.28"
|
|
UPGRADE_NOTICE_ACK="${OPENSHELL_ACK_BREAKING_UPGRADE:-}"
|
|
|
|
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.sh - Install OpenShell
|
|
|
|
USAGE:
|
|
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
|
|
|
|
ENVIRONMENT VARIABLES:
|
|
OPENSHELL_VERSION Release tag to install (default: latest tagged release).
|
|
Set OPENSHELL_VERSION=dev to install the rolling dev build.
|
|
Set OPENSHELL_VERSION=pre to install the latest prerelease.
|
|
Prereleases require an authenticated GitHub CLI session.
|
|
OPENSHELL_ACK_BREAKING_UPGRADE
|
|
Set to 1 only after backing up and cleaning up a
|
|
pre-v0.0.37 or non-snap installation.
|
|
OPENSHELL_INSTALL_METHOD
|
|
Linux package to install: snap, deb, or rpm. Unset
|
|
selects deb or rpm from the host package manager.
|
|
|
|
NOTES:
|
|
When OPENSHELL_VERSION is unset, this resolves the latest tagged release
|
|
from ${GITHUB_URL}/releases/latest.
|
|
|
|
Linux installs the Debian package on amd64/arm64 or the RPM packages on
|
|
x86_64/aarch64, depending on the host package manager. Set
|
|
OPENSHELL_INSTALL_METHOD=snap to install the OpenShell snap instead; hosts
|
|
that already have the OpenShell snap keep refreshing it. snap installs use
|
|
latest/stable by default and latest/edge for dev, and do not support
|
|
explicit release tags or prereleases. The OpenShell snap requires a running
|
|
Docker Engine installed from a system package or Docker's package
|
|
repository. The Docker snap is not currently compatible with OpenShell.
|
|
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"
|
|
}
|
|
|
|
semver_core() {
|
|
_version="${1#v}"
|
|
_version="${_version%%[-+]*}"
|
|
printf '%s\n' "$_version"
|
|
}
|
|
|
|
semver_at_least() {
|
|
_version="$(semver_core "$1")"
|
|
_minimum="$(semver_core "$2")"
|
|
|
|
_major="${_version%%.*}"
|
|
_rest="${_version#*.}"
|
|
[ "$_rest" != "$_version" ] || return 1
|
|
_minor="${_rest%%.*}"
|
|
_patch="${_rest#*.}"
|
|
_patch="${_patch%%.*}"
|
|
|
|
_min_major="${_minimum%%.*}"
|
|
_min_rest="${_minimum#*.}"
|
|
[ "$_min_rest" != "$_minimum" ] || return 1
|
|
_min_minor="${_min_rest%%.*}"
|
|
_min_patch="${_min_rest#*.}"
|
|
_min_patch="${_min_patch%%.*}"
|
|
|
|
case "$_major:$_minor:$_patch:$_min_major:$_min_minor:$_min_patch" in
|
|
*[!0-9:]* | *::*)
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
[ "$_major" -gt "$_min_major" ] && return 0
|
|
[ "$_major" -lt "$_min_major" ] && return 1
|
|
[ "$_minor" -gt "$_min_minor" ] && return 0
|
|
[ "$_minor" -lt "$_min_minor" ] && return 1
|
|
[ "$_patch" -ge "$_min_patch" ]
|
|
}
|
|
|
|
version_at_least_major_minor() {
|
|
_version="$1"
|
|
_minimum="$2"
|
|
|
|
_major="${_version%%.*}"
|
|
_minor="${_version#*.}"
|
|
_minor="${_minor%%.*}"
|
|
|
|
_min_major="${_minimum%%.*}"
|
|
_min_minor="${_minimum#*.}"
|
|
_min_minor="${_min_minor%%.*}"
|
|
|
|
case "$_major:$_minor:$_min_major:$_min_minor" in
|
|
*[!0-9:]* | *::*)
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
[ "$_major" -gt "$_min_major" ] && return 0
|
|
[ "$_major" -lt "$_min_major" ] && return 1
|
|
[ "$_minor" -ge "$_min_minor" ]
|
|
}
|
|
|
|
getconf_gnu_libc_version() {
|
|
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ] && [ "${OPENSHELL_TEST_GETCONF_UNAVAILABLE:-0}" = "1" ]; then
|
|
return 127
|
|
fi
|
|
|
|
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ] && [ "${OPENSHELL_TEST_GETCONF_OUTPUT+x}" = "x" ]; then
|
|
printf '%s\n' "$OPENSHELL_TEST_GETCONF_OUTPUT"
|
|
return 0
|
|
fi
|
|
|
|
getconf GNU_LIBC_VERSION 2>/dev/null
|
|
}
|
|
|
|
ldd_version_output() {
|
|
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ] && [ "${OPENSHELL_TEST_LDD_UNAVAILABLE:-0}" = "1" ]; then
|
|
return 127
|
|
fi
|
|
|
|
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ] && [ "${OPENSHELL_TEST_LDD_OUTPUT+x}" = "x" ]; then
|
|
printf '%s\n' "$OPENSHELL_TEST_LDD_OUTPUT"
|
|
return 0
|
|
fi
|
|
|
|
ldd --version 2>&1
|
|
}
|
|
|
|
detect_glibc_version() {
|
|
_ldd_output="$(ldd_version_output 2>&1 || true)"
|
|
case "$_ldd_output" in
|
|
*[Mm][Uu][Ss][Ll]* | *[Aa][Ll][Pp][Ii][Nn][Ee]*)
|
|
return 2
|
|
;;
|
|
esac
|
|
|
|
_ldd_version="$(printf '%s\n' "$_ldd_output" | awk 'FNR == 1 && match($NF, /^[0-9]+\.[0-9]+/) { print substr($NF, RSTART, RLENGTH); exit }')"
|
|
if [ -n "$_ldd_version" ]; then
|
|
printf '%s\n' "$_ldd_version"
|
|
return 0
|
|
fi
|
|
|
|
_getconf_output="$(getconf_gnu_libc_version 2>/dev/null || true)"
|
|
case "$_getconf_output" in
|
|
glibc\ [0-9]*.[0-9]*)
|
|
printf '%s\n' "${_getconf_output#glibc }"
|
|
return 0
|
|
;;
|
|
*[Mm][Uu][Ss][Ll]* | *[Aa][Ll][Pp][Ii][Nn][Ee]*)
|
|
return 2
|
|
;;
|
|
esac
|
|
|
|
return 1
|
|
}
|
|
|
|
require_linux_package_glibc() {
|
|
_glibc_status=0
|
|
_glibc_version="$(detect_glibc_version)" || _glibc_status=$?
|
|
|
|
case "$_glibc_status" in
|
|
0)
|
|
;;
|
|
2)
|
|
error "OpenShell Linux packages require glibc >= ${LINUX_PACKAGE_GLIBC_MIN_VERSION}; detected musl or unsupported libc."
|
|
;;
|
|
*)
|
|
error "OpenShell Linux packages require glibc >= ${LINUX_PACKAGE_GLIBC_MIN_VERSION}; could not detect glibc."
|
|
;;
|
|
esac
|
|
|
|
if ! version_at_least_major_minor "$_glibc_version" "$LINUX_PACKAGE_GLIBC_MIN_VERSION"; then
|
|
error "OpenShell Linux packages require glibc >= ${LINUX_PACKAGE_GLIBC_MIN_VERSION}; detected glibc ${_glibc_version}.
|
|
Please use a newer distribution or container environment."
|
|
fi
|
|
}
|
|
|
|
target_uses_breaking_gateway_model() {
|
|
case "$RELEASE_TAG" in
|
|
dev)
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
semver_at_least "$RELEASE_TAG" "$BREAKING_RELEASE_VERSION"
|
|
}
|
|
|
|
installed_version_needs_breaking_upgrade_notice() {
|
|
_version="$1"
|
|
|
|
if [ -z "$_version" ]; then
|
|
return 0
|
|
fi
|
|
|
|
! semver_at_least "$_version" "$BREAKING_RELEASE_VERSION"
|
|
}
|
|
|
|
find_existing_native_openshell_bin() {
|
|
_path="$(command -v openshell 2>/dev/null || true)"
|
|
case "$_path" in
|
|
/snap/*) ;;
|
|
*)
|
|
if [ -n "$_path" ] && [ -x "$_path" ]; then
|
|
printf '%s\n' "$_path"
|
|
return 0
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
for _candidate in \
|
|
"${TARGET_HOME:-}/.local/bin/openshell" \
|
|
/usr/local/bin/openshell \
|
|
/usr/bin/openshell \
|
|
/opt/homebrew/bin/openshell; do
|
|
if [ -n "$_candidate" ] && [ -x "$_candidate" ]; then
|
|
printf '%s\n' "$_candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
existing_openshell_version() {
|
|
_bin="$1"
|
|
_output="$("$_bin" --version 2>/dev/null | sed -n '1p' || true)"
|
|
printf '%s\n' "$_output" | awk '
|
|
{
|
|
for (i = 1; i <= NF; i++) {
|
|
if ($i ~ /^v?[0-9]+\.[0-9]+\.[0-9]+([-+][A-Za-z0-9.+~-]+)?$/) {
|
|
print $i
|
|
exit
|
|
}
|
|
}
|
|
}
|
|
'
|
|
}
|
|
|
|
print_breaking_upgrade_notice() {
|
|
_bin="$1"
|
|
_version="$2"
|
|
|
|
if [ -n "$_version" ]; then
|
|
warn "detected existing OpenShell ${_version} at ${_bin}"
|
|
else
|
|
warn "detected an existing OpenShell installation at ${_bin}"
|
|
fi
|
|
|
|
cat >&2 <<EOF
|
|
|
|
OpenShell ${BREAKING_RELEASE_VERSION} and later are incompatible with gateway
|
|
state created by earlier releases. Before installing OpenShell ${RELEASE_TAG}, back up
|
|
any files, artifacts, and configuration you need from existing sandboxes.
|
|
|
|
Then clean up the old runtime with the currently installed CLI:
|
|
|
|
openshell sandbox delete --all
|
|
openshell gateway destroy
|
|
|
|
Run these commands before upgrading because 'openshell gateway destroy' is not
|
|
available in OpenShell ${BREAKING_RELEASE_VERSION} and later.
|
|
|
|
After cleanup, rerun this installer or follow the installation guide:
|
|
|
|
https://docs.nvidia.com/openshell/latest/about/installation
|
|
|
|
If you have already backed up and cleaned up the old runtime, rerun with:
|
|
|
|
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_ACK_BREAKING_UPGRADE=1 sh
|
|
|
|
EOF
|
|
}
|
|
|
|
guard_breaking_upgrade() {
|
|
target_uses_breaking_gateway_model || return 0
|
|
|
|
_bin="$(find_existing_native_openshell_bin || true)"
|
|
[ -n "$_bin" ] || return 0
|
|
|
|
_version="$(existing_openshell_version "$_bin")"
|
|
installed_version_needs_breaking_upgrade_notice "$_version" || return 0
|
|
|
|
print_breaking_upgrade_notice "$_bin" "$_version"
|
|
|
|
if [ "$UPGRADE_NOTICE_ACK" = "1" ]; then
|
|
warn "continuing because OPENSHELL_ACK_BREAKING_UPGRADE=1 is set"
|
|
return 0
|
|
fi
|
|
|
|
error "manual cleanup is required before upgrading from this OpenShell installation"
|
|
}
|
|
|
|
print_native_to_snap_notice() {
|
|
_bin="$1"
|
|
_version="$2"
|
|
|
|
if [ -n "$_version" ]; then
|
|
warn "detected existing non-snap OpenShell ${_version} at ${_bin}"
|
|
else
|
|
warn "detected an existing non-snap OpenShell installation at ${_bin}"
|
|
fi
|
|
|
|
cat >&2 <<EOF
|
|
|
|
The OpenShell snap keeps gateway and CLI state in snap-specific directories and
|
|
does not import state from a non-snap installation. Before installing the snap,
|
|
back up anything you need and clean up sandboxes and runtime resources managed
|
|
by the existing installation.
|
|
|
|
For older installations that provide these commands, run:
|
|
|
|
${_bin} sandbox delete --all
|
|
${_bin} gateway destroy
|
|
|
|
Stop the non-snap gateway service and remove the native package or manual
|
|
installation before continuing. For package and service instructions, see:
|
|
|
|
https://docs.nvidia.com/openshell/latest/about/installation
|
|
|
|
If you have already backed up and cleaned up the non-snap installation, rerun with:
|
|
|
|
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_ACK_BREAKING_UPGRADE=1 sh
|
|
|
|
EOF
|
|
}
|
|
|
|
guard_native_to_snap_transition() {
|
|
_bin="$(find_existing_native_openshell_bin || true)"
|
|
[ -n "$_bin" ] || return 0
|
|
|
|
_version="$(existing_openshell_version "$_bin")"
|
|
print_native_to_snap_notice "$_bin" "$_version"
|
|
|
|
if [ "$UPGRADE_NOTICE_ACK" = "1" ]; then
|
|
warn "continuing because OPENSHELL_ACK_BREAKING_UPGRADE=1 is set"
|
|
return 0
|
|
fi
|
|
|
|
error "manual cleanup is required before replacing this non-snap OpenShell installation"
|
|
}
|
|
|
|
resolve_release_tag() {
|
|
if [ "${OPENSHELL_VERSION:-}" = "pre" ]; then
|
|
resolve_latest_prerelease_tag
|
|
return 0
|
|
fi
|
|
|
|
if [ -n "${OPENSHELL_VERSION:-}" ]; then
|
|
echo "$OPENSHELL_VERSION"
|
|
return 0
|
|
fi
|
|
|
|
info "resolving latest version..."
|
|
_latest_url="${GITHUB_URL}/releases/latest"
|
|
_resolved="$(curl -fLsS -o /dev/null -w '%{url_effective}' "$_latest_url")" || {
|
|
error "failed to resolve latest release from ${_latest_url}"
|
|
}
|
|
|
|
case "$_resolved" in
|
|
https://github.com/${REPO}/releases/*)
|
|
;;
|
|
*)
|
|
error "unexpected redirect target: ${_resolved} (expected https://github.com/${REPO}/releases/...)"
|
|
;;
|
|
esac
|
|
|
|
_version="${_resolved##*/}"
|
|
if [ -z "$_version" ] || [ "$_version" = "latest" ]; then
|
|
error "could not determine latest release version (resolved URL: ${_resolved})"
|
|
fi
|
|
|
|
echo "$_version"
|
|
}
|
|
|
|
resolve_latest_prerelease_tag() {
|
|
require_prerelease_github_access
|
|
|
|
info "resolving latest prerelease..."
|
|
_artifact_platform="$(prerelease_artifact_platform)"
|
|
_release_tags="$(gh api "repos/${REPO}/git/matching-refs/tags/v" --jq '
|
|
[.[].ref | sub("^refs/tags/"; "") |
|
|
select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+-pre\\.[1-9][0-9]*$"))] |
|
|
sort_by(split("-pre.") as $parts |
|
|
($parts[0] | ltrimstr("v") | split(".") | map(tonumber)) +
|
|
[($parts[1] | tonumber)]) | reverse | .[]')" || {
|
|
error "failed to list prerelease tags"
|
|
}
|
|
|
|
for _tag in $_release_tags; do
|
|
_artifact_name="openshell-${_tag}-${_artifact_platform}"
|
|
info "checking ${_tag} for ${_artifact_platform}..."
|
|
_run_ids="$(gh api \
|
|
"repos/${REPO}/actions/artifacts?name=${_artifact_name}&per_page=100" \
|
|
--jq '[.artifacts[] | select(.expired == false)] |
|
|
sort_by(.created_at) | reverse | .[] | .workflow_run.id')" || {
|
|
error "failed to find prerelease artifact ${_artifact_name}"
|
|
}
|
|
|
|
for _run_id in $_run_ids; do
|
|
# Packages are published before qualification; a failed or running
|
|
# qualification must not hide an already-published candidate.
|
|
_release_workflow="$(gh api "repos/${REPO}/actions/runs/${_run_id}" --jq '
|
|
(.path | split("@")[0]) |
|
|
. == ".github/workflows/release-prerelease.yml" or
|
|
. == ".github/workflows/release-tag.yml"')" || {
|
|
error "failed to check prerelease workflow run ${_run_id}"
|
|
}
|
|
if [ "$_release_workflow" = "true" ]; then
|
|
printf '%s\n' "$_tag"
|
|
return 0
|
|
fi
|
|
done
|
|
done
|
|
|
|
error "no unexpired prerelease artifacts found"
|
|
}
|
|
|
|
is_prerelease_tag() {
|
|
printf '%s\n' "$1" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+-pre\.[1-9][0-9]*$'
|
|
}
|
|
|
|
require_prerelease_github_access() {
|
|
require_cmd gh
|
|
if ! gh auth status --hostname github.com >/dev/null 2>&1; then
|
|
error "GitHub CLI authentication is required for prerelease artifacts; run 'gh auth login' and try again"
|
|
fi
|
|
}
|
|
|
|
prerelease_artifact_platform() {
|
|
case "${PLATFORM}:$(uname -m)" in
|
|
linux:x86_64 | linux:amd64)
|
|
case "$(linux_package_method)" in
|
|
deb) echo "linux-amd64-deb" ;;
|
|
rpm) echo "linux-x86_64-rpm" ;;
|
|
esac
|
|
;;
|
|
linux:aarch64 | linux:arm64)
|
|
case "$(linux_package_method)" in
|
|
deb) echo "linux-arm64-deb" ;;
|
|
rpm) echo "linux-aarch64-rpm" ;;
|
|
esac
|
|
;;
|
|
darwin:arm64 | darwin:aarch64) echo "macos-arm64" ;;
|
|
*) error "no prerelease artifact is available for ${PLATFORM}/$(uname -m)" ;;
|
|
esac
|
|
}
|
|
|
|
prepare_prerelease_assets() {
|
|
_destination="$1"
|
|
is_prerelease_tag "$RELEASE_TAG" || return 0
|
|
require_prerelease_github_access
|
|
|
|
_artifact_name="openshell-${RELEASE_TAG}-$(prerelease_artifact_platform)"
|
|
info "locating ${_artifact_name}..."
|
|
_run_id="$(gh api \
|
|
"repos/${REPO}/actions/artifacts?name=${_artifact_name}&per_page=100" \
|
|
--jq '[.artifacts[] | select(.expired == false)] | sort_by(.created_at) | last | .workflow_run.id')" || {
|
|
error "failed to find prerelease artifact ${_artifact_name}"
|
|
}
|
|
if [ -z "$_run_id" ] || [ "$_run_id" = "null" ] || ! printf '%s\n' "$_run_id" | grep -Eq '^[0-9]+$'; then
|
|
error "no unexpired prerelease artifact found for ${RELEASE_TAG} on this platform"
|
|
fi
|
|
|
|
RELEASE_ASSET_DIR="${_destination}/release"
|
|
info "downloading ${_artifact_name}..."
|
|
gh run download "$_run_id" \
|
|
--repo "$REPO" \
|
|
--name "$_artifact_name" \
|
|
--dir "$RELEASE_ASSET_DIR" || {
|
|
error "failed to download prerelease artifact ${_artifact_name}"
|
|
}
|
|
}
|
|
|
|
download_release_asset() {
|
|
_tag="$1"
|
|
_filename="$2"
|
|
_output="$3"
|
|
|
|
if [ -n "$RELEASE_ASSET_DIR" ]; then
|
|
_source="${RELEASE_ASSET_DIR}/${_filename}"
|
|
if [ -f "$_source" ]; then
|
|
cp "$_source" "$_output"
|
|
return 0
|
|
fi
|
|
return 1
|
|
fi
|
|
|
|
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}"
|
|
}
|
|
|
|
# Config dir the openshell CLI will use, resolved in the same environment the CLI
|
|
# runs in. The CLI takes $XDG_CONFIG_HOME when set and $HOME/.config otherwise,
|
|
# so asking through as_target_user covers both cases: the same-user branch
|
|
# inherits the caller's XDG_CONFIG_HOME, while the sudo and runuser branches
|
|
# reset the environment and fall back to the target user's home.
|
|
#
|
|
# An empty XDG_CONFIG_HOME falls back to $HOME/.config, per the XDG base
|
|
# directory spec. openshell_core::paths::xdg_config_dir() does the same, so the
|
|
# installer and the CLI agree on every value.
|
|
target_openshell_config_dir() {
|
|
# shellcheck disable=SC2016
|
|
as_target_user sh -c 'printf "%s\n" "${XDG_CONFIG_HOME:-$HOME/.config}/openshell"'
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
local_gateway_endpoint() {
|
|
case "${PLATFORM:-$(detect_platform)}" in
|
|
darwin)
|
|
printf 'https://localhost:%s\n' "$LOCAL_GATEWAY_PORT"
|
|
;;
|
|
*)
|
|
printf 'https://127.0.0.1:%s\n' "$LOCAL_GATEWAY_PORT"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
linux_package_method() {
|
|
case "${OPENSHELL_INSTALL_METHOD:-}" in
|
|
snap | deb | rpm)
|
|
echo "$OPENSHELL_INSTALL_METHOD"
|
|
return 0
|
|
;;
|
|
'') ;;
|
|
*) error "unsupported OPENSHELL_INSTALL_METHOD=${OPENSHELL_INSTALL_METHOD}; use snap, deb, or rpm" ;;
|
|
esac
|
|
|
|
# Keep refreshing an existing snap install instead of adding a second
|
|
# gateway on the same port.
|
|
case "${OPENSHELL_VERSION:-}" in
|
|
'' | dev)
|
|
if has_cmd snap && snap list openshell >/dev/null 2>&1; then
|
|
echo "snap"
|
|
return 0
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
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$"
|
|
;;
|
|
openshell-prover)
|
|
_dev_name="openshell-prover-dev-${_arch}.rpm"
|
|
_fallback_re="^openshell-prover-[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
|
|
|
|
}
|
|
|
|
patch_prerelease_homebrew_formula_urls() {
|
|
_formula_file="$1"
|
|
[ -n "$RELEASE_ASSET_DIR" ] || return 0
|
|
|
|
for _asset in "$HOMEBREW_CLI_ASSET" "$HOMEBREW_GATEWAY_ASSET" "$HOMEBREW_DRIVER_VM_ASSET" "$HOMEBREW_PROVER_ASSET"; do
|
|
if [ ! -f "${RELEASE_ASSET_DIR}/${_asset}" ]; then
|
|
error "prerelease artifact is missing the required macOS asset: ${_asset}"
|
|
fi
|
|
done
|
|
|
|
_release_asset_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}"
|
|
_local_asset_url="file://${RELEASE_ASSET_DIR}"
|
|
_patched_file="${_formula_file}.prerelease"
|
|
sed \
|
|
-e "s#${_release_asset_url}/${HOMEBREW_CLI_ASSET}#${_local_asset_url}/${HOMEBREW_CLI_ASSET}#g" \
|
|
-e "s#${_release_asset_url}/${HOMEBREW_GATEWAY_ASSET}#${_local_asset_url}/${HOMEBREW_GATEWAY_ASSET}#g" \
|
|
-e "s#${_release_asset_url}/${HOMEBREW_DRIVER_VM_ASSET}#${_local_asset_url}/${HOMEBREW_DRIVER_VM_ASSET}#g" \
|
|
-e "s#${_release_asset_url}/${HOMEBREW_PROVER_ASSET}#${_local_asset_url}/${HOMEBREW_PROVER_ASSET}#g" \
|
|
"$_formula_file" >"$_patched_file"
|
|
mv "$_patched_file" "$_formula_file"
|
|
}
|
|
|
|
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 user_gateway_service_failed
|
|
wait_for_local_gateway_status
|
|
}
|
|
|
|
# Succeeds when the gateway user service has failed or is waiting to restart
|
|
# after a failure. A unit that is starting or running does not match, even if
|
|
# it failed before it was restarted.
|
|
user_gateway_service_failed() {
|
|
_unit_state="$(as_target_user systemctl --user show openshell-gateway -p ActiveState -p SubState 2>/dev/null)" || return 1
|
|
case "$_unit_state" in
|
|
*ActiveState=failed* | *SubState=auto-restart*) return 0 ;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
dump_local_gateway_diagnostics() {
|
|
_lines="${OPENSHELL_INSTALL_LOG_LINES:-80}"
|
|
case "$_lines" in
|
|
"" | *[!0-9]*)
|
|
_lines=80
|
|
;;
|
|
esac
|
|
|
|
info "dumping recent local gateway diagnostics..."
|
|
case "${PLATFORM:-}" in
|
|
darwin)
|
|
dump_homebrew_gateway_diagnostics "$_lines"
|
|
;;
|
|
linux)
|
|
if [ "${LINUX_INSTALL_METHOD:-}" = "snap" ]; then
|
|
dump_snap_gateway_diagnostics "$_lines"
|
|
else
|
|
dump_user_service_gateway_diagnostics "$_lines"
|
|
fi
|
|
;;
|
|
*)
|
|
info "no gateway log collector is available for platform: ${PLATFORM:-unknown}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
dump_snap_gateway_diagnostics() {
|
|
_lines="$1"
|
|
|
|
info "OpenShell snap service status:"
|
|
as_root snap services openshell >&2 || true
|
|
info "OpenShell snap user service status for ${TARGET_USER}:"
|
|
as_target_user snap services openshell.user-gateway >&2 || true
|
|
info "OpenShell snap connections:"
|
|
snap connections openshell >&2 || true
|
|
if has_cmd journalctl; then
|
|
info "last ${_lines} lines from the legacy OpenShell snap gateway journal:"
|
|
journalctl -b -u snap.openshell.system-gateway.service --no-pager -n "$_lines" >&2 || true
|
|
info "last ${_lines} lines from the OpenShell snap user gateway journal:"
|
|
as_target_user journalctl --user -u snap.openshell.user-gateway --no-pager -n "$_lines" >&2 || true
|
|
fi
|
|
as_root snap logs openshell.system-gateway -n="$_lines" >&2 || true
|
|
as_target_user snap logs openshell.user-gateway -n="$_lines" >&2 || true
|
|
}
|
|
|
|
dump_homebrew_gateway_diagnostics() {
|
|
_lines="$1"
|
|
_brew_prefix="$(as_target_user brew --prefix 2>/dev/null || true)"
|
|
[ -n "$_brew_prefix" ] || _brew_prefix="/opt/homebrew"
|
|
|
|
info "Homebrew service status:"
|
|
as_target_user brew services info "${HOMEBREW_TAP}/${HOMEBREW_FORMULA_NAME}" >&2 || true
|
|
|
|
for _log_file in \
|
|
"${_brew_prefix}/var/log/openshell/openshell-gateway.err.log" \
|
|
"${_brew_prefix}/var/log/openshell/openshell-gateway.out.log"; do
|
|
if [ -f "$_log_file" ]; then
|
|
info "last ${_lines} lines from ${_log_file}:"
|
|
tail -n "$_lines" "$_log_file" >&2 || true
|
|
else
|
|
info "gateway log not found: ${_log_file}"
|
|
fi
|
|
done
|
|
}
|
|
|
|
dump_user_service_gateway_diagnostics() {
|
|
_lines="$1"
|
|
|
|
if has_cmd systemctl; then
|
|
info "openshell-gateway user service status:"
|
|
as_target_user systemctl --user status openshell-gateway --no-pager >&2 || true
|
|
fi
|
|
|
|
if has_cmd journalctl; then
|
|
info "last ${_lines} lines from openshell-gateway user journal:"
|
|
as_target_user journalctl --user -u openshell-gateway --no-pager -n "$_lines" >&2 || true
|
|
else
|
|
info "journalctl not found; cannot dump openshell-gateway user journal"
|
|
fi
|
|
}
|
|
|
|
# An optional command name stops the wait early when it succeeds, so a service
|
|
# that already failed does not run out the full timeout.
|
|
wait_for_local_gateway_listener() {
|
|
_failed_check="${1:-}"
|
|
_timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}"
|
|
_elapsed=0
|
|
_last_output=""
|
|
_service_failed=0
|
|
_probe_url="$(local_gateway_endpoint)/"
|
|
_mtls_dir="$(target_openshell_config_dir)/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
|
|
if [ -n "$_failed_check" ] && "$_failed_check"; then
|
|
_service_failed=1
|
|
break
|
|
fi
|
|
sleep 1
|
|
_elapsed=$((_elapsed + 1))
|
|
done
|
|
|
|
[ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2
|
|
dump_local_gateway_diagnostics
|
|
if [ "$_service_failed" -eq 1 ]; then
|
|
error "the openshell-gateway service failed to start; fix the cause shown above, then run: systemctl --user restart openshell-gateway"
|
|
fi
|
|
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
|
|
|
|
[ -z "$_status_output" ] || printf '%s\n' "$_status_output" >&2
|
|
dump_local_gateway_diagnostics
|
|
error "openshell status did not report connected within ${_timeout}s"
|
|
}
|
|
|
|
remove_local_gateway_registration_from() {
|
|
_config_dir="$1"
|
|
[ -n "$TARGET_HOME" ] || error "cannot resolve home directory for ${TARGET_USER}"
|
|
|
|
# Replace the CLI registration directly instead of asking `gateway destroy`
|
|
# to tear down package-managed 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"
|
|
}
|
|
|
|
remove_local_gateway_registration() {
|
|
remove_local_gateway_registration_from "$(target_openshell_config_dir)"
|
|
}
|
|
|
|
remove_snap_gateway_registration() {
|
|
remove_local_gateway_registration_from \
|
|
"${TARGET_HOME}/snap/openshell/common/.config/openshell"
|
|
}
|
|
|
|
register_local_gateway() {
|
|
_register_bin="${OPENSHELL_REGISTER_BIN:-openshell}"
|
|
_endpoint="$(local_gateway_endpoint)"
|
|
|
|
if _add_output="$(as_target_user "$_register_bin" gateway add "$_endpoint" --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 "$_endpoint" --local --name openshell
|
|
;;
|
|
*)
|
|
printf '%s\n' "$_add_output" >&2
|
|
return "$_add_status"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
print_gateway_add_output() {
|
|
_endpoint="$(local_gateway_endpoint)"
|
|
printf '%s\n' "$1" | while IFS= read -r _line; do
|
|
case "$_line" in
|
|
*"Gateway is not reachable at ${_endpoint}"*) ;;
|
|
*"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
|
|
prepare_prerelease_assets "$_tmpdir"
|
|
info "downloading ${RELEASE_TAG} release checksums..."
|
|
download_release_asset "$RELEASE_TAG" "$CHECKSUMS_NAME" "${_tmpdir}/${CHECKSUMS_NAME}" || {
|
|
error "failed to download ${CHECKSUMS_NAME} for ${RELEASE_TAG}"
|
|
}
|
|
|
|
_deb_file="$(find_deb_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch")"
|
|
if [ -z "$_deb_file" ]; then
|
|
error "no Debian package found for architecture: ${_arch}"
|
|
fi
|
|
|
|
_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_file} for ${RELEASE_TAG}"
|
|
}
|
|
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
|
|
prepare_prerelease_assets "$_tmpdir"
|
|
info "downloading ${RELEASE_TAG} release checksums..."
|
|
download_release_asset "$RELEASE_TAG" "$CHECKSUMS_NAME" "${_tmpdir}/${CHECKSUMS_NAME}" || {
|
|
error "failed to download ${CHECKSUMS_NAME} for ${RELEASE_TAG}"
|
|
}
|
|
|
|
_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
|
|
|
|
_prover_rpm_file="$(find_rpm_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch" openshell-prover)"
|
|
if [ -z "$_prover_rpm_file" ]; then
|
|
error "no openshell-prover RPM package found for architecture: ${_arch}"
|
|
fi
|
|
|
|
info "selected ${_rpm_file}, ${_gateway_rpm_file}, and ${_prover_rpm_file}"
|
|
|
|
for _package_file in "$_rpm_file" "$_gateway_rpm_file" "$_prover_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_file} for ${RELEASE_TAG}"
|
|
}
|
|
chmod 0644 "$_package_path"
|
|
|
|
info "verifying checksum for ${_package_file}..."
|
|
verify_checksum "$_package_path" "${_tmpdir}/${CHECKSUMS_NAME}" "$_package_file"
|
|
done
|
|
|
|
info "installing ${_rpm_file}, ${_gateway_rpm_file}, and ${_prover_rpm_file}..."
|
|
install_rpm_packages \
|
|
"${_tmpdir}/${_rpm_file}" \
|
|
"${_tmpdir}/${_gateway_rpm_file}" \
|
|
"${_tmpdir}/${_prover_rpm_file}"
|
|
info "installed ${APP_NAME} RPM packages from ${RELEASE_TAG}"
|
|
start_user_gateway
|
|
}
|
|
|
|
openshell_snap_channel() {
|
|
case "${OPENSHELL_VERSION:-}" in
|
|
dev) printf '%s\n' "latest/edge" ;;
|
|
'') printf '%s\n' "latest/stable" ;;
|
|
*) error "snap installs do not support OPENSHELL_VERSION=${OPENSHELL_VERSION}; use a native package" ;;
|
|
esac
|
|
}
|
|
|
|
wait_for_docker_daemon() {
|
|
_timeout="${OPENSHELL_INSTALL_DOCKER_TIMEOUT:-30}"
|
|
_elapsed=0
|
|
_last_output=""
|
|
|
|
info "waiting for Docker daemon at /var/run/docker.sock to become reachable..."
|
|
while [ "$_elapsed" -lt "$_timeout" ]; do
|
|
if _last_output="$(as_root env -u DOCKER_HOST -u DOCKER_CONTEXT docker --host unix:///var/run/docker.sock info 2>&1)"; then
|
|
info "Docker daemon at /var/run/docker.sock is reachable"
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
_elapsed=$((_elapsed + 1))
|
|
done
|
|
|
|
[ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2
|
|
if snap list docker >/dev/null 2>&1; then
|
|
snap services docker >&2 || true
|
|
snap changes >&2 || true
|
|
fi
|
|
error "Docker daemon at /var/run/docker.sock did not become reachable within ${_timeout}s"
|
|
}
|
|
|
|
wait_for_user_docker_daemon() {
|
|
_timeout="${OPENSHELL_INSTALL_DOCKER_TIMEOUT:-30}"
|
|
_elapsed=0
|
|
_last_output=""
|
|
|
|
info "waiting for Docker daemon at /var/run/docker.sock to become reachable as ${TARGET_USER}..."
|
|
while [ "$_elapsed" -lt "$_timeout" ]; do
|
|
if _last_output="$(as_target_user env -u DOCKER_HOST -u DOCKER_CONTEXT docker --host unix:///var/run/docker.sock info 2>&1)"; then
|
|
info "Docker daemon at /var/run/docker.sock is reachable as ${TARGET_USER}"
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
_elapsed=$((_elapsed + 1))
|
|
done
|
|
|
|
[ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2
|
|
error "Docker daemon at /var/run/docker.sock did not become reachable as ${TARGET_USER} within ${_timeout}s. Ensure ${TARGET_USER} can access the Docker socket, then log out and back in after changing group membership."
|
|
}
|
|
|
|
snap_gateway_mode() {
|
|
_mode="$(as_root snap get openshell gateway-mode 2>/dev/null || true)"
|
|
case "$_mode" in
|
|
user | system) printf '%s\n' "$_mode" ;;
|
|
disable) error "OpenShell snap gateway mode is disabled. Complete the gateway transition with 'sudo snap set openshell gateway-mode=user' or 'sudo snap set openshell gateway-mode=system', then rerun the installer." ;;
|
|
'') error "OpenShell snap gateway mode was not initialized" ;;
|
|
*) error "unsupported OpenShell snap gateway mode: ${_mode}" ;;
|
|
esac
|
|
}
|
|
|
|
# Copy the snap gateway's client bundle into the target user's snap state
|
|
# directory, where `openshell gateway add --local` imports it. Root only reads
|
|
# the source files; the target user writes the copies into their own home.
|
|
copy_snap_client_bundle() {
|
|
_src="${OPENSHELL_SNAP_TLS_DIR:-/var/snap/openshell/common/tls}"
|
|
_dst="${TARGET_HOME}/snap/openshell/common/.local/state/openshell/tls"
|
|
|
|
as_target_user mkdir -p "${_dst}/client"
|
|
as_target_user chmod 700 "$_dst" "${_dst}/client"
|
|
for _file in ca.crt client/tls.crt client/tls.key; do
|
|
as_root cat "${_src}/${_file}" |
|
|
as_target_user sh -c 'umask 077; cat >"$1"' sh "${_dst}/${_file}"
|
|
as_target_user chmod 600 "${_dst}/${_file}"
|
|
done
|
|
}
|
|
|
|
# snap revisions that require mTLS ship the post-refresh hook that migrates
|
|
# older plaintext configs.
|
|
snap_gateway_uses_mtls() {
|
|
[ -e "${OPENSHELL_SNAP_DIR:-/snap/openshell/current}/meta/hooks/post-refresh" ]
|
|
}
|
|
|
|
register_system_snap_gateway() {
|
|
_register_bin="${OPENSHELL_REGISTER_BIN:-/snap/bin/openshell}"
|
|
|
|
if snap_gateway_uses_mtls; then
|
|
_endpoint="https://127.0.0.1:${LOCAL_GATEWAY_PORT}"
|
|
info "copying the gateway client certificate for ${TARGET_USER}..."
|
|
copy_snap_client_bundle
|
|
else
|
|
_endpoint="http://127.0.0.1:${LOCAL_GATEWAY_PORT}"
|
|
warn "this OpenShell snap revision serves plaintext HTTP without client authentication; any local user can operate the gateway"
|
|
fi
|
|
|
|
if _add_output="$(as_target_user "$_register_bin" gateway add "$_endpoint" --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_snap_gateway_registration
|
|
as_target_user "$_register_bin" gateway add "$_endpoint" --local --name openshell
|
|
;;
|
|
*)
|
|
printf '%s\n' "$_add_output" >&2
|
|
return "$_add_status"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
register_user_snap_gateway() {
|
|
_register_bin="${OPENSHELL_REGISTER_BIN:-/snap/bin/openshell}"
|
|
_endpoint="https://127.0.0.1:${LOCAL_GATEWAY_PORT}"
|
|
|
|
if _add_output="$(as_target_user "$_register_bin" gateway add "$_endpoint" --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_snap_gateway_registration
|
|
as_target_user "$_register_bin" gateway add "$_endpoint" --local --name openshell
|
|
;;
|
|
*)
|
|
printf '%s\n' "$_add_output" >&2
|
|
return "$_add_status"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# The mTLS gateway rejects TLS handshakes without a client certificate, so
|
|
# probe it with the root-owned client bundle.
|
|
wait_for_system_snap_gateway_listener() {
|
|
_timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}"
|
|
_elapsed=0
|
|
_last_output=""
|
|
_tls_dir="${OPENSHELL_SNAP_TLS_DIR:-/var/snap/openshell/common/tls}"
|
|
|
|
if snap_gateway_uses_mtls; then
|
|
_probe_url="https://127.0.0.1:${LOCAL_GATEWAY_PORT}/"
|
|
_probe_as=as_root
|
|
set -- --cacert "${_tls_dir}/ca.crt" \
|
|
--cert "${_tls_dir}/client/tls.crt" --key "${_tls_dir}/client/tls.key"
|
|
else
|
|
_probe_url="http://127.0.0.1:${LOCAL_GATEWAY_PORT}/"
|
|
_probe_as=""
|
|
set --
|
|
fi
|
|
|
|
info "waiting for local gateway listener to become reachable..."
|
|
while [ "$_elapsed" -lt "$_timeout" ]; do
|
|
if _last_output="$($_probe_as curl -sS --max-time 2 "$@" -o /dev/null "$_probe_url" 2>&1)"; then
|
|
info "local gateway listener is reachable"
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
_elapsed=$((_elapsed + 1))
|
|
done
|
|
|
|
[ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2
|
|
dump_local_gateway_diagnostics
|
|
error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s"
|
|
}
|
|
|
|
wait_for_user_snap_gateway_listener() {
|
|
_timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}"
|
|
_elapsed=0
|
|
_last_output=""
|
|
_tls_dir="${TARGET_HOME}/snap/openshell/common/.local/state/openshell/tls"
|
|
_probe_url="https://127.0.0.1:${LOCAL_GATEWAY_PORT}/"
|
|
|
|
info "waiting for local gateway listener to become reachable..."
|
|
while [ "$_elapsed" -lt "$_timeout" ]; do
|
|
if _last_output="$(as_target_user curl -sS --max-time 2 \
|
|
--cacert "${_tls_dir}/ca.crt" \
|
|
--cert "${_tls_dir}/client/tls.crt" \
|
|
--key "${_tls_dir}/client/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
|
|
|
|
[ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2
|
|
dump_local_gateway_diagnostics
|
|
error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s"
|
|
}
|
|
|
|
install_linux_snap() {
|
|
require_cmd snap
|
|
set_linux_target_runtime_dir
|
|
|
|
if snap list openshell >/dev/null 2>&1; then
|
|
_existing_snap=true
|
|
_existing_mode="$(as_root snap get openshell gateway-mode 2>/dev/null || true)"
|
|
else
|
|
_existing_snap=false
|
|
_existing_mode=user
|
|
fi
|
|
|
|
if [ "$_existing_mode" = disable ]; then
|
|
error "OpenShell snap gateway mode is disabled. Complete the gateway transition with 'sudo snap set openshell gateway-mode=user' or 'sudo snap set openshell gateway-mode=system', then rerun the installer."
|
|
fi
|
|
|
|
if snap list docker >/dev/null 2>&1; then
|
|
error "the Docker snap is not currently compatible with OpenShell because its AppArmor confinement prevents OpenShell's hardened containers from starting.
|
|
Remove the Docker snap and install Docker Engine from a system package or Docker's package repository, then rerun this installer."
|
|
fi
|
|
if ! has_cmd docker; then
|
|
error "Docker is required before installing the OpenShell snap.
|
|
Install Docker Engine from a system package or Docker's package repository, then rerun this installer. The Docker snap is not currently compatible with OpenShell."
|
|
fi
|
|
info "using existing Docker installation"
|
|
if [ "$_existing_mode" = user ]; then
|
|
wait_for_user_docker_daemon
|
|
else
|
|
wait_for_docker_daemon
|
|
fi
|
|
|
|
_channel="$(openshell_snap_channel)"
|
|
if [ "$_existing_snap" = true ]; then
|
|
info "refreshing OpenShell snap from ${_channel}..."
|
|
as_root snap refresh openshell --channel="$_channel"
|
|
else
|
|
info "installing OpenShell snap from ${_channel}..."
|
|
as_root snap install openshell --channel="$_channel"
|
|
fi
|
|
|
|
_gateway_mode="$(snap_gateway_mode)"
|
|
if [ "$_gateway_mode" = system ]; then
|
|
warn "restarting the OpenShell gateway to use the refreshed snap; active sandbox sessions will be interrupted"
|
|
as_root snap restart openshell.system-gateway
|
|
else
|
|
# In case refreshing from an old snap which had a globally-enabled user
|
|
# gateway service, globally disable it, then just enable for the target user
|
|
as_root systemctl --global disable snap.openshell.user-gateway.service
|
|
as_target_user systemctl --user daemon-reload
|
|
as_target_user systemctl --user enable --now snap.openshell.user-gateway.service
|
|
if [ "$_existing_snap" = true ]; then
|
|
warn "restarting the OpenShell user gateway to use the refreshed snap; active sandbox sessions will be interrupted"
|
|
as_target_user systemctl --user restart snap.openshell.user-gateway.service
|
|
fi
|
|
fi
|
|
|
|
info "registering local gateway as ${TARGET_USER}..."
|
|
if [ "$_gateway_mode" = system ]; then
|
|
wait_for_system_snap_gateway_listener
|
|
register_system_snap_gateway
|
|
else
|
|
wait_for_user_snap_gateway_listener
|
|
register_user_snap_gateway
|
|
fi
|
|
OPENSHELL_REGISTER_BIN="/snap/bin/openshell"
|
|
wait_for_local_gateway_status
|
|
}
|
|
|
|
install_macos_homebrew() {
|
|
check_macos_platform
|
|
|
|
_tmpdir="$(mktemp -d)"
|
|
chmod 0755 "$_tmpdir"
|
|
trap 'rm -rf "$_tmpdir"' EXIT
|
|
prepare_prerelease_assets "$_tmpdir"
|
|
_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_prerelease_homebrew_formula_urls "$_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 $(local_gateway_endpoint) --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)"
|
|
if [ "$PLATFORM" = "linux" ]; then
|
|
LINUX_INSTALL_METHOD="$(linux_package_method)"
|
|
fi
|
|
|
|
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")"
|
|
|
|
if [ "${LINUX_INSTALL_METHOD:-}" = "snap" ]; then
|
|
guard_native_to_snap_transition
|
|
else
|
|
RELEASE_TAG="$(resolve_release_tag)"
|
|
guard_breaking_upgrade
|
|
fi
|
|
|
|
case "$PLATFORM" in
|
|
linux)
|
|
case "$LINUX_INSTALL_METHOD" in
|
|
snap)
|
|
install_linux_snap
|
|
;;
|
|
deb)
|
|
require_linux_package_glibc
|
|
install_linux_deb
|
|
;;
|
|
rpm)
|
|
require_linux_package_glibc
|
|
install_linux_rpm
|
|
;;
|
|
*)
|
|
error "unsupported Linux package method"
|
|
;;
|
|
esac
|
|
;;
|
|
darwin)
|
|
install_macos_homebrew
|
|
;;
|
|
*)
|
|
error "unsupported platform: ${PLATFORM}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" != "1" ]; then
|
|
main "$@"
|
|
fi
|