From 688d3d334a6d6bab84edc8d191fa0dd47ec9c5b8 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Wed, 29 Apr 2026 15:06:35 -0700 Subject: [PATCH 1/4] feat(homebrew): add OpenShell formulae Signed-off-by: Drew Newberry --- .github/workflows/homebrew-formulae.yml | 65 +++ .github/workflows/release-vm-dev.yml | 4 +- Formula/openshell-dev.rb | 118 ++++++ Formula/openshell.rb | 117 +++++ README.md | 9 + architecture/build-containers.md | 15 +- docs/get-started/quickstart.mdx | 9 + mise.toml | 2 +- tasks/scripts/homebrew/update_formulae.py | 495 ++++++++++++++++++++++ 9 files changed, 830 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/homebrew-formulae.yml create mode 100644 Formula/openshell-dev.rb create mode 100644 Formula/openshell.rb create mode 100644 tasks/scripts/homebrew/update_formulae.py diff --git a/.github/workflows/homebrew-formulae.yml b/.github/workflows/homebrew-formulae.yml new file mode 100644 index 000000000..8a15b2391 --- /dev/null +++ b/.github/workflows/homebrew-formulae.yml @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Update Homebrew Formulae + +on: + workflow_dispatch: + workflow_run: + workflows: + - Release Tag + - Release Dev + - Release VM Dev + types: + - completed + +permissions: + contents: write + +concurrency: + group: homebrew-formulae + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + update: + name: Update Formulae + if: github.repository == 'NVIDIA/OpenShell' && (github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success') + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - uses: astral-sh/setup-uv@v5 + with: + version: "0.10.12" + + - name: Render formulae + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + UV_NO_SYNC: "1" + run: uv run python tasks/scripts/homebrew/update_formulae.py + + - name: Commit formula updates + run: | + set -euo pipefail + if git diff --quiet -- Formula/openshell.rb Formula/openshell-dev.rb; then + echo "Formulae are already up to date." + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add Formula/openshell.rb Formula/openshell-dev.rb + git commit -m "chore(homebrew): update formulae [skip ci]" + git push origin HEAD:main diff --git a/.github/workflows/release-vm-dev.yml b/.github/workflows/release-vm-dev.yml index 6619d2a6b..7d04defb4 100644 --- a/.github/workflows/release-vm-dev.yml +++ b/.github/workflows/release-vm-dev.yml @@ -689,8 +689,8 @@ jobs: done vm_count=$(ls release-final/openshell-vm-*.tar.gz 2>/dev/null | wc -l) driver_count=$(ls release-final/openshell-driver-vm-*.tar.gz 2>/dev/null | wc -l) - if [ "$vm_count" -eq 0 ] || [ "$driver_count" -eq 0 ]; then - echo "ERROR: Missing binary tarballs (openshell-vm=${vm_count}, openshell-driver-vm=${driver_count})" >&2 + if [ "$vm_count" -ne 3 ] || [ "$driver_count" -ne 3 ]; then + echo "ERROR: Expected 3 openshell-vm and 3 openshell-driver-vm binary tarballs (openshell-vm=${vm_count}, openshell-driver-vm=${driver_count})" >&2 ls -la release/ || true exit 1 fi diff --git a/Formula/openshell-dev.rb b/Formula/openshell-dev.rb new file mode 100644 index 000000000..bb094ed8c --- /dev/null +++ b/Formula/openshell-dev.rb @@ -0,0 +1,118 @@ +# This file is generated by tasks/scripts/homebrew/update_formulae.py. + +class OpenshellDev < Formula + desc "Development build of the safe private runtime for autonomous AI agents" + homepage "https://github.com/NVIDIA/OpenShell" + version "0.0.0-dev.24724742a89d.vm.20ffc7253b6e" + license "Apache-2.0" + + on_macos do + depends_on arch: :arm64 + + on_arm do + url "https://github.com/NVIDIA/OpenShell/releases/download/dev/openshell-aarch64-apple-darwin.tar.gz" + sha256 "5a01a8137ee6dc5d12a22d090747d3d760ad9bba174d958d876a1b0dcea72abd" + + resource "openshell-gateway" do + url "https://github.com/NVIDIA/OpenShell/releases/download/dev/openshell-gateway-aarch64-apple-darwin.tar.gz" + sha256 "f32b2fc805f036f7ccdc21cdabfe9a190d4660b22d388d44bfb9e09fd9061be3" + end + + resource "openshell-driver-vm" do + url "https://github.com/NVIDIA/OpenShell/releases/download/vm-dev/openshell-driver-vm-aarch64-apple-darwin.tar.gz" + sha256 "29cea5657875848c2f8242388ca050d6a94bf0229435ea62f960154d392fade1" + end + end + end + + on_linux do + on_intel do + url "https://github.com/NVIDIA/OpenShell/releases/download/dev/openshell-x86_64-unknown-linux-musl.tar.gz" + sha256 "3e3ab88059f2d63a088d0be9df92b9efeb2630ac6e0cc6a0bcf2bddaa9a338c2" + + resource "openshell-gateway" do + url "https://github.com/NVIDIA/OpenShell/releases/download/dev/openshell-gateway-x86_64-unknown-linux-gnu.tar.gz" + sha256 "1afb277bd8ccd5dedfa2f8992550e65fd717ae417b39479a89d4874a1e5ed2be" + end + + resource "openshell-driver-vm" do + url "https://github.com/NVIDIA/OpenShell/releases/download/vm-dev/openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz" + sha256 "8d02f4ec0c7517f62119cd92c764c8b3f04647b8a8bd4b1fae2a23f3581cd488" + end + end + + on_arm do + url "https://github.com/NVIDIA/OpenShell/releases/download/dev/openshell-aarch64-unknown-linux-musl.tar.gz" + sha256 "9959ed0e0552ee91e36aa6da8a7b18a42832d61475a0a87f4c5da8f46ebdb990" + + resource "openshell-gateway" do + url "https://github.com/NVIDIA/OpenShell/releases/download/dev/openshell-gateway-aarch64-unknown-linux-gnu.tar.gz" + sha256 "00f972ff27e5ba0e1a78337dcabf84c4d21a77d9d865880b76c63f13279ed4c6" + end + + resource "openshell-driver-vm" do + url "https://github.com/NVIDIA/OpenShell/releases/download/vm-dev/openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz" + sha256 "0e6d16d642a65b57c81be7e83b07a06725dd3f4edd1762f67ac2d5248b550652" + end + end + end + + conflicts_with "openshell", because: "both install OpenShell command names" + + def install + bin.install "openshell" + + resource("openshell-gateway").stage do + libexec.install "openshell-gateway" + end + + driver_dir = libexec/"openshell" + driver_dir.mkpath + resource("openshell-driver-vm").stage do + driver_dir.install "openshell-driver-vm" + end + + if OS.mac? + entitlements = buildpath/"openshell-driver-vm-entitlements.plist" + entitlements.write <<~XML + + + + + com.apple.security.hypervisor + + + + XML + system "codesign", "--entitlements", entitlements, "--force", "-s", "-", driver_dir/"openshell-driver-vm" + end + + gateway_wrapper = bin/"openshell-gateway" + gateway_wrapper.write <<~SH + #!/bin/sh + export OPENSHELL_DRIVER_DIR="#{opt_libexec}/openshell" + exec "#{opt_libexec}/openshell-gateway" "$@" + SH + chmod 0555, gateway_wrapper + + ln_s "../libexec/openshell/openshell-driver-vm", bin/"openshell-driver-vm" + end + + test do + assert_match(/^openshell \S+/, shell_output("#{bin}/openshell --version")) + assert_match(/^openshell-gateway \S+/, shell_output("#{bin}/openshell-gateway --version")) + assert_match(/^openshell-driver-vm \S+/, shell_output("#{bin}/openshell-driver-vm --version")) + + driver = libexec/"openshell/openshell-driver-vm" + assert_path_exists driver + assert_match( + "OPENSHELL_DRIVER_DIR=\"#{opt_libexec}/openshell\"", + (bin/"openshell-gateway").read, + ) + + if OS.mac? + entitlements = shell_output("codesign -d --entitlements :- #{driver} 2>&1") + assert_match "com.apple.security.hypervisor", entitlements + end + end +end diff --git a/Formula/openshell.rb b/Formula/openshell.rb new file mode 100644 index 000000000..d290ff0d7 --- /dev/null +++ b/Formula/openshell.rb @@ -0,0 +1,117 @@ +# This file is generated by tasks/scripts/homebrew/update_formulae.py. + +class Openshell < Formula + desc "Safe private runtime for autonomous AI agents" + homepage "https://github.com/NVIDIA/OpenShell" + license "Apache-2.0" + + on_macos do + depends_on arch: :arm64 + + on_arm do + url "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.36/openshell-aarch64-apple-darwin.tar.gz" + sha256 "4d18ab9b966fcd68cdfc08c1189856ff9fff3734222f35fed038ddca9cd291c3" + + resource "openshell-gateway" do + url "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.36/openshell-gateway-aarch64-apple-darwin.tar.gz" + sha256 "34871f5415b3469c4490b728f2a1853c62d58279724ef8f9fe266f2433dd6ae6" + end + + resource "openshell-driver-vm" do + url "https://github.com/NVIDIA/OpenShell/releases/download/vm-dev/openshell-driver-vm-aarch64-apple-darwin.tar.gz" + sha256 "29cea5657875848c2f8242388ca050d6a94bf0229435ea62f960154d392fade1" + end + end + end + + on_linux do + on_intel do + url "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.36/openshell-x86_64-unknown-linux-musl.tar.gz" + sha256 "cd309b2b750b83b6d4699697071d647109a3212645d0c69de89164b650c6ec00" + + resource "openshell-gateway" do + url "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.36/openshell-gateway-x86_64-unknown-linux-gnu.tar.gz" + sha256 "8a4b12dd5d66fcb116e4eb562541b9dbf2825ad03b5cb681413de2e5b563e9bb" + end + + resource "openshell-driver-vm" do + url "https://github.com/NVIDIA/OpenShell/releases/download/vm-dev/openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz" + sha256 "8d02f4ec0c7517f62119cd92c764c8b3f04647b8a8bd4b1fae2a23f3581cd488" + end + end + + on_arm do + url "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.36/openshell-aarch64-unknown-linux-musl.tar.gz" + sha256 "6fae1119b5cf4e718c96ee5874ada6ab5e35c016aba6e1e0adb5d93e6ab63bc0" + + resource "openshell-gateway" do + url "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.36/openshell-gateway-aarch64-unknown-linux-gnu.tar.gz" + sha256 "62531994c3d966f0e985f45ceb06dd1c8c8a6b068e2779cadfcc1e20d7cac152" + end + + resource "openshell-driver-vm" do + url "https://github.com/NVIDIA/OpenShell/releases/download/vm-dev/openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz" + sha256 "0e6d16d642a65b57c81be7e83b07a06725dd3f4edd1762f67ac2d5248b550652" + end + end + end + + conflicts_with "openshell-dev", because: "both install OpenShell command names" + + def install + bin.install "openshell" + + resource("openshell-gateway").stage do + libexec.install "openshell-gateway" + end + + driver_dir = libexec/"openshell" + driver_dir.mkpath + resource("openshell-driver-vm").stage do + driver_dir.install "openshell-driver-vm" + end + + if OS.mac? + entitlements = buildpath/"openshell-driver-vm-entitlements.plist" + entitlements.write <<~XML + + + + + com.apple.security.hypervisor + + + + XML + system "codesign", "--entitlements", entitlements, "--force", "-s", "-", driver_dir/"openshell-driver-vm" + end + + gateway_wrapper = bin/"openshell-gateway" + gateway_wrapper.write <<~SH + #!/bin/sh + export OPENSHELL_DRIVER_DIR="#{opt_libexec}/openshell" + exec "#{opt_libexec}/openshell-gateway" "$@" + SH + chmod 0555, gateway_wrapper + + ln_s "../libexec/openshell/openshell-driver-vm", bin/"openshell-driver-vm" + end + + test do + assert_match(/^openshell \S+/, shell_output("#{bin}/openshell --version")) + assert_match(/^openshell-gateway \S+/, shell_output("#{bin}/openshell-gateway --version")) + assert_match(/^openshell-driver-vm \S+/, shell_output("#{bin}/openshell-driver-vm --version")) + + driver = libexec/"openshell/openshell-driver-vm" + assert_path_exists driver + assert_match( + "OPENSHELL_DRIVER_DIR=\"#{opt_libexec}/openshell\"", + (bin/"openshell-gateway").read, + ) + + if OS.mac? + entitlements = shell_output("codesign -d --entitlements :- #{driver} 2>&1") + assert_match "com.apple.security.hypervisor", entitlements + end + end +end diff --git a/README.md b/README.md index aaa851452..d600f90d4 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,15 @@ OpenShell is built agent-first. The project ships with agent skills for everythi curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` +**Homebrew tap (CLI, gateway, and VM driver):** + +```bash +brew tap nvidia/openshell https://github.com/NVIDIA/OpenShell.git +brew install nvidia/openshell/openshell +``` + +Use `brew install nvidia/openshell/openshell-dev` for the development track. + **From PyPI (requires [uv](https://docs.astral.sh/uv/)):** ```bash diff --git a/architecture/build-containers.md b/architecture/build-containers.md index 7886c766c..0fbd9c185 100644 --- a/architecture/build-containers.md +++ b/architecture/build-containers.md @@ -35,10 +35,23 @@ OpenShell also publishes a standalone `openshell-gateway` binary as a GitHub rel - **Artifact name**: `openshell-gateway-.tar.gz` - **Targets**: `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `aarch64-apple-darwin` - **Release workflows**: `.github/workflows/release-dev.yml`, `.github/workflows/release-tag.yml` -- **Installer**: None yet. The binary is a manual-download asset. +- **Installers**: `install-vm.sh` for the rolling development gateway, and the Homebrew formulae under `Formula/` for stable and development tracks. Both the standalone artifact and the deployed container image use the `openshell-gateway` binary. +## Homebrew Formulae + +This repository also acts as the Homebrew tap. Users must tap it with the full Git URL because the repository is not named `homebrew-*`: + +```bash +brew tap nvidia/openshell https://github.com/NVIDIA/OpenShell.git +brew install nvidia/openshell/openshell +``` + +`Formula/openshell.rb` installs the latest stable CLI and gateway assets plus the rolling `vm-dev` `openshell-driver-vm` asset. `Formula/openshell-dev.rb` installs the rolling `dev` CLI and gateway assets plus the same `vm-dev` driver. The formulae conflict because they expose the same command names. + +The formulae are generated by `tasks/scripts/homebrew/update_formulae.py`. The script reads the GitHub release checksum files and writes literal `sha256` values for every platform-specific asset. `.github/workflows/homebrew-formulae.yml` runs after the stable, dev, and VM dev release workflows complete successfully and commits updated formulae to `main`. + ## Python Wheels OpenShell also publishes Python wheels for `linux/amd64`, `linux/arm64`, and macOS ARM64. diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index 2f26c7bfb..ff83f1726 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -25,6 +25,15 @@ Run the install script: curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` +If you prefer Homebrew, tap this repository and install the stable track: + +```shell +brew tap nvidia/openshell https://github.com/NVIDIA/OpenShell.git +brew install nvidia/openshell/openshell +``` + +Install `nvidia/openshell/openshell-dev` instead to track the development release. + If you prefer [uv](https://docs.astral.sh/uv/): ```shell diff --git a/mise.toml b/mise.toml index fc4961db8..8998d4d62 100644 --- a/mise.toml +++ b/mise.toml @@ -59,7 +59,7 @@ DOCKER_BUILDKIT = "1" [vars] # Python paths to include in formatting/linting -python_paths = "python/ tasks/scripts/*.py deploy/sbom/*.py" +python_paths = "python/ tasks/scripts/*.py tasks/scripts/homebrew/*.py deploy/sbom/*.py" [task_config] includes = ["tasks/*.toml"] diff --git a/tasks/scripts/homebrew/update_formulae.py b/tasks/scripts/homebrew/update_formulae.py new file mode 100644 index 000000000..afc46dde6 --- /dev/null +++ b/tasks/scripts/homebrew/update_formulae.py @@ -0,0 +1,495 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Render Homebrew formulae from GitHub release checksums.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import subprocess +import sys +import time +import urllib.error +import urllib.request +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +REPO = "NVIDIA/OpenShell" +API_BASE = f"https://api.github.com/repos/{REPO}" +DOWNLOAD_BASE = f"https://github.com/{REPO}/releases/download" + +CLI_CHECKSUMS = "openshell-checksums-sha256.txt" +GATEWAY_CHECKSUMS = "openshell-gateway-checksums-sha256.txt" +VM_CHECKSUMS = "vm-binary-checksums-sha256.txt" + +TARGETS = { + "macos_arm64": { + "cli": "openshell-aarch64-apple-darwin.tar.gz", + "gateway": "openshell-gateway-aarch64-apple-darwin.tar.gz", + "driver": "openshell-driver-vm-aarch64-apple-darwin.tar.gz", + }, + "linux_x86_64": { + "cli": "openshell-x86_64-unknown-linux-musl.tar.gz", + "gateway": "openshell-gateway-x86_64-unknown-linux-gnu.tar.gz", + "driver": "openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz", + }, + "linux_arm64": { + "cli": "openshell-aarch64-unknown-linux-musl.tar.gz", + "gateway": "openshell-gateway-aarch64-unknown-linux-gnu.tar.gz", + "driver": "openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz", + }, +} + + +@dataclass(frozen=True) +class Release: + tag: str + target_commitish: str + published_at: str + html_url: str + assets: dict[str, str] + + +@dataclass(frozen=True) +class FormulaSource: + tag: str + version: str + cli: dict[str, str] + gateway: dict[str, str] + driver_tag: str + driver: dict[str, str] + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[3] + + +def request_headers() -> dict[str, str]: + headers = { + "Accept": "application/vnd.github+json", + "User-Agent": "openshell-homebrew-formula-updater", + "X-GitHub-Api-Version": "2022-11-28", + } + token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") + if token: + headers["Authorization"] = f"Bearer {token}" + return headers + + +def truncate_error_body(body: str) -> str: + if len(body) <= 500: + return body + return body[:500] + "... [truncated]" + + +def fetch_url(url: str, *, accept: str | None = None) -> bytes: + headers = request_headers() + if accept is not None: + headers["Accept"] = accept + request = urllib.request.Request(url, headers=headers) + retry_statuses = {502, 503, 504} + + for attempt in range(1, 4): + try: + with urllib.request.urlopen(request, timeout=30) as response: + return response.read() + except urllib.error.HTTPError as exc: + body = exc.read().decode("utf-8", errors="replace") + if exc.code in retry_statuses and attempt < 3: + time.sleep(attempt) + continue + raise RuntimeError( + f"failed to fetch {url}: HTTP {exc.code}: {truncate_error_body(body)}" + ) from exc + except urllib.error.URLError as exc: + if attempt < 3: + time.sleep(attempt) + continue + raise RuntimeError(f"failed to fetch {url}: {exc.reason}") from exc + + raise RuntimeError(f"failed to fetch {url}") + + +def fetch_json(url: str) -> dict[str, Any]: + return json.loads(fetch_url(url).decode("utf-8")) + + +def release_from_json(data: dict[str, Any]) -> Release: + assets = {} + for asset in data.get("assets", []): + name = asset["name"] + assets[name] = asset["url"] + return Release( + tag=data["tag_name"], + target_commitish=data.get("target_commitish", ""), + published_at=data.get("published_at", ""), + html_url=data.get("html_url", ""), + assets=assets, + ) + + +def fetch_latest_stable() -> Release: + return release_from_json(fetch_json(f"{API_BASE}/releases/latest")) + + +def fetch_release(tag: str) -> Release: + return release_from_json(fetch_json(f"{API_BASE}/releases/tags/{tag}")) + + +def fetch_release_asset(release: Release, name: str) -> str: + try: + url = release.assets[name] + except KeyError as exc: + available = ", ".join(sorted(release.assets)) + raise RuntimeError( + f"release {release.tag} is missing asset {name}; available: {available}" + ) from exc + return fetch_url(url, accept="application/octet-stream").decode("utf-8") + + +def parse_checksums(text: str) -> dict[str, str]: + checksums = {} + for line in text.splitlines(): + if not line.strip(): + continue + parts = line.split(maxsplit=1) + if len(parts) != 2: + raise RuntimeError(f"invalid checksum line: {line!r}") + sha, filename = parts + filename = filename.removeprefix("*") + if not re.fullmatch(r"[0-9a-f]{64}", sha): + raise RuntimeError(f"invalid sha256 for {filename}: {sha}") + checksums[filename] = sha + return checksums + + +def selected_checksums(checksums: dict[str, str], kind: str) -> dict[str, str]: + result = {} + for target, assets in TARGETS.items(): + name = assets[kind] + try: + result[target] = checksums[name] + except KeyError as exc: + raise RuntimeError(f"checksum file is missing {name}") from exc + return result + + +def ensure_release_assets(release: Release, kind: str) -> None: + missing = [] + for assets in TARGETS.values(): + name = assets[kind] + if name not in release.assets: + missing.append(name) + if missing: + missing_text = ", ".join(missing) + raise RuntimeError(f"release {release.tag} is missing assets: {missing_text}") + + +def release_version(tag: str) -> str: + return tag.removeprefix("v") + + +def short_ref(ref: str) -> str: + value = re.sub(r"[^0-9A-Za-z]+", ".", ref).strip(".") + return value[:12] or "unknown" + + +def existing_formula_metadata(path: Path) -> tuple[str | None, int]: + if not path.exists(): + return None, 0 + + text = path.read_text() + version_match = re.search(r'^\s*version "([^"]+)"$', text, re.MULTILINE) + url_version_match = re.search( + r"releases/download/v([^/]+)/openshell-", text, re.MULTILINE + ) + revision_match = re.search(r"^\s*revision (\d+)$", text, re.MULTILINE) + version = ( + version_match.group(1) + if version_match + else url_version_match.group(1) + if url_version_match + else None + ) + revision = int(revision_match.group(1)) if revision_match else 0 + return version, revision + + +def is_git_tracked(path: Path) -> bool: + root = repo_root() + try: + rel_path = path.relative_to(root) + except ValueError: + return False + + result = subprocess.run( + ["git", "-C", str(root), "ls-files", "--error-unmatch", str(rel_path)], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return result.returncode == 0 + + +def with_revision_bump(path: Path, version: str, render: Any) -> str: + existing_version, existing_revision = existing_formula_metadata(path) + revision = existing_revision if existing_version == version else 0 + rendered = render(revision) + + if ( + path.exists() + and is_git_tracked(path) + and existing_version == version + and rendered != path.read_text() + ): + rendered = render(revision + 1) + + return rendered + + +def formula_url(tag: str, asset: str) -> str: + return f"{DOWNLOAD_BASE}/{tag}/{asset}" + + +def resource_block(name: str, url: str, sha: str, indent: str = " ") -> str: + return ( + f'{indent}resource "{name}" do\n' + f'{indent} url "{url}"\n' + f'{indent} sha256 "{sha}"\n' + f"{indent}end\n" + ) + + +def render_platform_blocks(source: FormulaSource) -> str: + macos = TARGETS["macos_arm64"] + linux_x86 = TARGETS["linux_x86_64"] + linux_arm = TARGETS["linux_arm64"] + + return f""" on_macos do + depends_on arch: :arm64 + + on_arm do + url "{formula_url(source.tag, macos["cli"])}" + sha256 "{source.cli["macos_arm64"]}" + +{resource_block("openshell-gateway", formula_url(source.tag, macos["gateway"]), source.gateway["macos_arm64"], " ")} +{resource_block("openshell-driver-vm", formula_url(source.driver_tag, macos["driver"]), source.driver["macos_arm64"], " ")} end + end + + on_linux do + on_intel do + url "{formula_url(source.tag, linux_x86["cli"])}" + sha256 "{source.cli["linux_x86_64"]}" + +{resource_block("openshell-gateway", formula_url(source.tag, linux_x86["gateway"]), source.gateway["linux_x86_64"], " ")} +{resource_block("openshell-driver-vm", formula_url(source.driver_tag, linux_x86["driver"]), source.driver["linux_x86_64"], " ")} end + + on_arm do + url "{formula_url(source.tag, linux_arm["cli"])}" + sha256 "{source.cli["linux_arm64"]}" + +{resource_block("openshell-gateway", formula_url(source.tag, linux_arm["gateway"]), source.gateway["linux_arm64"], " ")} +{resource_block("openshell-driver-vm", formula_url(source.driver_tag, linux_arm["driver"]), source.driver["linux_arm64"], " ")} end + end +""" + + +def install_and_test_block() -> str: + return r""" def install + bin.install "openshell" + + resource("openshell-gateway").stage do + libexec.install "openshell-gateway" + end + + driver_dir = libexec/"openshell" + driver_dir.mkpath + resource("openshell-driver-vm").stage do + driver_dir.install "openshell-driver-vm" + end + + if OS.mac? + entitlements = buildpath/"openshell-driver-vm-entitlements.plist" + entitlements.write <<~XML + + + + + com.apple.security.hypervisor + + + + XML + system "codesign", "--entitlements", entitlements, "--force", "-s", "-", driver_dir/"openshell-driver-vm" + end + + gateway_wrapper = bin/"openshell-gateway" + gateway_wrapper.write <<~SH + #!/bin/sh + export OPENSHELL_DRIVER_DIR="#{opt_libexec}/openshell" + exec "#{opt_libexec}/openshell-gateway" "$@" + SH + chmod 0555, gateway_wrapper + + ln_s "../libexec/openshell/openshell-driver-vm", bin/"openshell-driver-vm" + end + + test do + assert_match(/^openshell \S+/, shell_output("#{bin}/openshell --version")) + assert_match(/^openshell-gateway \S+/, shell_output("#{bin}/openshell-gateway --version")) + assert_match(/^openshell-driver-vm \S+/, shell_output("#{bin}/openshell-driver-vm --version")) + + driver = libexec/"openshell/openshell-driver-vm" + assert_path_exists driver + assert_match( + "OPENSHELL_DRIVER_DIR=\"#{opt_libexec}/openshell\"", + (bin/"openshell-gateway").read, + ) + + if OS.mac? + entitlements = shell_output("codesign -d --entitlements :- #{driver} 2>&1") + assert_match "com.apple.security.hypervisor", entitlements + end + end +""" + + +def render_formula( + *, + class_name: str, + desc: str, + source: FormulaSource, + conflicts_with: str, + include_version: bool = True, + revision: int = 0, +) -> str: + version_line = f' version "{source.version}"\n' if include_version else "" + revision_line = f" revision {revision}\n" if revision else "" + return f'''# This file is generated by tasks/scripts/homebrew/update_formulae.py. + +class {class_name} < Formula + desc "{desc}" + homepage "https://github.com/NVIDIA/OpenShell" +{version_line} license "Apache-2.0" +{revision_line} +{render_platform_blocks(source)} + conflicts_with "{conflicts_with}", because: "both install OpenShell command names" + +{install_and_test_block()}end +''' + + +def build_formula_source( + release: Release, + driver_release: Release, + *, + version: str, +) -> FormulaSource: + ensure_release_assets(release, "cli") + ensure_release_assets(release, "gateway") + ensure_release_assets(driver_release, "driver") + + cli_checksums = parse_checksums(fetch_release_asset(release, CLI_CHECKSUMS)) + gateway_checksums = parse_checksums(fetch_release_asset(release, GATEWAY_CHECKSUMS)) + driver_checksums = parse_checksums( + fetch_release_asset(driver_release, VM_CHECKSUMS) + ) + + return FormulaSource( + tag=release.tag, + version=version, + cli=selected_checksums(cli_checksums, "cli"), + gateway=selected_checksums(gateway_checksums, "gateway"), + driver_tag=driver_release.tag, + driver=selected_checksums(driver_checksums, "driver"), + ) + + +def write_file(path: Path, content: str, *, check: bool) -> bool: + if path.exists() and path.read_text() == content: + return False + if check: + print(f"{path} is not up to date", file=sys.stderr) + return True + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + print(f"wrote {path}") + return True + + +def render_all(*, check: bool) -> bool: + root = repo_root() + formula_dir = root / "Formula" + + stable_release = fetch_latest_stable() + dev_release = fetch_release("dev") + vm_release = fetch_release("vm-dev") + + stable_version = release_version(stable_release.tag) + stable_source = build_formula_source( + stable_release, + vm_release, + version=stable_version, + ) + dev_source = build_formula_source( + dev_release, + vm_release, + version=( + f"0.0.0-dev.{short_ref(dev_release.target_commitish)}" + f".vm.{short_ref(vm_release.target_commitish)}" + ), + ) + + stable_path = formula_dir / "openshell.rb" + stable_formula = with_revision_bump( + stable_path, + stable_version, + lambda revision: render_formula( + class_name="Openshell", + desc="Safe private runtime for autonomous AI agents", + source=stable_source, + conflicts_with="openshell-dev", + include_version=False, + revision=revision, + ), + ) + + dev_formula = render_formula( + class_name="OpenshellDev", + desc="Development build of the safe private runtime for autonomous AI agents", + source=dev_source, + conflicts_with="openshell", + ) + + changed = False + changed |= write_file(stable_path, stable_formula, check=check) + changed |= write_file(formula_dir / "openshell-dev.rb", dev_formula, check=check) + return changed + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--check", + action="store_true", + help="exit non-zero if generated formulae differ from files on disk", + ) + args = parser.parse_args() + + try: + changed = render_all(check=args.check) + except RuntimeError as exc: + print(f"error: {exc}", file=sys.stderr) + raise SystemExit(1) from None + if args.check and changed: + raise SystemExit(1) + + +if __name__ == "__main__": + main() From d3c2e068b4a1268bbf41f7c23ae9a9c5faa86958 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Wed, 29 Apr 2026 15:10:11 -0700 Subject: [PATCH 2/4] docs(homebrew): revert quickstart install notes Signed-off-by: Drew Newberry --- docs/get-started/quickstart.mdx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index ff83f1726..2f26c7bfb 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -25,15 +25,6 @@ Run the install script: curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` -If you prefer Homebrew, tap this repository and install the stable track: - -```shell -brew tap nvidia/openshell https://github.com/NVIDIA/OpenShell.git -brew install nvidia/openshell/openshell -``` - -Install `nvidia/openshell/openshell-dev` instead to track the development release. - If you prefer [uv](https://docs.astral.sh/uv/): ```shell From 17841b4c376ce74190cad74d357403a1091a75b0 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Wed, 29 Apr 2026 21:34:22 -0700 Subject: [PATCH 3/4] feat(homebrew): switch dev install to cask Signed-off-by: Drew Newberry --- .github/workflows/homebrew-dev-cask.yml | 127 ++++++ .github/workflows/homebrew-formulae.yml | 65 --- Casks/openshell-dev.rb | 28 ++ Formula/openshell-dev.rb | 118 ------ Formula/openshell.rb | 117 ----- README.md | 6 +- architecture/build-containers.md | 12 +- mise.toml | 2 +- tasks/scripts/homebrew/update_formulae.py | 495 ---------------------- 9 files changed, 164 insertions(+), 806 deletions(-) create mode 100644 .github/workflows/homebrew-dev-cask.yml delete mode 100644 .github/workflows/homebrew-formulae.yml create mode 100644 Casks/openshell-dev.rb delete mode 100644 Formula/openshell-dev.rb delete mode 100644 Formula/openshell.rb delete mode 100644 tasks/scripts/homebrew/update_formulae.py diff --git a/.github/workflows/homebrew-dev-cask.yml b/.github/workflows/homebrew-dev-cask.yml new file mode 100644 index 000000000..fea137fe1 --- /dev/null +++ b/.github/workflows/homebrew-dev-cask.yml @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Publish Homebrew Dev Cask Bundle + +on: + workflow_dispatch: + workflow_run: + workflows: + - Release Dev + - Release VM Dev + types: + - completed + +permissions: + contents: write + +concurrency: + group: homebrew-dev-cask + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + publish: + name: Publish Dev Cask Bundle + if: github.repository == 'NVIDIA/OpenShell' && (github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success') + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUNDLE_ASSET: openshell-homebrew-dev-aarch64-apple-darwin.tar.gz + steps: + - name: Download release assets + run: | + set -euo pipefail + mkdir -p downloads + + gh release download dev \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern openshell-aarch64-apple-darwin.tar.gz \ + --pattern openshell-gateway-aarch64-apple-darwin.tar.gz \ + --dir downloads \ + --clobber + + gh release download vm-dev \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern openshell-driver-vm-aarch64-apple-darwin.tar.gz \ + --dir downloads \ + --clobber + + test -f downloads/openshell-aarch64-apple-darwin.tar.gz + test -f downloads/openshell-gateway-aarch64-apple-darwin.tar.gz + test -f downloads/openshell-driver-vm-aarch64-apple-darwin.tar.gz + + - name: Build cask bundle + run: | + set -euo pipefail + mkdir -p bundle/libexec/openshell artifacts + + tar -xzf downloads/openshell-aarch64-apple-darwin.tar.gz \ + -C bundle openshell + tar -xzf downloads/openshell-gateway-aarch64-apple-darwin.tar.gz \ + -C bundle/libexec openshell-gateway + tar -xzf downloads/openshell-driver-vm-aarch64-apple-darwin.tar.gz \ + -C bundle/libexec/openshell openshell-driver-vm + + cat > bundle/openshell-gateway <<'SH' + #!/bin/sh + set -e + + path=$0 + while [ -L "$path" ]; do + link=$(readlink "$path") + case "$link" in + /*) path=$link ;; + *) path=$(dirname "$path")/$link ;; + esac + done + + dir=$(CDPATH= cd -- "$(dirname -- "$path")" && pwd) + export OPENSHELL_DRIVER_DIR="${dir}/libexec/openshell" + exec "${dir}/libexec/openshell-gateway" "$@" + SH + + cat > bundle/openshell-driver-vm <<'SH' + #!/bin/sh + set -e + + path=$0 + while [ -L "$path" ]; do + link=$(readlink "$path") + case "$link" in + /*) path=$link ;; + *) path=$(dirname "$path")/$link ;; + esac + done + + dir=$(CDPATH= cd -- "$(dirname -- "$path")" && pwd) + exec "${dir}/libexec/openshell/openshell-driver-vm" "$@" + SH + + cat > bundle/libexec/openshell/openshell-driver-vm-entitlements.plist <<'XML' + + + + + com.apple.security.hypervisor + + + + XML + + chmod 0555 bundle/openshell bundle/openshell-gateway bundle/openshell-driver-vm + chmod 0555 bundle/libexec/openshell-gateway bundle/libexec/openshell/openshell-driver-vm + + tar -czf "artifacts/${BUNDLE_ASSET}" -C bundle . + tar -tzf "artifacts/${BUNDLE_ASSET}" + + - name: Upload cask bundle to dev release + run: | + set -euo pipefail + gh release upload dev "artifacts/${BUNDLE_ASSET}" \ + --repo "${GITHUB_REPOSITORY}" \ + --clobber diff --git a/.github/workflows/homebrew-formulae.yml b/.github/workflows/homebrew-formulae.yml deleted file mode 100644 index 8a15b2391..000000000 --- a/.github/workflows/homebrew-formulae.yml +++ /dev/null @@ -1,65 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -name: Update Homebrew Formulae - -on: - workflow_dispatch: - workflow_run: - workflows: - - Release Tag - - Release Dev - - Release VM Dev - types: - - completed - -permissions: - contents: write - -concurrency: - group: homebrew-formulae - cancel-in-progress: false - -defaults: - run: - shell: bash - -jobs: - update: - name: Update Formulae - if: github.repository == 'NVIDIA/OpenShell' && (github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success') - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v4 - with: - ref: main - fetch-depth: 0 - - - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - uses: astral-sh/setup-uv@v5 - with: - version: "0.10.12" - - - name: Render formulae - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - UV_NO_SYNC: "1" - run: uv run python tasks/scripts/homebrew/update_formulae.py - - - name: Commit formula updates - run: | - set -euo pipefail - if git diff --quiet -- Formula/openshell.rb Formula/openshell-dev.rb; then - echo "Formulae are already up to date." - exit 0 - fi - - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add Formula/openshell.rb Formula/openshell-dev.rb - git commit -m "chore(homebrew): update formulae [skip ci]" - git push origin HEAD:main diff --git a/Casks/openshell-dev.rb b/Casks/openshell-dev.rb new file mode 100644 index 000000000..f18d8939d --- /dev/null +++ b/Casks/openshell-dev.rb @@ -0,0 +1,28 @@ +cask "openshell-dev" do + version :latest + sha256 :no_check + + url "https://github.com/NVIDIA/OpenShell/releases/download/dev/openshell-homebrew-dev-aarch64-apple-darwin.tar.gz" + name "OpenShell Dev" + desc "Development build of the safe private runtime for autonomous AI agents" + homepage "https://github.com/NVIDIA/OpenShell" + + depends_on arch: :arm64 + depends_on macos: ">= :big_sur" + + binary "openshell" + binary "openshell-gateway" + binary "openshell-driver-vm" + + postflight do + system_command "/usr/bin/codesign", + args: [ + "--entitlements", + "#{staged_path}/libexec/openshell/openshell-driver-vm-entitlements.plist", + "--force", + "-s", + "-", + "#{staged_path}/libexec/openshell/openshell-driver-vm", + ] + end +end diff --git a/Formula/openshell-dev.rb b/Formula/openshell-dev.rb deleted file mode 100644 index bb094ed8c..000000000 --- a/Formula/openshell-dev.rb +++ /dev/null @@ -1,118 +0,0 @@ -# This file is generated by tasks/scripts/homebrew/update_formulae.py. - -class OpenshellDev < Formula - desc "Development build of the safe private runtime for autonomous AI agents" - homepage "https://github.com/NVIDIA/OpenShell" - version "0.0.0-dev.24724742a89d.vm.20ffc7253b6e" - license "Apache-2.0" - - on_macos do - depends_on arch: :arm64 - - on_arm do - url "https://github.com/NVIDIA/OpenShell/releases/download/dev/openshell-aarch64-apple-darwin.tar.gz" - sha256 "5a01a8137ee6dc5d12a22d090747d3d760ad9bba174d958d876a1b0dcea72abd" - - resource "openshell-gateway" do - url "https://github.com/NVIDIA/OpenShell/releases/download/dev/openshell-gateway-aarch64-apple-darwin.tar.gz" - sha256 "f32b2fc805f036f7ccdc21cdabfe9a190d4660b22d388d44bfb9e09fd9061be3" - end - - resource "openshell-driver-vm" do - url "https://github.com/NVIDIA/OpenShell/releases/download/vm-dev/openshell-driver-vm-aarch64-apple-darwin.tar.gz" - sha256 "29cea5657875848c2f8242388ca050d6a94bf0229435ea62f960154d392fade1" - end - end - end - - on_linux do - on_intel do - url "https://github.com/NVIDIA/OpenShell/releases/download/dev/openshell-x86_64-unknown-linux-musl.tar.gz" - sha256 "3e3ab88059f2d63a088d0be9df92b9efeb2630ac6e0cc6a0bcf2bddaa9a338c2" - - resource "openshell-gateway" do - url "https://github.com/NVIDIA/OpenShell/releases/download/dev/openshell-gateway-x86_64-unknown-linux-gnu.tar.gz" - sha256 "1afb277bd8ccd5dedfa2f8992550e65fd717ae417b39479a89d4874a1e5ed2be" - end - - resource "openshell-driver-vm" do - url "https://github.com/NVIDIA/OpenShell/releases/download/vm-dev/openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz" - sha256 "8d02f4ec0c7517f62119cd92c764c8b3f04647b8a8bd4b1fae2a23f3581cd488" - end - end - - on_arm do - url "https://github.com/NVIDIA/OpenShell/releases/download/dev/openshell-aarch64-unknown-linux-musl.tar.gz" - sha256 "9959ed0e0552ee91e36aa6da8a7b18a42832d61475a0a87f4c5da8f46ebdb990" - - resource "openshell-gateway" do - url "https://github.com/NVIDIA/OpenShell/releases/download/dev/openshell-gateway-aarch64-unknown-linux-gnu.tar.gz" - sha256 "00f972ff27e5ba0e1a78337dcabf84c4d21a77d9d865880b76c63f13279ed4c6" - end - - resource "openshell-driver-vm" do - url "https://github.com/NVIDIA/OpenShell/releases/download/vm-dev/openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz" - sha256 "0e6d16d642a65b57c81be7e83b07a06725dd3f4edd1762f67ac2d5248b550652" - end - end - end - - conflicts_with "openshell", because: "both install OpenShell command names" - - def install - bin.install "openshell" - - resource("openshell-gateway").stage do - libexec.install "openshell-gateway" - end - - driver_dir = libexec/"openshell" - driver_dir.mkpath - resource("openshell-driver-vm").stage do - driver_dir.install "openshell-driver-vm" - end - - if OS.mac? - entitlements = buildpath/"openshell-driver-vm-entitlements.plist" - entitlements.write <<~XML - - - - - com.apple.security.hypervisor - - - - XML - system "codesign", "--entitlements", entitlements, "--force", "-s", "-", driver_dir/"openshell-driver-vm" - end - - gateway_wrapper = bin/"openshell-gateway" - gateway_wrapper.write <<~SH - #!/bin/sh - export OPENSHELL_DRIVER_DIR="#{opt_libexec}/openshell" - exec "#{opt_libexec}/openshell-gateway" "$@" - SH - chmod 0555, gateway_wrapper - - ln_s "../libexec/openshell/openshell-driver-vm", bin/"openshell-driver-vm" - end - - test do - assert_match(/^openshell \S+/, shell_output("#{bin}/openshell --version")) - assert_match(/^openshell-gateway \S+/, shell_output("#{bin}/openshell-gateway --version")) - assert_match(/^openshell-driver-vm \S+/, shell_output("#{bin}/openshell-driver-vm --version")) - - driver = libexec/"openshell/openshell-driver-vm" - assert_path_exists driver - assert_match( - "OPENSHELL_DRIVER_DIR=\"#{opt_libexec}/openshell\"", - (bin/"openshell-gateway").read, - ) - - if OS.mac? - entitlements = shell_output("codesign -d --entitlements :- #{driver} 2>&1") - assert_match "com.apple.security.hypervisor", entitlements - end - end -end diff --git a/Formula/openshell.rb b/Formula/openshell.rb deleted file mode 100644 index d290ff0d7..000000000 --- a/Formula/openshell.rb +++ /dev/null @@ -1,117 +0,0 @@ -# This file is generated by tasks/scripts/homebrew/update_formulae.py. - -class Openshell < Formula - desc "Safe private runtime for autonomous AI agents" - homepage "https://github.com/NVIDIA/OpenShell" - license "Apache-2.0" - - on_macos do - depends_on arch: :arm64 - - on_arm do - url "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.36/openshell-aarch64-apple-darwin.tar.gz" - sha256 "4d18ab9b966fcd68cdfc08c1189856ff9fff3734222f35fed038ddca9cd291c3" - - resource "openshell-gateway" do - url "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.36/openshell-gateway-aarch64-apple-darwin.tar.gz" - sha256 "34871f5415b3469c4490b728f2a1853c62d58279724ef8f9fe266f2433dd6ae6" - end - - resource "openshell-driver-vm" do - url "https://github.com/NVIDIA/OpenShell/releases/download/vm-dev/openshell-driver-vm-aarch64-apple-darwin.tar.gz" - sha256 "29cea5657875848c2f8242388ca050d6a94bf0229435ea62f960154d392fade1" - end - end - end - - on_linux do - on_intel do - url "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.36/openshell-x86_64-unknown-linux-musl.tar.gz" - sha256 "cd309b2b750b83b6d4699697071d647109a3212645d0c69de89164b650c6ec00" - - resource "openshell-gateway" do - url "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.36/openshell-gateway-x86_64-unknown-linux-gnu.tar.gz" - sha256 "8a4b12dd5d66fcb116e4eb562541b9dbf2825ad03b5cb681413de2e5b563e9bb" - end - - resource "openshell-driver-vm" do - url "https://github.com/NVIDIA/OpenShell/releases/download/vm-dev/openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz" - sha256 "8d02f4ec0c7517f62119cd92c764c8b3f04647b8a8bd4b1fae2a23f3581cd488" - end - end - - on_arm do - url "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.36/openshell-aarch64-unknown-linux-musl.tar.gz" - sha256 "6fae1119b5cf4e718c96ee5874ada6ab5e35c016aba6e1e0adb5d93e6ab63bc0" - - resource "openshell-gateway" do - url "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.36/openshell-gateway-aarch64-unknown-linux-gnu.tar.gz" - sha256 "62531994c3d966f0e985f45ceb06dd1c8c8a6b068e2779cadfcc1e20d7cac152" - end - - resource "openshell-driver-vm" do - url "https://github.com/NVIDIA/OpenShell/releases/download/vm-dev/openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz" - sha256 "0e6d16d642a65b57c81be7e83b07a06725dd3f4edd1762f67ac2d5248b550652" - end - end - end - - conflicts_with "openshell-dev", because: "both install OpenShell command names" - - def install - bin.install "openshell" - - resource("openshell-gateway").stage do - libexec.install "openshell-gateway" - end - - driver_dir = libexec/"openshell" - driver_dir.mkpath - resource("openshell-driver-vm").stage do - driver_dir.install "openshell-driver-vm" - end - - if OS.mac? - entitlements = buildpath/"openshell-driver-vm-entitlements.plist" - entitlements.write <<~XML - - - - - com.apple.security.hypervisor - - - - XML - system "codesign", "--entitlements", entitlements, "--force", "-s", "-", driver_dir/"openshell-driver-vm" - end - - gateway_wrapper = bin/"openshell-gateway" - gateway_wrapper.write <<~SH - #!/bin/sh - export OPENSHELL_DRIVER_DIR="#{opt_libexec}/openshell" - exec "#{opt_libexec}/openshell-gateway" "$@" - SH - chmod 0555, gateway_wrapper - - ln_s "../libexec/openshell/openshell-driver-vm", bin/"openshell-driver-vm" - end - - test do - assert_match(/^openshell \S+/, shell_output("#{bin}/openshell --version")) - assert_match(/^openshell-gateway \S+/, shell_output("#{bin}/openshell-gateway --version")) - assert_match(/^openshell-driver-vm \S+/, shell_output("#{bin}/openshell-driver-vm --version")) - - driver = libexec/"openshell/openshell-driver-vm" - assert_path_exists driver - assert_match( - "OPENSHELL_DRIVER_DIR=\"#{opt_libexec}/openshell\"", - (bin/"openshell-gateway").read, - ) - - if OS.mac? - entitlements = shell_output("codesign -d --entitlements :- #{driver} 2>&1") - assert_match "com.apple.security.hypervisor", entitlements - end - end -end diff --git a/README.md b/README.md index d600f90d4..a777e9fc2 100644 --- a/README.md +++ b/README.md @@ -26,15 +26,13 @@ OpenShell is built agent-first. The project ships with agent skills for everythi curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` -**Homebrew tap (CLI, gateway, and VM driver):** +**Homebrew dev cask (macOS Apple Silicon):** ```bash brew tap nvidia/openshell https://github.com/NVIDIA/OpenShell.git -brew install nvidia/openshell/openshell +brew install --cask nvidia/openshell/openshell-dev ``` -Use `brew install nvidia/openshell/openshell-dev` for the development track. - **From PyPI (requires [uv](https://docs.astral.sh/uv/)):** ```bash diff --git a/architecture/build-containers.md b/architecture/build-containers.md index 0fbd9c185..2aea71dfe 100644 --- a/architecture/build-containers.md +++ b/architecture/build-containers.md @@ -35,22 +35,22 @@ OpenShell also publishes a standalone `openshell-gateway` binary as a GitHub rel - **Artifact name**: `openshell-gateway-.tar.gz` - **Targets**: `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `aarch64-apple-darwin` - **Release workflows**: `.github/workflows/release-dev.yml`, `.github/workflows/release-tag.yml` -- **Installers**: `install-vm.sh` for the rolling development gateway, and the Homebrew formulae under `Formula/` for stable and development tracks. +- **Installers**: `install-vm.sh` for the rolling development gateway, and the Homebrew dev cask under `Casks/`. Both the standalone artifact and the deployed container image use the `openshell-gateway` binary. -## Homebrew Formulae +## Homebrew Dev Cask -This repository also acts as the Homebrew tap. Users must tap it with the full Git URL because the repository is not named `homebrew-*`: +This repository also acts as the Homebrew tap for the rolling macOS Apple Silicon dev build. Users must tap it with the full Git URL because the repository is not named `homebrew-*`: ```bash brew tap nvidia/openshell https://github.com/NVIDIA/OpenShell.git -brew install nvidia/openshell/openshell +brew install --cask nvidia/openshell/openshell-dev ``` -`Formula/openshell.rb` installs the latest stable CLI and gateway assets plus the rolling `vm-dev` `openshell-driver-vm` asset. `Formula/openshell-dev.rb` installs the rolling `dev` CLI and gateway assets plus the same `vm-dev` driver. The formulae conflict because they expose the same command names. +`Casks/openshell-dev.rb` uses `version :latest` and `sha256 :no_check` because it installs a rolling development archive from the `dev` GitHub release. `.github/workflows/homebrew-dev-cask.yml` runs after the dev CLI/gateway release or VM dev driver release completes. It downloads the macOS ARM64 CLI, gateway, and VM driver assets, repacks them into `openshell-homebrew-dev-aarch64-apple-darwin.tar.gz`, and uploads that archive back to the `dev` release. -The formulae are generated by `tasks/scripts/homebrew/update_formulae.py`. The script reads the GitHub release checksum files and writes literal `sha256` values for every platform-specific asset. `.github/workflows/homebrew-formulae.yml` runs after the stable, dev, and VM dev release workflows complete successfully and commits updated formulae to `main`. +The archive exposes `openshell`, `openshell-gateway`, and `openshell-driver-vm` through cask `binary` stanzas. The gateway entrypoint is a wrapper that sets `OPENSHELL_DRIVER_DIR` to the bundled driver directory before executing the real gateway binary. The cask signs the VM driver with the Hypervisor entitlement during `postflight`. ## Python Wheels diff --git a/mise.toml b/mise.toml index 8998d4d62..fc4961db8 100644 --- a/mise.toml +++ b/mise.toml @@ -59,7 +59,7 @@ DOCKER_BUILDKIT = "1" [vars] # Python paths to include in formatting/linting -python_paths = "python/ tasks/scripts/*.py tasks/scripts/homebrew/*.py deploy/sbom/*.py" +python_paths = "python/ tasks/scripts/*.py deploy/sbom/*.py" [task_config] includes = ["tasks/*.toml"] diff --git a/tasks/scripts/homebrew/update_formulae.py b/tasks/scripts/homebrew/update_formulae.py deleted file mode 100644 index afc46dde6..000000000 --- a/tasks/scripts/homebrew/update_formulae.py +++ /dev/null @@ -1,495 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Render Homebrew formulae from GitHub release checksums.""" - -from __future__ import annotations - -import argparse -import json -import os -import re -import subprocess -import sys -import time -import urllib.error -import urllib.request -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -REPO = "NVIDIA/OpenShell" -API_BASE = f"https://api.github.com/repos/{REPO}" -DOWNLOAD_BASE = f"https://github.com/{REPO}/releases/download" - -CLI_CHECKSUMS = "openshell-checksums-sha256.txt" -GATEWAY_CHECKSUMS = "openshell-gateway-checksums-sha256.txt" -VM_CHECKSUMS = "vm-binary-checksums-sha256.txt" - -TARGETS = { - "macos_arm64": { - "cli": "openshell-aarch64-apple-darwin.tar.gz", - "gateway": "openshell-gateway-aarch64-apple-darwin.tar.gz", - "driver": "openshell-driver-vm-aarch64-apple-darwin.tar.gz", - }, - "linux_x86_64": { - "cli": "openshell-x86_64-unknown-linux-musl.tar.gz", - "gateway": "openshell-gateway-x86_64-unknown-linux-gnu.tar.gz", - "driver": "openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz", - }, - "linux_arm64": { - "cli": "openshell-aarch64-unknown-linux-musl.tar.gz", - "gateway": "openshell-gateway-aarch64-unknown-linux-gnu.tar.gz", - "driver": "openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz", - }, -} - - -@dataclass(frozen=True) -class Release: - tag: str - target_commitish: str - published_at: str - html_url: str - assets: dict[str, str] - - -@dataclass(frozen=True) -class FormulaSource: - tag: str - version: str - cli: dict[str, str] - gateway: dict[str, str] - driver_tag: str - driver: dict[str, str] - - -def repo_root() -> Path: - return Path(__file__).resolve().parents[3] - - -def request_headers() -> dict[str, str]: - headers = { - "Accept": "application/vnd.github+json", - "User-Agent": "openshell-homebrew-formula-updater", - "X-GitHub-Api-Version": "2022-11-28", - } - token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") - if token: - headers["Authorization"] = f"Bearer {token}" - return headers - - -def truncate_error_body(body: str) -> str: - if len(body) <= 500: - return body - return body[:500] + "... [truncated]" - - -def fetch_url(url: str, *, accept: str | None = None) -> bytes: - headers = request_headers() - if accept is not None: - headers["Accept"] = accept - request = urllib.request.Request(url, headers=headers) - retry_statuses = {502, 503, 504} - - for attempt in range(1, 4): - try: - with urllib.request.urlopen(request, timeout=30) as response: - return response.read() - except urllib.error.HTTPError as exc: - body = exc.read().decode("utf-8", errors="replace") - if exc.code in retry_statuses and attempt < 3: - time.sleep(attempt) - continue - raise RuntimeError( - f"failed to fetch {url}: HTTP {exc.code}: {truncate_error_body(body)}" - ) from exc - except urllib.error.URLError as exc: - if attempt < 3: - time.sleep(attempt) - continue - raise RuntimeError(f"failed to fetch {url}: {exc.reason}") from exc - - raise RuntimeError(f"failed to fetch {url}") - - -def fetch_json(url: str) -> dict[str, Any]: - return json.loads(fetch_url(url).decode("utf-8")) - - -def release_from_json(data: dict[str, Any]) -> Release: - assets = {} - for asset in data.get("assets", []): - name = asset["name"] - assets[name] = asset["url"] - return Release( - tag=data["tag_name"], - target_commitish=data.get("target_commitish", ""), - published_at=data.get("published_at", ""), - html_url=data.get("html_url", ""), - assets=assets, - ) - - -def fetch_latest_stable() -> Release: - return release_from_json(fetch_json(f"{API_BASE}/releases/latest")) - - -def fetch_release(tag: str) -> Release: - return release_from_json(fetch_json(f"{API_BASE}/releases/tags/{tag}")) - - -def fetch_release_asset(release: Release, name: str) -> str: - try: - url = release.assets[name] - except KeyError as exc: - available = ", ".join(sorted(release.assets)) - raise RuntimeError( - f"release {release.tag} is missing asset {name}; available: {available}" - ) from exc - return fetch_url(url, accept="application/octet-stream").decode("utf-8") - - -def parse_checksums(text: str) -> dict[str, str]: - checksums = {} - for line in text.splitlines(): - if not line.strip(): - continue - parts = line.split(maxsplit=1) - if len(parts) != 2: - raise RuntimeError(f"invalid checksum line: {line!r}") - sha, filename = parts - filename = filename.removeprefix("*") - if not re.fullmatch(r"[0-9a-f]{64}", sha): - raise RuntimeError(f"invalid sha256 for {filename}: {sha}") - checksums[filename] = sha - return checksums - - -def selected_checksums(checksums: dict[str, str], kind: str) -> dict[str, str]: - result = {} - for target, assets in TARGETS.items(): - name = assets[kind] - try: - result[target] = checksums[name] - except KeyError as exc: - raise RuntimeError(f"checksum file is missing {name}") from exc - return result - - -def ensure_release_assets(release: Release, kind: str) -> None: - missing = [] - for assets in TARGETS.values(): - name = assets[kind] - if name not in release.assets: - missing.append(name) - if missing: - missing_text = ", ".join(missing) - raise RuntimeError(f"release {release.tag} is missing assets: {missing_text}") - - -def release_version(tag: str) -> str: - return tag.removeprefix("v") - - -def short_ref(ref: str) -> str: - value = re.sub(r"[^0-9A-Za-z]+", ".", ref).strip(".") - return value[:12] or "unknown" - - -def existing_formula_metadata(path: Path) -> tuple[str | None, int]: - if not path.exists(): - return None, 0 - - text = path.read_text() - version_match = re.search(r'^\s*version "([^"]+)"$', text, re.MULTILINE) - url_version_match = re.search( - r"releases/download/v([^/]+)/openshell-", text, re.MULTILINE - ) - revision_match = re.search(r"^\s*revision (\d+)$", text, re.MULTILINE) - version = ( - version_match.group(1) - if version_match - else url_version_match.group(1) - if url_version_match - else None - ) - revision = int(revision_match.group(1)) if revision_match else 0 - return version, revision - - -def is_git_tracked(path: Path) -> bool: - root = repo_root() - try: - rel_path = path.relative_to(root) - except ValueError: - return False - - result = subprocess.run( - ["git", "-C", str(root), "ls-files", "--error-unmatch", str(rel_path)], - check=False, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - return result.returncode == 0 - - -def with_revision_bump(path: Path, version: str, render: Any) -> str: - existing_version, existing_revision = existing_formula_metadata(path) - revision = existing_revision if existing_version == version else 0 - rendered = render(revision) - - if ( - path.exists() - and is_git_tracked(path) - and existing_version == version - and rendered != path.read_text() - ): - rendered = render(revision + 1) - - return rendered - - -def formula_url(tag: str, asset: str) -> str: - return f"{DOWNLOAD_BASE}/{tag}/{asset}" - - -def resource_block(name: str, url: str, sha: str, indent: str = " ") -> str: - return ( - f'{indent}resource "{name}" do\n' - f'{indent} url "{url}"\n' - f'{indent} sha256 "{sha}"\n' - f"{indent}end\n" - ) - - -def render_platform_blocks(source: FormulaSource) -> str: - macos = TARGETS["macos_arm64"] - linux_x86 = TARGETS["linux_x86_64"] - linux_arm = TARGETS["linux_arm64"] - - return f""" on_macos do - depends_on arch: :arm64 - - on_arm do - url "{formula_url(source.tag, macos["cli"])}" - sha256 "{source.cli["macos_arm64"]}" - -{resource_block("openshell-gateway", formula_url(source.tag, macos["gateway"]), source.gateway["macos_arm64"], " ")} -{resource_block("openshell-driver-vm", formula_url(source.driver_tag, macos["driver"]), source.driver["macos_arm64"], " ")} end - end - - on_linux do - on_intel do - url "{formula_url(source.tag, linux_x86["cli"])}" - sha256 "{source.cli["linux_x86_64"]}" - -{resource_block("openshell-gateway", formula_url(source.tag, linux_x86["gateway"]), source.gateway["linux_x86_64"], " ")} -{resource_block("openshell-driver-vm", formula_url(source.driver_tag, linux_x86["driver"]), source.driver["linux_x86_64"], " ")} end - - on_arm do - url "{formula_url(source.tag, linux_arm["cli"])}" - sha256 "{source.cli["linux_arm64"]}" - -{resource_block("openshell-gateway", formula_url(source.tag, linux_arm["gateway"]), source.gateway["linux_arm64"], " ")} -{resource_block("openshell-driver-vm", formula_url(source.driver_tag, linux_arm["driver"]), source.driver["linux_arm64"], " ")} end - end -""" - - -def install_and_test_block() -> str: - return r""" def install - bin.install "openshell" - - resource("openshell-gateway").stage do - libexec.install "openshell-gateway" - end - - driver_dir = libexec/"openshell" - driver_dir.mkpath - resource("openshell-driver-vm").stage do - driver_dir.install "openshell-driver-vm" - end - - if OS.mac? - entitlements = buildpath/"openshell-driver-vm-entitlements.plist" - entitlements.write <<~XML - - - - - com.apple.security.hypervisor - - - - XML - system "codesign", "--entitlements", entitlements, "--force", "-s", "-", driver_dir/"openshell-driver-vm" - end - - gateway_wrapper = bin/"openshell-gateway" - gateway_wrapper.write <<~SH - #!/bin/sh - export OPENSHELL_DRIVER_DIR="#{opt_libexec}/openshell" - exec "#{opt_libexec}/openshell-gateway" "$@" - SH - chmod 0555, gateway_wrapper - - ln_s "../libexec/openshell/openshell-driver-vm", bin/"openshell-driver-vm" - end - - test do - assert_match(/^openshell \S+/, shell_output("#{bin}/openshell --version")) - assert_match(/^openshell-gateway \S+/, shell_output("#{bin}/openshell-gateway --version")) - assert_match(/^openshell-driver-vm \S+/, shell_output("#{bin}/openshell-driver-vm --version")) - - driver = libexec/"openshell/openshell-driver-vm" - assert_path_exists driver - assert_match( - "OPENSHELL_DRIVER_DIR=\"#{opt_libexec}/openshell\"", - (bin/"openshell-gateway").read, - ) - - if OS.mac? - entitlements = shell_output("codesign -d --entitlements :- #{driver} 2>&1") - assert_match "com.apple.security.hypervisor", entitlements - end - end -""" - - -def render_formula( - *, - class_name: str, - desc: str, - source: FormulaSource, - conflicts_with: str, - include_version: bool = True, - revision: int = 0, -) -> str: - version_line = f' version "{source.version}"\n' if include_version else "" - revision_line = f" revision {revision}\n" if revision else "" - return f'''# This file is generated by tasks/scripts/homebrew/update_formulae.py. - -class {class_name} < Formula - desc "{desc}" - homepage "https://github.com/NVIDIA/OpenShell" -{version_line} license "Apache-2.0" -{revision_line} -{render_platform_blocks(source)} - conflicts_with "{conflicts_with}", because: "both install OpenShell command names" - -{install_and_test_block()}end -''' - - -def build_formula_source( - release: Release, - driver_release: Release, - *, - version: str, -) -> FormulaSource: - ensure_release_assets(release, "cli") - ensure_release_assets(release, "gateway") - ensure_release_assets(driver_release, "driver") - - cli_checksums = parse_checksums(fetch_release_asset(release, CLI_CHECKSUMS)) - gateway_checksums = parse_checksums(fetch_release_asset(release, GATEWAY_CHECKSUMS)) - driver_checksums = parse_checksums( - fetch_release_asset(driver_release, VM_CHECKSUMS) - ) - - return FormulaSource( - tag=release.tag, - version=version, - cli=selected_checksums(cli_checksums, "cli"), - gateway=selected_checksums(gateway_checksums, "gateway"), - driver_tag=driver_release.tag, - driver=selected_checksums(driver_checksums, "driver"), - ) - - -def write_file(path: Path, content: str, *, check: bool) -> bool: - if path.exists() and path.read_text() == content: - return False - if check: - print(f"{path} is not up to date", file=sys.stderr) - return True - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content) - print(f"wrote {path}") - return True - - -def render_all(*, check: bool) -> bool: - root = repo_root() - formula_dir = root / "Formula" - - stable_release = fetch_latest_stable() - dev_release = fetch_release("dev") - vm_release = fetch_release("vm-dev") - - stable_version = release_version(stable_release.tag) - stable_source = build_formula_source( - stable_release, - vm_release, - version=stable_version, - ) - dev_source = build_formula_source( - dev_release, - vm_release, - version=( - f"0.0.0-dev.{short_ref(dev_release.target_commitish)}" - f".vm.{short_ref(vm_release.target_commitish)}" - ), - ) - - stable_path = formula_dir / "openshell.rb" - stable_formula = with_revision_bump( - stable_path, - stable_version, - lambda revision: render_formula( - class_name="Openshell", - desc="Safe private runtime for autonomous AI agents", - source=stable_source, - conflicts_with="openshell-dev", - include_version=False, - revision=revision, - ), - ) - - dev_formula = render_formula( - class_name="OpenshellDev", - desc="Development build of the safe private runtime for autonomous AI agents", - source=dev_source, - conflicts_with="openshell", - ) - - changed = False - changed |= write_file(stable_path, stable_formula, check=check) - changed |= write_file(formula_dir / "openshell-dev.rb", dev_formula, check=check) - return changed - - -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--check", - action="store_true", - help="exit non-zero if generated formulae differ from files on disk", - ) - args = parser.parse_args() - - try: - changed = render_all(check=args.check) - except RuntimeError as exc: - print(f"error: {exc}", file=sys.stderr) - raise SystemExit(1) from None - if args.check and changed: - raise SystemExit(1) - - -if __name__ == "__main__": - main() From 8072eae0839aaebbfca7e62b0544f2b85dfd673e Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 1 May 2026 08:27:25 -0700 Subject: [PATCH 4/4] feat(homebrew): install dev cask from script Signed-off-by: Drew Newberry --- README.md | 6 +++ architecture/build-containers.md | 2 + install-dev.sh | 73 +++++++++++++++++++++++++++++--- 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a777e9fc2..b9863ee3a 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,12 @@ curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | **Homebrew dev cask (macOS Apple Silicon):** +```bash +curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install-dev.sh | sh +``` + +Or install the cask directly: + ```bash brew tap nvidia/openshell https://github.com/NVIDIA/OpenShell.git brew install --cask nvidia/openshell/openshell-dev diff --git a/architecture/build-containers.md b/architecture/build-containers.md index 2aea71dfe..78ac421e4 100644 --- a/architecture/build-containers.md +++ b/architecture/build-containers.md @@ -52,6 +52,8 @@ brew install --cask nvidia/openshell/openshell-dev The archive exposes `openshell`, `openshell-gateway`, and `openshell-driver-vm` through cask `binary` stanzas. The gateway entrypoint is a wrapper that sets `OPENSHELL_DRIVER_DIR` to the bundled driver directory before executing the real gateway binary. The cask signs the VM driver with the Hypervisor entitlement during `postflight`. +`install-dev.sh` installs the same dev cask automatically on macOS Apple Silicon. On Linux amd64 and arm64, it installs the rolling development Debian package. + ## Python Wheels OpenShell also publishes Python wheels for `linux/amd64`, `linux/arm64`, and macOS ARM64. diff --git a/install-dev.sh b/install-dev.sh index dc0666cd6..d98de09cc 100755 --- a/install-dev.sh +++ b/install-dev.sh @@ -5,7 +5,8 @@ # Install the OpenShell development build from the rolling GitHub `dev` release. # # This script is intended as a convenient installer for development builds. It -# currently supports Debian packages on Linux amd64 and arm64 only. +# supports Debian packages on Linux amd64/arm64 and the Homebrew dev cask on +# macOS Apple Silicon. # set -e @@ -14,6 +15,9 @@ REPO="NVIDIA/OpenShell" GITHUB_URL="https://github.com/${REPO}" RELEASE_TAG="dev" CHECKSUMS_NAME="openshell-checksums-sha256.txt" +HOMEBREW_TAP="nvidia/openshell" +HOMEBREW_TAP_URL="${GITHUB_URL}.git" +HOMEBREW_CASK="${HOMEBREW_TAP}/openshell-dev" info() { printf '%s: %s\n' "$APP_NAME" "$*" >&2 @@ -26,7 +30,7 @@ error() { usage() { cat </dev/null 2>&1; then + info "reinstalling ${HOMEBREW_CASK}..." + brew reinstall --cask "${HOMEBREW_CASK}" + else + info "installing ${HOMEBREW_CASK}..." + brew install --cask "${HOMEBREW_CASK}" + fi + + _brew_prefix="$(brew --prefix)" + _openshell_bin="${_brew_prefix}/bin/openshell" + if [ -x "$_openshell_bin" ]; then + _installed_version="$("$_openshell_bin" --version 2>/dev/null || true)" + if [ -n "$_installed_version" ]; then + info "installed ${_installed_version} via Homebrew" + else + info "installed OpenShell development cask via Homebrew" + fi + else + info "installed OpenShell development cask via Homebrew" + fi +} + main() { - while [ "$#" -gt 0 ]; do - case "$1" in + for arg in "$@"; do + case "$arg" in --help) usage exit 0 ;; *) - error "unknown option: $1" + error "unknown option: $arg" ;; esac - shift done require_cmd curl + + if [ "$(uname -s)" = "Darwin" ]; then + install_homebrew_cask + return 0 + fi + check_platform TARGET_USER="$(target_user)"