From a97073c141d91db00795c8529c0a293781b24b91 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 5 Jun 2026 13:16:31 +0200 Subject: [PATCH 1/2] feat(install): prefer Snap on Linux, add preflight checks and tests Add snap install support: resolve_linux_install_method auto-selects snap when snapd is available, with OPENSHELL_INSTALL_METHOD=classic fallback. Add preflight checks for Docker conflicts and interface connections. Update test suite to cover has_snapd, resolve_snap_channel, and resolve_linux_install_method functions. Signed-off-by: Zygmunt Krynicki --- install.sh | 165 ++++++++++++++++++++++++++++--- tasks/scripts/test-install-sh.sh | 131 ++++++++++++++++++++++++ 2 files changed, 282 insertions(+), 14 deletions(-) diff --git a/install.sh b/install.sh index 6a8bc3029..70c158ff8 100755 --- a/install.sh +++ b/install.sh @@ -4,9 +4,10 @@ # # 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. +# Linux prefers the Snap path when snapd is available. Otherwise Linux installs +# 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 @@ -21,6 +22,12 @@ HOMEBREW_FORMULA_NAME="openshell" BREAKING_RELEASE_VERSION="0.0.37" LINUX_PACKAGE_GLIBC_MIN_VERSION="2.31" UPGRADE_NOTICE_ACK="${OPENSHELL_ACK_BREAKING_UPGRADE:-}" +SNAP_PACKAGE_NAME="openshell" +SNAP_DOCKER_SLOT="docker:docker-daemon" +SNAP_DEFAULT_CHANNEL="latest/stable" +SNAP_DEV_CHANNEL="latest/edge" +LINUX_INSTALL_METHOD_SNAP="snap" +LINUX_INSTALL_METHOD_CLASSIC="classic" info() { printf '%s: %s\n' "$APP_NAME" "$*" >&2 @@ -54,13 +61,27 @@ ENVIRONMENT VARIABLES: OPENSHELL_ACK_BREAKING_UPGRADE Set to 1 only after backing up and cleaning up a pre-v0.0.37 installation. + OPENSHELL_INSTALL_METHOD + Linux only. Selects snap or classic. Accepted values: + snap install from the Snap Store + classic install via dpkg or rpm + When unset, snap is used when snapd is available and + running, otherwise the classic package manager is used. + Set OPENSHELL_INSTALL_METHOD=classic on distros where + snapd is not desired. 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. + Linux prefers the snap method when snapd is available. The Snap channel + follows OPENSHELL_VERSION: 'dev' selects ${SNAP_DEV_CHANNEL:-latest/edge}, + tagged releases select ${SNAP_DEFAULT_CHANNEL:-latest/stable}. snapd + manages the gateway as the 'openshell.gateway' service. + + On systems without snapd, 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 @@ -477,6 +498,71 @@ linux_package_method() { fi } +has_snapd() { + if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ]; then + case "${OPENSHELL_TEST_SNAPD_AVAILABLE:-0}" in + 1) return 0 ;; + 0) return 1 ;; + esac + fi + + command -v snap >/dev/null 2>&1 || return 1 + [ -S /run/snapd.socket ] || [ -S /var/run/snapd.socket ] || return 1 + return 0 +} + +host_supports_native_package() { + has_cmd dpkg || has_cmd rpm +} + +has_native_docker() { + if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ] && [ -n "${OPENSHELL_TEST_NATIVE_DOCKER+x}" ]; then + case "${OPENSHELL_TEST_NATIVE_DOCKER}" in + 1) return 0 ;; + *) return 1 ;; + esac + fi + + command -v docker >/dev/null 2>&1 || return 1 + case "$(command -v docker)" in + */snap/bin/docker | */var/lib/snapd/snap/bin/docker) + return 1 + ;; + esac + return 0 +} + +resolve_linux_install_method() { + _method="${OPENSHELL_INSTALL_METHOD:-}" + if [ -n "$_method" ]; then + case "$_method" in + "$LINUX_INSTALL_METHOD_SNAP" | "$LINUX_INSTALL_METHOD_CLASSIC") + echo "$_method" + return 0 + ;; + *) + error "OPENSHELL_INSTALL_METHOD must be '${LINUX_INSTALL_METHOD_SNAP}' or '${LINUX_INSTALL_METHOD_CLASSIC}' (got: ${_method})" + ;; + esac + fi + + if has_snapd && ! has_native_docker; then + echo "$LINUX_INSTALL_METHOD_SNAP" + elif host_supports_native_package; then + echo "$LINUX_INSTALL_METHOD_CLASSIC" + else + error "OpenShell Linux installs require either snapd or dpkg/rpm" + fi +} + +resolve_snap_channel() { + if [ "${RELEASE_TAG:-}" = "dev" ]; then + echo "$SNAP_DEV_CHANNEL" + else + echo "$SNAP_DEFAULT_CHANNEL" + fi +} + set_linux_target_runtime_dir() { if [ "$(id -u)" -eq "$TARGET_UID" ] && [ -n "${XDG_RUNTIME_DIR:-}" ]; then TARGET_RUNTIME_DIR="$XDG_RUNTIME_DIR" @@ -866,6 +952,50 @@ print_gateway_add_output() { done } +install_linux_snap() { + _channel="$(resolve_snap_channel)" + local _snap_installed=false + + _snap_clean() { + if [ "$_snap_installed" = true ]; then + warn "removing ${APP_NAME} snap due to interrupted install..." + as_root snap remove --purge "$SNAP_PACKAGE_NAME" || \ + warn "could not remove ${APP_NAME} snap; please remove it manually" + fi + } + trap '_snap_clean' EXIT INT TERM + + info "installing ${APP_NAME} from the Snap Store (channel: ${_channel})..." + as_root snap install "$SNAP_PACKAGE_NAME" --channel="$_channel" + _snap_installed=true + + info "pre-installing the Docker snap..." + as_root snap install docker + + info "connecting required interfaces..." + as_root snap connect "${SNAP_PACKAGE_NAME}:docker" "$SNAP_DOCKER_SLOT" || \ + warn "could not connect ${SNAP_PACKAGE_NAME}:docker to ${SNAP_DOCKER_SLOT}; the Docker driver will not work until this is fixed" + for _plug in log-observe system-observe ssh-keys; do + as_root snap connect "${SNAP_PACKAGE_NAME}:${_plug}" || \ + warn "could not connect ${SNAP_PACKAGE_NAME}:${_plug}; some features may be limited" + done + + trap - EXIT INT TERM + info "installed ${APP_NAME} from the Snap Store (${_channel})" + info "snapd manages the OpenShell gateway as the 'openshell.gateway' service." + info "" + info "Next steps:" + info " snap services openshell" + info " openshell status" + info " openshell gateway add https://127.0.0.1:17670 --local --name openshell" + if ! command -v snap >/dev/null 2>&1; then + info "" + info "note: the 'openshell' binary is provided by the snap." + info "Make sure snap is in your PATH, or invoke openshell with the" + info "absolute path, before running the next-step commands." + fi +} + install_linux_deb() { check_linux_deb_platform set_linux_target_runtime_dir @@ -1033,16 +1163,23 @@ main() { case "$PLATFORM" in linux) - require_linux_package_glibc - case "$(linux_package_method)" in - deb) - install_linux_deb - ;; - rpm) - install_linux_rpm + case "$(resolve_linux_install_method)" in + "$LINUX_INSTALL_METHOD_SNAP") + install_linux_snap ;; - *) - error "unsupported Linux package method" + "$LINUX_INSTALL_METHOD_CLASSIC") + require_linux_package_glibc + case "$(linux_package_method)" in + deb) + install_linux_deb + ;; + rpm) + install_linux_rpm + ;; + *) + error "unsupported Linux package method" + ;; + esac ;; esac ;; diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index 5bf98071a..506c12ac2 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -99,4 +99,135 @@ assert_glibc_preflight_fails \ "OpenShell Linux packages require glibc >= 2.31; detected musl or unsupported libc." \ setup_ldd_musl +assert_eq() { + local name=$1 + local expected=$2 + local actual=$3 + if [ "$expected" != "$actual" ]; then + echo "FAIL: ${name}: expected '${expected}', got '${actual}'" >&2 + exit 1 + fi +} + +# has_snapd: with the test-mode override, OPENSHELL_TEST_SNAPD_AVAILABLE selects +# the result. In real environments the function probes for `snap` and the snapd +# socket, which the test cannot exercise without root or a real snapd. +(export OPENSHELL_TEST_SNAPD_AVAILABLE=1; has_snapd) || { + echo "FAIL: has_snapd with OPENSHELL_TEST_SNAPD_AVAILABLE=1" >&2 + exit 1 +} +(export OPENSHELL_TEST_SNAPD_AVAILABLE=0; has_snapd) && { + echo "FAIL: has_snapd with OPENSHELL_TEST_SNAPD_AVAILABLE=0 should fail" >&2 + exit 1 +} + +# has_native_docker: uses PATH to resolve docker, so we create a mock directory +# with symlinks that make command -v return the desired path. +# Make sure test-mode override is cleared so PATH-based detection is exercised. +unset OPENSHELL_TEST_NATIVE_DOCKER +_real_path="$PATH" +_mock_dir="${tmpdir}/mock-bin" +mkdir -p "$_mock_dir/snap/bin" \ + "$_mock_dir/var/lib/snapd/snap/bin" \ + "$_mock_dir/usr/bin" \ + "${tmpdir}/empty-dir" +# The actual no-op executable lives in the "native" directory +touch "$_mock_dir/usr/bin/docker" && chmod +x "$_mock_dir/usr/bin/docker" +# Symlinks in snap paths so command -v resolves them correctly +ln -s "$_mock_dir/usr/bin/docker" "$_mock_dir/snap/bin/docker" +ln -s "$_mock_dir/usr/bin/docker" "$_mock_dir/var/lib/snapd/snap/bin/docker" + +# No docker in PATH → has_native_docker should fail (return 1) +PATH="${tmpdir}/empty-dir" +if has_native_docker; then + echo "FAIL: has_native_docker without docker should return 1" >&2 + exit 1 +fi + +# docker from /snap/bin → has_native_docker should fail (return 1) +PATH="$_mock_dir/snap/bin" +if has_native_docker; then + echo "FAIL: has_native_docker with snap docker should return 1" >&2 + exit 1 +fi + +# docker from non-snap path → has_native_docker should succeed (return 0) +PATH="$_mock_dir/usr/bin" +if ! has_native_docker; then + echo "FAIL: has_native_docker with native docker should return 0" >&2 + exit 1 +fi + +# docker from /var/lib/snapd/snap/bin → has_native_docker should fail (return 1) +PATH="$_mock_dir/var/lib/snapd/snap/bin" +if has_native_docker; then + echo "FAIL: has_native_docker with snap docker (var path) should return 1" >&2 + exit 1 +fi + +PATH="$_real_path" + +# resolve_snap_channel +export RELEASE_TAG="" +assert_eq "resolve_snap_channel default" "latest/stable" "$(resolve_snap_channel)" +export RELEASE_TAG="dev" +assert_eq "resolve_snap_channel dev" "latest/edge" "$(resolve_snap_channel)" +export RELEASE_TAG="v0.0.37" +assert_eq "resolve_snap_channel tagged" "latest/stable" "$(resolve_snap_channel)" +export RELEASE_TAG="" +unset RELEASE_TAG + +# resolve_linux_install_method: explicit env var wins over the probe +export OPENSHELL_TEST_SNAPD_AVAILABLE=1 +export OPENSHELL_INSTALL_METHOD=snap +assert_eq "resolve_linux_install_method snap explicit" "snap" "$(resolve_linux_install_method)" +export OPENSHELL_INSTALL_METHOD=classic +assert_eq "resolve_linux_install_method classic explicit overrides snap probe" "classic" "$(resolve_linux_install_method)" +export OPENSHELL_TEST_SNAPD_AVAILABLE=0 +export OPENSHELL_INSTALL_METHOD=classic +assert_eq "resolve_linux_install_method classic explicit" "classic" "$(resolve_linux_install_method)" +unset OPENSHELL_INSTALL_METHOD + +# resolve_linux_install_method: snap when available, no native docker +export OPENSHELL_TEST_SNAPD_AVAILABLE=1 +unset OPENSHELL_TEST_NATIVE_DOCKER OPENSHELL_INSTALL_METHOD +assert_eq "resolve_linux_install_method snap auto" "snap" "$(resolve_linux_install_method)" + +# resolve_linux_install_method: snapd present + native docker → fallback to classic +export OPENSHELL_TEST_SNAPD_AVAILABLE=1 +export OPENSHELL_TEST_NATIVE_DOCKER=1 +unset OPENSHELL_INSTALL_METHOD +assert_eq "resolve_linux_install_method snapd + native docker → classic" "classic" "$(resolve_linux_install_method)" +unset OPENSHELL_TEST_NATIVE_DOCKER + +# resolve_linux_install_method: invalid env var exits non-zero +export OPENSHELL_INSTALL_METHOD=invalid +_snap_err="${tmpdir}/snap-err" +if (resolve_linux_install_method) >/dev/null 2>"$_snap_err"; then + echo "FAIL: resolve_linux_install_method should fail on invalid value" >&2 + exit 1 +fi +if ! grep -Fq "OPENSHELL_INSTALL_METHOD must be" "$_snap_err"; then + echo "FAIL: resolve_linux_install_method: missing expected error message" >&2 + cat "$_snap_err" >&2 || true + exit 1 +fi +unset OPENSHELL_INSTALL_METHOD + +# resolve_linux_install_method: snap absent + clean PATH (no dpkg/rpm) exits non-zero +export OPENSHELL_TEST_SNAPD_AVAILABLE=0 +_real_path="$PATH" +_clean_path="${tmpdir}/clean-path" +mkdir -p "$_clean_path" +PATH="$_clean_path" +unset OPENSHELL_INSTALL_METHOD +if (resolve_linux_install_method) >/dev/null 2>"$_snap_err"; then + PATH="$_real_path" + echo "FAIL: resolve_linux_install_method should fail without snap or native" >&2 + exit 1 +fi +PATH="$_real_path" +export OPENSHELL_TEST_SNAPD_AVAILABLE=1 +unset OPENSHELL_INSTALL_METHOD + echo "install.sh libc preflight tests passed" From a95c5fd5a6d11aaa2d346ad566b75d0ef207459a Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 5 Jun 2026 13:14:23 +0200 Subject: [PATCH 2/2] docs(installation): document snap install path Signed-off-by: Zygmunt Krynicki --- docs/about/installation.mdx | 87 ++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index cd9973f13..494ada001 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -16,7 +16,10 @@ Install OpenShell with a single command: curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` -The script detects your operating system and installs the OpenShell CLI and gateway with your native package manager. It then starts the local gateway server so you can begin creating sandboxes. +The script detects your operating system and installs the OpenShell CLI and +gateway. On Linux, the Snap path is preferred when `snapd` is available; +otherwise the script uses the native DEB or RPM package. The gateway then +starts automatically so you can begin creating sandboxes. You can also download release artifacts directly from the [OpenShell GitHub Releases](https://github.com/NVIDIA/OpenShell/releases) page. @@ -51,6 +54,13 @@ brew services restart openshell ## Linux +On distributions that ship with `snapd`, the install script uses the Snap path +described below, provided no non-snap Docker is detected. If a native Docker +package is present, the installer exits with an error and recommends installing +via DEB or RPM instead. On hosts without `snapd` (or with +`OPENSHELL_INSTALL_METHOD=classic` set), the script falls back to the classic +package manager. + On Fedora and RHEL, the install script uses RPM packages. The RPM installs the `openshell` CLI, the `openshell-gateway` daemon, and a systemd user service. On Debian and Ubuntu, the install script uses a Debian package. The Debian package installs the `openshell` CLI, the `openshell-gateway` daemon, VM sandbox support, and a systemd user service. @@ -75,6 +85,81 @@ To keep the user service running after logout, enable linger: sudo loginctl enable-linger $USER ``` +## Snap + +On Linux distributions that ship with `snapd`, the install script installs +OpenShell from the Snap Store. The OpenShell snap bundles the CLI, the terminal +UI, and a managed gateway daemon. snapd handles upgrades and rollback; the +gateway runs as a system service inside the snap. + +You can also install the snap directly: + +```shell +sudo snap install openshell +``` + +The snap requires the Docker snap. `default-provider: docker` on the OpenShell +snap installs the Docker snap automatically on first use, but you can install +it up front with: + +```shell +sudo snap install docker +``` + +### Connect the required interfaces + +Strict confinement requires explicit `snap connect` for several interfaces. The +installer runs these for you on Snap installs; run them by hand if you install +the snap manually: + +```shell +sudo snap connect openshell:docker docker:docker-daemon +sudo snap connect openshell:log-observe +sudo snap connect openshell:ssh-keys +sudo snap connect openshell:system-observe +``` + +The Docker slot is the Docker snap's `docker-daemon` slot; OpenShell does not +work with a host-installed Docker Engine. The installer is best-effort: if a +connect fails (for example because the Docker snap is not yet running), the +snap still installs and the installer prints a warning. + +### Verify the gateway + +The snap-managed gateway service is `openshell.gateway`. Inspect it with: + +```shell +snap services openshell +snap logs -n 100 openshell.gateway +``` + +Register the gateway with the CLI: + +```shell +openshell gateway add https://127.0.0.1:17670 --local --name openshell +openshell status +``` + +The gateway listens on `https://127.0.0.1:17670` and stores its state under +`/var/snap/openshell/common/`. Override gateway settings by creating +`/var/snap/openshell/common/gateway.toml`. + +### When to choose Snap + +Use Snap when `snapd` is available and no native Docker Engine is installed, +and you want atomic upgrades and rollback, a single self-contained install that +bundles the Docker provider, or a desktop launcher that surfaces the OpenShell +terminal UI in the application menu. + +Use DEB or RPM when `snapd` is unavailable or presents temporary limitations +awaiting snapd 2.76 release: `systemd --user` integration for the gateway, or +when you already run Docker Engine from a non-snap source. The installer will +refuse Snap installs on hosts with native Docker — use the DEB or RPM package +instead. + +Set `OPENSHELL_INSTALL_METHOD=classic` to force the classic package on hosts +that also have `snapd` available. + ## Kubernetes Kubernetes deployments use the OpenShell Helm chart. For step-by-step installation, refer to [Kubernetes Setup](/kubernetes/setup). For chart values and packaging details, refer to the [Helm chart README](https://github.com/NVIDIA/OpenShell/blob/main/deploy/helm/openshell/README.md).