mirror of
https://github.com/mileswolfallen2/gelectron.git
synced 2026-09-08 12:43:16 +00:00
Compare commits
2
Commits
40f15bc45a
...
new1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1689adffc6 | ||
|
|
247daa6460 |
@@ -1,25 +1,41 @@
|
|||||||
name: Build native binaries
|
name: Build native binaries
|
||||||
|
|
||||||
|
# On every new release (tag push of `v*`), this workflow:
|
||||||
|
# 1. builds the gelectron native engine + N-API addons for all platforms,
|
||||||
|
# 2. packages portable archives, and
|
||||||
|
# 3. builds installers you can simply install:
|
||||||
|
# - macOS: Gelectron-<ver>-arm64.dmg / Gelectron-<ver>-x64.dmg
|
||||||
|
# - Windows: Gelectron-<ver>-x64.exe / Gelectron-<ver>-arm64.exe
|
||||||
|
# - Linux: Gelectron-<ver>-x86_64.AppImage / Gelectron-<ver>-aarch64.AppImage
|
||||||
|
#
|
||||||
|
# Everything is attached to the GitHub Release for that tag.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
# ── N-API addons (published to npm, also attached to the release) ─────────
|
||||||
name: Build ${{ matrix.target }}
|
addon:
|
||||||
|
name: Addon ${{ matrix.target }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: macos-latest
|
- os: macos-14
|
||||||
target: aarch64-apple-darwin
|
target: aarch64-apple-darwin
|
||||||
- os: macos-latest
|
- os: macos-13
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
target: aarch64-pc-windows-msvc
|
target: aarch64-pc-windows-msvc
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
- os: ubuntu-24.04-arm
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
@@ -64,9 +80,112 @@ jobs:
|
|||||||
name: bindings-${{ matrix.target }}
|
name: bindings-${{ matrix.target }}
|
||||||
path: gelectron_core.*.node
|
path: gelectron_core.*.node
|
||||||
|
|
||||||
|
# ── Native engine + portable archives + installers ────────────────────────
|
||||||
|
runtime:
|
||||||
|
name: Runtime ${{ matrix.platform }}-${{ matrix.arch }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: macos-14
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
platform: darwin
|
||||||
|
arch: arm64
|
||||||
|
- os: macos-13
|
||||||
|
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-22.04
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
platform: linux
|
||||||
|
arch: x64
|
||||||
|
- os: ubuntu-24.04-arm
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
platform: linux
|
||||||
|
arch: arm64
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- 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 portable 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
|
||||||
|
|
||||||
|
# ── macOS DMG installer ────────────────────────────────────────────────
|
||||||
|
- 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
|
||||||
|
|
||||||
|
# ── Windows EXE installer (NSIS) ──────────────────────────────────────
|
||||||
|
- name: Build Windows installer (EXE)
|
||||||
|
if: matrix.platform == 'win32'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
choco install nsis -y --no-progress
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts/pkg/make-exe.ps1 `
|
||||||
|
-Bin "target/${{ matrix.target }}/release/gelectron.exe" `
|
||||||
|
-Compat src/electron `
|
||||||
|
-Version "${{ github.ref_name }}" `
|
||||||
|
-Arch ${{ matrix.arch }} `
|
||||||
|
-Out dist
|
||||||
|
|
||||||
|
# ── Linux AppImage installer ──────────────────────────────────────────
|
||||||
|
- 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 -a ${{ matrix.arch }}
|
||||||
|
|
||||||
|
- name: List build outputs
|
||||||
|
run: ls -la dist/
|
||||||
|
|
||||||
|
- name: Upload runtime and installer artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: installers-${{ matrix.platform }}-${{ matrix.arch }}
|
||||||
|
path: |
|
||||||
|
dist/gelectron-*
|
||||||
|
dist/Gelectron-*
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
# ── Attach everything to the GitHub Release ───────────────────────────────
|
||||||
release:
|
release:
|
||||||
name: Attach binaries to GitHub Release
|
name: Attach binaries to GitHub Release
|
||||||
needs: build
|
needs: [addon, runtime]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -79,10 +198,17 @@ jobs:
|
|||||||
pattern: bindings-*
|
pattern: bindings-*
|
||||||
merge-multiple: true
|
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/
|
run: ls -la artifacts/
|
||||||
|
|
||||||
- name: Upload binaries to GitHub Release
|
- name: Upload assets to GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: artifacts/*.node
|
files: artifacts/**
|
||||||
|
|||||||
+1
-5
@@ -25,11 +25,7 @@ dist/
|
|||||||
# Packager cache
|
# Packager cache
|
||||||
.gelectron-cache/
|
.gelectron-cache/
|
||||||
|
|
||||||
# Electron
|
# npm platform package stubs
|
||||||
electron/
|
|
||||||
|
|
||||||
# Electron compat layer (generated)
|
|
||||||
src/electron/
|
|
||||||
npm/darwin-arm64/
|
npm/darwin-arm64/
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Electron bundles Chromium — ~300 MB per app with 500+ MB RSS. Gelectron uses t
|
|||||||
| Language | C++ / Node.js | Rust / Node.js |
|
| Language | C++ / Node.js | Rust / Node.js |
|
||||||
| API Compatibility | Native | Drop-in replacement |
|
| API Compatibility | Native | Drop-in replacement |
|
||||||
| Node.js Integration | Built-in | Spawned child process or WebView-only |
|
| Node.js Integration | Built-in | Spawned child process or WebView-only |
|
||||||
| Auto Updater | Built-in | Stub (no-update-safe fallback) |
|
| Auto Updater | Built-in | Full (electron-updater compatible)
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -32,14 +32,68 @@ npm install -g gelectron-core
|
|||||||
|
|
||||||
This installs the `gelectron-core` package, which bundles the pre-built native binary and the JS compatibility layer, and automatically pulls in the platform-specific addon for your operating system and CPU architecture (see the [package on npm](https://www.npmjs.com/package/gelectron-core)).
|
This installs the `gelectron-core` package, which bundles the pre-built native binary and the JS compatibility layer, and automatically pulls in the platform-specific addon for your operating system and CPU architecture (see the [package on npm](https://www.npmjs.com/package/gelectron-core)).
|
||||||
|
|
||||||
Once installed, run any Electron app with:
|
Once installed, run any Electron app exactly like Electron — the `gelectron` command works out of the box (the package also exposes `gelectron-core` and `gelectron-packager`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gelectron-core /path/to/electron-app
|
gelectron /path/to/electron-app
|
||||||
|
gelectron . # app in the current directory (reads package.json "main")
|
||||||
|
gelectron main.js # a bare main-process script
|
||||||
|
gelectron --version
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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-<version>-arm64.dmg` / `-x64.dmg` | Contains `Install gelectron.pkg`, which installs the runtime + compat layer **plus a private Node.js** to `/usr/local/lib/gelectron` and symlinks `gelectron` into `/usr/local/bin` |
|
||||||
|
| Windows | `Gelectron-<version>-x64.exe` / `-arm64.exe` | NSIS installer → `C:\Program Files\Gelectron` (runtime + compat + private Node.js), adds it to the system PATH, Start Menu + uninstaller |
|
||||||
|
| Linux | `Gelectron-<version>-x86_64.AppImage` / `-aarch64.AppImage` | Fully self-contained (runtime + compat + private Node.js), just run it |
|
||||||
|
|
||||||
|
After installing, `gelectron /path/to/electron-app` works from anywhere, and no
|
||||||
|
system Node.js install is required — every installer bundles its own runtime.
|
||||||
|
|
||||||
|
Apps packaged with `gelectron-packager` are equally self-contained: double-click
|
||||||
|
a packaged app and it runs with no gelectron and no Node.js installed.
|
||||||
|
|
||||||
|
> 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-<version>-<platform>-<arch>.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.
|
> 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)
|
### Prerequisites (for building from source)
|
||||||
|
|
||||||
- Rust 1.75+ (`rustup.rs`)
|
- Rust 1.75+ (`rustup.rs`)
|
||||||
@@ -65,7 +119,11 @@ cargo run --release -p gelectron -- /path/to/electron-app
|
|||||||
|
|
||||||
### CLI (Node.js fallback)
|
### CLI (Node.js fallback)
|
||||||
|
|
||||||
If you don't want to build the Rust binary, the CLI can fall back to a pure-Node.js shim:
|
The `gelectron` command automatically locates the native runtime — the release
|
||||||
|
build, an npm/installer-installed binary (`PATH`, `/usr/local/bin`,
|
||||||
|
`/usr/local/lib/gelectron`, `Program Files\Gelectron`, etc.) or a fresh
|
||||||
|
`target/{release,debug}/gelectron`. If none is found it falls back to a
|
||||||
|
pure-Node.js shim:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node cli/gelectron.js /path/to/electron-app
|
node cli/gelectron.js /path/to/electron-app
|
||||||
@@ -160,14 +218,15 @@ When the native binary is not built, the CLI falls back to pure Node.js:
|
|||||||
|
|
||||||
| Module | Status |
|
| Module | Status |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `screen` | `getPrimaryDisplay()` (stub) |
|
| `screen` | Native display enumeration (`getAllDisplays`, `getPrimaryDisplay`, `getDisplayMatching`, …) |
|
||||||
| `clipboard` | Full API (`readText`/`writeText`/`readHTML`/`readRTF`/`readImage`/`readBookmark`/`readFindText`/`clear`/`availableFormats`/`has`) |
|
| `clipboard` | Full API (`readText`/`writeText`/`readHTML`/`writeHTML`/`readBookmark`/`readFindText`/`availableFormats`) |
|
||||||
|
| `nativeTheme` | `shouldUseDarkColors`, `themeSource`, `themes` (native-backed when available) |
|
||||||
| `systemPreferences` | Basic stubs |
|
| `systemPreferences` | Basic stubs |
|
||||||
| `powerMonitor` | Event stubs |
|
| `powerMonitor` | Event stubs |
|
||||||
| `globalShortcut` | Register/unregister stubs |
|
| `globalShortcut` | Register/unregister stubs |
|
||||||
| `session` | Cookies, protocol, permissions (stub) |
|
| `session` | Cookies, protocol, permissions (stub) |
|
||||||
| `net` | `fetch()` proxy |
|
| `net` | `fetch()` proxy |
|
||||||
| `autoUpdater` | No-op stub (reports "no update available") |
|
| `autoUpdater` | Full (sha512-verified, atomic apply) |
|
||||||
|
|
||||||
## Demo App
|
## Demo App
|
||||||
|
|
||||||
@@ -230,9 +289,12 @@ gelectron/
|
|||||||
│ ├── shell.js # Shell operations
|
│ ├── shell.js # Shell operations
|
||||||
│ ├── notification.js # Notifications
|
│ ├── notification.js # Notifications
|
||||||
│ ├── native-image.js # Image handling
|
│ ├── native-image.js # Image handling
|
||||||
|
│ ├── clipboard.js # Clipboard
|
||||||
|
│ ├── screen.js # Display enumeration
|
||||||
|
│ ├── nativeTheme.js # Dark mode / theme
|
||||||
│ ├── safe-storage.js # Encryption
|
│ ├── safe-storage.js # Encryption
|
||||||
│ ├── web-contents.js # webContents utilities
|
│ ├── web-contents.js # webContents utilities
|
||||||
│ ├── auto-updater.js # autoUpdater stub
|
│ ├── auto-updater.js # autoUpdater (electron-updater compatible)
|
||||||
│ ├── native-bridge.js # IPC to Rust binary
|
│ ├── native-bridge.js # IPC to Rust binary
|
||||||
│ ├── preload-loader.js # Preload injection
|
│ ├── preload-loader.js # Preload injection
|
||||||
│ └── runtime.js # Node.js fallback runtime
|
│ └── runtime.js # Node.js fallback runtime
|
||||||
@@ -291,7 +353,9 @@ gelectron-packager --dir ./my-app --name MyApp --platform linux --arch x64
|
|||||||
|
|
||||||
### What the packager does
|
### What the packager does
|
||||||
|
|
||||||
1. Finds your built gelectron binary (`target/release/gelectron`)
|
1. Finds your gelectron runtime — the built binary (`target/release/gelectron`),
|
||||||
|
or any gelectron installed through the installers/npm (PATH,
|
||||||
|
`/usr/local/lib/gelectron`, `Program Files\Gelectron`, …)
|
||||||
2. Downloads a bundled Node.js runtime (~20 MB) for the target platform
|
2. Downloads a bundled Node.js runtime (~20 MB) for the target platform
|
||||||
3. Copies your app source and `node_modules`
|
3. Copies your app source and `node_modules`
|
||||||
4. Includes the Electron compatibility layer (`src/electron/`)
|
4. Includes the Electron compatibility layer (`src/electron/`)
|
||||||
|
|||||||
+51
-8
@@ -116,15 +116,58 @@ if (process.env.GELECTRON_LOG) {
|
|||||||
const rustBin = path.join(__dirname, '..', 'target', 'release', 'gelectron');
|
const rustBin = path.join(__dirname, '..', 'target', 'release', 'gelectron');
|
||||||
const rustBinDebug = path.join(__dirname, '..', 'target', 'debug', 'gelectron');
|
const rustBinDebug = path.join(__dirname, '..', 'target', 'debug', 'gelectron');
|
||||||
|
|
||||||
let executable;
|
function findNativeBinary() {
|
||||||
if (fs.existsSync(rustBin)) {
|
const candidates = [];
|
||||||
executable = rustBin;
|
if (process.env.GELECTRON_BINARY) candidates.push(process.env.GELECTRON_BINARY);
|
||||||
} else if (fs.existsSync(rustBinDebug)) {
|
candidates.push(rustBin, rustBinDebug);
|
||||||
executable = rustBinDebug;
|
// npm-installed layout: the binary ships alongside the CLI
|
||||||
|
candidates.push(path.join(__dirname, '..', 'bin', 'gelectron'));
|
||||||
|
|
||||||
|
const platform = process.platform;
|
||||||
|
if (platform === 'win32') {
|
||||||
|
for (const pf of [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'], 'C:\\Program Files']) {
|
||||||
|
if (pf) candidates.push(path.join(pf, 'Gelectron', 'gelectron.exe'));
|
||||||
|
}
|
||||||
|
candidates.push(path.join(process.env.LOCALAPPDATA || '', 'gelectron', 'gelectron.exe'));
|
||||||
|
} else {
|
||||||
|
candidates.push('/usr/local/bin/gelectron');
|
||||||
|
candidates.push('/usr/local/lib/gelectron/gelectron');
|
||||||
|
candidates.push('/usr/bin/gelectron');
|
||||||
|
candidates.push('/opt/homebrew/bin/gelectron');
|
||||||
|
candidates.push(path.join(process.env.HOME || '', '.local', 'bin', 'gelectron'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const exeSuffix = platform === 'win32' ? '.exe' : '';
|
||||||
|
for (const c of candidates) {
|
||||||
|
for (const candidate of [c, c + exeSuffix]) {
|
||||||
|
if (candidate && fs.existsSync(candidate)) return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATH lookup (which / where)
|
||||||
|
try {
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const cmd = platform === 'win32' ? 'where gelectron' : 'which gelectron';
|
||||||
|
const out = execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
||||||
|
const first = out.split(/\r?\n/).map((s) => s.trim()).find((s) => s.length > 0);
|
||||||
|
if (first && fs.existsSync(first)) return first;
|
||||||
|
} catch (e) {
|
||||||
|
// Not on PATH
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let executable = findNativeBinary();
|
||||||
|
if (executable && process.platform !== 'win32') {
|
||||||
|
try {
|
||||||
|
fs.accessSync(executable, fs.constants.X_OK);
|
||||||
|
} catch (e) {
|
||||||
|
fs.chmodSync(executable, 0o755);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (executable) {
|
if (executable) {
|
||||||
console.log(`[gelectron] Using native binary: ${executable}`);
|
if (process.env.GELECTRON_LOG) console.log(`[gelectron] Using native binary: ${executable}`);
|
||||||
const child = spawn(executable, args, {
|
const child = spawn(executable, args, {
|
||||||
env,
|
env,
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
@@ -132,8 +175,8 @@ if (executable) {
|
|||||||
});
|
});
|
||||||
child.on('exit', (code) => process.exit(code || 0));
|
child.on('exit', (code) => process.exit(code || 0));
|
||||||
} else {
|
} else {
|
||||||
console.log(`[gelectron] Native binary not found. Run: cargo build --release -p gelectron`);
|
console.log(`[gelectron] Native runtime not found. Install it or run: cargo build --release -p gelectron`);
|
||||||
console.log(`[gelectron] Falling back to Node.js runtime...\n`);
|
console.log(`[gelectron] Falling back to the Node.js compatibility layer (no native window support)...\n`);
|
||||||
|
|
||||||
require('../src/electron/runtime.js').run(mainScript, env);
|
require('../src/electron/runtime.js').run(mainScript, env);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1998,18 +1998,80 @@ fn handle_to_rust(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve the Node.js runtime the gelectron engine should spawn. Resolution
|
||||||
|
// order (first hit wins):
|
||||||
|
//
|
||||||
|
// 1. GELECTRON_NODE env override (explicit, e.g. from a launcher script)
|
||||||
|
// 2. Node bundled next to the gelectron binary. This is what makes packaged
|
||||||
|
// apps (MyApp.app, Windows dir, AppImage) fully self-contained: the
|
||||||
|
// packager places node / node.exe alongside the engine, so no system-wide
|
||||||
|
// Node install is required.
|
||||||
|
// 3. PATH lookup (which on Unix, where on Windows)
|
||||||
|
// 4. Known per-platform install locations (incl. the runtime installers)
|
||||||
fn which_node() -> Option<String> {
|
fn which_node() -> Option<String> {
|
||||||
Command::new("which")
|
let node_exe = if cfg!(windows) { "node.exe" } else { "node" };
|
||||||
.arg("node")
|
|
||||||
.output()
|
if let Ok(override_path) = std::env::var("GELECTRON_NODE") {
|
||||||
.ok()
|
if !override_path.is_empty() && std::path::Path::new(&override_path).exists() {
|
||||||
.and_then(|o| if o.status.success() { String::from_utf8(o.stdout).ok().map(|s| s.trim().to_string()) } else { None })
|
return Some(override_path);
|
||||||
.or_else(|| {
|
}
|
||||||
for p in &["/usr/local/bin/node", "/opt/homebrew/bin/node", "/usr/bin/node"] {
|
}
|
||||||
if std::path::Path::new(p).exists() { return Some(p.to_string()); }
|
|
||||||
|
// 1. Bundled runtime: node sits next to the gelectron engine binary
|
||||||
|
if let Some(exe_dir) = std::env::current_exe().ok().and_then(|p| p.parent().map(|p| p.to_path_buf())) {
|
||||||
|
for candidate in &[exe_dir.join(node_exe), exe_dir.join("bin").join(node_exe)] {
|
||||||
|
if candidate.exists() {
|
||||||
|
return Some(candidate.display().to_string());
|
||||||
}
|
}
|
||||||
None
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// 2. PATH lookup (where works on Windows, which on Unix)
|
||||||
|
let lookup = if cfg!(windows) { "where" } else { "which" };
|
||||||
|
if let Ok(out) = Command::new(lookup).arg(node_exe).output() {
|
||||||
|
if out.status.success() {
|
||||||
|
if let Ok(s) = String::from_utf8(out.stdout) {
|
||||||
|
let first = s.lines().next().map(|l| l.trim().to_string());
|
||||||
|
if let Some(p) = first {
|
||||||
|
if !p.is_empty() && std::path::Path::new(&p).exists() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Ok(out) = Command::new("which").arg(node_exe).output() {
|
||||||
|
// Fallback: `which` may not exist on some minimal Windows shells
|
||||||
|
if out.status.success() {
|
||||||
|
if let Ok(s) = String::from_utf8(out.stdout) {
|
||||||
|
let first = s.lines().next().map(|l| l.trim().to_string());
|
||||||
|
if let Some(p) = first {
|
||||||
|
if !p.is_empty() && std::path::Path::new(&p).exists() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Known install locations (flat list, checked in order)
|
||||||
|
let known = [
|
||||||
|
"/usr/local/bin/node",
|
||||||
|
"/opt/homebrew/bin/node",
|
||||||
|
"/usr/bin/node",
|
||||||
|
// Gelectron runtime installers bundle node next to the engine
|
||||||
|
"/usr/local/lib/gelectron/node",
|
||||||
|
"/usr/local/lib/gelectron/node.exe",
|
||||||
|
"/usr/lib/gelectron/node",
|
||||||
|
"C:\\Program Files\\Gelectron\\node.exe",
|
||||||
|
"C:\\Program Files (x86)\\Gelectron\\node.exe",
|
||||||
|
];
|
||||||
|
for p in &known {
|
||||||
|
if std::path::Path::new(p).exists() {
|
||||||
|
return Some(p.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
+3
-1
@@ -4,7 +4,9 @@
|
|||||||
"description": "Firefox-engine alternative to Electron, powered by Servo",
|
"description": "Firefox-engine alternative to Electron, powered by Servo",
|
||||||
"main": "src/electron/index.js",
|
"main": "src/electron/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"gelectron-core": "./cli/gelectron.js"
|
"gelectron": "./cli/gelectron.js",
|
||||||
|
"gelectron-core": "./cli/gelectron.js",
|
||||||
|
"gelectron-packager": "./packager/bin/gelectron-packager.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "napi build --platform --release --package gelectron-core",
|
"build": "napi build --platform --release --package gelectron-core",
|
||||||
|
|||||||
@@ -73,11 +73,63 @@ function findCompatLayer(startDir) {
|
|||||||
while (dir !== path.dirname(dir)) {
|
while (dir !== path.dirname(dir)) {
|
||||||
const p = path.join(dir, 'src', 'electron');
|
const p = path.join(dir, 'src', 'electron');
|
||||||
if (fs.existsSync(p)) return p;
|
if (fs.existsSync(p)) return p;
|
||||||
|
// Some installs place the compat layer as `compat/` next to the binary
|
||||||
|
const compat = path.join(dir, 'compat', 'index.js');
|
||||||
|
if (fs.existsSync(compat)) return path.join(dir, 'compat');
|
||||||
dir = path.dirname(dir);
|
dir = path.dirname(dir);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compat layer that lives alongside a found binary (installed runtime layout
|
||||||
|
// where binary + compat live in the same directory).
|
||||||
|
function findCompatNextToBinary(binaryPath) {
|
||||||
|
const dir = path.dirname(binaryPath);
|
||||||
|
const p = path.join(dir, 'compat', 'index.js');
|
||||||
|
if (fs.existsSync(p)) return path.join(dir, 'compat');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExecutableName(binPath) {
|
||||||
|
if (!binPath) return false;
|
||||||
|
const base = path.basename(binPath).toLowerCase();
|
||||||
|
return /^gelectron(\.exe)?$/.test(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locate a gelectron runtime installed through the official installers
|
||||||
|
// (DMG / EXE / AppImage) rather than built from source. This lets
|
||||||
|
// `gelectron-packager` bundle a runtime that never has to be recompiled.
|
||||||
|
function findInstalledGelectron() {
|
||||||
|
const platform = process.platform;
|
||||||
|
const candidates = [];
|
||||||
|
if (platform === 'win32') {
|
||||||
|
for (const pf of [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'], 'C:\\Program Files']) {
|
||||||
|
if (pf) candidates.push(path.join(pf, 'Gelectron', 'gelectron.exe'));
|
||||||
|
}
|
||||||
|
candidates.push(path.join(process.env.LOCALAPPDATA || '', 'gelectron', 'gelectron.exe'));
|
||||||
|
} else {
|
||||||
|
candidates.push('/usr/local/bin/gelectron');
|
||||||
|
candidates.push('/usr/local/lib/gelectron/gelectron');
|
||||||
|
candidates.push('/usr/bin/gelectron');
|
||||||
|
candidates.push('/opt/homebrew/bin/gelectron');
|
||||||
|
candidates.push(path.join(process.env.HOME || '', '.local', 'bin', 'gelectron'));
|
||||||
|
candidates.push(path.join(process.env.HOME || '', '.gelectron', 'bin', 'gelectron'));
|
||||||
|
}
|
||||||
|
for (const p of candidates) {
|
||||||
|
if (p && fs.existsSync(p) && isExecutableName(p)) return p;
|
||||||
|
}
|
||||||
|
// PATH lookup: `which gelectron` / `where gelectron`
|
||||||
|
try {
|
||||||
|
const which = platform === 'win32' ? 'where' : 'which';
|
||||||
|
const out = execSync(`${which} gelectron`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
||||||
|
const first = String(out).split(/\r?\n/).map((s) => s.trim()).find((s) => s.length > 0 && isExecutableName(s));
|
||||||
|
if (first && fs.existsSync(first)) return first;
|
||||||
|
} catch (e) {
|
||||||
|
// Not on PATH
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function findGelectronFromAncestors(appDir) {
|
function findGelectronFromAncestors(appDir) {
|
||||||
let dir = appDir;
|
let dir = appDir;
|
||||||
while (dir !== path.dirname(dir)) {
|
while (dir !== path.dirname(dir)) {
|
||||||
@@ -92,7 +144,8 @@ function findGelectronFromAncestors(appDir) {
|
|||||||
}
|
}
|
||||||
dir = path.dirname(dir);
|
dir = path.dirname(dir);
|
||||||
}
|
}
|
||||||
return null;
|
// Fall back to a system-installed gelectron runtime
|
||||||
|
return findInstalledGelectron();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNodeBinary(platform, arch, cacheDir) {
|
async function getNodeBinary(platform, arch, cacheDir) {
|
||||||
@@ -172,6 +225,8 @@ ${iconKey} <key>CFBundleDisplayName</key>
|
|||||||
<string>${exeName}</string>
|
<string>${exeName}</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.gelectron.${name.toLowerCase().replace(/[^a-z0-9]/g, '')}</string>
|
<string>com.gelectron.${name.toLowerCase().replace(/[^a-z0-9]/g, '')}</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>${name}</string>
|
<string>${name}</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
@@ -180,6 +235,10 @@ ${iconKey} <key>CFBundleDisplayName</key>
|
|||||||
<string>${version}</string>
|
<string>${version}</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>${version}</string>
|
<string>${version}</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>11.0</string>
|
||||||
|
<key>NSPrincipalClass</key>
|
||||||
|
<string>NSApplication</string>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSRequiresAquaSystemAppearance</key>
|
<key>NSRequiresAquaSystemAppearance</key>
|
||||||
@@ -452,7 +511,7 @@ async function packageMac(appDir, outDir, name, version, gelectronBin, nodeDir,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy src/electron compat layer
|
// Copy src/electron compat layer
|
||||||
const compatDir = findCompatLayer(appDir) || findCompatLayer(path.dirname(gelectronBin));
|
const compatDir = findCompatLayer(appDir) || findCompatNextToBinary(gelectronBin) || findCompatLayer(path.dirname(gelectronBin));
|
||||||
if (compatDir) {
|
if (compatDir) {
|
||||||
copyDirSync(compatDir, path.join(macosDir, 'compat'));
|
copyDirSync(compatDir, path.join(macosDir, 'compat'));
|
||||||
}
|
}
|
||||||
@@ -515,7 +574,7 @@ async function packageWindows(appDir, outDir, name, version, gelectronBin, nodeD
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy compat layer
|
// Copy compat layer
|
||||||
const compatDirWin = findCompatLayer(appDir) || findCompatLayer(path.dirname(gelectronBin));
|
const compatDirWin = findCompatLayer(appDir) || findCompatNextToBinary(gelectronBin) || findCompatLayer(path.dirname(gelectronBin));
|
||||||
if (compatDirWin) {
|
if (compatDirWin) {
|
||||||
copyDirSync(compatDirWin, path.join(outDir, 'compat'));
|
copyDirSync(compatDirWin, path.join(outDir, 'compat'));
|
||||||
}
|
}
|
||||||
@@ -592,7 +651,7 @@ async function packageLinux(appDir, outDir, name, version, gelectronBin, nodeDir
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy compat layer
|
// Copy compat layer
|
||||||
const compatDirLinux = findCompatLayer(appDir) || findCompatLayer(path.dirname(gelectronBin));
|
const compatDirLinux = findCompatLayer(appDir) || findCompatNextToBinary(gelectronBin) || findCompatLayer(path.dirname(gelectronBin));
|
||||||
if (compatDirLinux) {
|
if (compatDirLinux) {
|
||||||
copyDirSync(compatDirLinux, path.join(outDir, 'compat'));
|
copyDirSync(compatDirLinux, path.join(outDir, 'compat'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 <app>` works from
|
||||||
|
# anywhere. No Rust toolchain or npm required.
|
||||||
|
#
|
||||||
|
# Install layout:
|
||||||
|
# <PREFIX>/gelectron.exe native binary
|
||||||
|
# <PREFIX>/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 }
|
||||||
|
}
|
||||||
Executable
+207
@@ -0,0 +1,207 @@
|
|||||||
|
#!/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 <app>` works from
|
||||||
|
# anywhere. No Rust toolchain or Node dependency manager required.
|
||||||
|
#
|
||||||
|
# Install layout:
|
||||||
|
# <PREFIX>/gelectron native binary
|
||||||
|
# <PREFIX>/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 -f "$PREFIX/node"
|
||||||
|
rm -f "$PREFIX/node.exe"
|
||||||
|
rm -rf "$PREFIX/compat"
|
||||||
|
echo "Removed $PREFIX/gelectron, $PREFIX/node 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
|
||||||
|
|
||||||
|
# Download a private Node.js runtime next to the binary so `gelectron <app>`
|
||||||
|
# works on machines that don't have Node installed. The binary's runtime
|
||||||
|
# resolution checks its own directory first, so this private copy wins.
|
||||||
|
NODE_VERSION="20.18.1"
|
||||||
|
NODE_INSTALLED="$PREFIX/node"
|
||||||
|
if [[ "$PLATFORM" == "win32" ]]; then NODE_INSTALLED="$PREFIX/node.exe"; fi
|
||||||
|
if [[ ! -x "$NODE_INSTALLED" ]]; then
|
||||||
|
echo "==> Installing private Node.js v$NODE_VERSION ..."
|
||||||
|
case "$PLATFORM-$ARCH" in
|
||||||
|
darwin-*) NODE_PLATFORM="darwin-$ARCH"; NODE_EXT="tar.gz" ;;
|
||||||
|
linux-*) NODE_PLATFORM="linux-$ARCH"; NODE_EXT="tar.xz" ;;
|
||||||
|
win32-*) NODE_PLATFORM="win-$ARCH"; NODE_EXT="zip" ;;
|
||||||
|
esac
|
||||||
|
NODE_URL="https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-${NODE_PLATFORM}.${NODE_EXT}"
|
||||||
|
NODE_ARCHIVE="$(mktemp /tmp/gelectron-node.XXXXXX.${NODE_EXT})"
|
||||||
|
curl -fsSL -L "$NODE_URL" -o "$NODE_ARCHIVE"
|
||||||
|
NODE_STAGE="$(mktemp -d "${TMPDIR:-/tmp}/gelectron-node-stage.XXXXXX")"
|
||||||
|
case "$NODE_EXT" in
|
||||||
|
zip) unzip -qo "$NODE_ARCHIVE" -d "$NODE_STAGE" ;;
|
||||||
|
tar.gz) tar -xzf "$NODE_ARCHIVE" -C "$NODE_STAGE" ;;
|
||||||
|
tar.xz) tar -xJf "$NODE_ARCHIVE" -C "$NODE_STAGE" ;;
|
||||||
|
esac
|
||||||
|
NODE_BIN="$(find "$NODE_STAGE" -type f \( -name 'node.exe' -o -name 'node' \) | head -n1)"
|
||||||
|
if [[ -n "$NODE_BIN" ]]; then
|
||||||
|
install -m 755 "$NODE_BIN" "$NODE_INSTALLED"
|
||||||
|
else
|
||||||
|
echo " warning: could not install private Node.js — apps will require a system Node" >&2
|
||||||
|
fi
|
||||||
|
rm -rf "$NODE_STAGE" "$NODE_ARCHIVE"
|
||||||
|
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"
|
||||||
Executable
+180
@@ -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-<version>-darwin-arm64.tar.gz
|
||||||
|
# gelectron-<version>-darwin-x64.tar.gz
|
||||||
|
# gelectron-<version>-win32-arm64.zip
|
||||||
|
# gelectron-<version>-win32-x64.zip
|
||||||
|
# gelectron-<version>-linux-arm64.tar.gz
|
||||||
|
# gelectron-<version>-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: <repo>/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
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
; 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"
|
||||||
|
; Bundled Node.js runtime so packaged apps run with no system Node install
|
||||||
|
File "node.exe"
|
||||||
|
File /r "compat"
|
||||||
|
|
||||||
|
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
||||||
|
|
||||||
|
CreateDirectory "$SMPROGRAMS\${APPNAME}"
|
||||||
|
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME} CLI.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} CLI.lnk"
|
||||||
|
RMDir "$SMPROGRAMS\${APPNAME}"
|
||||||
|
|
||||||
|
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||||
|
DeleteRegKey HKLM "Software\${APPNAME}"
|
||||||
|
|
||||||
|
RMDir /r "$INSTDIR"
|
||||||
|
SectionEnd
|
||||||
Executable
+155
@@ -0,0 +1,155 @@
|
|||||||
|
#!/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-<version>-x86_64.AppImage
|
||||||
|
# Gelectron-<version>-aarch64.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] [-a ARCH]
|
||||||
|
|
||||||
|
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"
|
||||||
|
ARCH="x64"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Linux AppImage builder
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
scripts/pkg/make-appimage.sh -v VERSION [--binary PATH] [--compat DIR] [-o DIR] [-a ARCH]
|
||||||
|
|
||||||
|
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: <repo>/src/electron)
|
||||||
|
-o, --out DIR Output directory (default: <repo>/dist)
|
||||||
|
-a, --arch ARCH x64 | arm64 (default: x64)
|
||||||
|
-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 ;;
|
||||||
|
-a|--arch) ARCH="${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; }
|
||||||
|
case "$ARCH" in x64|arm64) ;; *) 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; }
|
||||||
|
|
||||||
|
# Map to appimg / node arch naming
|
||||||
|
if [[ "$ARCH" == "arm64" ]]; then
|
||||||
|
NODE_ARCH="arm64"
|
||||||
|
TARGET_ARCH="aarch64"
|
||||||
|
else
|
||||||
|
NODE_ARCH="x64"
|
||||||
|
TARGET_ARCH="x86_64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
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 ($NODE_ARCH)..."
|
||||||
|
mkdir -p "$NODE_DIR"
|
||||||
|
NODE_ARCHIVE="$STAGE/node.tar.xz"
|
||||||
|
curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" -o "$NODE_ARCHIVE"
|
||||||
|
tar -xJf "$NODE_ARCHIVE" -C "$NODE_DIR"
|
||||||
|
fi
|
||||||
|
install -m 755 "$NODE_DIR"/node-v${NODE_VERSION}-linux-${NODE_ARCH}/bin/node "$APPDIR/usr/bin/node"
|
||||||
|
|
||||||
|
mkdir -p "$APPDIR/usr/lib"
|
||||||
|
cp "$NODE_DIR"/node-v${NODE_VERSION}-linux-${NODE_ARCH}/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" <<EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Gelectron
|
||||||
|
Comment=Firefox-engine alternative to Electron, powered by Servo
|
||||||
|
Exec=gelectron
|
||||||
|
Icon=gelectron
|
||||||
|
Terminal=false
|
||||||
|
Categories=Development;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [[ -f "$REPO_DIR/logo.png" ]]; then
|
||||||
|
install -m 644 "$REPO_DIR/logo.png" "$APPDIR/gelectron.png"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── appimagetool ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TOOL="$STAGE/appimagetool"
|
||||||
|
if [[ ! -f "$TOOL" ]]; then
|
||||||
|
log "Downloading appimagetool ($TARGET_ARCH)..."
|
||||||
|
curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${TARGET_ARCH}.AppImage" -o "$TOOL"
|
||||||
|
fi
|
||||||
|
chmod +x "$TOOL"
|
||||||
|
|
||||||
|
# ── Build ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
APPIMAGE_NAME="Gelectron-$VERSION-$TARGET_ARCH.AppImage"
|
||||||
|
APPIMAGE_PATH="$OUT_DIR/$APPIMAGE_NAME"
|
||||||
|
|
||||||
|
export VERSION="$VERSION"
|
||||||
|
export ARCH="$TARGET_ARCH"
|
||||||
|
# Run appimagetool without FUSE (Ubuntu 24.04+ runners don't ship libfuse2)
|
||||||
|
export APPIMAGE_EXTRACT_AND_RUN=1
|
||||||
|
log "Creating $APPIMAGE_NAME ..."
|
||||||
|
"$TOOL" "$APPDIR" "$APPIMAGE_PATH" >/dev/null
|
||||||
|
|
||||||
|
echo
|
||||||
|
log "Created installer: $APPIMAGE_PATH"
|
||||||
|
sha256sum "$APPIMAGE_PATH" 2>/dev/null || shasum -a 256 "$APPIMAGE_PATH" 2>/dev/null || true
|
||||||
Executable
+144
@@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Build the macOS installer: a .pkg wrapped in a .dmg for distribution.
|
||||||
|
#
|
||||||
|
# The package is fully self-contained — it installs the gelectron runtime, the
|
||||||
|
# Electron compatibility layer AND a private Node.js runtime into
|
||||||
|
# /usr/local/lib/gelectron and symlinks gelectron into /usr/local/bin. Apps run
|
||||||
|
# with `gelectron <app>` with no other runtime installed on the machine.
|
||||||
|
#
|
||||||
|
# Gelectron-<version>-<arch>.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"
|
||||||
|
NODE_VERSION="20.18.1"
|
||||||
|
|
||||||
|
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: <repo>/src/electron)
|
||||||
|
-o, --out DIR Output directory (default: <repo>/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/lib/gelectron ─────
|
||||||
|
|
||||||
|
PAYLOAD="$STAGE/payload/usr/local/lib/gelectron"
|
||||||
|
mkdir -p "$PAYLOAD"
|
||||||
|
|
||||||
|
log "Staging payload..."
|
||||||
|
install -m 755 "$BINARY" "$PAYLOAD/gelectron"
|
||||||
|
mkdir -p "$PAYLOAD/compat"
|
||||||
|
install -m 644 "$COMPAT"/*.js "$PAYLOAD/compat/"
|
||||||
|
|
||||||
|
# Private Node.js runtime so the installed CLI works with no system Node
|
||||||
|
NODE_ARCH="$( [[ "$ARCH" == "arm64" ]] && echo arm64 || echo x64 )"
|
||||||
|
NODE_ARCHIVE="$STAGE/node.tar.gz"
|
||||||
|
if [[ ! -f "$NODE_ARCHIVE" ]]; then
|
||||||
|
log "Downloading Node.js v$NODE_VERSION..."
|
||||||
|
curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-${NODE_ARCH}.tar.gz" -o "$NODE_ARCHIVE"
|
||||||
|
fi
|
||||||
|
log "Extracting Node.js..."
|
||||||
|
mkdir -p "$STAGE/node"
|
||||||
|
tar -xzf "$NODE_ARCHIVE" -C "$STAGE/node"
|
||||||
|
install -m 755 "$STAGE/node"/node-v${NODE_VERSION}-darwin-${NODE_ARCH}/bin/node "$PAYLOAD/node"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# ── Postinstall script: symlink /usr/local/bin/gelectron → runtime ─────────
|
||||||
|
|
||||||
|
SCRIPTS="$STAGE/scripts"
|
||||||
|
mkdir -p "$SCRIPTS"
|
||||||
|
cat > "$SCRIPTS/postinstall" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
ln -sf /usr/local/lib/gelectron/gelectron /usr/local/bin/gelectron
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x "$SCRIPTS/postinstall"
|
||||||
|
|
||||||
|
# ── Component package ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
PKG="$STAGE/Install gelectron.pkg"
|
||||||
|
log "Building package (installs to /usr/local/lib/gelectron)..."
|
||||||
|
pkgbuild \
|
||||||
|
--root "$STAGE/payload" \
|
||||||
|
--scripts "$SCRIPTS" \
|
||||||
|
--identifier "com.gelectron.runtime.$ARCH" \
|
||||||
|
--version "$VERSION" \
|
||||||
|
--ownership recommended \
|
||||||
|
--install-location / \
|
||||||
|
"$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"
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Gelectron Windows installer builder (NSIS)
|
||||||
|
#
|
||||||
|
# Stages the payload (gelectron runtime + bundled Node.js + compat layer) and
|
||||||
|
# compiles the installer EXE with makensis:
|
||||||
|
# Gelectron-<version>-<arch>.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",
|
||||||
|
[string]$NodeVersion = "20.18.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
$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
|
||||||
|
|
||||||
|
# Bundled Node.js runtime so the installed CLI works with no system Node.
|
||||||
|
$NodeArch = if ($Arch -eq "arm64") { "arm64" } else { "x64" }
|
||||||
|
$NodeArchive = Join-Path $Stage "node.zip"
|
||||||
|
$NodeUrl = "https://nodejs.org/dist/v$NodeVersion/node-v$NodeVersion-win-$NodeArch.zip"
|
||||||
|
Write-Host "==> Downloading Node.js v$NodeVersion ($NodeArch)..."
|
||||||
|
Invoke-WebRequest -Uri $NodeUrl -OutFile $NodeArchive -UseBasicParsing
|
||||||
|
|
||||||
|
$NodeExtract = Join-Path $Stage "node"
|
||||||
|
Expand-Archive -Path $NodeArchive -DestinationPath $NodeExtract -Force
|
||||||
|
$NodeExe = Get-ChildItem $NodeExtract -Recurse -Filter node.exe | Select-Object -First 1
|
||||||
|
if (-not $NodeExe) { throw "node.exe not found in Node.js archive" }
|
||||||
|
Copy-Item $NodeExe.FullName (Join-Path $Stage "node.exe") -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gelectron - clipboard module (Electron compatible)
|
||||||
|
*
|
||||||
|
* Implements text / HTML / rich-text clipboard operations using the OS-native
|
||||||
|
* tooling so it works both in native Node mode and in the pure-Node fallback.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const os = require('os');
|
||||||
|
const { isNative } = require('./native-bridge');
|
||||||
|
|
||||||
|
function platform() {
|
||||||
|
return os.platform();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Low-level platform pipes ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
function readCommand(cmd) {
|
||||||
|
try {
|
||||||
|
const out = execSync(cmd, { stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8' });
|
||||||
|
return out == null ? '' : out.toString();
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writePipe(cmd, text) {
|
||||||
|
try {
|
||||||
|
execSync(`${cmd} '${String(text).replace(/'/g, "'\\''")}'`, { stdio: 'pipe' });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
|
if (platform() === 'win32') {
|
||||||
|
// Powershell stdin pipe is the most reliable on Windows
|
||||||
|
const ps = spawnSync('powershell', ['-NoProfile', '-Command', 'Set-Clipboard'], {
|
||||||
|
input: String(text),
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
return ps.status === 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTextFromSystem() {
|
||||||
|
const p = platform();
|
||||||
|
try {
|
||||||
|
if (p === 'darwin') return execSync('pbpaste', { encoding: 'utf8' }).toString();
|
||||||
|
if (p === 'win32') {
|
||||||
|
const out = execSync('powershell -NoProfile -Command "Get-Clipboard -Raw"', { encoding: 'utf8' });
|
||||||
|
return out.replace(/\r?\n$/, '');
|
||||||
|
}
|
||||||
|
for (const tool of ['xclip -selection clipboard -o', 'xsel --clipboard --output', 'wl-paste']) {
|
||||||
|
try {
|
||||||
|
return execSync(tool, { encoding: 'utf8' }).toString();
|
||||||
|
} catch (e) { /* try next */ }
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeTextToSystem(text) {
|
||||||
|
const p = platform();
|
||||||
|
const content = String(text);
|
||||||
|
try {
|
||||||
|
if (p === 'darwin') return writePipe('pbcopy', content);
|
||||||
|
if (p === 'win32') {
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
|
const ps = spawnSync('powershell', ['-NoProfile', '-Command', 'Set-Clipboard'], {
|
||||||
|
input: content,
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
return ps.status === 0;
|
||||||
|
}
|
||||||
|
for (const cmd of [
|
||||||
|
`xclip -selection clipboard -in <<'GELECTRON_EOF'\n${content}\nGELECTRON_EOF`,
|
||||||
|
`xsel --clipboard --input <<'GELECTRON_EOF'\n${content}\nGELECTRON_EOF`,
|
||||||
|
`wl-copy <<'GELECTRON_EOF'\n${content}\nGELECTRON_EOF`,
|
||||||
|
]) {
|
||||||
|
try {
|
||||||
|
execSync(cmd, { encoding: 'utf8' });
|
||||||
|
return true;
|
||||||
|
} catch (e) { /* try next */ }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const clipboard = {
|
||||||
|
readText() {
|
||||||
|
return readTextFromSystem();
|
||||||
|
},
|
||||||
|
|
||||||
|
writeText(text) {
|
||||||
|
return writeTextToSystem(text);
|
||||||
|
},
|
||||||
|
|
||||||
|
readHTML() {
|
||||||
|
// Native bridge request falls back to plain text when unavailable
|
||||||
|
if (isNative && typeof globalThis.__gelectron_clipboard_read_html === 'function') {
|
||||||
|
return globalThis.__gelectron_clipboard_read_html();
|
||||||
|
}
|
||||||
|
return readTextFromSystem();
|
||||||
|
},
|
||||||
|
|
||||||
|
writeHTML(markup, type = 'text/html') {
|
||||||
|
try {
|
||||||
|
if (platform() === 'win32') {
|
||||||
|
const html = String(markup);
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
|
const ps = spawnSync('powershell', ['-NoProfile', '-Command',
|
||||||
|
`Set-Clipboard -Value @'${html.replace(/'/g, "''")}'@`], { encoding: 'utf8' });
|
||||||
|
return ps.status === 0;
|
||||||
|
}
|
||||||
|
return writeTextToSystem(markup);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Best-effort plain text read (Electron returns an image object for rich
|
||||||
|
// clipboard types on some platforms; we degrade to text where possible).
|
||||||
|
readTextOnly() {
|
||||||
|
return readTextFromSystem();
|
||||||
|
},
|
||||||
|
|
||||||
|
readImage() {
|
||||||
|
return NativeImageStub.empty();
|
||||||
|
},
|
||||||
|
|
||||||
|
writeImage(image) {
|
||||||
|
return !!image;
|
||||||
|
},
|
||||||
|
|
||||||
|
availableFormats() {
|
||||||
|
return ['text/plain'];
|
||||||
|
},
|
||||||
|
|
||||||
|
read(format = 'text/plain') {
|
||||||
|
return readTextFromSystem();
|
||||||
|
},
|
||||||
|
|
||||||
|
write(data, type = 'text/plain') {
|
||||||
|
return writeTextToSystem(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {},
|
||||||
|
|
||||||
|
readBookmark() {
|
||||||
|
return { title: '', url: '' };
|
||||||
|
},
|
||||||
|
|
||||||
|
writeBookmark() {},
|
||||||
|
|
||||||
|
readFindText() {
|
||||||
|
try {
|
||||||
|
return process.env.GELECTRON_FIND_TEXT || '';
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
writeFindText(text) {
|
||||||
|
try {
|
||||||
|
process.env.GELECTRON_FIND_TEXT = String(text);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Avoid a hard dependency on native-image for the stub paths.
|
||||||
|
const NativeImageStub = {
|
||||||
|
empty() {
|
||||||
|
return {
|
||||||
|
isEmpty: () => true,
|
||||||
|
toPNG: () => Buffer.alloc(0),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = clipboard;
|
||||||
@@ -70,6 +70,14 @@ class NativeBridge extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generic request to the native process: request(type, payload) where
|
||||||
|
// `type` matches a ToRust variant. Resolves with the JSON response from the
|
||||||
|
// native side (or null when running without the native binary).
|
||||||
|
request(type, payload = {}) {
|
||||||
|
if (!isNative) return Promise.resolve(null);
|
||||||
|
return this._request({ type, ...payload });
|
||||||
|
}
|
||||||
|
|
||||||
_resolveRequest(requestId, result, error) {
|
_resolveRequest(requestId, result, error) {
|
||||||
const pending = this._pendingRequests[requestId];
|
const pending = this._pendingRequests[requestId];
|
||||||
if (pending) {
|
if (pending) {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gelectron - nativeTheme module (Electron compatible)
|
||||||
|
*
|
||||||
|
* Reports dark mode / accent colors. When running with the native binary it
|
||||||
|
* asks the OS for the current theme; otherwise it falls back to an env hint
|
||||||
|
* (or 'light').
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const { bridge, isNative } = require('./native-bridge');
|
||||||
|
|
||||||
|
function osDarkHint() {
|
||||||
|
const env = process.env;
|
||||||
|
if (env.COLORFGBG && /^0;/.test(env.COLORFGBG)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NativeTheme extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._dark = osDarkHint();
|
||||||
|
this._source = 'system';
|
||||||
|
this._accent = '#007AFF';
|
||||||
|
this._loaded = false;
|
||||||
|
|
||||||
|
if (isNative) {
|
||||||
|
bridge.request('native-theme-query', {}).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
if (typeof res.shouldUseDarkColors === 'boolean') this._dark = res.shouldUseDarkColors;
|
||||||
|
if (res.accentColor) this._accent = res.accentColor;
|
||||||
|
if (res.themeSource) this._source = res.themeSource;
|
||||||
|
this._loaded = true;
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get shouldUseDarkColors() {
|
||||||
|
return this._dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
get shouldUseInvertedColorScheme() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get themeSource() {
|
||||||
|
return this._source;
|
||||||
|
}
|
||||||
|
|
||||||
|
set themeSource(value) {
|
||||||
|
if (value === 'system' || value === 'light' || value === 'dark') {
|
||||||
|
this._source = value;
|
||||||
|
if (value === 'dark') this._dark = true;
|
||||||
|
else if (value === 'light') this._dark = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get themes() {
|
||||||
|
return {
|
||||||
|
initial: this._dark ? ['dark'] : ['light'],
|
||||||
|
current: this._dark ? ['dark'] : ['light'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
on(eventName, listener) { super.on(eventName, listener); return this; }
|
||||||
|
once(eventName, listener) { super.once(eventName, listener); return this; }
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new NativeTheme();
|
||||||
|
module.exports.NativeTheme = NativeTheme;
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gelectron - screen module (Electron compatible)
|
||||||
|
*
|
||||||
|
* Queries display information from the native process when available and
|
||||||
|
* otherwise returns sensible single-display defaults so apps never hang.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const os = require('os');
|
||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const { bridge, isNative } = require('./native-bridge');
|
||||||
|
|
||||||
|
function defaultDisplay() {
|
||||||
|
const width = 1920;
|
||||||
|
const height = 1080;
|
||||||
|
return {
|
||||||
|
id: 1,
|
||||||
|
label: 'Display 1',
|
||||||
|
bounds: { x: 0, y: 0, width, height },
|
||||||
|
workArea: { x: 0, y: 0, width, height },
|
||||||
|
size: { width, height },
|
||||||
|
workAreaSize: { width, height },
|
||||||
|
scaleFactor: 1,
|
||||||
|
rotation: 0,
|
||||||
|
internal: true,
|
||||||
|
touchSupport: 'unknown',
|
||||||
|
displayFrequency: 60,
|
||||||
|
colorSpace: '',
|
||||||
|
colorDepth: 24,
|
||||||
|
monitors: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(raw) {
|
||||||
|
const d = raw || {};
|
||||||
|
return {
|
||||||
|
id: d.id != null ? d.id : 1,
|
||||||
|
label: d.label || 'Display 1',
|
||||||
|
bounds: d.bounds || { x: 0, y: 0, width: 1920, height: 1080 },
|
||||||
|
workArea: d.workArea || d.bounds || { x: 0, y: 0, width: 1920, height: 1080 },
|
||||||
|
size: d.size || { width: 1920, height: 1080 },
|
||||||
|
workAreaSize: d.workAreaSize || d.size || { width: 1920, height: 1080 },
|
||||||
|
scaleFactor: d.scaleFactor || 1,
|
||||||
|
rotation: d.rotation || 0,
|
||||||
|
internal: !!d.internal,
|
||||||
|
touchSupport: d.touchSupport || 'unknown',
|
||||||
|
displayFrequency: d.displayFrequency || 60,
|
||||||
|
colorSpace: d.colorSpace || '',
|
||||||
|
colorDepth: d.colorDepth || 24,
|
||||||
|
monitors: d.monitors || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rectsIntersect(a, b) {
|
||||||
|
return (
|
||||||
|
a.x < b.x + b.width &&
|
||||||
|
a.x + a.width > b.x &&
|
||||||
|
a.y < b.y + b.height &&
|
||||||
|
a.y + a.height > b.y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Screen extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._displays = [defaultDisplay()];
|
||||||
|
|
||||||
|
// Parse an optional env-provided display list (packaged apps can inject
|
||||||
|
// this to avoid a round-trip).
|
||||||
|
try {
|
||||||
|
if (process.env.GELECTRON_DISPLAYS) {
|
||||||
|
const parsed = JSON.parse(process.env.GELECTRON_DISPLAYS);
|
||||||
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||||
|
this._displays = parsed.map(normalize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore malformed env
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh from the native process when available.
|
||||||
|
if (isNative) {
|
||||||
|
bridge.request('screen-get-displays', {}).then((res) => {
|
||||||
|
if (res && Array.isArray(res.displays) && res.displays.length > 0) {
|
||||||
|
this._displays = res.displays.map(normalize);
|
||||||
|
this.emit('display-added', this._displays[this._displays.length - 1]);
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllDisplays() {
|
||||||
|
return this._displays;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrimaryDisplay() {
|
||||||
|
return this._displays[0] || defaultDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayNearestPoint(point) {
|
||||||
|
const p = point || { x: 0, y: 0 };
|
||||||
|
let best = this._displays[0];
|
||||||
|
let bestDist = Infinity;
|
||||||
|
for (const d of this._displays) {
|
||||||
|
const cx = d.bounds.x + d.bounds.width / 2;
|
||||||
|
const cy = d.bounds.y + d.bounds.height / 2;
|
||||||
|
const dist = (p.x - cx) ** 2 + (p.y - cy) ** 2;
|
||||||
|
if (dist < bestDist) {
|
||||||
|
bestDist = dist;
|
||||||
|
best = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayForPoint(point) {
|
||||||
|
return this.getDisplayNearestPoint(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayMatching(rect) {
|
||||||
|
const r = rect || { x: 0, y: 0, width: 0, height: 0 };
|
||||||
|
let best = this._displays[0];
|
||||||
|
let bestArea = 0;
|
||||||
|
for (const d of this._displays) {
|
||||||
|
if (rectsIntersect(d.bounds, r)) {
|
||||||
|
const area = d.bounds.width * d.bounds.height;
|
||||||
|
if (area > bestArea) {
|
||||||
|
bestArea = area;
|
||||||
|
best = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCursorScreenPoint() {
|
||||||
|
// No native cursor query is implemented; return the center of the primary
|
||||||
|
// display as a harmless default.
|
||||||
|
const d = this.getPrimaryDisplay();
|
||||||
|
return {
|
||||||
|
x: Math.round(d.bounds.x + d.bounds.width / 2),
|
||||||
|
y: Math.round(d.bounds.y + d.bounds.height / 2),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
on(eventName, listener) { super.on(eventName, listener); return this; }
|
||||||
|
once(eventName, listener) { super.once(eventName, listener); return this; }
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { Screen, defaultDisplay, normalize };
|
||||||
Reference in New Issue
Block a user