mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(installer): add one-command installer flow and release smoke checks (#123)
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
name: Installer Smoke
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
smoke-unix:
|
||||
name: Smoke install.sh (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-22.04, macos-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run installer dry-run
|
||||
run: bash scripts/install.sh --dry-run --verbose
|
||||
|
||||
smoke-windows:
|
||||
name: Smoke install.ps1 (windows-latest)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run installer dry-run
|
||||
shell: pwsh
|
||||
run: ./scripts/install.ps1 -DryRun
|
||||
@@ -221,10 +221,10 @@ jobs:
|
||||
args: --target x86_64-unknown-linux-gnu
|
||||
target: x86_64-unknown-linux-gnu
|
||||
artifact_suffix: ubuntu
|
||||
# - platform: windows-latest
|
||||
# args: --target x86_64-pc-windows-msvc
|
||||
# target: x86_64-pc-windows-msvc
|
||||
# artifact_suffix: windows
|
||||
- platform: windows-latest
|
||||
args: --target x86_64-pc-windows-msvc
|
||||
target: x86_64-pc-windows-msvc
|
||||
artifact_suffix: windows
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
@@ -691,6 +691,35 @@ jobs:
|
||||
needs: [create-release, build-artifacts]
|
||||
if: needs.build-artifacts.result == 'success'
|
||||
steps:
|
||||
- name: Validate required installer assets exist
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const releaseId = Number('${{ needs.create-release.outputs.release_id }}');
|
||||
const { owner, repo } = context.repo;
|
||||
const { data: assets } = await github.rest.repos.listReleaseAssets({
|
||||
owner,
|
||||
repo,
|
||||
release_id: releaseId,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const names = assets.map((a) => a.name);
|
||||
const requiredPatterns = [
|
||||
/OpenHuman_.*_aarch64\.dmg$/,
|
||||
/OpenHuman_.*_x64\.dmg$/,
|
||||
/OpenHuman_.*_amd64\.AppImage$/,
|
||||
/OpenHuman_.*_amd64\.deb$/,
|
||||
/(OpenHuman_.*_x64-setup\.exe$|OpenHuman_.*_x64.*\.msi$)/,
|
||||
];
|
||||
|
||||
const missing = requiredPatterns.filter((pattern) => !names.some((name) => pattern.test(name)));
|
||||
if (missing.length > 0) {
|
||||
core.setFailed(`Missing required installer assets. Got: ${names.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
core.info('All required installer assets are present.');
|
||||
|
||||
- name: Publish release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
|
||||
@@ -55,12 +55,24 @@ If you need an older version, browse [all releases](https://github.com/tinyhuman
|
||||
|
||||
If you want to build from source, see [`docs/BUILDING.md`](docs/BUILDING.md).
|
||||
|
||||
Or you can basically run this global install script
|
||||
Install with one command:
|
||||
|
||||
```
|
||||
COMING SOON curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash
|
||||
curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
On Windows, use PowerShell:
|
||||
`irm https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.ps1 | iex`
|
||||
|
||||
What setup does:
|
||||
|
||||
- Resolves the latest stable release for your OS/arch
|
||||
- Verifies release digest when available
|
||||
- Installs locally without requiring system-wide admin rights by default
|
||||
- macOS: installs `OpenHuman.app` in `~/Applications`
|
||||
- Linux: installs `openhuman` AppImage in `~/.local/bin/openhuman` and creates a desktop entry
|
||||
- Windows: installs from latest release MSI/EXE in per-user mode where supported
|
||||
|
||||
# Under the hood (Architecture)
|
||||
|
||||
OpenHuman is a **desktop monorepo**: **Rust** owns **business logic and execution**; the **UI** owns **interaction, layout, and OS integration**.
|
||||
|
||||
+29
-41
@@ -42,55 +42,43 @@ yarn dev
|
||||
|
||||
## Install latest stable release (macOS/Linux)
|
||||
|
||||
Use this basic script to detect platform/arch and download the latest stable artifact from GitHub Releases:
|
||||
Primary install command:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO="tinyhumansai/openhuman"
|
||||
BASE="https://github.com/${REPO}/releases/latest/download"
|
||||
OS="$(uname -s)"
|
||||
ARCH="$(uname -m)"
|
||||
|
||||
if [[ "$OS" == "Darwin" ]]; then
|
||||
if [[ "$ARCH" == "arm64" ]]; then
|
||||
FILE="OpenHuman_0.49.32_aarch64.dmg"
|
||||
else
|
||||
FILE="OpenHuman_0.49.32_x64.dmg"
|
||||
fi
|
||||
elif [[ "$OS" == "Linux" ]]; then
|
||||
# Prefer AppImage for broad compatibility.
|
||||
FILE="OpenHuman_0.49.32_amd64.AppImage"
|
||||
else
|
||||
echo "Unsupported OS: $OS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
URL="${BASE}/${FILE}"
|
||||
echo "Downloading: $URL"
|
||||
curl -fL "$URL" -o "$FILE"
|
||||
|
||||
echo "Downloaded $FILE"
|
||||
echo "Install it with your platform's normal installer flow."
|
||||
curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
Notes:
|
||||
Installer behavior:
|
||||
|
||||
- The filename includes the release version and should be updated when a new stable release is cut.
|
||||
- You can always manually download from:
|
||||
- Website: https://tinyhuman.ai/openhuman
|
||||
- Latest release: https://github.com/tinyhumansai/openhuman/releases/latest
|
||||
- Resolves latest stable OpenHuman release for your platform
|
||||
- Validates artifact digest when available
|
||||
- Installs locally (no sudo by default)
|
||||
- macOS: installs `OpenHuman.app` into `~/Applications`
|
||||
- Linux: installs AppImage as `~/.local/bin/openhuman` and writes a desktop entry
|
||||
|
||||
Useful flags:
|
||||
|
||||
```bash
|
||||
# Preview actions without writing files
|
||||
curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash -s -- --dry-run
|
||||
```
|
||||
|
||||
## Windows (latest stable)
|
||||
|
||||
Download directly from the website or latest release page:
|
||||
Use PowerShell:
|
||||
|
||||
- https://tinyhuman.ai/openhuman
|
||||
- https://github.com/tinyhumansai/openhuman/releases/latest
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
## Future improvements
|
||||
Windows installer behavior:
|
||||
|
||||
- Replace hardcoded filenames with `latest.json` parsing
|
||||
- Add checksum/signature verification
|
||||
- Publish one-step global installers for all platforms
|
||||
- Resolves latest stable release
|
||||
- Downloads MSI/EXE for x64
|
||||
- Verifies digest when available
|
||||
- Runs per-user install where supported by installer package
|
||||
|
||||
Manual download links (all platforms):
|
||||
|
||||
- Website: https://tinyhuman.ai/openhuman
|
||||
- Latest release: https://github.com/tinyhumansai/openhuman/releases/latest
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env pwsh
|
||||
<#
|
||||
.SYNOPSIS
|
||||
OpenHuman installer for Windows.
|
||||
|
||||
.DESCRIPTION
|
||||
Intended for:
|
||||
irm https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.ps1 | iex
|
||||
#>
|
||||
|
||||
param(
|
||||
[switch]$Help,
|
||||
[switch]$Version,
|
||||
[string]$Channel = "stable",
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$InstallerVersion = "1.0.0"
|
||||
$Repo = "tinyhumansai/openhuman"
|
||||
$LatestJsonUrl = "https://github.com/$Repo/releases/latest/download/latest.json"
|
||||
$LatestReleaseApiUrl = "https://api.github.com/repos/$Repo/releases/latest"
|
||||
|
||||
function Write-Info([string]$Message) { Write-Host "-> $Message" -ForegroundColor Cyan }
|
||||
function Write-Ok([string]$Message) { Write-Host "OK $Message" -ForegroundColor Green }
|
||||
function Write-WarnMsg([string]$Message) { Write-Host "! $Message" -ForegroundColor Yellow }
|
||||
function Write-Err([string]$Message) { Write-Host "x $Message" -ForegroundColor Red }
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
OpenHuman Installer (Windows)
|
||||
|
||||
Usage:
|
||||
install.ps1 [-Channel stable] [-DryRun] [-Help] [-Version]
|
||||
|
||||
Examples:
|
||||
irm https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.ps1 | iex
|
||||
.\scripts\install.ps1 -DryRun
|
||||
"@
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($Version) {
|
||||
Write-Output "openhuman-installer $InstallerVersion"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($Channel -ne "stable") {
|
||||
Write-Err "Only -Channel stable is currently supported."
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($env:OS -ne "Windows_NT") {
|
||||
Write-Err "This installer is for Windows only."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
|
||||
if ($arch -notin @("x64", "amd64")) {
|
||||
Write-Err "Unsupported architecture: $arch (Windows x64 required)."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Ok "Detected platform: windows/x64"
|
||||
|
||||
$release = $null
|
||||
$releaseTag = ""
|
||||
$assetName = ""
|
||||
$assetUrl = ""
|
||||
$assetDigest = ""
|
||||
|
||||
function Select-WindowsAssetFromRelease([object]$Rel) {
|
||||
$assets = @($Rel.assets)
|
||||
if (-not $assets -or $assets.Count -eq 0) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$msi = $assets | Where-Object { $_.name -match 'OpenHuman_.*x64.*\.msi$' } | Select-Object -First 1
|
||||
if ($msi) { return $msi }
|
||||
|
||||
$exe = $assets | Where-Object { $_.name -match 'OpenHuman_.*x64.*\.exe$' } | Select-Object -First 1
|
||||
if ($exe) { return $exe }
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
$release = Invoke-RestMethod -Uri $LatestReleaseApiUrl
|
||||
$releaseTag = ($release.tag_name -replace '^v', '')
|
||||
$selected = Select-WindowsAssetFromRelease -Rel $release
|
||||
if ($selected) {
|
||||
$assetName = $selected.name
|
||||
$assetUrl = $selected.browser_download_url
|
||||
if ($selected.digest) {
|
||||
$assetDigest = ($selected.digest -replace '^sha256:', '')
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-WarnMsg "Could not query release API: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if (-not $assetUrl) {
|
||||
Write-Err "No Windows x64 installer artifact found in latest release."
|
||||
Write-Err "Ensure release workflow publishes Windows MSI/EXE assets."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Ok "Resolved latest release ($releaseTag): $assetName"
|
||||
|
||||
$tmpFile = Join-Path $env:TEMP $assetName
|
||||
if ($DryRun) {
|
||||
Write-Output "DRY RUN: download $assetUrl -> $tmpFile"
|
||||
} else {
|
||||
Write-Info "Downloading $assetName"
|
||||
Invoke-WebRequest -Uri $assetUrl -OutFile $tmpFile
|
||||
}
|
||||
|
||||
if ($assetDigest) {
|
||||
if ($DryRun) {
|
||||
Write-Output "DRY RUN: verify SHA256 $assetDigest"
|
||||
} else {
|
||||
$fileHash = (Get-FileHash -Path $tmpFile -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($fileHash -ne $assetDigest.ToLowerInvariant()) {
|
||||
Write-Err "SHA256 mismatch for $assetName"
|
||||
Write-Err "Expected: $assetDigest"
|
||||
Write-Err "Actual: $fileHash"
|
||||
exit 1
|
||||
}
|
||||
Write-Ok "Integrity verified (sha256)"
|
||||
}
|
||||
} else {
|
||||
Write-WarnMsg "No SHA256 digest available for $assetName; skipping integrity verification."
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
if ($assetName -like "*.msi") {
|
||||
Write-Output "DRY RUN: msiexec /i `"$tmpFile`" MSIINSTALLPERUSER=1 ALLUSERS=2 /qn /norestart"
|
||||
} else {
|
||||
Write-Output "DRY RUN: Start-Process `"$tmpFile`" -Wait"
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Info "Installing OpenHuman"
|
||||
if ($assetName -like "*.msi") {
|
||||
$args = "/i `"$tmpFile`" MSIINSTALLPERUSER=1 ALLUSERS=2 /qn /norestart"
|
||||
$proc = Start-Process -FilePath "msiexec.exe" -ArgumentList $args -Wait -PassThru
|
||||
if ($proc.ExitCode -ne 0) {
|
||||
Write-Err "MSI install failed with exit code $($proc.ExitCode)."
|
||||
exit 1
|
||||
}
|
||||
} elseif ($assetName -like "*.exe") {
|
||||
$proc = Start-Process -FilePath $tmpFile -Wait -PassThru
|
||||
if ($proc.ExitCode -ne 0) {
|
||||
Write-Err "Installer exited with code $($proc.ExitCode)."
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Err "Unsupported Windows installer type: $assetName"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$expectedPaths = @(
|
||||
"$env:LOCALAPPDATA\Programs\OpenHuman\OpenHuman.exe",
|
||||
"$env:ProgramFiles\OpenHuman\OpenHuman.exe"
|
||||
)
|
||||
$launchPath = $expectedPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
|
||||
Write-Output ""
|
||||
Write-Output "OpenHuman is ready."
|
||||
if ($launchPath) {
|
||||
Write-Output "Launch: `"$launchPath`""
|
||||
Write-Output "Uninstall: Settings -> Apps -> Installed apps -> OpenHuman"
|
||||
} else {
|
||||
Write-WarnMsg "Could not locate installed executable automatically."
|
||||
Write-Output "Try launching OpenHuman from Start Menu."
|
||||
Write-Output "Uninstall: Settings -> Apps -> Installed apps -> OpenHuman"
|
||||
}
|
||||
+481
-1
@@ -1 +1,481 @@
|
||||
echo "Coming soon. For now, you can download the latest desktop build from the website at [tinyhuman.ai/openhuman](https://tinyhuman.ai/openhuman)."
|
||||
#!/usr/bin/env bash
|
||||
# OpenHuman Installer (macOS/Linux)
|
||||
# Usage:
|
||||
# curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INSTALLER_VERSION="1.0.0"
|
||||
REPO="tinyhumansai/openhuman"
|
||||
LATEST_JSON_URL="https://github.com/${REPO}/releases/latest/download/latest.json"
|
||||
LATEST_RELEASE_API_URL="https://api.github.com/repos/${REPO}/releases/latest"
|
||||
|
||||
CHANNEL="stable"
|
||||
DRY_RUN=false
|
||||
VERBOSE=false
|
||||
|
||||
if [ -t 1 ]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
CYAN=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
log_info() { echo -e "${CYAN}→${NC} $*"; }
|
||||
log_ok() { echo -e "${GREEN}✓${NC} $*"; }
|
||||
log_warn() { echo -e "${YELLOW}!${NC} $*"; }
|
||||
log_err() { echo -e "${RED}x${NC} $*" >&2; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
OpenHuman Installer
|
||||
|
||||
Usage: install.sh [OPTIONS]
|
||||
|
||||
Options:
|
||||
--help Show help
|
||||
--version Show installer version
|
||||
--channel VALUE Release channel (default: stable)
|
||||
--dry-run Print actions without mutating local files
|
||||
--verbose Enable verbose output
|
||||
|
||||
Examples:
|
||||
curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash
|
||||
curl -fsSL ... | bash -s -- --dry-run
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--version)
|
||||
echo "openhuman-installer ${INSTALLER_VERSION}"
|
||||
exit 0
|
||||
;;
|
||||
--channel)
|
||||
CHANNEL="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
--verbose)
|
||||
VERBOSE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_err "Unknown option: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "${CHANNEL}" != "stable" ]; then
|
||||
log_err "Only --channel stable is currently supported."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cmd in curl mktemp tar; do
|
||||
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
||||
log_err "Missing required command: ${cmd}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
OS_RAW="$(uname -s)"
|
||||
ARCH_RAW="$(uname -m)"
|
||||
OS=""
|
||||
ARCH=""
|
||||
PLATFORM_KEY=""
|
||||
|
||||
case "${OS_RAW}" in
|
||||
Darwin) OS="darwin" ;;
|
||||
Linux) OS="linux" ;;
|
||||
CYGWIN*|MINGW*|MSYS*)
|
||||
log_err "Windows detected. Use PowerShell installer:"
|
||||
echo " irm https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.ps1 | iex"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
log_err "Unsupported OS: ${OS_RAW}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${ARCH_RAW}" in
|
||||
x86_64|amd64) ARCH="x86_64" ;;
|
||||
arm64|aarch64) ARCH="aarch64" ;;
|
||||
*)
|
||||
log_err "Unsupported architecture: ${ARCH_RAW}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "${OS}" = "linux" ] && [ "${ARCH}" != "x86_64" ]; then
|
||||
log_err "Linux installer currently supports x86_64 only."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${OS}" = "darwin" ] && [ "${ARCH}" = "aarch64" ]; then
|
||||
PLATFORM_KEY="darwin-aarch64"
|
||||
elif [ "${OS}" = "darwin" ] && [ "${ARCH}" = "x86_64" ]; then
|
||||
PLATFORM_KEY="darwin-x86_64"
|
||||
elif [ "${OS}" = "linux" ] && [ "${ARCH}" = "x86_64" ]; then
|
||||
PLATFORM_KEY="linux-x86_64"
|
||||
fi
|
||||
|
||||
log_ok "Detected platform: ${OS}/${ARCH}"
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "${TMP_DIR}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
LATEST_JSON_PATH="${TMP_DIR}/latest.json"
|
||||
RELEASE_JSON_PATH="${TMP_DIR}/release.json"
|
||||
|
||||
LATEST_VERSION=""
|
||||
ASSET_URL=""
|
||||
ASSET_NAME=""
|
||||
ASSET_SHA256=""
|
||||
|
||||
resolve_from_latest_json() {
|
||||
if ! curl -fsSL "${LATEST_JSON_URL}" -o "${LATEST_JSON_PATH}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
log_warn "python3 is not available; cannot parse latest.json reliably."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local parsed
|
||||
parsed="$(python3 - "${LATEST_JSON_PATH}" "${PLATFORM_KEY}" <<'PY'
|
||||
import json, sys
|
||||
path, key = sys.argv[1], sys.argv[2]
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
version = data.get("version", "")
|
||||
platforms = data.get("platforms", {})
|
||||
entry = platforms.get(key) or platforms.get(f"{key}-appimage") or platforms.get(f"{key}-app")
|
||||
url = ""
|
||||
if isinstance(entry, dict):
|
||||
url = entry.get("url", "")
|
||||
print(version)
|
||||
print(url)
|
||||
PY
|
||||
)" || return 1
|
||||
|
||||
LATEST_VERSION="$(echo "${parsed}" | sed -n '1p')"
|
||||
ASSET_URL="$(echo "${parsed}" | sed -n '2p')"
|
||||
ASSET_NAME="$(basename "${ASSET_URL}")"
|
||||
|
||||
[ -n "${ASSET_URL}" ]
|
||||
}
|
||||
|
||||
resolve_from_release_api() {
|
||||
if ! curl -fsSL "${LATEST_RELEASE_API_URL}" -o "${RELEASE_JSON_PATH}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
log_warn "python3 is not available; cannot parse release API fallback."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local parsed
|
||||
parsed="$(python3 - "${RELEASE_JSON_PATH}" "${OS}" "${ARCH}" <<'PY'
|
||||
import json, re, sys
|
||||
path, os_name, arch = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
tag = (data.get("tag_name") or "").lstrip("v")
|
||||
assets = data.get("assets", [])
|
||||
|
||||
def choose_asset():
|
||||
names = [a.get("name", "") for a in assets]
|
||||
chosen = None
|
||||
if os_name == "darwin" and arch == "aarch64":
|
||||
for n in names:
|
||||
if re.search(r"aarch64.*\\.app\\.tar\\.gz$", n):
|
||||
chosen = n
|
||||
break
|
||||
if not chosen:
|
||||
for n in names:
|
||||
if re.search(r"aarch64\\.dmg$", n):
|
||||
chosen = n
|
||||
break
|
||||
elif os_name == "darwin" and arch == "x86_64":
|
||||
for n in names:
|
||||
if re.search(r"(x86_64-apple-darwin|x64).*\\.app\\.tar\\.gz$", n):
|
||||
chosen = n
|
||||
break
|
||||
if not chosen:
|
||||
for n in names:
|
||||
if re.search(r"x64\\.dmg$", n):
|
||||
chosen = n
|
||||
break
|
||||
elif os_name == "linux" and arch == "x86_64":
|
||||
for n in names:
|
||||
if n.endswith(".AppImage"):
|
||||
chosen = n
|
||||
break
|
||||
if not chosen:
|
||||
return "", "", ""
|
||||
for asset in assets:
|
||||
if asset.get("name") == chosen:
|
||||
return chosen, asset.get("browser_download_url", ""), (asset.get("digest", "") or "").replace("sha256:", "")
|
||||
return "", "", ""
|
||||
|
||||
name, url, digest = choose_asset()
|
||||
print(tag)
|
||||
print(name)
|
||||
print(url)
|
||||
print(digest)
|
||||
PY
|
||||
)" || return 1
|
||||
|
||||
if [ -z "${LATEST_VERSION}" ]; then
|
||||
LATEST_VERSION="$(echo "${parsed}" | sed -n '1p')"
|
||||
fi
|
||||
ASSET_NAME="$(echo "${parsed}" | sed -n '2p')"
|
||||
ASSET_URL="$(echo "${parsed}" | sed -n '3p')"
|
||||
ASSET_SHA256="$(echo "${parsed}" | sed -n '4p')"
|
||||
|
||||
[ -n "${ASSET_URL}" ]
|
||||
}
|
||||
|
||||
resolve_release_digest() {
|
||||
if [ -z "${ASSET_NAME}" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ ! -s "${RELEASE_JSON_PATH}" ]; then
|
||||
if ! curl -fsSL "${LATEST_RELEASE_API_URL}" -o "${RELEASE_JSON_PATH}"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
local digest
|
||||
digest="$(python3 - "${RELEASE_JSON_PATH}" "${ASSET_NAME}" <<'PY'
|
||||
import json, sys
|
||||
path, name = sys.argv[1], sys.argv[2]
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
for asset in data.get("assets", []):
|
||||
if asset.get("name") == name:
|
||||
d = asset.get("digest", "") or ""
|
||||
print(d.replace("sha256:", ""))
|
||||
break
|
||||
PY
|
||||
)"
|
||||
if [ -n "${digest}" ]; then
|
||||
ASSET_SHA256="${digest}"
|
||||
fi
|
||||
}
|
||||
|
||||
if resolve_from_latest_json; then
|
||||
log_ok "Resolved latest release via latest.json (${LATEST_VERSION})"
|
||||
else
|
||||
log_warn "latest.json lookup failed. Falling back to releases API."
|
||||
if ! resolve_from_release_api; then
|
||||
log_err "Could not resolve a compatible asset for ${OS}/${ARCH}."
|
||||
exit 1
|
||||
fi
|
||||
log_ok "Resolved latest release via releases API (${LATEST_VERSION})"
|
||||
fi
|
||||
|
||||
resolve_release_digest
|
||||
|
||||
if [ -z "${ASSET_URL}" ]; then
|
||||
log_err "Could not determine download URL for ${OS}/${ARCH}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DOWNLOAD_PATH="${TMP_DIR}/${ASSET_NAME}"
|
||||
log_info "Downloading ${ASSET_NAME}"
|
||||
if [ "${DRY_RUN}" = true ]; then
|
||||
echo "DRY RUN: curl -fL ${ASSET_URL} -o ${DOWNLOAD_PATH}"
|
||||
else
|
||||
curl -fL "${ASSET_URL}" -o "${DOWNLOAD_PATH}"
|
||||
fi
|
||||
|
||||
compute_sha256() {
|
||||
local file="$1"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "${file}" | awk '{print $1}'
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "${file}" | awk '{print $1}'
|
||||
elif command -v openssl >/dev/null 2>&1; then
|
||||
openssl dgst -sha256 "${file}" | awk '{print $2}'
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -n "${ASSET_SHA256}" ]; then
|
||||
if [ "${DRY_RUN}" = true ]; then
|
||||
echo "DRY RUN: verify sha256 ${ASSET_SHA256} for ${DOWNLOAD_PATH}"
|
||||
else
|
||||
actual_sha256="$(compute_sha256 "${DOWNLOAD_PATH}" || true)"
|
||||
if [ -z "${actual_sha256}" ]; then
|
||||
log_warn "No checksum command available; skipping digest verification."
|
||||
elif [ "${actual_sha256}" != "${ASSET_SHA256}" ]; then
|
||||
log_err "SHA256 mismatch for ${ASSET_NAME}"
|
||||
log_err "Expected: ${ASSET_SHA256}"
|
||||
log_err "Actual: ${actual_sha256}"
|
||||
exit 1
|
||||
else
|
||||
log_ok "Integrity verified (sha256)"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log_warn "No SHA256 digest available for ${ASSET_NAME}; skipping integrity verification."
|
||||
fi
|
||||
|
||||
ensure_local_bin_path() {
|
||||
local bin_dir="${HOME}/.local/bin"
|
||||
if echo ":${PATH}:" | grep -q ":${bin_dir}:"; then
|
||||
return 0
|
||||
fi
|
||||
local shell_name config_file
|
||||
shell_name="$(basename "${SHELL:-/bin/bash}")"
|
||||
case "${shell_name}" in
|
||||
zsh) config_file="${HOME}/.zshrc" ;;
|
||||
bash) config_file="${HOME}/.bashrc" ;;
|
||||
*) config_file="${HOME}/.profile" ;;
|
||||
esac
|
||||
|
||||
if [ "${DRY_RUN}" = true ]; then
|
||||
echo "DRY RUN: ensure ${bin_dir} in PATH via ${config_file}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f "${config_file}" ]; then
|
||||
touch "${config_file}"
|
||||
fi
|
||||
if ! grep -q '.local/bin' "${config_file}"; then
|
||||
{
|
||||
echo ""
|
||||
echo '# OpenHuman installer - ensure local user binaries are on PATH'
|
||||
echo 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
} >> "${config_file}"
|
||||
log_ok "Added ~/.local/bin to ${config_file}"
|
||||
fi
|
||||
}
|
||||
|
||||
install_macos() {
|
||||
local apps_dir="${HOME}/Applications"
|
||||
local app_path="${apps_dir}/OpenHuman.app"
|
||||
mkdir -p "${apps_dir}"
|
||||
|
||||
if [[ "${ASSET_NAME}" == *.app.tar.gz ]]; then
|
||||
log_info "Installing OpenHuman.app into ${apps_dir}"
|
||||
if [ "${DRY_RUN}" = true ]; then
|
||||
echo "DRY RUN: tar -xzf ${DOWNLOAD_PATH} -C ${TMP_DIR}"
|
||||
echo "DRY RUN: replace ${app_path}"
|
||||
else
|
||||
tar -xzf "${DOWNLOAD_PATH}" -C "${TMP_DIR}"
|
||||
if [ ! -d "${TMP_DIR}/OpenHuman.app" ]; then
|
||||
log_err "Archive did not contain OpenHuman.app"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "${app_path}"
|
||||
cp -R "${TMP_DIR}/OpenHuman.app" "${app_path}"
|
||||
fi
|
||||
elif [[ "${ASSET_NAME}" == *.dmg ]]; then
|
||||
log_info "Mounting DMG and copying OpenHuman.app"
|
||||
if [ "${DRY_RUN}" = true ]; then
|
||||
echo "DRY RUN: hdiutil attach ${DOWNLOAD_PATH}"
|
||||
echo "DRY RUN: copy OpenHuman.app to ${app_path}"
|
||||
else
|
||||
if ! command -v hdiutil >/dev/null 2>&1; then
|
||||
log_err "hdiutil not available, cannot install from DMG."
|
||||
exit 1
|
||||
fi
|
||||
mount_output="$(hdiutil attach "${DOWNLOAD_PATH}" -nobrowse)"
|
||||
mount_point="$(echo "${mount_output}" | awk '/\/Volumes\// {print $NF; exit}')"
|
||||
if [ -z "${mount_point}" ] || [ ! -d "${mount_point}/OpenHuman.app" ]; then
|
||||
log_err "Could not find OpenHuman.app in mounted DMG."
|
||||
echo "${mount_output}"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "${app_path}"
|
||||
cp -R "${mount_point}/OpenHuman.app" "${app_path}"
|
||||
hdiutil detach "${mount_point}" >/dev/null
|
||||
fi
|
||||
else
|
||||
log_err "Unsupported macOS asset type: ${ASSET_NAME}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_ok "Installed at ${app_path}"
|
||||
echo ""
|
||||
echo "OpenHuman is ready."
|
||||
echo "Launch: open \"${app_path}\""
|
||||
echo "Uninstall: rm -rf \"${app_path}\""
|
||||
}
|
||||
|
||||
install_linux() {
|
||||
local bin_dir="${HOME}/.local/bin"
|
||||
local app_path="${bin_dir}/openhuman"
|
||||
local desktop_dir="${HOME}/.local/share/applications"
|
||||
local desktop_file="${desktop_dir}/openhuman.desktop"
|
||||
|
||||
mkdir -p "${bin_dir}" "${desktop_dir}"
|
||||
|
||||
if [[ "${ASSET_NAME}" != *.AppImage ]]; then
|
||||
log_err "Expected AppImage for Linux install, got: ${ASSET_NAME}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${DRY_RUN}" = true ]; then
|
||||
echo "DRY RUN: install ${DOWNLOAD_PATH} -> ${app_path}"
|
||||
else
|
||||
cp "${DOWNLOAD_PATH}" "${app_path}"
|
||||
chmod +x "${app_path}"
|
||||
fi
|
||||
|
||||
ensure_local_bin_path
|
||||
|
||||
if [ "${DRY_RUN}" = true ]; then
|
||||
echo "DRY RUN: write ${desktop_file}"
|
||||
else
|
||||
cat > "${desktop_file}" <<EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=OpenHuman
|
||||
Comment=OpenHuman desktop assistant
|
||||
Exec=${app_path}
|
||||
TryExec=${app_path}
|
||||
Terminal=false
|
||||
Categories=Utility;
|
||||
EOF
|
||||
fi
|
||||
|
||||
log_ok "Installed binary at ${app_path}"
|
||||
echo ""
|
||||
echo "OpenHuman is ready."
|
||||
echo "Launch: ${app_path}"
|
||||
echo "Uninstall: rm -f \"${app_path}\" \"${desktop_file}\""
|
||||
}
|
||||
|
||||
if [ "${OS}" = "darwin" ]; then
|
||||
install_macos
|
||||
else
|
||||
install_linux
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user