From a059c3de3070c780a70ef611de00af4b2b3ba89f Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Tue, 5 May 2026 17:08:32 -0700 Subject: [PATCH 1/3] feat(installer): support macOS dev installs Signed-off-by: Drew Newberry --- .github/workflows/release-dev.yml | 3 + .github/workflows/release-tag.yml | 3 + architecture/custom-vm-runtime.md | 9 +- crates/openshell-driver-vm/README.md | 7 +- install-dev.sh | 508 +++++++++++++++++++++++++-- 5 files changed, 486 insertions(+), 44 deletions(-) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 258ec9bc6..b86a25791 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -768,6 +768,9 @@ jobs: openshell-x86_64-unknown-linux-musl.tar.gz \ openshell-aarch64-unknown-linux-musl.tar.gz \ openshell-aarch64-apple-darwin.tar.gz \ + openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz \ + openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz \ + openshell-driver-vm-aarch64-apple-darwin.tar.gz \ *.deb \ openshell-*.rpm \ *.whl > openshell-checksums-sha256.txt diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 0e9fab9bc..e11d5af53 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -772,6 +772,9 @@ jobs: openshell-x86_64-unknown-linux-musl.tar.gz \ openshell-aarch64-unknown-linux-musl.tar.gz \ openshell-aarch64-apple-darwin.tar.gz \ + openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz \ + openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz \ + openshell-driver-vm-aarch64-apple-darwin.tar.gz \ openshell_*.deb \ openshell-*.rpm \ *.whl > openshell-checksums-sha256.txt diff --git a/architecture/custom-vm-runtime.md b/architecture/custom-vm-runtime.md index 9f723d8d7..fb3512bfd 100644 --- a/architecture/custom-vm-runtime.md +++ b/architecture/custom-vm-runtime.md @@ -323,10 +323,11 @@ run `cargo build --release -p openshell-driver-vm`. The macOS driver is cross-compiled via osxcross (no macOS runner needed for the binary build — only for the kernel build). -macOS driver binaries produced via osxcross are not codesigned. Local -development builds are signed automatically by `tasks/scripts/gateway-vm.sh` -(registered as `mise run gateway:vm`). Release tarball users on macOS must -ad-hoc sign `openshell-driver-vm` before running VM sandboxes. +macOS driver binaries produced via osxcross are not codesigned. Development +builds are signed automatically by `tasks/scripts/gateway-vm.sh` +(registered as `mise run gateway:vm`) and by `install-dev.sh` when it installs +the Apple Silicon development tarballs from the selected release. A packaged +release needs signing in CI. ## Rollout Strategy diff --git a/crates/openshell-driver-vm/README.md b/crates/openshell-driver-vm/README.md index e71f926ed..7b071dfec 100644 --- a/crates/openshell-driver-vm/README.md +++ b/crates/openshell-driver-vm/README.md @@ -182,6 +182,11 @@ On Linux amd64 and arm64, `install-dev.sh` installs the Debian package from the selected `OPENSHELL_VERSION` release tag. That package includes `openshell-gateway` and `openshell-driver-vm`. +On Apple Silicon macOS, `install-dev.sh` installs `openshell`, +`openshell-gateway`, and `openshell-driver-vm` from the selected release +tarballs. It ad-hoc signs `openshell-driver-vm` with the Hypervisor entitlement +before registering the local LaunchAgent gateway. + ## Relationship to `openshell-vm` `openshell-vm` is a separate, legacy crate that runs the **whole OpenShell gateway inside a single VM**. It remains in the repository for later deprecation or removal, but is excluded from normal workspace builds and release paths. `openshell-driver-vm` is the active compute driver called by a host-resident gateway to spawn **per-sandbox VMs**. The driver vendors its own rootfs handling and runtime loader so `openshell-server` never has to link libkrun. @@ -189,4 +194,4 @@ selected `OPENSHELL_VERSION` release tag. That package includes ## TODOs - The gateway still configures the driver via CLI args; this will move to a gRPC bootstrap call so the driver interface is uniform across backends. See the `TODO(driver-abstraction)` notes in `crates/openshell-server/src/lib.rs` and `crates/openshell-server/src/compute/vm.rs`. -- macOS local builds are codesigned by `tasks/scripts/gateway-vm.sh`; release tarball users must ad-hoc sign `openshell-driver-vm` before running VM sandboxes. +- macOS local builds are codesigned by `tasks/scripts/gateway-vm.sh`; `install-dev.sh` signs the release tarball driver for local dev installs. diff --git a/install-dev.sh b/install-dev.sh index fb98a841d..162fb05cb 100755 --- a/install-dev.sh +++ b/install-dev.sh @@ -2,11 +2,11 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # -# Install an OpenShell Debian package from a GitHub release. +# Install the OpenShell development build from a GitHub release. # -# This script defaults to the rolling `dev` release and supports Debian -# packages on Linux amd64 and arm64 only. The package installs the CLI, -# gateway, and VM compute driver. +# This script is intended as a convenient installer for development builds. It +# supports Debian packages on Linux amd64/arm64 and a manual per-user install +# on Apple Silicon macOS. # set -e @@ -15,11 +15,21 @@ REPO="NVIDIA/OpenShell" GITHUB_URL="https://github.com/${REPO}" RELEASE_TAG="${OPENSHELL_VERSION:-dev}" CHECKSUMS_NAME="openshell-checksums-sha256.txt" +GATEWAY_CHECKSUMS_NAME="openshell-gateway-checksums-sha256.txt" +CLI_BIN="openshell" +GATEWAY_BIN="openshell-gateway" +DRIVER_VM_BIN="openshell-driver-vm" +MACOS_LAUNCH_AGENT_LABEL="com.nvidia.openshell.gateway" +LOCAL_GATEWAY_PORT="17670" 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 @@ -27,7 +37,7 @@ error() { usage() { cat </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" "$@" @@ -139,14 +179,40 @@ as_target_user() { fi } -check_platform() { - if [ "$(uname -s)" != "Linux" ]; then - error "unsupported OS: $(uname -s); dev Debian packages require Linux" - 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 +} +check_linux_platform() { require_cmd dpkg } +get_macos_target() { + _arch="$(uname -m)" + + case "$_arch" in + arm64|aarch64) + echo "aarch64-apple-darwin" + ;; + 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 +} + get_deb_arch() { _arch="$(dpkg --print-architecture)" @@ -209,6 +275,302 @@ install_deb_package() { fi } +target_group() { + id -gn "$TARGET_USER" 2>/dev/null || echo "$TARGET_USER" +} + +is_target_home_path() { + _path="$1" + case "$_path" in + "$TARGET_HOME"|"$TARGET_HOME"/*) + return 0 + ;; + *) + return 1 + ;; + esac +} + +chown_to_target_user() { + _path="$1" + + if [ "${TARGET_UID:-0}" = "0" ] || ! is_target_home_path "$_path"; then + return 0 + fi + + if [ "$(id -u)" -eq "$TARGET_UID" ]; then + return 0 + fi + + _owner="${TARGET_USER}:$(target_group)" + if [ "$(id -u)" -eq 0 ]; then + chown "$_owner" "$_path" 2>/dev/null || chown "$TARGET_USER" "$_path" 2>/dev/null || true + elif has_cmd sudo; then + sudo chown "$_owner" "$_path" 2>/dev/null || sudo chown "$TARGET_USER" "$_path" 2>/dev/null || true + fi +} + +ensure_dir() { + _dir="$1" + + if mkdir -p "$_dir" 2>/dev/null; then + : + else + as_root mkdir -p "$_dir" + fi + + chown_to_target_user "$_dir" +} + +install_executable() { + _src="$1" + _dst="$2" + _dst_dir="$(dirname "$_dst")" + + if mkdir -p "$_dst_dir" 2>/dev/null && [ -w "$_dst_dir" ]; then + install -m 0755 "$_src" "$_dst" + else + info "elevated permissions required to install to ${_dst_dir}" + as_root mkdir -p "$_dst_dir" + as_root install -m 0755 "$_src" "$_dst" + fi + + chown_to_target_user "$_dst" +} + +require_absolute_path() { + _name="$1" + _path="$2" + + case "$_path" in + /*) + ;; + *) + error "${_name} must be an absolute path on macOS; got ${_path}" + ;; + esac +} + +macos_install_dir() { + if [ -n "${OPENSHELL_INSTALL_DIR:-}" ]; then + echo "$OPENSHELL_INSTALL_DIR" + else + echo "${TARGET_HOME}/.local/bin" + fi +} + +macos_driver_dir() { + if [ -n "${OPENSHELL_DRIVER_DIR:-}" ]; then + echo "$OPENSHELL_DRIVER_DIR" + else + echo "${TARGET_HOME}/.local/libexec/openshell" + fi +} + +install_release_binary() { + _bin="$1" + _tag="$2" + _target="$3" + _checksums_name="$4" + _dest_dir="$5" + _work_dir="$6" + + _filename="${_bin}-${_target}.tar.gz" + _asset_path="${_work_dir}/${_filename}" + _checksums_path="${_work_dir}/${_bin}-${_tag}-checksums.txt" + _asset_url="${GITHUB_URL}/releases/download/${_tag}/${_filename}" + _checksums_url="${GITHUB_URL}/releases/download/${_tag}/${_checksums_name}" + + info "downloading ${_filename}..." + download_release_asset "$_tag" "$_filename" "$_asset_path" || { + error "failed to download ${_asset_url}" + } + + info "downloading ${_tag} release checksums for ${_bin}..." + download "$_checksums_url" "$_checksums_path" || { + error "failed to download ${_checksums_url}" + } + + info "verifying ${_filename}..." + verify_checksum "$_asset_path" "$_checksums_path" "$_filename" + + info "installing ${_bin}..." + tar -xzf "$_asset_path" -C "$_work_dir" "$_bin" + install_executable "${_work_dir}/${_bin}" "${_dest_dir}/${_bin}" +} + +codesign_driver_vm() { + _binary="$1" + _work_dir="$2" + + if ! has_cmd codesign; then + warn "codesign not found; ${DRIVER_VM_BIN} will fail without the Hypervisor entitlement" + return 0 + fi + + info "codesigning ${DRIVER_VM_BIN} with Hypervisor entitlement..." + _entitlements="${_work_dir}/entitlements.plist" + cat > "$_entitlements" <<'PLIST' + + + + + com.apple.security.hypervisor + + + +PLIST + codesign --entitlements "$_entitlements" --force -s - "$_binary" +} + +xml_escape() { + printf '%s' "$1" | sed \ + -e 's/&/\&/g' \ + -e 's//\>/g' \ + -e 's/"/\"/g' +} + +write_macos_launch_agent() { + _gateway_bin="$1" + _driver_dir="$2" + + MACOS_GATEWAY_STATE_DIR="${TARGET_HOME}/.local/state/openshell/gateway" + MACOS_VM_STATE_DIR="${TARGET_HOME}/.local/state/openshell/vm-driver" + MACOS_LAUNCH_AGENT_DIR="${TARGET_HOME}/Library/LaunchAgents" + MACOS_LAUNCH_AGENT_PLIST="${MACOS_LAUNCH_AGENT_DIR}/${MACOS_LAUNCH_AGENT_LABEL}.plist" + + ensure_dir "$MACOS_GATEWAY_STATE_DIR" + ensure_dir "$MACOS_VM_STATE_DIR" + ensure_dir "$MACOS_LAUNCH_AGENT_DIR" + + _gateway_bin_xml="$(xml_escape "$_gateway_bin")" + _driver_dir_xml="$(xml_escape "$_driver_dir")" + _db_url_xml="$(xml_escape "sqlite:${MACOS_GATEWAY_STATE_DIR}/openshell.db")" + _vm_state_xml="$(xml_escape "$MACOS_VM_STATE_DIR")" + _stdout_xml="$(xml_escape "${MACOS_GATEWAY_STATE_DIR}/openshell-gateway.out.log")" + _stderr_xml="$(xml_escape "${MACOS_GATEWAY_STATE_DIR}/openshell-gateway.err.log")" + + cat > "$MACOS_LAUNCH_AGENT_PLIST" < + + + + Label + ${MACOS_LAUNCH_AGENT_LABEL} + ProgramArguments + + ${_gateway_bin_xml} + + EnvironmentVariables + + OPENSHELL_BIND_ADDRESS + 127.0.0.1 + OPENSHELL_SERVER_PORT + ${LOCAL_GATEWAY_PORT} + OPENSHELL_DISABLE_TLS + true + OPENSHELL_DISABLE_GATEWAY_AUTH + true + OPENSHELL_DB_URL + ${_db_url_xml} + OPENSHELL_DRIVERS + vm + OPENSHELL_GRPC_ENDPOINT + http://127.0.0.1:${LOCAL_GATEWAY_PORT} + OPENSHELL_SSH_GATEWAY_HOST + 127.0.0.1 + OPENSHELL_SSH_GATEWAY_PORT + ${LOCAL_GATEWAY_PORT} + OPENSHELL_VM_DRIVER_STATE_DIR + ${_vm_state_xml} + OPENSHELL_DRIVER_DIR + ${_driver_dir_xml} + + RunAtLoad + + KeepAlive + + SuccessfulExit + + + StandardOutPath + ${_stdout_xml} + StandardErrorPath + ${_stderr_xml} + + +EOF + + chmod 0644 "$MACOS_LAUNCH_AGENT_PLIST" + chown_to_target_user "$MACOS_LAUNCH_AGENT_PLIST" +} + +print_macos_manual_commands() { + _gateway_bin="$1" + _cli_bin="$2" + _driver_dir="$3" + + info "start the gateway manually with:" + cat >&2 </dev/null 2>&1 || true + + if launchctl bootstrap "$_domain" "$MACOS_LAUNCH_AGENT_PLIST" >/dev/null 2>&1; then + if ! launchctl kickstart -k "${_domain}/${MACOS_LAUNCH_AGENT_LABEL}" >/dev/null 2>&1; then + warn "launchctl kickstart failed; skipping automatic gateway registration" + print_macos_manual_commands "$_gateway_bin" "$_cli_bin" "$_driver_dir" + return 0 + fi + elif launchctl load -w "$MACOS_LAUNCH_AGENT_PLIST" >/dev/null 2>&1; then + : + else + warn "launchctl could not load ${MACOS_LAUNCH_AGENT_PLIST}; skipping automatic gateway registration" + print_macos_manual_commands "$_gateway_bin" "$_cli_bin" "$_driver_dir" + return 0 + fi + + if ! launchctl print "${_domain}/${MACOS_LAUNCH_AGENT_LABEL}" >/dev/null 2>&1; then + warn "LaunchAgent did not appear in launchd; skipping automatic gateway registration" + print_macos_manual_commands "$_gateway_bin" "$_cli_bin" "$_driver_dir" + return 0 + fi + + info "registering local gateway as ${TARGET_USER}..." + OPENSHELL_REGISTER_BIN="$_cli_bin" + register_local_gateway +} + start_user_gateway() { info "restarting openshell-gateway user service as ${TARGET_USER}..." @@ -233,6 +595,7 @@ remove_local_gateway_registration() { # 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" @@ -244,7 +607,9 @@ remove_local_gateway_registration() { } register_local_gateway() { - if _add_output="$(as_target_user openshell gateway add http://127.0.0.1:17670 --local --name local 2>&1)"; then + _register_bin="${OPENSHELL_REGISTER_BIN:-openshell}" + + if _add_output="$(as_target_user "$_register_bin" gateway add "http://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name local 2>&1)"; then [ -z "$_add_output" ] || printf '%s\n' "$_add_output" >&2 return 0 else @@ -255,7 +620,7 @@ register_local_gateway() { *"already exists"*) info "local gateway already exists; removing and re-adding it..." remove_local_gateway_registration - as_target_user openshell gateway add http://127.0.0.1:17670 --local --name local + as_target_user "$_register_bin" gateway add "http://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name local ;; *) printf '%s\n' "$_add_output" >&2 @@ -264,27 +629,9 @@ register_local_gateway() { esac } -main() { - while [ "$#" -gt 0 ]; do - case "$1" in - --help) - usage - exit 0 - ;; - *) - error "unknown option: $1" - ;; - esac - shift - done - - require_cmd curl - check_platform +install_linux_deb() { + check_linux_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")" if [ "$(id -u)" -eq "$TARGET_UID" ] && [ -n "${XDG_RUNTIME_DIR:-}" ]; then TARGET_RUNTIME_DIR="$XDG_RUNTIME_DIR" else @@ -313,7 +660,7 @@ main() { info "selected ${_deb_file}" info "downloading ${_deb_file}..." - download_release_asset "$_deb_file" "$_deb_path" || { + download_release_asset "$RELEASE_TAG" "$_deb_file" "$_deb_path" || { error "failed to download ${_deb_url}" } chmod 0644 "$_deb_path" @@ -327,4 +674,87 @@ main() { start_user_gateway } +install_macos_tarballs() { + require_cmd tar + require_cmd install + + _target="$(get_macos_target)" + _install_dir="$(macos_install_dir)" + _driver_dir="$(macos_driver_dir)" + require_absolute_path "OPENSHELL_INSTALL_DIR" "$_install_dir" + require_absolute_path "OPENSHELL_DRIVER_DIR" "$_driver_dir" + + _tmpdir="$(mktemp -d)" + chmod 0755 "$_tmpdir" + trap 'rm -rf "$_tmpdir"' EXIT + + install_release_binary \ + "$CLI_BIN" \ + "$RELEASE_TAG" \ + "$_target" \ + "$CHECKSUMS_NAME" \ + "$_install_dir" \ + "$_tmpdir" + + install_release_binary \ + "$GATEWAY_BIN" \ + "$RELEASE_TAG" \ + "$_target" \ + "$GATEWAY_CHECKSUMS_NAME" \ + "$_install_dir" \ + "$_tmpdir" + + install_release_binary \ + "$DRIVER_VM_BIN" \ + "$RELEASE_TAG" \ + "$_target" \ + "$CHECKSUMS_NAME" \ + "$_driver_dir" \ + "$_tmpdir" + + codesign_driver_vm "${_driver_dir}/${DRIVER_VM_BIN}" "$_tmpdir" + write_macos_launch_agent "${_install_dir}/${GATEWAY_BIN}" "$_driver_dir" + + info "installed ${CLI_BIN} to ${_install_dir}/${CLI_BIN}" + info "installed ${GATEWAY_BIN} to ${_install_dir}/${GATEWAY_BIN}" + info "installed ${DRIVER_VM_BIN} to ${_driver_dir}/${DRIVER_VM_BIN}" + info "installed LaunchAgent to ${MACOS_LAUNCH_AGENT_PLIST}" + + start_macos_gateway "${_install_dir}/${GATEWAY_BIN}" "${_install_dir}/${CLI_BIN}" "$_driver_dir" +} + +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) + install_linux_deb + ;; + darwin) + install_macos_tarballs + ;; + *) + error "unsupported platform: ${PLATFORM}" + ;; + esac +} + main "$@" From 6fb01a50abd299c43327fafe59a03ef046a0e87b Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Wed, 6 May 2026 11:34:42 -0700 Subject: [PATCH 2/3] feat(release): publish Homebrew formula for tags Signed-off-by: Drew Newberry --- .github/workflows/release-tag.yml | 19 ++ README.md | 17 +- architecture/custom-vm-runtime.md | 5 +- crates/openshell-driver-vm/README.md | 6 +- docs/get-started/quickstart.mdx | 11 ++ python/openshell/release_formula_test.py | 55 ++++++ tasks/scripts/release.py | 213 +++++++++++++++++++++++ 7 files changed, 322 insertions(+), 4 deletions(-) create mode 100644 python/openshell/release_formula_test.py diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index e11d5af53..31275fe93 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -789,6 +789,15 @@ jobs: openshell-sandbox-aarch64-unknown-linux-gnu.tar.gz > openshell-sandbox-checksums-sha256.txt cat openshell-sandbox-checksums-sha256.txt + - name: Generate Homebrew formula + run: | + set -euo pipefail + python3 tasks/scripts/release.py generate-homebrew-formula \ + --release-tag "${RELEASE_TAG}" \ + --release-dir release \ + --output release/openshell.rb + cat release/openshell.rb + - name: Attest VM driver artifacts uses: actions/attest@v4 with: @@ -835,6 +844,15 @@ jobs: curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_VERSION=${{ env.RELEASE_TAG }} sh ``` + ### Apple Silicon macOS local gateway via Homebrew + + ```bash + curl -fsSLo /tmp/openshell.rb https://github.com/NVIDIA/OpenShell/releases/download/${{ env.RELEASE_TAG }}/openshell.rb + brew install /tmp/openshell.rb + brew services restart openshell + openshell gateway add http://127.0.0.1:17670 --local --name local + ``` + files: | release/openshell-x86_64-unknown-linux-musl.tar.gz release/openshell-aarch64-unknown-linux-musl.tar.gz @@ -850,6 +868,7 @@ jobs: release/openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz release/openshell-driver-vm-aarch64-apple-darwin.tar.gz release/*.whl + release/openshell.rb release/openshell-checksums-sha256.txt release/openshell-gateway-checksums-sha256.txt release/openshell-sandbox-checksums-sha256.txt diff --git a/README.md b/README.md index b77385328..47c398065 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,22 @@ curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | uv tool install -U openshell ``` -Both methods install the latest stable release by default. To install a specific version, set `OPENSHELL_VERSION` (binary) or pin the version with `uv tool install openshell==`. A [`dev` release](https://github.com/NVIDIA/OpenShell/releases/tag/dev) is also available that tracks the latest commit on `main`. +**Apple Silicon macOS local gateway via Homebrew formula:** + +```bash +VERSION=v0.0.10 # replace with the release tag you want +curl -fsSLo /tmp/openshell.rb "https://github.com/NVIDIA/OpenShell/releases/download/${VERSION}/openshell.rb" +brew install /tmp/openshell.rb +brew services restart openshell +openshell gateway add http://127.0.0.1:17670 --local --name local +``` + +The binary and PyPI methods install the latest stable release by default. To +install a specific version, set `OPENSHELL_VERSION` (binary), pin the version +with `uv tool install openshell==`, or download the matching +`openshell.rb` formula from a tagged release. A +[`dev` release](https://github.com/NVIDIA/OpenShell/releases/tag/dev) is also +available that tracks the latest commit on `main`. ### Create a sandbox diff --git a/architecture/custom-vm-runtime.md b/architecture/custom-vm-runtime.md index fb3512bfd..8f3815030 100644 --- a/architecture/custom-vm-runtime.md +++ b/architecture/custom-vm-runtime.md @@ -326,8 +326,9 @@ only for the kernel build). macOS driver binaries produced via osxcross are not codesigned. Development builds are signed automatically by `tasks/scripts/gateway-vm.sh` (registered as `mise run gateway:vm`) and by `install-dev.sh` when it installs -the Apple Silicon development tarballs from the selected release. A packaged -release needs signing in CI. +the Apple Silicon development tarballs from the selected release. Tagged +releases also publish a generated Homebrew formula that ad-hoc signs the +installed driver in `post_install`. A packaged release needs signing in CI. ## Rollout Strategy diff --git a/crates/openshell-driver-vm/README.md b/crates/openshell-driver-vm/README.md index 7b071dfec..dfae5972a 100644 --- a/crates/openshell-driver-vm/README.md +++ b/crates/openshell-driver-vm/README.md @@ -187,6 +187,10 @@ On Apple Silicon macOS, `install-dev.sh` installs `openshell`, tarballs. It ad-hoc signs `openshell-driver-vm` with the Hypervisor entitlement before registering the local LaunchAgent gateway. +Tagged releases also publish an `openshell.rb` release asset. Installing that +formula by path lets Homebrew own the binary layout and `brew services` gateway +lifecycle without requiring a tap or cask yet. + ## Relationship to `openshell-vm` `openshell-vm` is a separate, legacy crate that runs the **whole OpenShell gateway inside a single VM**. It remains in the repository for later deprecation or removal, but is excluded from normal workspace builds and release paths. `openshell-driver-vm` is the active compute driver called by a host-resident gateway to spawn **per-sandbox VMs**. The driver vendors its own rootfs handling and runtime loader so `openshell-server` never has to link libkrun. @@ -194,4 +198,4 @@ before registering the local LaunchAgent gateway. ## TODOs - The gateway still configures the driver via CLI args; this will move to a gRPC bootstrap call so the driver interface is uniform across backends. See the `TODO(driver-abstraction)` notes in `crates/openshell-server/src/lib.rs` and `crates/openshell-server/src/compute/vm.rs`. -- macOS local builds are codesigned by `tasks/scripts/gateway-vm.sh`; `install-dev.sh` signs the release tarball driver for local dev installs. +- macOS local builds are codesigned by `tasks/scripts/gateway-vm.sh`; `install-dev.sh` and the tagged-release Homebrew formula sign the release tarball driver for local installs. diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index 903133876..d545af104 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -33,6 +33,17 @@ If you prefer [uv](https://docs.astral.sh/uv/): uv tool install -U openshell ``` +Tagged releases also publish a temporary Homebrew formula for Apple Silicon +macOS local gateways: + +```shell +VERSION=v0.0.10 # replace with the release tag you want +curl -fsSLo /tmp/openshell.rb "https://github.com/NVIDIA/OpenShell/releases/download/${VERSION}/openshell.rb" +brew install /tmp/openshell.rb +brew services restart openshell +openshell gateway add http://127.0.0.1:17670 --local --name local +``` + After installing the CLI, run `openshell --help` in your terminal to see the full CLI reference. diff --git a/python/openshell/release_formula_test.py b/python/openshell/release_formula_test.py new file mode 100644 index 000000000..414cbe199 --- /dev/null +++ b/python/openshell/release_formula_test.py @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +def test_generate_homebrew_formula_uses_tagged_macos_driver_asset( + tmp_path: Path, +) -> None: + release_dir = tmp_path / "release" + release_dir.mkdir() + (release_dir / "openshell-checksums-sha256.txt").write_text( + "\n".join( + [ + "a" * 64 + " openshell-aarch64-apple-darwin.tar.gz", + "b" * 64 + " openshell-driver-vm-aarch64-apple-darwin.tar.gz", + ] + ) + + "\n", + encoding="utf-8", + ) + (release_dir / "openshell-gateway-checksums-sha256.txt").write_text( + "d" * 64 + " openshell-gateway-aarch64-apple-darwin.tar.gz\n", + encoding="utf-8", + ) + + repo_root = Path(__file__).resolve().parents[2] + output = tmp_path / "openshell.rb" + subprocess.run( + [ + sys.executable, + str(repo_root / "tasks/scripts/release.py"), + "generate-homebrew-formula", + "--release-tag", + "v0.0.10", + "--release-dir", + str(release_dir), + "--output", + str(output), + ], + check=True, + ) + + formula = output.read_text(encoding="utf-8") + assert ( + "https://github.com/NVIDIA/OpenShell/releases/download/" + "v0.0.10/openshell-driver-vm-aarch64-apple-darwin.tar.gz" + ) in formula + assert 'sha256 "' + "b" * 64 + '"' in formula + assert 'OPENSHELL_DRIVER_DIR: "#{opt_libexec}"' in formula + assert "brew services restart openshell" in formula diff --git a/tasks/scripts/release.py b/tasks/scripts/release.py index 0fb229022..d1bfc2b2c 100644 --- a/tasks/scripts/release.py +++ b/tasks/scripts/release.py @@ -29,6 +29,16 @@ class Versions: git_distance: int +HOMEBREW_TARGET = "aarch64-apple-darwin" +HOMEBREW_CLI_ASSET = f"openshell-{HOMEBREW_TARGET}.tar.gz" +HOMEBREW_GATEWAY_ASSET = f"openshell-gateway-{HOMEBREW_TARGET}.tar.gz" +HOMEBREW_DRIVER_VM_ASSET = f"openshell-driver-vm-{HOMEBREW_TARGET}.tar.gz" +GITHUB_RELEASE_DOWNLOADS = "https://github.com/NVIDIA/OpenShell/releases/download" +LOCAL_GATEWAY_PORT = 17670 +_SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$") +_RELEASE_TAG_RE = re.compile(r"^[A-Za-z0-9._-]+$") + + def _repo_root() -> Path: return Path(__file__).resolve().parents[2] @@ -166,6 +176,181 @@ def get_version(format: str) -> None: _print_env(versions) +def _parse_sha256_file(path: Path) -> dict[str, str]: + checksums: dict[str, str] = {} + for line_number, line in enumerate( + path.read_text(encoding="utf-8").splitlines(), 1 + ): + line = line.strip() + if not line: + continue + + parts = line.split() + if len(parts) < 2: + raise ValueError(f"{path}:{line_number}: malformed checksum line") + + digest = parts[0].lower() + if not _SHA256_RE.fullmatch(digest): + raise ValueError(f"{path}:{line_number}: invalid SHA-256 digest") + + filename = parts[1].lstrip("*") + checksums[filename] = digest + + return checksums + + +def _required_checksum( + checksums: dict[str, str], + filename: str, + checksum_path: Path, +) -> str: + try: + return checksums[filename] + except KeyError as exc: + raise ValueError(f"{checksum_path}: missing checksum for {filename}") from exc + + +def _asset_url(release_tag: str, filename: str) -> str: + return f"{GITHUB_RELEASE_DOWNLOADS}/{release_tag}/{filename}" + + +def render_homebrew_formula( + *, + release_tag: str, + cli_sha256: str, + gateway_sha256: str, + driver_vm_sha256: str, +) -> str: + if not _RELEASE_TAG_RE.fullmatch(release_tag): + raise ValueError(f"release tag contains unsupported characters: {release_tag}") + + version = release_tag.removeprefix("v") + return f"""# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Generated by tasks/scripts/release.py. Do not edit by hand. + +class Openshell < Formula + desc "Safe, private runtime for autonomous AI agents" + homepage "https://github.com/NVIDIA/OpenShell" + url "{_asset_url(release_tag, HOMEBREW_CLI_ASSET)}" + sha256 "{cli_sha256}" + version "{version}" + license "Apache-2.0" + + depends_on macos: :big_sur + depends_on arch: :arm64 + + resource "openshell-gateway" do + url "{_asset_url(release_tag, HOMEBREW_GATEWAY_ASSET)}" + sha256 "{gateway_sha256}" + end + + resource "openshell-driver-vm" do + url "{_asset_url(release_tag, HOMEBREW_DRIVER_VM_ASSET)}" + sha256 "{driver_vm_sha256}" + end + + def install + odie "OpenShell Homebrew formula currently supports macOS only" unless OS.mac? + + bin.install "openshell" + + resource("openshell-gateway").stage do + bin.install "openshell-gateway" + end + + resource("openshell-driver-vm").stage do + libexec.install "openshell-driver-vm" + end + end + + def post_install + (var/"openshell/gateway").mkpath + (var/"openshell/vm-driver").mkpath + (var/"log/openshell").mkpath + + entitlements = var/"openshell/openshell-driver-vm.entitlements.plist" + entitlements.write <<~XML + + + + + com.apple.security.hypervisor + + + + XML + + system "/usr/bin/codesign", "--entitlements", entitlements, "--force", "-s", "-", libexec/"openshell-driver-vm" + end + + service do + run opt_bin/"openshell-gateway" + environment_variables( + OPENSHELL_BIND_ADDRESS: "127.0.0.1", + OPENSHELL_SERVER_PORT: "{LOCAL_GATEWAY_PORT}", + OPENSHELL_DISABLE_TLS: "true", + OPENSHELL_DISABLE_GATEWAY_AUTH: "true", + OPENSHELL_DB_URL: "sqlite:#{{var}}/openshell/gateway/openshell.db", + OPENSHELL_DRIVERS: "vm", + OPENSHELL_GRPC_ENDPOINT: "http://127.0.0.1:{LOCAL_GATEWAY_PORT}", + OPENSHELL_SSH_GATEWAY_HOST: "127.0.0.1", + OPENSHELL_SSH_GATEWAY_PORT: "{LOCAL_GATEWAY_PORT}", + OPENSHELL_VM_DRIVER_STATE_DIR: "#{{var}}/openshell/vm-driver", + OPENSHELL_DRIVER_DIR: "#{{opt_libexec}}", + ) + keep_alive successful_exit: false + log_path var/"log/openshell/openshell-gateway.out.log" + error_log_path var/"log/openshell/openshell-gateway.err.log" + end + + def caveats + <<~EOS + Start or restart the local gateway with: + brew services restart openshell + + Register it with the OpenShell CLI: + openshell gateway add http://127.0.0.1:{LOCAL_GATEWAY_PORT} --local --name local + EOS + end + + test do + assert_match "openshell ", shell_output("#{{bin}}/openshell --version") + end +end +""" + + +def generate_homebrew_formula( + *, + release_tag: str, + release_dir: Path, + output: Path, +) -> None: + checksums_path = release_dir / "openshell-checksums-sha256.txt" + gateway_checksums_path = release_dir / "openshell-gateway-checksums-sha256.txt" + checksums = _parse_sha256_file(checksums_path) + gateway_checksums = _parse_sha256_file(gateway_checksums_path) + + formula = render_homebrew_formula( + release_tag=release_tag, + cli_sha256=_required_checksum(checksums, HOMEBREW_CLI_ASSET, checksums_path), + gateway_sha256=_required_checksum( + gateway_checksums, + HOMEBREW_GATEWAY_ASSET, + gateway_checksums_path, + ), + driver_vm_sha256=_required_checksum( + checksums, + HOMEBREW_DRIVER_VM_ASSET, + checksums_path, + ), + ) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(formula, encoding="utf-8") + + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="OpenClaw release tooling.") sub = parser.add_subparsers(dest="command", required=True) @@ -193,6 +378,28 @@ def build_parser() -> argparse.ArgumentParser: "--json", action="store_true", help="Print all versions as JSON." ) + formula_parser = sub.add_parser( + "generate-homebrew-formula", + help="Generate the per-release Homebrew formula asset.", + ) + formula_parser.add_argument( + "--release-tag", + required=True, + help="GitHub release tag that owns the formula assets.", + ) + formula_parser.add_argument( + "--release-dir", + type=Path, + required=True, + help="Directory containing release artifacts and checksum files.", + ) + formula_parser.add_argument( + "--output", + type=Path, + required=True, + help="Path to write the generated Formula Ruby file.", + ) + return parser @@ -217,6 +424,12 @@ def main() -> None: get_version("json") else: get_version("all") + elif args.command == "generate-homebrew-formula": + generate_homebrew_formula( + release_tag=args.release_tag, + release_dir=args.release_dir, + output=args.output, + ) if __name__ == "__main__": From 12f56b957226f38dc14414837a5d8127e8e84c28 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Wed, 6 May 2026 12:44:19 -0700 Subject: [PATCH 3/3] refactor(installer): use Homebrew for macOS dev installs Signed-off-by: Drew Newberry --- .github/workflows/release-dev.yml | 10 + .github/workflows/release-tag.yml | 9 - README.md | 17 +- architecture/custom-vm-runtime.md | 7 +- crates/openshell-driver-vm/README.md | 15 +- docs/get-started/quickstart.mdx | 11 - install-dev.sh | 401 +++------------------------ 7 files changed, 65 insertions(+), 405 deletions(-) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index b86a25791..452cc8209 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -785,6 +785,15 @@ jobs: openshell-sandbox-aarch64-unknown-linux-gnu.tar.gz > openshell-sandbox-checksums-sha256.txt cat openshell-sandbox-checksums-sha256.txt + - name: Generate Homebrew formula + run: | + set -euo pipefail + python3 tasks/scripts/release.py generate-homebrew-formula \ + --release-tag dev \ + --release-dir release \ + --output release/openshell.rb + cat release/openshell.rb + - name: Attest VM driver artifacts uses: actions/attest@v4 with: @@ -882,6 +891,7 @@ jobs: release/openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz release/openshell-driver-vm-aarch64-apple-darwin.tar.gz release/*.whl + release/openshell.rb release/openshell-checksums-sha256.txt release/openshell-gateway-checksums-sha256.txt release/openshell-sandbox-checksums-sha256.txt diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 31275fe93..5001c6aea 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -844,15 +844,6 @@ jobs: curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_VERSION=${{ env.RELEASE_TAG }} sh ``` - ### Apple Silicon macOS local gateway via Homebrew - - ```bash - curl -fsSLo /tmp/openshell.rb https://github.com/NVIDIA/OpenShell/releases/download/${{ env.RELEASE_TAG }}/openshell.rb - brew install /tmp/openshell.rb - brew services restart openshell - openshell gateway add http://127.0.0.1:17670 --local --name local - ``` - files: | release/openshell-x86_64-unknown-linux-musl.tar.gz release/openshell-aarch64-unknown-linux-musl.tar.gz diff --git a/README.md b/README.md index 47c398065..b77385328 100644 --- a/README.md +++ b/README.md @@ -33,22 +33,7 @@ curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | uv tool install -U openshell ``` -**Apple Silicon macOS local gateway via Homebrew formula:** - -```bash -VERSION=v0.0.10 # replace with the release tag you want -curl -fsSLo /tmp/openshell.rb "https://github.com/NVIDIA/OpenShell/releases/download/${VERSION}/openshell.rb" -brew install /tmp/openshell.rb -brew services restart openshell -openshell gateway add http://127.0.0.1:17670 --local --name local -``` - -The binary and PyPI methods install the latest stable release by default. To -install a specific version, set `OPENSHELL_VERSION` (binary), pin the version -with `uv tool install openshell==`, or download the matching -`openshell.rb` formula from a tagged release. A -[`dev` release](https://github.com/NVIDIA/OpenShell/releases/tag/dev) is also -available that tracks the latest commit on `main`. +Both methods install the latest stable release by default. To install a specific version, set `OPENSHELL_VERSION` (binary) or pin the version with `uv tool install openshell==`. A [`dev` release](https://github.com/NVIDIA/OpenShell/releases/tag/dev) is also available that tracks the latest commit on `main`. ### Create a sandbox diff --git a/architecture/custom-vm-runtime.md b/architecture/custom-vm-runtime.md index 8f3815030..40fd29dcb 100644 --- a/architecture/custom-vm-runtime.md +++ b/architecture/custom-vm-runtime.md @@ -325,10 +325,9 @@ only for the kernel build). macOS driver binaries produced via osxcross are not codesigned. Development builds are signed automatically by `tasks/scripts/gateway-vm.sh` -(registered as `mise run gateway:vm`) and by `install-dev.sh` when it installs -the Apple Silicon development tarballs from the selected release. Tagged -releases also publish a generated Homebrew formula that ad-hoc signs the -installed driver in `post_install`. A packaged release needs signing in CI. +(registered as `mise run gateway:vm`) and by the generated Homebrew formula +when `install-dev.sh` installs the selected release on Apple Silicon macOS. A +packaged release needs signing in CI. ## Rollout Strategy diff --git a/crates/openshell-driver-vm/README.md b/crates/openshell-driver-vm/README.md index dfae5972a..d90d4b843 100644 --- a/crates/openshell-driver-vm/README.md +++ b/crates/openshell-driver-vm/README.md @@ -182,14 +182,11 @@ On Linux amd64 and arm64, `install-dev.sh` installs the Debian package from the selected `OPENSHELL_VERSION` release tag. That package includes `openshell-gateway` and `openshell-driver-vm`. -On Apple Silicon macOS, `install-dev.sh` installs `openshell`, -`openshell-gateway`, and `openshell-driver-vm` from the selected release -tarballs. It ad-hoc signs `openshell-driver-vm` with the Hypervisor entitlement -before registering the local LaunchAgent gateway. - -Tagged releases also publish an `openshell.rb` release asset. Installing that -formula by path lets Homebrew own the binary layout and `brew services` gateway -lifecycle without requiring a tap or cask yet. +On Apple Silicon macOS, `install-dev.sh` installs the generated `openshell.rb` +formula from the selected release. Homebrew installs `openshell`, +`openshell-gateway`, and `openshell-driver-vm`, ad-hoc signs the driver with +the Hypervisor entitlement in `post_install`, and owns the `brew services` +gateway lifecycle. ## Relationship to `openshell-vm` @@ -198,4 +195,4 @@ lifecycle without requiring a tap or cask yet. ## TODOs - The gateway still configures the driver via CLI args; this will move to a gRPC bootstrap call so the driver interface is uniform across backends. See the `TODO(driver-abstraction)` notes in `crates/openshell-server/src/lib.rs` and `crates/openshell-server/src/compute/vm.rs`. -- macOS local builds are codesigned by `tasks/scripts/gateway-vm.sh`; `install-dev.sh` and the tagged-release Homebrew formula sign the release tarball driver for local installs. +- macOS local builds are codesigned by `tasks/scripts/gateway-vm.sh`; the generated Homebrew formula signs the release tarball driver for local installs. diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index d545af104..903133876 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -33,17 +33,6 @@ If you prefer [uv](https://docs.astral.sh/uv/): uv tool install -U openshell ``` -Tagged releases also publish a temporary Homebrew formula for Apple Silicon -macOS local gateways: - -```shell -VERSION=v0.0.10 # replace with the release tag you want -curl -fsSLo /tmp/openshell.rb "https://github.com/NVIDIA/OpenShell/releases/download/${VERSION}/openshell.rb" -brew install /tmp/openshell.rb -brew services restart openshell -openshell gateway add http://127.0.0.1:17670 --local --name local -``` - After installing the CLI, run `openshell --help` in your terminal to see the full CLI reference. diff --git a/install-dev.sh b/install-dev.sh index 162fb05cb..f53c99bed 100755 --- a/install-dev.sh +++ b/install-dev.sh @@ -4,9 +4,9 @@ # # Install the OpenShell development build from a GitHub release. # -# This script is intended as a convenient installer for development builds. It -# supports Debian packages on Linux amd64/arm64 and a manual per-user install -# on Apple Silicon macOS. +# Linux keeps the Debian package install path. Apple Silicon macOS installs the +# generated Homebrew formula from the selected release, so Homebrew owns the +# binary layout and launchd service lifecycle. # set -e @@ -15,11 +15,6 @@ REPO="NVIDIA/OpenShell" GITHUB_URL="https://github.com/${REPO}" RELEASE_TAG="${OPENSHELL_VERSION:-dev}" CHECKSUMS_NAME="openshell-checksums-sha256.txt" -GATEWAY_CHECKSUMS_NAME="openshell-gateway-checksums-sha256.txt" -CLI_BIN="openshell" -GATEWAY_BIN="openshell-gateway" -DRIVER_VM_BIN="openshell-driver-vm" -MACOS_LAUNCH_AGENT_LABEL="com.nvidia.openshell.gateway" LOCAL_GATEWAY_PORT="17670" info() { @@ -49,19 +44,15 @@ OPTIONS: --help Print this help message ENVIRONMENT VARIABLES: - OPENSHELL_VERSION Release tag to install (default: dev). - OPENSHELL_INSTALL_DIR macOS directory for openshell and openshell-gateway - (default: ~/.local/bin). - OPENSHELL_DRIVER_DIR macOS directory for openshell-driver-vm - (default: ~/.local/libexec/openshell). + 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 and arm64. - macOS installs a per-user Apple Silicon build from release tarballs and - starts a LaunchAgent-backed local gateway. + macOS installs the release Homebrew formula on Apple Silicon and starts a + brew services-backed local gateway. EOF } @@ -197,12 +188,11 @@ check_linux_platform() { require_cmd dpkg } -get_macos_target() { +check_macos_platform() { _arch="$(uname -m)" case "$_arch" in arm64|aarch64) - echo "aarch64-apple-darwin" ;; x86_64|amd64) error "Intel macOS is not supported because no x86_64-apple-darwin dev assets are published" @@ -211,6 +201,10 @@ get_macos_target() { 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() { @@ -275,302 +269,6 @@ install_deb_package() { fi } -target_group() { - id -gn "$TARGET_USER" 2>/dev/null || echo "$TARGET_USER" -} - -is_target_home_path() { - _path="$1" - case "$_path" in - "$TARGET_HOME"|"$TARGET_HOME"/*) - return 0 - ;; - *) - return 1 - ;; - esac -} - -chown_to_target_user() { - _path="$1" - - if [ "${TARGET_UID:-0}" = "0" ] || ! is_target_home_path "$_path"; then - return 0 - fi - - if [ "$(id -u)" -eq "$TARGET_UID" ]; then - return 0 - fi - - _owner="${TARGET_USER}:$(target_group)" - if [ "$(id -u)" -eq 0 ]; then - chown "$_owner" "$_path" 2>/dev/null || chown "$TARGET_USER" "$_path" 2>/dev/null || true - elif has_cmd sudo; then - sudo chown "$_owner" "$_path" 2>/dev/null || sudo chown "$TARGET_USER" "$_path" 2>/dev/null || true - fi -} - -ensure_dir() { - _dir="$1" - - if mkdir -p "$_dir" 2>/dev/null; then - : - else - as_root mkdir -p "$_dir" - fi - - chown_to_target_user "$_dir" -} - -install_executable() { - _src="$1" - _dst="$2" - _dst_dir="$(dirname "$_dst")" - - if mkdir -p "$_dst_dir" 2>/dev/null && [ -w "$_dst_dir" ]; then - install -m 0755 "$_src" "$_dst" - else - info "elevated permissions required to install to ${_dst_dir}" - as_root mkdir -p "$_dst_dir" - as_root install -m 0755 "$_src" "$_dst" - fi - - chown_to_target_user "$_dst" -} - -require_absolute_path() { - _name="$1" - _path="$2" - - case "$_path" in - /*) - ;; - *) - error "${_name} must be an absolute path on macOS; got ${_path}" - ;; - esac -} - -macos_install_dir() { - if [ -n "${OPENSHELL_INSTALL_DIR:-}" ]; then - echo "$OPENSHELL_INSTALL_DIR" - else - echo "${TARGET_HOME}/.local/bin" - fi -} - -macos_driver_dir() { - if [ -n "${OPENSHELL_DRIVER_DIR:-}" ]; then - echo "$OPENSHELL_DRIVER_DIR" - else - echo "${TARGET_HOME}/.local/libexec/openshell" - fi -} - -install_release_binary() { - _bin="$1" - _tag="$2" - _target="$3" - _checksums_name="$4" - _dest_dir="$5" - _work_dir="$6" - - _filename="${_bin}-${_target}.tar.gz" - _asset_path="${_work_dir}/${_filename}" - _checksums_path="${_work_dir}/${_bin}-${_tag}-checksums.txt" - _asset_url="${GITHUB_URL}/releases/download/${_tag}/${_filename}" - _checksums_url="${GITHUB_URL}/releases/download/${_tag}/${_checksums_name}" - - info "downloading ${_filename}..." - download_release_asset "$_tag" "$_filename" "$_asset_path" || { - error "failed to download ${_asset_url}" - } - - info "downloading ${_tag} release checksums for ${_bin}..." - download "$_checksums_url" "$_checksums_path" || { - error "failed to download ${_checksums_url}" - } - - info "verifying ${_filename}..." - verify_checksum "$_asset_path" "$_checksums_path" "$_filename" - - info "installing ${_bin}..." - tar -xzf "$_asset_path" -C "$_work_dir" "$_bin" - install_executable "${_work_dir}/${_bin}" "${_dest_dir}/${_bin}" -} - -codesign_driver_vm() { - _binary="$1" - _work_dir="$2" - - if ! has_cmd codesign; then - warn "codesign not found; ${DRIVER_VM_BIN} will fail without the Hypervisor entitlement" - return 0 - fi - - info "codesigning ${DRIVER_VM_BIN} with Hypervisor entitlement..." - _entitlements="${_work_dir}/entitlements.plist" - cat > "$_entitlements" <<'PLIST' - - - - - com.apple.security.hypervisor - - - -PLIST - codesign --entitlements "$_entitlements" --force -s - "$_binary" -} - -xml_escape() { - printf '%s' "$1" | sed \ - -e 's/&/\&/g' \ - -e 's//\>/g' \ - -e 's/"/\"/g' -} - -write_macos_launch_agent() { - _gateway_bin="$1" - _driver_dir="$2" - - MACOS_GATEWAY_STATE_DIR="${TARGET_HOME}/.local/state/openshell/gateway" - MACOS_VM_STATE_DIR="${TARGET_HOME}/.local/state/openshell/vm-driver" - MACOS_LAUNCH_AGENT_DIR="${TARGET_HOME}/Library/LaunchAgents" - MACOS_LAUNCH_AGENT_PLIST="${MACOS_LAUNCH_AGENT_DIR}/${MACOS_LAUNCH_AGENT_LABEL}.plist" - - ensure_dir "$MACOS_GATEWAY_STATE_DIR" - ensure_dir "$MACOS_VM_STATE_DIR" - ensure_dir "$MACOS_LAUNCH_AGENT_DIR" - - _gateway_bin_xml="$(xml_escape "$_gateway_bin")" - _driver_dir_xml="$(xml_escape "$_driver_dir")" - _db_url_xml="$(xml_escape "sqlite:${MACOS_GATEWAY_STATE_DIR}/openshell.db")" - _vm_state_xml="$(xml_escape "$MACOS_VM_STATE_DIR")" - _stdout_xml="$(xml_escape "${MACOS_GATEWAY_STATE_DIR}/openshell-gateway.out.log")" - _stderr_xml="$(xml_escape "${MACOS_GATEWAY_STATE_DIR}/openshell-gateway.err.log")" - - cat > "$MACOS_LAUNCH_AGENT_PLIST" < - - - - Label - ${MACOS_LAUNCH_AGENT_LABEL} - ProgramArguments - - ${_gateway_bin_xml} - - EnvironmentVariables - - OPENSHELL_BIND_ADDRESS - 127.0.0.1 - OPENSHELL_SERVER_PORT - ${LOCAL_GATEWAY_PORT} - OPENSHELL_DISABLE_TLS - true - OPENSHELL_DISABLE_GATEWAY_AUTH - true - OPENSHELL_DB_URL - ${_db_url_xml} - OPENSHELL_DRIVERS - vm - OPENSHELL_GRPC_ENDPOINT - http://127.0.0.1:${LOCAL_GATEWAY_PORT} - OPENSHELL_SSH_GATEWAY_HOST - 127.0.0.1 - OPENSHELL_SSH_GATEWAY_PORT - ${LOCAL_GATEWAY_PORT} - OPENSHELL_VM_DRIVER_STATE_DIR - ${_vm_state_xml} - OPENSHELL_DRIVER_DIR - ${_driver_dir_xml} - - RunAtLoad - - KeepAlive - - SuccessfulExit - - - StandardOutPath - ${_stdout_xml} - StandardErrorPath - ${_stderr_xml} - - -EOF - - chmod 0644 "$MACOS_LAUNCH_AGENT_PLIST" - chown_to_target_user "$MACOS_LAUNCH_AGENT_PLIST" -} - -print_macos_manual_commands() { - _gateway_bin="$1" - _cli_bin="$2" - _driver_dir="$3" - - info "start the gateway manually with:" - cat >&2 </dev/null 2>&1 || true - - if launchctl bootstrap "$_domain" "$MACOS_LAUNCH_AGENT_PLIST" >/dev/null 2>&1; then - if ! launchctl kickstart -k "${_domain}/${MACOS_LAUNCH_AGENT_LABEL}" >/dev/null 2>&1; then - warn "launchctl kickstart failed; skipping automatic gateway registration" - print_macos_manual_commands "$_gateway_bin" "$_cli_bin" "$_driver_dir" - return 0 - fi - elif launchctl load -w "$MACOS_LAUNCH_AGENT_PLIST" >/dev/null 2>&1; then - : - else - warn "launchctl could not load ${MACOS_LAUNCH_AGENT_PLIST}; skipping automatic gateway registration" - print_macos_manual_commands "$_gateway_bin" "$_cli_bin" "$_driver_dir" - return 0 - fi - - if ! launchctl print "${_domain}/${MACOS_LAUNCH_AGENT_LABEL}" >/dev/null 2>&1; then - warn "LaunchAgent did not appear in launchd; skipping automatic gateway registration" - print_macos_manual_commands "$_gateway_bin" "$_cli_bin" "$_driver_dir" - return 0 - fi - - info "registering local gateway as ${TARGET_USER}..." - OPENSHELL_REGISTER_BIN="$_cli_bin" - register_local_gateway -} - start_user_gateway() { info "restarting openshell-gateway user service as ${TARGET_USER}..." @@ -674,53 +372,44 @@ install_linux_deb() { start_user_gateway } -install_macos_tarballs() { - require_cmd tar - require_cmd install - - _target="$(get_macos_target)" - _install_dir="$(macos_install_dir)" - _driver_dir="$(macos_driver_dir)" - require_absolute_path "OPENSHELL_INSTALL_DIR" "$_install_dir" - require_absolute_path "OPENSHELL_DRIVER_DIR" "$_driver_dir" +install_macos_homebrew() { + check_macos_platform _tmpdir="$(mktemp -d)" chmod 0755 "$_tmpdir" trap 'rm -rf "$_tmpdir"' EXIT - install_release_binary \ - "$CLI_BIN" \ - "$RELEASE_TAG" \ - "$_target" \ - "$CHECKSUMS_NAME" \ - "$_install_dir" \ - "$_tmpdir" - - install_release_binary \ - "$GATEWAY_BIN" \ - "$RELEASE_TAG" \ - "$_target" \ - "$GATEWAY_CHECKSUMS_NAME" \ - "$_install_dir" \ - "$_tmpdir" - - install_release_binary \ - "$DRIVER_VM_BIN" \ - "$RELEASE_TAG" \ - "$_target" \ - "$CHECKSUMS_NAME" \ - "$_driver_dir" \ - "$_tmpdir" - - codesign_driver_vm "${_driver_dir}/${DRIVER_VM_BIN}" "$_tmpdir" - write_macos_launch_agent "${_install_dir}/${GATEWAY_BIN}" "$_driver_dir" - - info "installed ${CLI_BIN} to ${_install_dir}/${CLI_BIN}" - info "installed ${GATEWAY_BIN} to ${_install_dir}/${GATEWAY_BIN}" - info "installed ${DRIVER_VM_BIN} to ${_driver_dir}/${DRIVER_VM_BIN}" - info "installed LaunchAgent to ${MACOS_LAUNCH_AGENT_PLIST}" - - start_macos_gateway "${_install_dir}/${GATEWAY_BIN}" "${_install_dir}/${CLI_BIN}" "$_driver_dir" + _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" + } + + 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_file" + else + info "installing OpenShell with Homebrew..." + as_target_user brew install --formula "$_formula_file" + fi + + info "restarting OpenShell Homebrew service..." + if ! as_target_user brew services restart openshell; then + warn "could not restart the OpenShell Homebrew service" + info "restart it later with: brew services restart openshell" + info "then register it with: openshell gateway add http://127.0.0.1:${LOCAL_GATEWAY_PORT} --local --name local" + 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 } main() { @@ -749,7 +438,7 @@ main() { install_linux_deb ;; darwin) - install_macos_tarballs + install_macos_homebrew ;; *) error "unsupported platform: ${PLATFORM}"