8 Commits
Author SHA1 Message Date
miles 1689adffc6 feat: enhance native binary resolution and add bundled Node.js runtime
- Refactored gelectron.js to improve native binary discovery with additional paths and environment variable support.
- Implemented a function to find the Node.js runtime, allowing bundled Node.js to be used when available.
- Updated package.json to include gelectron-packager as a CLI command.
- Enhanced gelectron-packager.js to locate installed gelectron runtime and added compatibility for bundled Node.js.
- Modified install-release.sh to download and install a private Node.js runtime alongside the gelectron binary.
- Updated NSIS installer script to include bundled Node.js runtime.
- Enhanced AppImage and DMG packaging scripts to include Node.js runtime.
- Added clipboard, nativeTheme, and screen modules for Electron compatibility.
- Implemented native bridge request handling for clipboard operations.
2026-09-05 22:23:37 -05:00
miles 247daa6460 Add native installers (DMG/EXE/AppImage) to releases 2026-09-01 16:27:07 -05:00
miles 40f15bc45a Update README and package.json files for Gelectron installation instructions and native addon details 2026-08-30 22:44:24 -05:00
miles c96d6dfc5a Remove Linux from CI build for now 2026-08-30 22:24:05 -05:00
miles 3ea1fb7daa Switch to build + GitHub Release; fix Linux GTK deps 2026-08-30 22:18:43 -05:00
miles 04aedc53c6 Fix napi resolution in CI and bump Node to 20 2026-08-30 22:12:32 -05:00
miles 28d332d210 Bump version to 0.1.1 and add CI publish workflow 2026-08-30 22:10:54 -05:00
miles f09c02ec30 Add CI publish workflow 2026-08-30 22:09:54 -05:00
36 changed files with 2570 additions and 183 deletions
+1
View File
@@ -0,0 +1 @@
1db4284c-fc55-41be-a7dd-cde2036997da
+214
View File
@@ -0,0 +1,214 @@
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:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
# ── N-API addons (published to npm, also attached to the release) ─────────
addon:
name: Addon ${{ matrix.target }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-14
target: aarch64-apple-darwin
- os: macos-13
target: x86_64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
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 }}
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
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Build N-API addon for ${{ matrix.target }}
run: npx napi build --platform --release --target ${{ matrix.target }} --package gelectron-core
- name: Rename .node file with platform-specific name
shell: bash
run: |
case "${{ matrix.target }}" in
x86_64-apple-darwin) name="gelectron_core.darwin-x64.node" ;;
aarch64-apple-darwin) name="gelectron_core.darwin-arm64.node" ;;
x86_64-pc-windows-msvc) name="gelectron_core.win32-x64-msvc.node" ;;
aarch64-pc-windows-msvc) name="gelectron_core.win32-arm64-msvc.node" ;;
x86_64-unknown-linux-gnu) name="gelectron_core.linux-x64-gnu.node" ;;
aarch64-unknown-linux-gnu) name="gelectron_core.linux-arm64-gnu.node" ;;
esac
find crates/gelectron-core -name "*.node" -exec mv {} "$name" \;
ls -la *.node
- name: Upload .node artifact
uses: actions/upload-artifact@v4
with:
name: bindings-${{ matrix.target }}
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:
name: Attach binaries to GitHub Release
needs: [addon, runtime]
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all binaries
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: bindings-*
merge-multiple: true
- 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 assets to GitHub Release
uses: softprops/action-gh-release@v2
with:
files: artifacts/**
+2 -5
View File
@@ -7,6 +7,7 @@ node_modules/
npm-debug.log*
package-lock.json
al/
# Build artifacts
*.node
*.dylib
@@ -24,11 +25,7 @@ dist/
# Packager cache
.gelectron-cache/
# Electron
electron/
# Electron compat layer (generated)
src/electron/
# npm platform package stubs
npm/darwin-arm64/
# OS
+260 -9
View File
@@ -18,17 +18,89 @@ Electron bundles Chromium — ~300 MB per app with 500+ MB RSS. Gelectron uses t
| Language | C++ / Node.js | Rust / Node.js |
| API Compatibility | Native | Drop-in replacement |
| 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
### Prerequisites
### Install from npm (recommended)
The built native binary and the Electron compatibility layer are published to npm. This is the main and easiest way to get Gelectron — no Rust toolchain required:
```bash
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)).
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
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.
[Releases page]: https://github.com/mileswolfallen2/gelectron/releases
### Prerequisites (for building from source)
- Rust 1.75+ (`rustup.rs`)
- Node.js 18+
- npm
### Build & Run
### Build & Run (from source)
```bash
git clone https://github.com/mileswolfallen2/gelectron.git
@@ -47,7 +119,11 @@ cargo run --release -p gelectron -- /path/to/electron-app
### 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
node cli/gelectron.js /path/to/electron-app
@@ -142,14 +218,15 @@ When the native binary is not built, the CLI falls back to pure Node.js:
| Module | Status |
|---|---|
| `screen` | `getPrimaryDisplay()` (stub) |
| `clipboard` | Full API (`readText`/`writeText`/`readHTML`/`readRTF`/`readImage`/`readBookmark`/`readFindText`/`clear`/`availableFormats`/`has`) |
| `screen` | Native display enumeration (`getAllDisplays`, `getPrimaryDisplay`, `getDisplayMatching`, …) |
| `clipboard` | Full API (`readText`/`writeText`/`readHTML`/`writeHTML`/`readBookmark`/`readFindText`/`availableFormats`) |
| `nativeTheme` | `shouldUseDarkColors`, `themeSource`, `themes` (native-backed when available) |
| `systemPreferences` | Basic stubs |
| `powerMonitor` | Event stubs |
| `globalShortcut` | Register/unregister stubs |
| `session` | Cookies, protocol, permissions (stub) |
| `net` | `fetch()` proxy |
| `autoUpdater` | No-op stub (reports "no update available") |
| `autoUpdater` | Full (sha512-verified, atomic apply) |
## Demo App
@@ -212,9 +289,12 @@ gelectron/
│ ├── shell.js # Shell operations
│ ├── notification.js # Notifications
│ ├── native-image.js # Image handling
│ ├── clipboard.js # Clipboard
│ ├── screen.js # Display enumeration
│ ├── nativeTheme.js # Dark mode / theme
│ ├── safe-storage.js # Encryption
│ ├── web-contents.js # webContents utilities
│ ├── auto-updater.js # autoUpdater stub
│ ├── auto-updater.js # autoUpdater (electron-updater compatible)
│ ├── native-bridge.js # IPC to Rust binary
│ ├── preload-loader.js # Preload injection
│ └── runtime.js # Node.js fallback runtime
@@ -273,7 +353,9 @@ gelectron-packager --dir ./my-app --name MyApp --platform linux --arch x64
### 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
3. Copies your app source and `node_modules`
4. Includes the Electron compatibility layer (`src/electron/`)
@@ -316,6 +398,175 @@ cargo build --release -p gelectron-core
The N-API addon compiles to a `.node` file that can be loaded directly into Node.js.
## Publishing to npm
Gelectron uses [napi-rs](https://napi.rs/) to produce platform-specific native addons. The main `gelectron` npm package ships platform-specific optional packages so that `npm install gelectron` automatically pulls the right binary for the user's OS.
### Prerequisites
- Rust 1.75+ (`rustup.rs`)
- Node.js 18+
- npm
- An [npm account](https://www.npmjs.com/signup) with publish access
- Each target platform needs to be built on that platform (or via CI)
### Step 1: Build the native addon for your platform
```bash
# Build the N-API addon (produces crates/gelectron-core/*.node)
npm run build
# Or build with debug symbols for development
npm run build:debug
```
This compiles the Rust N-API addon (`gelectron-core`) into a `.node` file that Node.js can load.
### Step 2: Create platform-specific npm packages
For each platform you want to support, create a directory under `npm/` with a `package.json`:
```bash
# Example for macOS ARM64
mkdir -p npm/darwin-arm64
cat > npm/darwin-arm64/package.json << 'EOF'
{
"name": "gelectron-darwin-arm64",
"version": "0.1.0",
"description": "Gelectron native addon for macOS ARM64",
"main": "index.darwin-arm64.node",
"files": ["index.darwin-arm64.node"],
"os": ["darwin"],
"cpu": ["arm64"],
"license": "MIT"
}
EOF
# Copy the built .node file
cp crates/gelectron-core/gelectron_core.darwin-arm64.node npm/darwin-arm64/
```
Repeat for each platform:
| Directory | os | cpu |
|---|---|---|
| `npm/darwin-arm64/` | `darwin` | `arm64` |
| `npm/darwin-x64/` | `darwin` | `x64` |
| `npm/win32-x64-msvc/` | `win32` | `x64` |
| `npm/win32-arm64-msvc/` | `win32` | `arm64` |
| `npm/linux-x64-gnu/` | `linux` | `x64` |
| `npm/linux-arm64-gnu/` | `linux` | `arm64` |
### Step 3: Publish platform packages first
Each platform package must be published before the main package:
```bash
# Publish each platform package
npm publish npm/darwin-arm64 --access public
npm publish npm/darwin-x64 --access public
npm publish npm/win32-x64-msvc --access public
# ... etc for each platform
```
### Step 4: Prepare and publish the main package
```bash
# Run prepublish hook (generates napi artifacts metadata)
npm run prepublishOnly
# Publish the main package
npm publish --access public
```
### Using napi-rs CLI (recommended)
The `@napi-rs/cli` handles cross-compilation and artifact management:
```bash
# Install napi-rs CLI globally (if not already installed)
npm install -g @napi-rs/cli
# Build for all configured targets
napi build --platform --release
# Generate artifact metadata for npm publishing
napi prepublish -t npm
# Create a GitHub release with platform binaries
napi artifacts
```
### CI/CD Publishing (recommended)
For multi-platform publishing, use GitHub Actions to build on each OS:
```yaml
# .github/workflows/publish.yml
name: Publish to npm
on:
push:
tags: ['v*']
jobs:
build:
strategy:
matrix:
include:
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: windows-latest
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm ci
- run: napi build --platform --release --target ${{ matrix.target }}
- run: napi prepublish -t npm
- uses: actions/upload-artifact@v4
with:
name: bindings-${{ matrix.target }}
path: npm/
publish:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
### Quick publish (single platform)
If you only need to publish for your current platform:
```bash
# Build
npm run build
# Preview what will be published
npm pack --dry-run
# Publish
npm run prepublishOnly
npm publish --access public
```
> **Tip:** Use `npm pack` to create a tarball locally and inspect it before publishing. Run `npm pack` and then `tar -tzf gelectron-0.1.0.tgz` to verify the contents.
## Testing with OmniEmu2.0
OmniEmu2.0 is a full Electron app used to validate Gelectron compatibility:
+4 -1
View File
@@ -2,5 +2,8 @@
"name": "gelectron-benchmark",
"version": "1.0.0",
"description": "Simple web benchmark for comparing Electron vs Gelectron",
"main": "main.js"
"main": "main.js",
"dependencies": {
"electron": "^43.4.0"
}
}
@@ -1,28 +0,0 @@
{
"timestamp": "2026-07-29T03:22:15Z",
"runs": 10,
"platform": "Darwin arm64",
"electron": {
"version": "v43.2.0",
"avg_startup_s": 5.671,
"min_startup_s": 5.668,
"max_startup_s": 5.674,
"avg_memory_mb": 585.9,
"min_memory_mb": 583.3,
"max_memory_mb": 587.4,
"runtime_size_mb": 296,
"raw_times": [5.670,5.672,5.674,5.672,5.671,5.668,5.670,5.668,5.673,5.672],
"raw_memory": [583.7,586.5,583.3,587.1,586.5,587.4,586.5,587.1,584.1,586.3]
},
"gelectron": {
"avg_startup_s": 5.601,
"min_startup_s": 5.598,
"max_startup_s": 5.604,
"avg_memory_mb": 131.4,
"min_memory_mb": 131.0,
"max_memory_mb": 131.7,
"runtime_size_mb": 3,
"raw_times": [5.600,5.601,5.602,5.602,5.598,5.600,5.601,5.602,5.602,5.604],
"raw_memory": [131.6,131.7,131.3,131.5,131.3,131.7,131.0,131.5,131.3,131.5]
}
}
@@ -0,0 +1,28 @@
{
"timestamp": "2026-08-14T21:54:50Z",
"runs": 10,
"platform": "Darwin arm64",
"electron": {
"version": "v43.2.0",
"avg_startup_s": 5.670,
"min_startup_s": 5.651,
"max_startup_s": 5.676,
"avg_memory_mb": 586.3,
"min_memory_mb": 574.0,
"max_memory_mb": 590.7,
"runtime_size_mb": 0,
"raw_times": [5.651,5.676,5.663,5.673,5.675,5.672,5.672,5.672,5.670,5.673],
"raw_memory": [584.1,590.5,574.0,589.4,590.7,589.5,589.1,586.9,588.9,580.3]
},
"gelectron": {
"avg_startup_s": 5.603,
"min_startup_s": 5.601,
"max_startup_s": 5.606,
"avg_memory_mb": 142.6,
"min_memory_mb": 142.3,
"max_memory_mb": 142.9,
"runtime_size_mb": 3,
"raw_times": [5.606,5.601,5.604,5.604,5.605,5.601,5.604,5.604,5.603,5.601],
"raw_memory": [142.8,142.8,142.5,142.5,142.9,142.7,142.6,142.3,142.8,142.5]
}
}
+51 -8
View File
@@ -116,15 +116,58 @@ if (process.env.GELECTRON_LOG) {
const rustBin = path.join(__dirname, '..', 'target', 'release', 'gelectron');
const rustBinDebug = path.join(__dirname, '..', 'target', 'debug', 'gelectron');
let executable;
if (fs.existsSync(rustBin)) {
executable = rustBin;
} else if (fs.existsSync(rustBinDebug)) {
executable = rustBinDebug;
function findNativeBinary() {
const candidates = [];
if (process.env.GELECTRON_BINARY) candidates.push(process.env.GELECTRON_BINARY);
candidates.push(rustBin, 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) {
console.log(`[gelectron] Using native binary: ${executable}`);
if (process.env.GELECTRON_LOG) console.log(`[gelectron] Using native binary: ${executable}`);
const child = spawn(executable, args, {
env,
stdio: 'inherit',
@@ -132,8 +175,8 @@ if (executable) {
});
child.on('exit', (code) => process.exit(code || 0));
} else {
console.log(`[gelectron] Native binary not found. Run: cargo build --release -p gelectron`);
console.log(`[gelectron] Falling back to Node.js runtime...\n`);
console.log(`[gelectron] Native runtime not found. Install it or run: cargo build --release -p gelectron`);
console.log(`[gelectron] Falling back to the Node.js compatibility layer (no native window support)...\n`);
require('../src/electron/runtime.js').run(mainScript, env);
}
+157 -96
View File
@@ -11,6 +11,13 @@ use std::sync::mpsc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::sync::Arc;
use std::time::{Duration, Instant};
// Heartbeat interval for the event loop. We avoid ControlFlow::Poll (which
// busy-spins at 100% CPU when idle) and instead wake the loop on a fixed
// cadence to drain IPC channels, while still responding immediately to real
// window events.
const POLL_INTERVAL: Duration = Duration::from_millis(16);
use tao::event::{Event, StartCause, WindowEvent};
use tao::event_loop::{ControlFlow, EventLoopBuilder};
use tao::window::{Fullscreen, WindowBuilder, WindowId};
@@ -236,7 +243,7 @@ struct NotificationOpts {
struct WindowPair {
#[allow(dead_code)]
window: tao::window::Window,
webview: WebView,
webview: Option<WebView>,
}
struct AppState {
@@ -548,7 +555,7 @@ require('{}');
}
event_loop.run(move |event, event_loop_target, control_flow| {
*control_flow = ControlFlow::Poll;
*control_flow = ControlFlow::WaitUntil(Instant::now() + POLL_INTERVAL);
let mut st = state.borrow_mut();
if st.node_exited.load(Ordering::SeqCst) {
@@ -559,7 +566,7 @@ require('{}');
}
match event {
Event::NewEvents(StartCause::Poll) => {
Event::NewEvents(StartCause::Poll | StartCause::ResumeTimeReached { .. }) => {
// Drain async responses from background threads (dialogs, clipboard, etc.)
while let Ok((request_id, result)) = response_rx.try_recv() {
st.send_to_node(&ToNode::Response {
@@ -650,20 +657,22 @@ window.__gelectron_run_main(`{}`);
}
event_loop.run(move |event, event_loop_target, control_flow| {
*control_flow = ControlFlow::Poll;
*control_flow = ControlFlow::WaitUntil(Instant::now() + POLL_INTERVAL);
let mut st = state.borrow_mut();
match event {
Event::NewEvents(StartCause::Poll) => {
Event::NewEvents(StartCause::Poll | StartCause::ResumeTimeReached { .. }) => {
// Drain async responses from background threads (dialogs, clipboard, etc.)
while let Ok((request_id, result)) = response_rx.try_recv() {
if let Some(pair) = st.windows.get(&1u32) {
if let Some(webview) = &pair.webview {
let js = format!(
"window.__gelectron_response('{}', {});",
request_id,
serde_json::to_string(&result).unwrap_or_default()
);
let _ = pair.webview.evaluate_script(&js);
let _ = webview.evaluate_script(&js);
}
}
}
@@ -690,9 +699,9 @@ window.__gelectron_run_main(`{}`);
serde_json::json!({"__from_node":true,"type":"notification-event","id":id,"event":event,"action_index":action_index,"action":action,"reply":reply})
}
};
let _ = pair.webview.evaluate_script(
let _ = pair.webview.as_ref().map(|wv| wv.evaluate_script(
&format!("window.postMessage({},'*');", serde_json::to_string(&js_msg).unwrap())
);
));
}
}
@@ -719,7 +728,7 @@ window.__gelectron_run_main(`{}`);
let js = serde_json::to_string(&serde_json::json!({
"__from_node": true, "type": "window-closed", "id": id,
})).unwrap();
let _ = pair.webview.evaluate_script(&format!("window.postMessage({},'*');", js));
let _ = pair.webview.as_ref().map(|wv| wv.evaluate_script(&format!("window.postMessage({},'*');", js)));
}
st.windows.remove(&id);
st.window_wids.remove(&window_id);
@@ -733,7 +742,7 @@ window.__gelectron_run_main(`{}`);
let js = serde_json::to_string(&serde_json::json!({
"__from_node": true, "type": "window-focus", "id": id,
})).unwrap();
let _ = pair.webview.evaluate_script(&format!("window.postMessage({},'*');", js));
let _ = pair.webview.as_ref().map(|wv| wv.evaluate_script(&format!("window.postMessage({},'*');", js)));
}
}
}
@@ -742,6 +751,47 @@ window.__gelectron_run_main(`{}`);
});
}
fn create_webview(
window: &tao::window::Window,
url: &str,
init: &str,
id: u32,
ipc_tx: &mpsc::Sender<ToNode>,
to_rust_tx: Option<&Arc<mpsc::Sender<ToRust>>>,
) -> wry::Result<WebView> {
let ipc_tx_clone = ipc_tx.clone();
let to_rust_tx_clone = to_rust_tx.cloned();
let wid_for_ipc = id;
WebViewBuilder::new()
.with_url(url)
.with_initialization_script(init)
.with_devtools(false)
.with_ipc_handler(move |req| {
let body = req.body().to_string();
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&body) {
// Try parsing as a ToRust command (load-file, load-url, etc.)
if let Some(ref tx) = to_rust_tx_clone {
if let Ok(cmd) = serde_json::from_value::<ToRust>(val.clone()) {
let _ = tx.send(cmd);
return;
}
}
// Fall back to ipc-send handling
let msg_type = val.get("type").and_then(|v| v.as_str()).unwrap_or("");
if msg_type == "ipc-send" {
let channel = val.get("channel").and_then(|v| v.as_str()).unwrap_or("");
let args = val.get("args").cloned().unwrap_or(serde_json::Value::Null);
let _ = ipc_tx_clone.send(ToNode::IpcMessage { id: wid_for_ipc, channel: channel.to_string(), data: args });
} else if msg_type == "quit" {
if let Some(ref tx) = to_rust_tx_clone {
let _ = tx.send(ToRust::Quit);
}
}
}
})
.build(window)
}
fn create_initial_webview_window(
event_loop_target: &tao::event_loop::EventLoopWindowTarget<()>,
state: &Rc<RefCell<AppState>>,
@@ -783,7 +833,7 @@ fn create_initial_webview_window(
Ok(webview) => {
let wid = window.id();
let mut st = state.borrow_mut();
st.windows.insert(window_id, WindowPair { window, webview });
st.windows.insert(window_id, WindowPair { window, webview: Some(webview) });
st.window_wids.insert(wid, window_id);
log::info!("Initial WebView window created (running compat layer)");
}
@@ -1459,40 +1509,17 @@ fn handle_to_rust(
Ok(window) => {
let url = options.url.unwrap_or_else(|| "about:blank".into());
log::info!("Creating window {} - '{}'", id, url);
let ipc_tx_clone = ipc_tx.clone();
let to_rust_tx_clone = to_rust_tx.cloned();
let wid_for_ipc = id;
let init = st.bundle_js.clone().unwrap_or_else(|| preload_script());
match WebViewBuilder::new()
.with_url(&url)
.with_initialization_script(&init)
.with_devtools(false)
.with_ipc_handler(move |req| {
let body = req.body().to_string();
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&body) {
// Try parsing as a ToRust command (load-file, load-url, etc.)
if let Some(ref tx) = to_rust_tx_clone {
if let Ok(cmd) = serde_json::from_value::<ToRust>(val.clone()) {
let _ = tx.send(cmd);
return;
// Build the WebView immediately (about:blank). Init scripts
// and the ipc message handler are registered at build time
// and survive subsequent in-place navigations (LoadUrl/LoadFile).
let webview = match create_webview(&window, &url, &init, id, ipc_tx, to_rust_tx) {
Ok(wv) => Some(wv),
Err(e) => {
log::error!("WebView error: {}", e);
None
}
}
// Fall back to ipc-send handling
let msg_type = val.get("type").and_then(|v| v.as_str()).unwrap_or("");
if msg_type == "ipc-send" {
let channel = val.get("channel").and_then(|v| v.as_str()).unwrap_or("");
let args = val.get("args").cloned().unwrap_or(serde_json::Value::Null);
let _ = ipc_tx_clone.send(ToNode::IpcMessage { id: wid_for_ipc, channel: channel.to_string(), data: args });
} else if msg_type == "quit" {
if let Some(ref tx) = to_rust_tx_clone {
let _ = tx.send(ToRust::Quit);
}
}
}
})
.build(&window)
{
Ok(webview) => {
};
let wid = window.id();
if let Some(icon) = options
.icon
@@ -1509,64 +1536,36 @@ fn handle_to_rust(
st.window_wids.insert(wid, id);
log::info!("Window {} ready", id);
}
Err(e) => log::error!("WebView error: {}", e),
}
}
Err(e) => log::error!("Window error: {}", e),
}
}
ToRust::LoadUrl { id, url } => {
log::info!("Loading url in window {}: {}", id, url);
if let Some(pair) = st.windows.get(&id) {
let _ = pair.webview.evaluate_script(&format!(
if let Some(webview) = &pair.webview {
let _ = webview.evaluate_script(&format!(
"window.location.replace({});",
serde_json::to_string(&url).unwrap()
));
}
}
}
ToRust::LoadFile { id, path } => {
let url = url::Url::from_file_path(std::fs::canonicalize(&path).unwrap_or_default())
.map(|u| u.to_string())
.unwrap_or_else(|_| "about:blank".into());
log::info!("Loading file in window {}: {}", id, url);
let init = st.bundle_js.clone().unwrap_or_else(|| preload_script());
if let Some(pair) = st.windows.get_mut(&id) {
let window = &pair.window;
let ipc_tx_clone = ipc_tx.clone();
let to_rust_tx_clone = to_rust_tx.cloned();
let wid_for_ipc = id;
match WebViewBuilder::new()
.with_url(&url)
.with_initialization_script(&init)
.with_devtools(false)
.with_ipc_handler(move |req| {
let body = req.body().to_string();
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&body) {
if let Some(ref tx) = to_rust_tx_clone {
if let Ok(cmd) = serde_json::from_value::<ToRust>(val.clone()) {
let _ = tx.send(cmd);
return;
}
}
let msg_type = val.get("type").and_then(|v| v.as_str()).unwrap_or("");
if msg_type == "ipc-send" {
let channel = val.get("channel").and_then(|v| v.as_str()).unwrap_or("");
let args = val.get("args").cloned().unwrap_or(serde_json::Value::Null);
let _ = ipc_tx_clone.send(ToNode::IpcMessage { id: wid_for_ipc, channel: channel.to_string(), data: args });
} else if msg_type == "quit" {
if let Some(ref tx) = to_rust_tx_clone {
let _ = tx.send(ToRust::Quit);
}
}
}
})
.build(window)
{
Ok(webview) => {
pair.webview = webview;
log::info!("WebView rebuilt for window {}", id);
}
Err(e) => log::error!("WebView rebuild error: {}", e),
// Navigate in-place instead of rebuilding the WebView. Rebuilding on
// macOS creates a stray 500x500 NSWindow artifact when the target
// window is still hidden (the vanilla app loads its splash this way).
// The WKUserScript init script re-runs on navigation, so window.gelectron
// (and any bundle) is re-established on the new document.
if let Some(pair) = st.windows.get(&id) {
if let Some(webview) = &pair.webview {
let _ = webview.evaluate_script(&format!(
"window.location.replace({});",
serde_json::to_string(&url).unwrap()
));
}
}
}
@@ -1584,12 +1583,12 @@ fn handle_to_rust(
if let Some(pair) = st.windows.get(&id) {
let msg = serde_json::json!({"__from_node":true,"channel":channel,"data":data});
let js = format!("window.postMessage({},'*');", serde_json::to_string(&msg).unwrap());
let _ = pair.webview.evaluate_script(&js);
let _ = pair.webview.as_ref().map(|wv| wv.evaluate_script(&js));
}
}
ToRust::EvalJs { id, script } => {
if let Some(pair) = st.windows.get(&id) {
let _ = pair.webview.evaluate_script(&script);
let _ = pair.webview.as_ref().map(|wv| wv.evaluate_script(&script));
}
}
ToRust::Quit => {
@@ -1999,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> {
Command::new("which")
.arg("node")
.output()
.ok()
.and_then(|o| if o.status.success() { String::from_utf8(o.stdout).ok().map(|s| s.trim().to_string()) } else { None })
.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()); }
let node_exe = if cfg!(windows) { "node.exe" } else { "node" };
if let Ok(override_path) = std::env::var("GELECTRON_NODE") {
if !override_path.is_empty() && std::path::Path::new(&override_path).exists() {
return Some(override_path);
}
}
// 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());
}
}
}
// 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)]
+4 -3
View File
@@ -1,7 +1,7 @@
{
"name": "gelectron-darwin-arm64",
"version": "0.1.0",
"description": "Gelectron native binary for macOS ARM64",
"version": "0.1.1",
"description": "Gelectron native addon for macOS ARM64",
"main": "gelectron_core.darwin-arm64.node",
"files": [
"gelectron_core.darwin-arm64.node"
@@ -12,5 +12,6 @@
"cpu": [
"arm64"
],
"license": "MIT"
"license": "MIT",
"author": "mileswa1q22"
}
+17
View File
@@ -0,0 +1,17 @@
# Gelectron (macOS x64)
Gelectron native addon for **macOS on x64 (Intel)**.
This package provides the native `gelectron_core` binary for macOS x64. It is an internal dependency of [`gelectron`](https://www.npmjs.com/package/gelectron) and is installed automatically — you should not need to install it directly.
> Gelectron is a drop-in replacement for Electron using native web views (WKWebView / WebView2 / WebKitGTK) instead of Chromium.
## Usage
Install the platform-specific addon for your system:
```bash
npm install gelectron
```
The correct platform binary is selected automatically via `optionalDependencies`.
+17
View File
@@ -0,0 +1,17 @@
{
"name": "gelectron-darwin-x64",
"version": "0.1.1",
"description": "Gelectron native addon for macOS x64",
"main": "gelectron_core.darwin-x64.node",
"files": [
"gelectron_core.darwin-x64.node"
],
"os": [
"darwin"
],
"cpu": [
"x64"
],
"license": "MIT",
"author": "mileswa1q22"
}
+17
View File
@@ -0,0 +1,17 @@
# Gelectron (Linux ARM64)
Gelectron native addon for **Linux on ARM64 (GNU)**.
This package provides the native `gelectron_core` binary for Linux ARM64. It is an internal dependency of [`gelectron`](https://www.npmjs.com/package/gelectron) and is installed automatically — you should not need to install it directly.
> Gelectron is a drop-in replacement for Electron using native web views (WKWebView / WebView2 / WebKitGTK) instead of Chromium.
## Usage
Install the platform-specific addon for your system:
```bash
npm install gelectron
```
The correct platform binary is selected automatically via `optionalDependencies`.
+17
View File
@@ -0,0 +1,17 @@
{
"name": "gelectron-linux-arm64-gnu",
"version": "0.1.1",
"description": "Gelectron native addon for Linux ARM64 (GNU)",
"main": "gelectron_core.linux-arm64-gnu.node",
"files": [
"gelectron_core.linux-arm64-gnu.node"
],
"os": [
"linux"
],
"cpu": [
"arm64"
],
"license": "MIT",
"author": "mileswa1q22"
}
+17
View File
@@ -0,0 +1,17 @@
# Gelectron (Linux x64)
Gelectron native addon for **Linux on x64 (GNU)**.
This package provides the native `gelectron_core` binary for Linux x64. It is an internal dependency of [`gelectron`](https://www.npmjs.com/package/gelectron) and is installed automatically — you should not need to install it directly.
> Gelectron is a drop-in replacement for Electron using native web views (WKWebView / WebView2 / WebKitGTK) instead of Chromium.
## Usage
Install the platform-specific addon for your system:
```bash
npm install gelectron
```
The correct platform binary is selected automatically via `optionalDependencies`.
+17
View File
@@ -0,0 +1,17 @@
{
"name": "gelectron-linux-x64-gnu",
"version": "0.1.1",
"description": "Gelectron native addon for Linux x64 (GNU)",
"main": "gelectron_core.linux-x64-gnu.node",
"files": [
"gelectron_core.linux-x64-gnu.node"
],
"os": [
"linux"
],
"cpu": [
"x64"
],
"license": "MIT",
"author": "mileswa1q22"
}
+17
View File
@@ -0,0 +1,17 @@
# Gelectron (Windows ARM64)
Gelectron native addon for **Windows on ARM64**.
This package provides the native `gelectron_core` binary for Windows ARM64. It is an internal dependency of [`gelectron`](https://www.npmjs.com/package/gelectron) and is installed automatically — you should not need to install it directly.
> Gelectron is a drop-in replacement for Electron using native web views (WKWebView / WebView2 / WebKitGTK) instead of Chromium.
## Usage
Install the platform-specific addon for your system:
```bash
npm install gelectron
```
The correct platform binary is selected automatically via `optionalDependencies`.
+17
View File
@@ -0,0 +1,17 @@
{
"name": "gelectron-win32-arm64-msvc",
"version": "0.1.1",
"description": "Gelectron native addon for Windows ARM64",
"main": "gelectron_core.win32-arm64-msvc.node",
"files": [
"gelectron_core.win32-arm64-msvc.node"
],
"os": [
"win32"
],
"cpu": [
"arm64"
],
"license": "MIT",
"author": "mileswa1q22"
}
+17
View File
@@ -0,0 +1,17 @@
# Gelectron (Windows x64)
Gelectron native addon for **Windows on x64**.
This package provides the native `gelectron_core` binary for Windows x64. It is an internal dependency of [`gelectron`](https://www.npmjs.com/package/gelectron) and is installed automatically — you should not need to install it directly.
> Gelectron is a drop-in replacement for Electron using native web views (WKWebView / WebView2 / WebKitGTK) instead of Chromium.
## Usage
Install the platform-specific addon for your system:
```bash
npm install gelectron
```
The correct platform binary is selected automatically via `optionalDependencies`.
+17
View File
@@ -0,0 +1,17 @@
{
"name": "gelectron-win32-x64-msvc",
"version": "0.1.1",
"description": "Gelectron native addon for Windows x64",
"main": "gelectron_core.win32-x64-msvc.node",
"files": [
"gelectron_core.win32-x64-msvc.node"
],
"os": [
"win32"
],
"cpu": [
"x64"
],
"license": "MIT",
"author": "mileswa1q22"
}
+13
View File
@@ -1875,6 +1875,19 @@
"fast-string-width": "^3.0.2"
}
},
"node_modules/gelectron-darwin-arm64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/gelectron-darwin-arm64/-/gelectron-darwin-arm64-0.1.0.tgz",
"integrity": "sha512-K0bmA9pfSNlA7CLPD+BOBNdvb0Cb8qMWcYry9BYuP3DC0gcL8Gfq1iM2h3CHNKJB/YEZdBA0uCgTd3+0SMvQ3w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+17 -9
View File
@@ -1,10 +1,12 @@
{
"name": "gelectron",
"version": "0.1.0",
"name": "gelectron-core",
"version": "0.1.1",
"description": "Firefox-engine alternative to Electron, powered by Servo",
"main": "src/electron/index.js",
"bin": {
"gelectron": "./cli/gelectron.js"
"gelectron": "./cli/gelectron.js",
"gelectron-core": "./cli/gelectron.js",
"gelectron-packager": "./packager/bin/gelectron-packager.js"
},
"scripts": {
"build": "napi build --platform --release --package gelectron-core",
@@ -58,11 +60,17 @@
"node": ">=18.0.0"
},
"optionalDependencies": {
"gelectron-darwin-arm64": "0.1.0",
"gelectron-darwin-x64": "0.1.0",
"gelectron-linux-arm64-gnu": "0.1.0",
"gelectron-linux-x64-gnu": "0.1.0",
"gelectron-win32-arm64-msvc": "0.1.0",
"gelectron-win32-x64-msvc": "0.1.0"
"gelectron-darwin-arm64": "0.1.1",
"gelectron-darwin-x64": "0.1.1",
"gelectron-linux-arm64-gnu": "0.1.1",
"gelectron-linux-x64-gnu": "0.1.1",
"gelectron-win32-arm64-msvc": "0.1.1",
"gelectron-win32-x64-msvc": "0.1.1",
"gelectron-core-darwin-x64": "0.1.1",
"gelectron-core-darwin-arm64": "0.1.1",
"gelectron-core-win32-x64-msvc": "0.1.1",
"gelectron-core-win32-arm64-msvc": "0.1.1",
"gelectron-core-linux-x64-gnu": "0.1.1",
"gelectron-core-linux-arm64-gnu": "0.1.1"
}
}
+63 -4
View File
@@ -73,11 +73,63 @@ function findCompatLayer(startDir) {
while (dir !== path.dirname(dir)) {
const p = path.join(dir, 'src', 'electron');
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);
}
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) {
let dir = appDir;
while (dir !== path.dirname(dir)) {
@@ -92,7 +144,8 @@ function findGelectronFromAncestors(appDir) {
}
dir = path.dirname(dir);
}
return null;
// Fall back to a system-installed gelectron runtime
return findInstalledGelectron();
}
async function getNodeBinary(platform, arch, cacheDir) {
@@ -172,6 +225,8 @@ ${iconKey} <key>CFBundleDisplayName</key>
<string>${exeName}</string>
<key>CFBundleIdentifier</key>
<string>com.gelectron.${name.toLowerCase().replace(/[^a-z0-9]/g, '')}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${name}</string>
<key>CFBundlePackageType</key>
@@ -180,6 +235,10 @@ ${iconKey} <key>CFBundleDisplayName</key>
<string>${version}</string>
<key>CFBundleVersion</key>
<string>${version}</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSRequiresAquaSystemAppearance</key>
@@ -452,7 +511,7 @@ async function packageMac(appDir, outDir, name, version, gelectronBin, nodeDir,
}
// 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) {
copyDirSync(compatDir, path.join(macosDir, 'compat'));
}
@@ -515,7 +574,7 @@ async function packageWindows(appDir, outDir, name, version, gelectronBin, nodeD
}
// Copy compat layer
const compatDirWin = findCompatLayer(appDir) || findCompatLayer(path.dirname(gelectronBin));
const compatDirWin = findCompatLayer(appDir) || findCompatNextToBinary(gelectronBin) || findCompatLayer(path.dirname(gelectronBin));
if (compatDirWin) {
copyDirSync(compatDirWin, path.join(outDir, 'compat'));
}
@@ -592,7 +651,7 @@ async function packageLinux(appDir, outDir, name, version, gelectronBin, nodeDir
}
// Copy compat layer
const compatDirLinux = findCompatLayer(appDir) || findCompatLayer(path.dirname(gelectronBin));
const compatDirLinux = findCompatLayer(appDir) || findCompatNextToBinary(gelectronBin) || findCompatLayer(path.dirname(gelectronBin));
if (compatDirLinux) {
copyDirSync(compatDirLinux, path.join(outDir, 'compat'));
}
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
#
# build-npm.sh
#
# Builds the gelectron-core N-API addon for every supported platform target
# and copies the resulting .node files into the corresponding npm/ package
# folders.
#
# Each target is attempted. If the toolchain or linker isn't available,
# it's skipped with a warning. To build more targets:
# - Linux: brew install mingw-w64 (for Windows), or use Docker/cross
# - Run this script on each target OS for best results
#
set -eo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_DIR"
HOST_TARGET="$(rustc -vV | grep '^host:' | awk '{print $2}')"
HAS_RUSTUP=false
command -v rustup &>/dev/null && HAS_RUSTUP=true
NAPI="$REPO_DIR/node_modules/.bin/napi"
if [ ! -x "$NAPI" ]; then
echo "Error: napi not found. Run 'npm install' first."
exit 1
fi
# Each line: target_triple|npm_folder|node_filename
TARGETS=(
"aarch64-apple-darwin|npm/darwin-arm64|gelectron_core.darwin-arm64.node"
"x86_64-apple-darwin|npm/darwin-x64|gelectron_core.darwin-x64.node"
"x86_64-pc-windows-msvc|npm/win32-x64-msvc|gelectron_core.win32-x64-msvc.node"
"aarch64-pc-windows-msvc|npm/win32-arm64-msvc|gelectron_core.win32-arm64-msvc.node"
"x86_64-unknown-linux-gnu|npm/linux-x64-gnu|gelectron_core.linux-x64-gnu.node"
"aarch64-unknown-linux-gnu|npm/linux-arm64-gnu|gelectron_core.linux-arm64-gnu.node"
)
built=0
skipped=0
echo "═══════════════════════════════════════════════════"
echo " Gelectron NPM Builder"
echo "═══════════════════════════════════════════════════"
echo " Host target: $HOST_TARGET"
echo " rustup: $HAS_RUSTUP"
echo "═══════════════════════════════════════════════════"
echo ""
for entry in "${TARGETS[@]}"; do
target="$(echo "$entry" | cut -d'|' -f1)"
dest_dir="$(echo "$entry" | cut -d'|' -f2)"
node_name="$(echo "$entry" | cut -d'|' -f3)"
echo "── $target ──"
# Add the Rust target
if $HAS_RUSTUP; then
if ! rustup target add "$target" 2>/dev/null; then
echo " ⚠ Could not install Rust target — skipping"
skipped=$((skipped + 1))
echo ""
continue
fi
elif [ "$target" != "$HOST_TARGET" ]; then
echo " ⚠ No rustup and not host target — skipping"
echo " Install rustup: brew install rustup && rustup-init"
skipped=$((skipped + 1))
echo ""
continue
fi
# Build
echo " Building..."
if $NAPI build --platform --release --target "$target" --package gelectron-core 2>/dev/null; then
node_file="crates/gelectron-core/${node_name}"
if [ ! -f "$node_file" ]; then
node_file=$(ls crates/gelectron-core/gelectron_core.*.node 2>/dev/null | head -1 || true)
fi
if [ -n "$node_file" ] && [ -f "$node_file" ]; then
mkdir -p "$dest_dir"
cp "$node_file" "$dest_dir/$node_name"
size=$(ls -lh "$dest_dir/$node_name" | awk '{print $5}')
echo " ✓ Built ($size) → $dest_dir/$node_name"
built=$((built + 1))
else
echo " ✗ Build succeeded but .node file not found"
skipped=$((skipped + 1))
fi
else
echo " ✗ Build failed (missing linker or toolchain)"
skipped=$((skipped + 1))
fi
echo ""
done
echo "═══════════════════════════════════════════════════"
echo " Done: $built built, $skipped skipped"
echo "═══════════════════════════════════════════════════"
+119
View File
@@ -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 }
}
+207
View File
@@ -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"
+180
View File
@@ -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
+92
View File
@@ -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
+155
View File
@@ -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
+144
View File
@@ -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"
+73
View File
@@ -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
}
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
#
# publish-npm.sh
#
# Publishes all platform packages and the main gelectron package to npm.
# Run this after build-npm.sh has placed .node files in the npm/ folders.
#
set -eo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_DIR"
PLATFORMS=(
"npm/darwin-arm64"
"npm/darwin-x64"
"npm/win32-x64-msvc"
"npm/win32-arm64-msvc"
"npm/linux-x64-gnu"
"npm/linux-arm64-gnu"
)
echo "═══════════════════════════════════════════════════"
echo " Gelectron NPM Publisher"
echo "═══════════════════════════════════════════════════"
echo ""
# Check if logged in
if ! npm whoami &>/dev/null; then
echo "Not logged in to npm. Run 'npm login' first."
exit 1
fi
published=0
skipped=0
for dir in "${PLATFORMS[@]}"; do
pkg_name=$(node -e "console.log(require('./$dir/package.json').name)")
has_node=false
# Check if the .node file exists
for f in "$dir"/*.node; do
if [ -f "$f" ]; then
has_node=true
break
fi
done
if $has_node; then
echo "Publishing $pkg_name..."
(cd "$dir" && npm publish --access public)
published=$((published + 1))
else
echo "Skipping $pkg_name (no .node file)"
skipped=$((skipped + 1))
fi
done
echo ""
echo "Publishing main gelectron package..."
npm run prepublishOnly
npm publish --access public
published=$((published + 1))
echo ""
echo "═══════════════════════════════════════════════════"
echo " Done: $published published, $skipped skipped"
echo "═══════════════════════════════════════════════════"
+195
View File
@@ -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;
+8
View File
@@ -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) {
const pending = this._pendingRequests[requestId];
if (pending) {
+72
View File
@@ -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;
+151
View File
@@ -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 };