diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cbfcee2..4bf00c0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -64,9 +64,89 @@ jobs: name: bindings-${{ matrix.target }} path: gelectron_core.*.node + runtime: + name: Runtime ${{ matrix.target }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + target: aarch64-apple-darwin + platform: darwin + arch: arm64 + - os: macos-latest + target: x86_64-apple-darwin + platform: darwin + arch: x64 + - os: windows-latest + target: x86_64-pc-windows-msvc + platform: win32 + arch: x64 + - os: windows-latest + target: aarch64-pc-windows-msvc + platform: win32 + arch: arm64 + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + platform: linux + arch: x64 + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install Linux system dependencies + if: matrix.platform == 'linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libsoup-3.0-dev + + - name: Build gelectron binary + run: cargo build --release -p gelectron --target ${{ matrix.target }} + + - name: Package release archive + run: | + bash scripts/make-release.sh \ + -v "${{ github.ref_name }}" \ + -p ${{ matrix.platform }} \ + -a ${{ matrix.arch }} \ + -b "target/${{ matrix.target }}/release/gelectron" \ + --no-build + + - name: Build macOS installer (DMG) + if: matrix.platform == 'darwin' + run: bash scripts/pkg/make-dmg.sh -v "${{ github.ref_name }}" -a ${{ matrix.arch }} -b "target/${{ matrix.target }}/release/gelectron" -o dist + + - name: Build Windows installer (EXE) + if: matrix.platform == 'win32' && matrix.arch == 'x64' + shell: pwsh + run: | + choco install nsis -y --no-progress + powershell -File scripts/pkg/make-exe.ps1 -Bin "target/${{ matrix.target }}/release/gelectron.exe" -Compat src/electron -Version "${{ github.ref_name }}" -Arch x64 -Out dist + + - name: Build Linux installer (AppImage) + if: matrix.platform == 'linux' + run: bash scripts/pkg/make-appimage.sh -v "${{ github.ref_name }}" -b "target/${{ matrix.target }}/release/gelectron" -o dist + + - name: Upload installer artifacts + uses: actions/upload-artifact@v4 + with: + name: installers-${{ matrix.target }} + path: | + dist/gelectron-* + dist/Gelectron-* + if-no-files-found: error + release: name: Attach binaries to GitHub Release - needs: build + needs: [build, runtime] runs-on: ubuntu-latest permissions: contents: write @@ -79,10 +159,17 @@ jobs: pattern: bindings-* merge-multiple: true - - name: List binaries + - name: Download all installers + uses: actions/download-artifact@v4 + with: + path: artifacts + pattern: installers-* + merge-multiple: true + + - name: List artifacts run: ls -la artifacts/ - - name: Upload binaries to GitHub Release + - name: Upload assets to GitHub Release uses: softprops/action-gh-release@v2 with: - files: artifacts/*.node + files: artifacts/** diff --git a/README.md b/README.md index d030b1e..a014cbe 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,55 @@ Once installed, run any Electron app with: gelectron-core /path/to/electron-app ``` +### Download a native installer (double-click, no tools needed) + +Every new release tag (pushed as `v*`) is pre-compiled in CI and published as +platform-native, double-clickable installers on the [Releases page]: + +| Platform | Installer | What it does | +|---|---|---| +| macOS | `Gelectron--arm64.dmg` / `-x64.dmg` | Contains `Install gelectron.pkg`, which installs the runtime + compat layer to `/usr/local/bin` | +| Windows | `Gelectron--x64.exe` | NSIS installer → `C:\Program Files\Gelectron`, adds it to the system PATH, Start Menu + uninstaller | +| Linux | `Gelectron--x86_64.AppImage` | Self-contained bundle (includes Node.js + compat), just run it | + +After installing, `gelectron /path/to/electron-app` works from anywhere. + +> macOS installers are ad-hoc signed (no Developer ID), so on another Mac the +> first launch shows a Gatekeeper "unidentified developer" warning — right-click +> → Open to run it. + +### Install from GitHub Releases (no npm) + +Every new release tag (pushed as `v*`) is pre-compiled in CI and published as +a self-contained installer archive. Install the latest pre-built runtime with +a single command — no Rust toolchain, no npm: + +```bash +# macOS / Linux +curl -fsSL https://raw.githubusercontent.com/mileswolfallen2/gelectron/main/scripts/install-release.sh | bash + +# Windows (PowerShell) +irm https://raw.githubusercontent.com/mileswolfallen2/gelectron/main/scripts/install-release.ps1 | iex +``` + +The installer downloads `gelectron---.tar.gz` (or +`.zip`) from the latest GitHub Release and puts the `gelectron` binary plus +the JS compatibility layer into `~/.local/bin` (macOS/Linux) or +`%LOCALAPPDATA%\gelectron\bin` (Windows). Customize with: + +```bash +bash scripts/install-release.sh --version v0.1.1 # specific tag +bash scripts/install-release.sh --prefix ~/bin # custom location +bash scripts/install-release.sh --uninstall # remove +``` + +Archives can also be downloaded directly from the [Releases page] and +unpacked manually — the binary just needs the `compat/` folder next to it. + > The `gelectron-core` npm package is the recommended distribution channel. Building from source (below) is only needed if you're developing Gelectron itself or want the bleeding-edge version. +[Releases page]: https://github.com/mileswolfallen2/gelectron/releases + ### Prerequisites (for building from source) - Rust 1.75+ (`rustup.rs`) diff --git a/scripts/install-release.ps1 b/scripts/install-release.ps1 new file mode 100644 index 0000000..99f40a6 --- /dev/null +++ b/scripts/install-release.ps1 @@ -0,0 +1,119 @@ +# gelectron installer (PowerShell / Windows) +# +# Downloads the pre-built gelectron binary + Electron compatibility layer from +# GitHub Releases and installs it into PREFIX so `gelectron ` works from +# anywhere. No Rust toolchain or npm required. +# +# Install layout: +# /gelectron.exe native binary +# /compat/*.js Electron API compat layer +# +# Usage: +# .\scripts\install-release.ps1 latest release +# .\scripts\install-release.ps1 -Version v0.1.1 +# .\scripts\install-release.ps1 -File C:\path\to\gelectron-0.1.1-win32-x64.zip +# .\scripts\install-release.ps1 -Prefix "$HOME\bin" +# .\scripts\install-release.ps1 -Uninstall + +param( + [string]$Repo = "mileswolfallen2/gelectron", + [string]$Prefix = "", + [string]$Version = "", + [string]$File = "", + [switch]$Uninstall, + [switch]$Help +) + +$ErrorActionPreference = "Stop" + +function Show-Help { + @" + gelectron installer - install pre-built gelectron from GitHub Releases + + Usage: + install-release.ps1 [options] + + Options: + -Repo owner/repo GitHub repo (default: mileswolfallen2/gelectron) + -Prefix DIR Install directory (default: %LOCALAPPDATA%\gelectron\bin) + -Version TAG Install a specific release tag (default: latest) + -File PATH Install from a local archive instead of downloading + -Uninstall Remove previously installed files + -Help Show this help +"@ | Write-Host +} + +if ($Help) { Show-Help; exit 0 } +if (-not $Prefix) { $Prefix = Join-Path $env:LOCALAPPDATA "gelectron\bin" } + +# ── Detect platform / arch ────────────────────────────────────────────────── + +$Platform = "win32" +switch ($env:PROCESSOR_ARCHITECTURE) { + "ARM64" { $Arch = "arm64" } + default { $Arch = "x64" } +} +$ArchiveExt = "zip" + +# ── Uninstall ─────────────────────────────────────────────────────────────── + +if ($Uninstall) { + foreach ($p in @((Join-Path $Prefix "gelectron.exe"), (Join-Path $Prefix "compat"))) { + if (Test-Path $p) { Remove-Item -Recurse -Force $p } + } + Write-Host "Removed $Prefix\gelectron.exe and $Prefix\compat" + exit 0 +} + +# ── Resolve version + archive ─────────────────────────────────────────────── + +$ArchiveFile = $File +if (-not $ArchiveFile) { + if ($Version) { + $Tag = "v" + ($Version.TrimStart("v")) + } else { + Write-Host "==> Resolving latest release from $Repo..." + $latest = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" -Headers @{ "User-Agent" = "gelectron-installer" } + $Tag = $latest.tag_name + } + $Ver = $Tag.TrimStart("v") + $Asset = "gelectron-$Ver-$Platform-$Arch.$ArchiveExt" + $Url = "https://github.com/$Repo/releases/download/$Tag/$Asset" + Write-Host "==> Downloading $Asset ..." + $ArchiveFile = Join-Path $env:TEMP "gelectron-install-$([Guid]::NewGuid().ToString('N')).$ArchiveExt" + Invoke-WebRequest -Uri $Url -OutFile $ArchiveFile -UseBasicParsing +} + +# ── Extract ───────────────────────────────────────────────────────────────── + +$Stage = Join-Path $env:TEMP ("gelectron-install-stage-" + [Guid]::NewGuid().ToString("N")) +New-Item -ItemType Directory -Path $Stage | Out-Null +try { + Write-Host "==> Extracting archive..." + Expand-Archive -Path $ArchiveFile -DestinationPath $Stage -Force + + $Bin = Join-Path $Stage "gelectron.exe" + if (-not (Test-Path $Bin)) { + throw "archive does not contain gelectron.exe" + } + + # ── Install ───────────────────────────────────────────────────────────── + + Write-Host "==> Installing to $Prefix" + New-Item -ItemType Directory -Path $Prefix -Force | Out-Null + Copy-Item $Bin (Join-Path $Prefix "gelectron.exe") -Force + if (Test-Path (Join-Path $Stage "compat")) { + Copy-Item -Recurse -Force (Join-Path $Stage "compat") $Prefix + } + + Write-Host "" + Write-Host " OK Installed: $Prefix\gelectron.exe" + Write-Host " OK Installed: $Prefix\compat\" + Write-Host "" + Write-Host " Add $Prefix to your PATH, then run:" + Write-Host ' gelectron C:\path\to\electron-app' +} +finally { + Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue + if (-not $File) { Remove-Item -Force $ArchiveFile -ErrorAction SilentlyContinue } +} \ No newline at end of file diff --git a/scripts/install-release.sh b/scripts/install-release.sh new file mode 100755 index 0000000..dad638d --- /dev/null +++ b/scripts/install-release.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +# +# gelectron installer +# +# Downloads the pre-built gelectron binary + Electron compatibility layer from +# GitHub Releases and installs it into PREFIX so `gelectron ` works from +# anywhere. No Rust toolchain or Node dependency manager required. +# +# Install layout: +# /gelectron native binary +# /compat/*.js Electron API compat layer +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/mileswolfallen2/gelectron/main/scripts/install-release.sh | bash +# ./scripts/install-release.sh latest release +# ./scripts/install-release.sh --version v0.1.1 +# ./scripts/install-release.sh --file /path/to/gelectron-0.1.1-darwin-arm64.tar.gz +# PREFIX=~/bin ./scripts/install-release.sh custom prefix +# ./scripts/install-release.sh --uninstall remove installed files + +set -euo pipefail + +DEFAULT_REPO="mileswolfallen2/gelectron" +DEFAULT_PREFIX="${HOME}/.local/bin" + +REPO="$DEFAULT_REPO" +PREFIX="${PREFIX:-$DEFAULT_PREFIX}" +VERSION="" +FILE="" +UNINSTALL=0 + +usage() { + cat <<'EOF' + gelectron installer — install pre-built gelectron from GitHub Releases + + Usage: + install-release.sh [options] + + Options: + --repo owner/repo GitHub repo to fetch releases from (default: mileswolfallen2/gelectron) + --prefix DIR Install directory (env: PREFIX, default: ~/.local/bin) + --version TAG Install a specific release tag (default: latest) + --file PATH Install from a local release archive instead of downloading + --uninstall Remove previously installed files + -h, --help Show this help +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --repo) REPO="${2:-}"; shift 2 ;; + --prefix) PREFIX="${2:-}"; shift 2 ;; + --version) VERSION="${2:-}"; shift 2 ;; + --file) FILE="${2:-}"; shift 2 ;; + --uninstall) UNINSTALL=1; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "error: unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +# ── Detect platform / arch ────────────────────────────────────────────────── + +case "$(uname -s)" in + Darwin) PLATFORM="darwin" ;; + MINGW*|MSYS*|CYGWIN*) PLATFORM="win32" ;; + Linux) PLATFORM="linux" ;; + *) echo "error: unsupported platform: $(uname -s)" >&2; exit 1 ;; +esac + +case "$(uname -m)" in + arm64|aarch64) ARCH="arm64" ;; + x86_64|amd64) ARCH="x64" ;; + *) echo "error: unsupported arch: $(uname -m)" >&2; exit 1 ;; +esac + +[[ "$PLATFORM" == "win32" ]] && ARCHIVE_EXT="zip" || ARCHIVE_EXT="tar.gz" + +# ── Uninstall ─────────────────────────────────────────────────────────────── + +if [[ "$UNINSTALL" == "1" ]]; then + rm -f "$PREFIX/gelectron" + rm -f "$PREFIX/gelectron.exe" + rm -rf "$PREFIX/compat" + echo "Removed $PREFIX/gelectron and $PREFIX/compat" + exit 0 +fi + +# ── Resolve version + archive ─────────────────────────────────────────────── + +if [[ -n "$FILE" ]]; then + ARCHIVE_FILE="$FILE" +else + if [[ -n "$VERSION" ]]; then + TAG="${VERSION#v}" + TAG="v${TAG}" + else + echo "==> Resolving latest release from $REPO..." + if ! command -v curl >/dev/null 2>&1; then + echo "error: curl is required" >&2 + exit 1 + fi + API_URL="https://api.github.com/repos/$REPO/releases/latest" + TAG="$(curl -fsSL "$API_URL" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n1)" + if [[ -z "$TAG" ]]; then + echo "error: could not resolve latest release from $API_URL" >&2 + exit 1 + fi + fi + VER="${TAG#v}" + ASSET="gelectron-$VER-$PLATFORM-$ARCH.$ARCHIVE_EXT" + URL="https://github.com/$REPO/releases/download/$TAG/$ASSET" + echo "==> Downloading $ASSET ..." + ARCHIVE_FILE="$(mktemp /tmp/gelectron-install.XXXXXX.$ARCHIVE_EXT)" + trap 'rm -f "$ARCHIVE_FILE"' EXIT + curl -fsSL -L "$URL" -o "$ARCHIVE_FILE" +fi + +# ── Extract ───────────────────────────────────────────────────────────────── + +STAGE="$(mktemp -d "${TMPDIR:-/tmp}/gelectron-install-stage.XXXXXX")" +trap 'rm -rf "$STAGE" "${ARCHIVE_FILE:-}"' EXIT + +echo "==> Extracting archive..." +case "$ARCHIVE_EXT" in + zip) unzip -qo "$ARCHIVE_FILE" -d "$STAGE" ;; + *) tar -xzf "$ARCHIVE_FILE" -C "$STAGE" ;; +esac + +BIN_ABS="$STAGE/gelectron" +[[ -f "$STAGE/gelectron.exe" ]] && BIN_ABS="$STAGE/gelectron.exe" +if [[ ! -f "$BIN_ABS" ]]; then + echo "error: archive does not contain gelectron" >&2 + ls -la "$STAGE" >&2 + exit 1 +fi + +# ── Install ───────────────────────────────────────────────────────────────── + +echo "==> Installing to $PREFIX" +mkdir -p "$PREFIX" + +install -m 755 "$BIN_ABS" "$PREFIX/$(basename "$BIN_ABS")" +if [[ -d "$STAGE/compat" ]]; then + mkdir -p "$PREFIX/compat" + install -m 644 "$STAGE"/compat/*.js "$PREFIX/compat/" +fi + +# Downloaded-from-GitHub binaries carry a quarantine attribute on macOS and +# Gatekeeper will block the first launch. Clear it — the binary is ad-hoc +# signed, so this is the only thing standing in the way. +if [[ "$PLATFORM" == "darwin" ]]; then + xattr -dr com.apple.quarantine "$PREFIX/$(basename "$BIN_ABS")" 2>/dev/null || true +fi + +echo +echo " ✓ Installed: $PREFIX/$(basename "$BIN_ABS")" +echo " ✓ Installed: $PREFIX/compat/" + +if ! command -v gelectron >/dev/null 2>&1 || [[ "$(command -v gelectron)" != "$PREFIX/gelectron" ]]; then + echo + echo " warning: $PREFIX is not on your PATH." + case "$SHELL" in + *zsh) RC="$HOME/.zshrc" ;; + *bash) RC="$HOME/.bashrc" ;; + *) RC="$HOME/.profile" ;; + esac + echo " Add this line to $RC:" + echo " export PATH=\"$PREFIX:\$PATH\"" +fi + +echo +"$PREFIX/$(basename "$BIN_ABS")" --version >/dev/null 2>&1 && \ + echo " Run: gelectron /path/to/electron-app" || \ + echo " Hint: $PREFIX/$(basename "$BIN_ABS") --version" \ No newline at end of file diff --git a/scripts/make-release.sh b/scripts/make-release.sh new file mode 100755 index 0000000..1c06c6b --- /dev/null +++ b/scripts/make-release.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# +# gelectron release builder +# +# Pre-compiles the gelectron native binary and bundles it with the Electron +# compatibility layer (src/electron/*.js) into a single, self-contained +# installer archive per platform/arch: +# +# gelectron--darwin-arm64.tar.gz +# gelectron--darwin-x64.tar.gz +# gelectron--win32-arm64.zip +# gelectron--win32-x64.zip +# gelectron--linux-arm64.tar.gz +# gelectron--linux-x64.tar.gz +# +# Archive layout (the install scripts unpack this into PREFIX): +# gelectron | gelectron.exe native binary +# compat/*.js Electron API compatibility layer +# +# This script is what the GitHub Actions workflow runs on every new release +# tag; it can also be run locally for testing or manual distributions. + +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +VERSION="" +PLATFORM="" +ARCH="" +BINARY="" +OUT_DIR="$REPO_DIR/dist" +BUILD=1 + +usage() { + cat <<'EOF' + gelectron release builder + + Usage: + scripts/make-release.sh [options] + + Options: + -v, --version VER Release version (default: from package.json, minus v) + -p, --platform P Target OS: darwin, win32, linux (default: current) + -a, --arch A Target arch: x64, arm64 (default: current) + -b, --binary PATH Use an already-built gelectron binary (skips cargo build) + -o, --out DIR Output directory (default: /dist) + -n, --no-build Do not compile (requires --binary or an existing build) + -h, --help Show this help +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -v|--version) VERSION="${2:-}"; shift 2 ;; + -p|--platform) PLATFORM="${2:-}"; shift 2 ;; + -a|--arch) ARCH="${2:-}"; shift 2 ;; + -b|--binary) BINARY="${2:-}"; shift 2 ;; + -o|--out) OUT_DIR="${2:-}"; shift 2 ;; + -n|--no-build) BUILD=0; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "error: unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +# ── Resolve defaults ──────────────────────────────────────────────────────── + +if [[ -z "$VERSION" ]]; then + VERSION="$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo '0.0.0')" +fi +VERSION="${VERSION#v}" + +if [[ -z "$PLATFORM" ]]; then + case "$(uname -s)" in + Darwin) PLATFORM="darwin" ;; + MINGW*|MSYS*|CYGWIN*) PLATFORM="win32" ;; + Linux) PLATFORM="linux" ;; + *) echo "error: cannot detect platform" >&2; exit 1 ;; + esac +fi + +if [[ -z "$ARCH" ]]; then + case "$(uname -m)" in + arm64|aarch64) ARCH="arm64" ;; + x86_64|amd64) ARCH="x64" ;; + *) echo "error: cannot detect arch" >&2; exit 1 ;; + esac +fi + +case "$PLATFORM-$ARCH" in + darwin-arm64|darwin-x64|win32-arm64|win32-x64|linux-arm64|linux-x64) ;; + *) echo "error: unsupported platform/arch: $PLATFORM/$ARCH" >&2; exit 1 ;; +esac + +EXE_SUFFIX="" +[[ "$PLATFORM" == "win32" ]] && EXE_SUFFIX=".exe" +[[ "$PLATFORM" == "win32" ]] && ARCHIVE_EXT="zip" || ARCHIVE_EXT="tar.gz" + +log() { echo "==> $*"; } + +# ── Locate or build the gelectron binary ──────────────────────────────────── + +BIN="$BINARY" +if [[ -z "$BIN" && "$BUILD" == "1" ]]; then + CANDIDATES=( + "$REPO_DIR/target/release/gelectron$EXE_SUFFIX" + "$REPO_DIR/target/debug/gelectron$EXE_SUFFIX" + ) + for c in "${CANDIDATES[@]}"; do + if [[ -f "$c" ]]; then BIN="$c"; break; fi + done +fi + +if [[ -z "$BIN" && "$BUILD" == "1" ]]; then + log "Building gelectron (release)..." + cargo build --release --manifest-path "$REPO_DIR/Cargo.toml" -p gelectron + BIN="$REPO_DIR/target/release/gelectron$EXE_SUFFIX" +fi + +# A --binary path may omit the platform extension (e.g. CI passes the target +# path before the .exe suffix is appended). +if [[ -n "$BIN" && ! -f "$BIN" && -n "$EXE_SUFFIX" && -f "$BIN$EXE_SUFFIX" ]]; then + BIN="$BIN$EXE_SUFFIX" +fi + +if [[ -z "$BIN" || ! -f "$BIN" ]]; then + echo "error: gelectron binary not found. Build it first (cargo build --release -p gelectron) or pass --binary." >&2 + exit 1 +fi +BIN="$(cd "$(dirname "$BIN")" && pwd)/$(basename "$BIN")" + +# ── Stage the payload ─────────────────────────────────────────────────────── + +STAGE="$(mktemp -d "${TMPDIR:-/tmp}/gelectron-release.XXXXXX")" +trap 'rm -rf "$STAGE"' EXIT + +PAYLOAD="$STAGE/payload" +mkdir -p "$PAYLOAD" + +log "Copying binary ($(basename "$BIN"))..." +install -m 755 "$BIN" "$PAYLOAD/gelectron$EXE_SUFFIX" + +COMPAT_SRC="$REPO_DIR/src/electron" +if [[ ! -d "$COMPAT_SRC" || -z "$(ls "$COMPAT_SRC"/*.js 2>/dev/null)" ]]; then + echo "error: compat layer not found in $COMPAT_SRC" >&2 + exit 1 +fi +log "Copying compat layer..." +mkdir -p "$PAYLOAD/compat" +install -m 644 "$COMPAT_SRC"/*.js "$PAYLOAD/compat/" + +# Ad-hoc sign so the binary actually runs on arm64 Macs (unsigned binaries +# are killed by the kernel). No certificate required. +if [[ "$PLATFORM" == "darwin" ]]; then + if command -v codesign >/dev/null 2>&1; then + log "Ad-hoc signing binary..." + codesign --force --sign - "$PAYLOAD/gelectron" 2>/dev/null || echo " (warning: codesign failed)" + fi +fi + +# ── Archive ───────────────────────────────────────────────────────────────── + +mkdir -p "$OUT_DIR" +ARCHIVE_NAME="gelectron-$VERSION-$PLATFORM-$ARCH.$ARCHIVE_EXT" +ARCHIVE_PATH="$OUT_DIR/$ARCHIVE_NAME" + +if [[ "$ARCHIVE_EXT" == "zip" ]]; then + log "Creating $ARCHIVE_NAME..." + if command -v powershell >/dev/null 2>&1; then + powershell -NoProfile -Command "Compress-Archive -Path '$PAYLOAD/*' -DestinationPath '$ARCHIVE_PATH' -Force" + else + (cd "$PAYLOAD" && zip -qr "$ARCHIVE_PATH" .) + fi +else + log "Creating $ARCHIVE_NAME..." + (cd "$PAYLOAD" && tar -czf "$ARCHIVE_PATH" .) +fi + +echo +log "Created installer: $ARCHIVE_PATH" +sha256sum "$ARCHIVE_PATH" 2>/dev/null || shasum -a 256 "$ARCHIVE_PATH" 2>/dev/null || true \ No newline at end of file diff --git a/scripts/pkg/gelectron.nsi b/scripts/pkg/gelectron.nsi new file mode 100644 index 0000000..06a2604 --- /dev/null +++ b/scripts/pkg/gelectron.nsi @@ -0,0 +1,90 @@ +; Gelectron Windows installer (NSIS) +; +; Build with makensis from a staging directory containing: +; gelectron.exe +; compat\*.js +; +; makensis /DVERSION=0.1.1 /DARCH=x64 Gelectron.nsi +; +; Installs to $PROGRAMFILES64\Gelectron, adds it to the machine PATH, creates +; Start Menu + Add/Remove Programs entries, and writes an uninstaller. + +!include "MUI2.nsh" +!include "WordFunc.nsh" + +!ifndef VERSION + !define VERSION "0.0.0" +!endif +!ifndef ARCH + !define ARCH "x64" +!endif + +!define APPNAME "Gelectron" +!define COMPANY "gelectron" +!define ENV_KEY 'HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"' +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" + +Name "${APPNAME}" +OutFile "Gelectron-${VERSION}-${ARCH}.exe" +InstallDir "$PROGRAMFILES64\${APPNAME}" +InstallDirRegKey HKLM "Software\${APPNAME}" "InstallDir" +RequestExecutionLevel admin + +; ── Modern UI ────────────────────────────────────────────────────────────── + +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "English" + +; ── Install section ───────────────────────────────────────────────────────── + +Section "Install" + SetShellVarContext all + SetOutPath "$INSTDIR" + + File "gelectron.exe" + File /r "compat" + + WriteUninstaller "$INSTDIR\Uninstall.exe" + + CreateDirectory "$SMPROGRAMS\${APPNAME}" + CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" "$INSTDIR\gelectron.exe" + + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${APPNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${VERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${COMPANY}" + WriteRegStr HKLM "${UNINST_KEY}" "InstallLocation" "$INSTDIR" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" '"$INSTDIR\Uninstall.exe"' + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\gelectron.exe" + + ; Add $INSTDIR to the machine PATH (add-if-not-present via WordFunc) + ReadRegStr $0 ${ENV_KEY} "Path" + ${WordAdd} $0 ";" "+$INSTDIR" $1 + WriteRegExpandStr ${ENV_KEY} "Path" $1 + SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment" +SectionEnd + +; ── Uninstall section ─────────────────────────────────────────────────────── + +Section "Uninstall" + SetShellVarContext all + ; Remove $INSTDIR from the machine PATH + ReadRegStr $0 ${ENV_KEY} "Path" + ${WordAdd} $0 ";" "-$INSTDIR" $1 + WriteRegExpandStr ${ENV_KEY} "Path" $1 + SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment" + + Delete "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" + RMDir "$SMPROGRAMS\${APPNAME}" + + DeleteRegKey HKLM "${UNINST_KEY}" + DeleteRegKey HKLM "Software\${APPNAME}" + + RMDir /r "$INSTDIR" +SectionEnd \ No newline at end of file diff --git a/scripts/pkg/make-appimage.sh b/scripts/pkg/make-appimage.sh new file mode 100755 index 0000000..30dba1b --- /dev/null +++ b/scripts/pkg/make-appimage.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# +# Build the Linux AppImage: bundles the gelectron binary, the compat layer +# and a Node.js runtime into a single self-contained, downloadable AppImage. +# +# Gelectron--x86_64.AppImage +# +# Requires: the gelectron binary (built against webkit2gtk-4.1), compat layer, +# and network access (downloads Node.js + appimagetool). +# +# Usage: +# scripts/pkg/make-appimage.sh -v VERSION [--binary PATH] [--compat DIR] [-o DIR] + +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +NODE_VERSION="20.18.1" + +VERSION="" +BINARY="" +COMPAT="$REPO_DIR/src/electron" +OUT_DIR="$REPO_DIR/dist" + +usage() { + cat <<'EOF' + Linux AppImage builder + + Usage: + scripts/pkg/make-appimage.sh -v VERSION [--binary PATH] [--compat DIR] [-o DIR] + + Options: + -v, --version VER Version string (e.g. 0.1.1) + -b, --binary PATH Path to the gelectron binary + -c, --compat DIR Path to the compat layer (default: /src/electron) + -o, --out DIR Output directory (default: /dist) + -h, --help Show this help +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -v|--version) VERSION="${2:-}"; shift 2 ;; + -b|--binary) BINARY="${2:-}"; shift 2 ;; + -c|--compat) COMPAT="${2:-}"; shift 2 ;; + -o|--out) OUT_DIR="${2:-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "error: unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +VERSION="${VERSION#v}" +[[ -n "$VERSION" ]] || { echo "error: --version required" >&2; exit 1; } +if [[ -z "$BINARY" ]]; then + BINARY="$REPO_DIR/target/release/gelectron" +fi +[[ -f "$BINARY" ]] || { echo "error: binary not found: $BINARY" >&2; exit 1; } +[[ -d "$COMPAT" ]] || { echo "error: compat dir not found: $COMPAT" >&2; exit 1; } + +log() { echo "==> $*"; } + +STAGE="$(mktemp -d "${TMPDIR:-/tmp}/gelectron-appimage.XXXXXX")" +trap 'rm -rf "$STAGE"' EXIT + +APPDIR="$STAGE/Gelectron.AppDir" +mkdir -p "$APPDIR/usr/bin" + +# ── Binaries ──────────────────────────────────────────────────────────────── + +log "Staging binary..." +install -m 755 "$BINARY" "$APPDIR/usr/bin/gelectron" + +log "Staging compat layer (usr/bin/compat, resolved next to the binary)..." +mkdir -p "$APPDIR/usr/bin/compat" +install -m 644 "$COMPAT"/*.js "$APPDIR/usr/bin/compat/" + +# Node.js runtime (gelectron's Node mode requires node on PATH) +NODE_DIR="$STAGE/node" +if [[ ! -d "$NODE_DIR" ]]; then + log "Downloading Node.js v$NODE_VERSION..." + mkdir -p "$NODE_DIR" + NODE_ARCHIVE="$STAGE/node.tar.xz" + curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" -o "$NODE_ARCHIVE" + tar -xJf "$NODE_ARCHIVE" -C "$NODE_DIR" +fi +install -m 755 "$NODE_DIR"/node-v${NODE_VERSION}-linux-x64/bin/node "$APPDIR/usr/bin/node" +install -m 644 "$NODE_DIR"/node-v${NODE_VERSION}-linux-x64/lib/libnode.so* "$APPDIR/usr/lib/" 2>/dev/null || true + +mkdir -p "$APPDIR/usr/lib" +cp "$NODE_DIR"/node-v${NODE_VERSION}-linux-x64/lib/libnode.so* "$APPDIR/usr/lib/" 2>/dev/null || true + +# ── AppImage metadata ─────────────────────────────────────────────────────── + +cat > "$APPDIR/AppRun" <<'EOF' +#!/bin/sh +SELF="$(dirname "$(readlink -f "$0")")" +export PATH="$SELF/usr/bin:$PATH" +export LD_LIBRARY_PATH="$SELF/usr/lib:${LD_LIBRARY_PATH:-}" +export GELECTRON_NATIVE=1 +exec "$SELF/usr/bin/gelectron" "$@" +EOF +chmod +x "$APPDIR/AppRun" + +cat > "$APPDIR/gelectron.desktop" </dev/null + +echo +log "Created installer: $APPIMAGE_PATH" +sha256sum "$APPIMAGE_PATH" 2>/dev/null || shasum -a 256 "$APPIMAGE_PATH" 2>/dev/null || true \ No newline at end of file diff --git a/scripts/pkg/make-dmg.sh b/scripts/pkg/make-dmg.sh new file mode 100755 index 0000000..60c8afb --- /dev/null +++ b/scripts/pkg/make-dmg.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# +# Build the macOS installer: a .pkg (installs the gelectron runtime + +# compat layer to /usr/local/bin) wrapped in a .dmg for distribution. +# +# Gelectron--.dmg +# └── Install gelectron.pkg (double-click, runs macOS Installer as root) +# +# Requires macOS (pkgbuild + hdiutil). Ad-hoc signing only — no Developer ID +# certificate, so first launch shows a Gatekeeper warning on other machines. + +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +VERSION="" +ARCH="" +BINARY="" +COMPAT="$REPO_DIR/src/electron" +OUT_DIR="$REPO_DIR/dist" + +usage() { + cat <<'EOF' + macOS installer builder (pkg inside DMG) + + Usage: + scripts/pkg/make-dmg.sh -v VERSION -a ARCH [--binary PATH] [--compat DIR] [-o DIR] + + Options: + -v, --version VER Version string (e.g. 0.1.1) + -a, --arch A arm64 | x64 + -b, --binary PATH Path to the gelectron binary + -c, --compat DIR Path to the compat layer (default: /src/electron) + -o, --out DIR Output directory (default: /dist) + -h, --help Show this help +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -v|--version) VERSION="${2:-}"; shift 2 ;; + -a|--arch) ARCH="${2:-}"; shift 2 ;; + -b|--binary) BINARY="${2:-}"; shift 2 ;; + -c|--compat) COMPAT="${2:-}"; shift 2 ;; + -o|--out) OUT_DIR="${2:-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "error: unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +[[ -n "$VERSION" ]] || { echo "error: --version required" >&2; exit 1; } +VERSION="${VERSION#v}" +[[ -n "$ARCH" ]] || { echo "error: --arch required" >&2; exit 1; } +case "$ARCH" in arm64|x64) ;; *) echo "error: unsupported arch: $ARCH" >&2; exit 1 ;; esac + +if [[ -z "$BINARY" ]]; then + BINARY="$REPO_DIR/target/release/gelectron" +fi +[[ -f "$BINARY" ]] || { echo "error: binary not found: $BINARY" >&2; exit 1; } +[[ -d "$COMPAT" ]] || { echo "error: compat dir not found: $COMPAT" >&2; exit 1; } + +log() { echo "==> $*"; } + +STAGE="$(mktemp -d "${TMPDIR:-/tmp}/gelectron-dmg.XXXXXX")" +trap 'rm -rf "$STAGE"' EXIT + +# ── Payload root (what pkgbuild installs into /usr/local/bin) ───────────── + +PAYLOAD="$STAGE/gelectron" +mkdir -p "$PAYLOAD" + +log "Staging payload..." +install -m 755 "$BINARY" "$PAYLOAD/gelectron" +mkdir -p "$PAYLOAD/compat" +install -m 644 "$COMPAT"/*.js "$PAYLOAD/compat/" + +if command -v codesign >/dev/null 2>&1; then + log "Ad-hoc signing binary..." + codesign --force --sign - "$PAYLOAD/gelectron" 2>/dev/null || echo " (warning: codesign failed)" +fi + +# ── Component package ─────────────────────────────────────────────────────── + +PKG="$STAGE/Install gelectron.pkg" +log "Building package (installs to /usr/local/bin)..." +pkgbuild \ + --root "$PAYLOAD" \ + --identifier "com.gelectron.runtime.$ARCH" \ + --version "$VERSION" \ + --ownership recommended \ + --install-location /usr/local/bin \ + "$PKG" + +# ── DMG ──────────────────────────────────────────────────────────────────── + +mkdir -p "$OUT_DIR" +DMG_NAME="Gelectron-$VERSION-$ARCH.dmg" +DMG_PATH="$OUT_DIR/$DMG_NAME" + +DMG_STAGE="$STAGE/dmg" +mkdir -p "$DMG_STAGE" +cp "$PKG" "$DMG_STAGE/" + +log "Creating $DMG_NAME ..." +hdiutil create \ + -volname "Gelectron $VERSION" \ + -srcfolder "$DMG_STAGE" \ + -format UDZO \ + -ov \ + "$DMG_PATH" >/dev/null + +echo +log "Created installer: $DMG_PATH" +shasum -a 256 "$DMG_PATH" \ No newline at end of file diff --git a/scripts/pkg/make-exe.ps1 b/scripts/pkg/make-exe.ps1 new file mode 100644 index 0000000..145cb98 --- /dev/null +++ b/scripts/pkg/make-exe.ps1 @@ -0,0 +1,58 @@ +# Gelectron Windows installer builder (NSIS) +# +# Stages the payload and compiles the installer EXE with makensis: +# Gelectron--.exe +# +# Usage: +# powershell -File scripts/pkg/make-exe.ps1 ` +# -Bin target\release\gelectron.exe ` +# -Compat src\electron ` +# -Version 0.1.1 -Arch x64 -Out dist +# +# Requires makensis on PATH (install via: choco install nsis -y) + +param( + [Parameter(Mandatory = $true)][string]$Bin, + [Parameter(Mandatory = $true)][string]$Compat, + [Parameter(Mandatory = $true)][string]$Version, + [string]$Arch = "x64", + [string]$Out = "dist", + [string]$NsiPath = "scripts/pkg/gelectron.nsi" +) + +$ErrorActionPreference = "Stop" + +if (-not (Get-Command makensis -ErrorAction SilentlyContinue)) { + throw "makensis not found. Install NSIS first (choco install nsis -y)." +} + +$RealityVersion = $Version.TrimStart("v") + +$Stage = Join-Path $env:TEMP ("gelectron-nsis-" + [Guid]::NewGuid().ToString("N")) +New-Item -ItemType Directory -Path $Stage -Force | Out-Null +try { + Copy-Item $Bin (Join-Path $Stage "gelectron.exe") -Force + Copy-Item -Recurse -Force $Compat (Join-Path $Stage "compat") + Copy-Item $NsiPath (Join-Path $Stage "gelectron.nsi") -Force + + Write-Host "==> Compiling installer (makensis)..." + Push-Location $Stage + try { + & makensis "-DVERSION=$RealityVersion" "-DARCH=$Arch" gelectron.nsi + if ($LASTEXITCODE -ne 0) { throw "makensis failed with exit code $LASTEXITCODE" } + } + finally { + Pop-Location + } + + $Installer = Get-ChildItem $Stage -Filter "Gelectron-*.exe" | Select-Object -First 1 + if (-not $Installer) { throw "makensis produced no installer" } + + New-Item -ItemType Directory -Path $Out -Force | Out-Null + Copy-Item $Installer.FullName (Join-Path $Out $Installer.Name) -Force + Write-Host "" + Write-Host "==> Created installer: $(Join-Path $Out $Installer.Name)" +} +finally { + Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue +} \ No newline at end of file