7 Commits
30 changed files with 1799 additions and 149 deletions
+1
View File
@@ -0,0 +1 @@
1db4284c-fc55-41be-a7dd-cde2036997da
+175
View File
@@ -0,0 +1,175 @@
name: Build native binaries
on:
push:
tags:
- 'v*'
jobs:
build:
name: Build ${{ matrix.target }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: aarch64-pc-windows-msvc
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
runtime:
name: Runtime ${{ matrix.target }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
target: aarch64-apple-darwin
platform: darwin
arch: arm64
- os: macos-latest
target: x86_64-apple-darwin
platform: darwin
arch: x64
- os: windows-latest
target: x86_64-pc-windows-msvc
platform: win32
arch: x64
- os: windows-latest
target: aarch64-pc-windows-msvc
platform: win32
arch: arm64
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
platform: linux
arch: x64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install Linux system dependencies
if: matrix.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libsoup-3.0-dev
- name: Build gelectron binary
run: cargo build --release -p gelectron --target ${{ matrix.target }}
- name: Package release archive
run: |
bash scripts/make-release.sh \
-v "${{ github.ref_name }}" \
-p ${{ matrix.platform }} \
-a ${{ matrix.arch }} \
-b "target/${{ matrix.target }}/release/gelectron" \
--no-build
- name: Build macOS installer (DMG)
if: matrix.platform == 'darwin'
run: bash scripts/pkg/make-dmg.sh -v "${{ github.ref_name }}" -a ${{ matrix.arch }} -b "target/${{ matrix.target }}/release/gelectron" -o dist
- name: Build Windows installer (EXE)
if: matrix.platform == 'win32' && matrix.arch == 'x64'
shell: pwsh
run: |
choco install nsis -y --no-progress
powershell -File scripts/pkg/make-exe.ps1 -Bin "target/${{ matrix.target }}/release/gelectron.exe" -Compat src/electron -Version "${{ github.ref_name }}" -Arch x64 -Out dist
- name: Build Linux installer (AppImage)
if: matrix.platform == 'linux'
run: bash scripts/pkg/make-appimage.sh -v "${{ github.ref_name }}" -b "target/${{ matrix.target }}/release/gelectron" -o dist
- name: Upload installer artifacts
uses: actions/upload-artifact@v4
with:
name: installers-${{ matrix.target }}
path: |
dist/gelectron-*
dist/Gelectron-*
if-no-files-found: error
release:
name: Attach binaries to GitHub Release
needs: [build, runtime]
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/**
+1
View File
@@ -7,6 +7,7 @@ node_modules/
npm-debug.log* npm-debug.log*
package-lock.json package-lock.json
al/
# Build artifacts # Build artifacts
*.node *.node
*.dylib *.dylib
+236 -2
View File
@@ -22,13 +22,78 @@ Electron bundles Chromium — ~300 MB per app with 500+ MB RSS. Gelectron uses t
## Quick Start ## 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 with:
```bash
gelectron-core /path/to/electron-app
```
### Download a native installer (double-click, no tools needed)
Every new release tag (pushed as `v*`) is pre-compiled in CI and published as
platform-native, double-clickable installers on the [Releases page]:
| Platform | Installer | What it does |
|---|---|---|
| macOS | `Gelectron-<version>-arm64.dmg` / `-x64.dmg` | Contains `Install gelectron.pkg`, which installs the runtime + compat layer to `/usr/local/bin` |
| Windows | `Gelectron-<version>-x64.exe` | NSIS installer → `C:\Program Files\Gelectron`, adds it to the system PATH, Start Menu + uninstaller |
| Linux | `Gelectron-<version>-x86_64.AppImage` | Self-contained bundle (includes Node.js + compat), just run it |
After installing, `gelectron /path/to/electron-app` works from anywhere.
> macOS installers are ad-hoc signed (no Developer ID), so on another Mac the
> first launch shows a Gatekeeper "unidentified developer" warning — right-click
> → Open to run it.
### Install from GitHub Releases (no npm)
Every new release tag (pushed as `v*`) is pre-compiled in CI and published as
a self-contained installer archive. Install the latest pre-built runtime with
a single command — no Rust toolchain, no npm:
```bash
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/mileswolfallen2/gelectron/main/scripts/install-release.sh | bash
# Windows (PowerShell)
irm https://raw.githubusercontent.com/mileswolfallen2/gelectron/main/scripts/install-release.ps1 | iex
```
The installer downloads `gelectron-<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`) - Rust 1.75+ (`rustup.rs`)
- Node.js 18+ - Node.js 18+
- npm - npm
### Build & Run ### Build & Run (from source)
```bash ```bash
git clone https://github.com/mileswolfallen2/gelectron.git git clone https://github.com/mileswolfallen2/gelectron.git
@@ -316,6 +381,175 @@ cargo build --release -p gelectron-core
The N-API addon compiles to a `.node` file that can be loaded directly into Node.js. 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 ## Testing with OmniEmu2.0
OmniEmu2.0 is a full Electron app used to validate Gelectron compatibility: OmniEmu2.0 is a full Electron app used to validate Gelectron compatibility:
+4 -1
View File
@@ -2,5 +2,8 @@
"name": "gelectron-benchmark", "name": "gelectron-benchmark",
"version": "1.0.0", "version": "1.0.0",
"description": "Simple web benchmark for comparing Electron vs Gelectron", "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]
}
}
+86 -87
View File
@@ -11,6 +11,13 @@ use std::sync::mpsc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::thread; use std::thread;
use std::sync::Arc; 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::{Event, StartCause, WindowEvent};
use tao::event_loop::{ControlFlow, EventLoopBuilder}; use tao::event_loop::{ControlFlow, EventLoopBuilder};
use tao::window::{Fullscreen, WindowBuilder, WindowId}; use tao::window::{Fullscreen, WindowBuilder, WindowId};
@@ -236,7 +243,7 @@ struct NotificationOpts {
struct WindowPair { struct WindowPair {
#[allow(dead_code)] #[allow(dead_code)]
window: tao::window::Window, window: tao::window::Window,
webview: WebView, webview: Option<WebView>,
} }
struct AppState { struct AppState {
@@ -548,7 +555,7 @@ require('{}');
} }
event_loop.run(move |event, event_loop_target, control_flow| { 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(); let mut st = state.borrow_mut();
if st.node_exited.load(Ordering::SeqCst) { if st.node_exited.load(Ordering::SeqCst) {
@@ -559,7 +566,7 @@ require('{}');
} }
match event { match event {
Event::NewEvents(StartCause::Poll) => { Event::NewEvents(StartCause::Poll | StartCause::ResumeTimeReached { .. }) => {
// Drain async responses from background threads (dialogs, clipboard, etc.) // Drain async responses from background threads (dialogs, clipboard, etc.)
while let Ok((request_id, result)) = response_rx.try_recv() { while let Ok((request_id, result)) = response_rx.try_recv() {
st.send_to_node(&ToNode::Response { st.send_to_node(&ToNode::Response {
@@ -650,20 +657,22 @@ window.__gelectron_run_main(`{}`);
} }
event_loop.run(move |event, event_loop_target, control_flow| { 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(); let mut st = state.borrow_mut();
match event { match event {
Event::NewEvents(StartCause::Poll) => { Event::NewEvents(StartCause::Poll | StartCause::ResumeTimeReached { .. }) => {
// Drain async responses from background threads (dialogs, clipboard, etc.) // Drain async responses from background threads (dialogs, clipboard, etc.)
while let Ok((request_id, result)) = response_rx.try_recv() { while let Ok((request_id, result)) = response_rx.try_recv() {
if let Some(pair) = st.windows.get(&1u32) { if let Some(pair) = st.windows.get(&1u32) {
if let Some(webview) = &pair.webview {
let js = format!( let js = format!(
"window.__gelectron_response('{}', {});", "window.__gelectron_response('{}', {});",
request_id, request_id,
serde_json::to_string(&result).unwrap_or_default() 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}) 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()) &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!({ let js = serde_json::to_string(&serde_json::json!({
"__from_node": true, "type": "window-closed", "id": id, "__from_node": true, "type": "window-closed", "id": id,
})).unwrap(); })).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.windows.remove(&id);
st.window_wids.remove(&window_id); st.window_wids.remove(&window_id);
@@ -733,7 +742,7 @@ window.__gelectron_run_main(`{}`);
let js = serde_json::to_string(&serde_json::json!({ let js = serde_json::to_string(&serde_json::json!({
"__from_node": true, "type": "window-focus", "id": id, "__from_node": true, "type": "window-focus", "id": id,
})).unwrap(); })).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( fn create_initial_webview_window(
event_loop_target: &tao::event_loop::EventLoopWindowTarget<()>, event_loop_target: &tao::event_loop::EventLoopWindowTarget<()>,
state: &Rc<RefCell<AppState>>, state: &Rc<RefCell<AppState>>,
@@ -783,7 +833,7 @@ fn create_initial_webview_window(
Ok(webview) => { Ok(webview) => {
let wid = window.id(); let wid = window.id();
let mut st = state.borrow_mut(); 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); st.window_wids.insert(wid, window_id);
log::info!("Initial WebView window created (running compat layer)"); log::info!("Initial WebView window created (running compat layer)");
} }
@@ -1459,40 +1509,17 @@ fn handle_to_rust(
Ok(window) => { Ok(window) => {
let url = options.url.unwrap_or_else(|| "about:blank".into()); let url = options.url.unwrap_or_else(|| "about:blank".into());
log::info!("Creating window {} - '{}'", id, url); 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()); let init = st.bundle_js.clone().unwrap_or_else(|| preload_script());
match WebViewBuilder::new() // Build the WebView immediately (about:blank). Init scripts
.with_url(&url) // and the ipc message handler are registered at build time
.with_initialization_script(&init) // and survive subsequent in-place navigations (LoadUrl/LoadFile).
.with_devtools(false) let webview = match create_webview(&window, &url, &init, id, ipc_tx, to_rust_tx) {
.with_ipc_handler(move |req| { Ok(wv) => Some(wv),
let body = req.body().to_string(); Err(e) => {
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&body) { log::error!("WebView error: {}", e);
// Try parsing as a ToRust command (load-file, load-url, etc.) None
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)
{
Ok(webview) => {
let wid = window.id(); let wid = window.id();
if let Some(icon) = options if let Some(icon) = options
.icon .icon
@@ -1509,64 +1536,36 @@ fn handle_to_rust(
st.window_wids.insert(wid, id); st.window_wids.insert(wid, id);
log::info!("Window {} ready", id); log::info!("Window {} ready", id);
} }
Err(e) => log::error!("WebView error: {}", e),
}
}
Err(e) => log::error!("Window error: {}", e), Err(e) => log::error!("Window error: {}", e),
} }
} }
ToRust::LoadUrl { id, url } => { ToRust::LoadUrl { id, url } => {
log::info!("Loading url in window {}: {}", id, url); log::info!("Loading url in window {}: {}", id, url);
if let Some(pair) = st.windows.get(&id) { 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({});", "window.location.replace({});",
serde_json::to_string(&url).unwrap() serde_json::to_string(&url).unwrap()
)); ));
} }
} }
}
ToRust::LoadFile { id, path } => { ToRust::LoadFile { id, path } => {
let url = url::Url::from_file_path(std::fs::canonicalize(&path).unwrap_or_default()) let url = url::Url::from_file_path(std::fs::canonicalize(&path).unwrap_or_default())
.map(|u| u.to_string()) .map(|u| u.to_string())
.unwrap_or_else(|_| "about:blank".into()); .unwrap_or_else(|_| "about:blank".into());
log::info!("Loading file in window {}: {}", id, url); log::info!("Loading file in window {}: {}", id, url);
let init = st.bundle_js.clone().unwrap_or_else(|| preload_script()); // Navigate in-place instead of rebuilding the WebView. Rebuilding on
if let Some(pair) = st.windows.get_mut(&id) { // macOS creates a stray 500x500 NSWindow artifact when the target
let window = &pair.window; // window is still hidden (the vanilla app loads its splash this way).
let ipc_tx_clone = ipc_tx.clone(); // The WKUserScript init script re-runs on navigation, so window.gelectron
let to_rust_tx_clone = to_rust_tx.cloned(); // (and any bundle) is re-established on the new document.
let wid_for_ipc = id; if let Some(pair) = st.windows.get(&id) {
match WebViewBuilder::new() if let Some(webview) = &pair.webview {
.with_url(&url) let _ = webview.evaluate_script(&format!(
.with_initialization_script(&init) "window.location.replace({});",
.with_devtools(false) serde_json::to_string(&url).unwrap()
.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),
} }
} }
} }
@@ -1584,12 +1583,12 @@ fn handle_to_rust(
if let Some(pair) = st.windows.get(&id) { if let Some(pair) = st.windows.get(&id) {
let msg = serde_json::json!({"__from_node":true,"channel":channel,"data":data}); let msg = serde_json::json!({"__from_node":true,"channel":channel,"data":data});
let js = format!("window.postMessage({},'*');", serde_json::to_string(&msg).unwrap()); 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 } => { ToRust::EvalJs { id, script } => {
if let Some(pair) = st.windows.get(&id) { 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 => { ToRust::Quit => {
+4 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "gelectron-darwin-arm64", "name": "gelectron-darwin-arm64",
"version": "0.1.0", "version": "0.1.1",
"description": "Gelectron native binary for macOS ARM64", "description": "Gelectron native addon for macOS ARM64",
"main": "gelectron_core.darwin-arm64.node", "main": "gelectron_core.darwin-arm64.node",
"files": [ "files": [
"gelectron_core.darwin-arm64.node" "gelectron_core.darwin-arm64.node"
@@ -12,5 +12,6 @@
"cpu": [ "cpu": [
"arm64" "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" "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": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+15 -9
View File
@@ -1,10 +1,10 @@
{ {
"name": "gelectron", "name": "gelectron-core",
"version": "0.1.0", "version": "0.1.1",
"description": "Firefox-engine alternative to Electron, powered by Servo", "description": "Firefox-engine alternative to Electron, powered by Servo",
"main": "src/electron/index.js", "main": "src/electron/index.js",
"bin": { "bin": {
"gelectron": "./cli/gelectron.js" "gelectron-core": "./cli/gelectron.js"
}, },
"scripts": { "scripts": {
"build": "napi build --platform --release --package gelectron-core", "build": "napi build --platform --release --package gelectron-core",
@@ -58,11 +58,17 @@
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"gelectron-darwin-arm64": "0.1.0", "gelectron-darwin-arm64": "0.1.1",
"gelectron-darwin-x64": "0.1.0", "gelectron-darwin-x64": "0.1.1",
"gelectron-linux-arm64-gnu": "0.1.0", "gelectron-linux-arm64-gnu": "0.1.1",
"gelectron-linux-x64-gnu": "0.1.0", "gelectron-linux-x64-gnu": "0.1.1",
"gelectron-win32-arm64-msvc": "0.1.0", "gelectron-win32-arm64-msvc": "0.1.1",
"gelectron-win32-x64-msvc": "0.1.0" "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"
} }
} }
+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 }
}
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env bash
#
# gelectron installer
#
# Downloads the pre-built gelectron binary + Electron compatibility layer from
# GitHub Releases and installs it into PREFIX so `gelectron <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 -rf "$PREFIX/compat"
echo "Removed $PREFIX/gelectron and $PREFIX/compat"
exit 0
fi
# ── Resolve version + archive ───────────────────────────────────────────────
if [[ -n "$FILE" ]]; then
ARCHIVE_FILE="$FILE"
else
if [[ -n "$VERSION" ]]; then
TAG="${VERSION#v}"
TAG="v${TAG}"
else
echo "==> Resolving latest release from $REPO..."
if ! command -v curl >/dev/null 2>&1; then
echo "error: curl is required" >&2
exit 1
fi
API_URL="https://api.github.com/repos/$REPO/releases/latest"
TAG="$(curl -fsSL "$API_URL" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n1)"
if [[ -z "$TAG" ]]; then
echo "error: could not resolve latest release from $API_URL" >&2
exit 1
fi
fi
VER="${TAG#v}"
ASSET="gelectron-$VER-$PLATFORM-$ARCH.$ARCHIVE_EXT"
URL="https://github.com/$REPO/releases/download/$TAG/$ASSET"
echo "==> Downloading $ASSET ..."
ARCHIVE_FILE="$(mktemp /tmp/gelectron-install.XXXXXX.$ARCHIVE_EXT)"
trap 'rm -f "$ARCHIVE_FILE"' EXIT
curl -fsSL -L "$URL" -o "$ARCHIVE_FILE"
fi
# ── Extract ─────────────────────────────────────────────────────────────────
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/gelectron-install-stage.XXXXXX")"
trap 'rm -rf "$STAGE" "${ARCHIVE_FILE:-}"' EXIT
echo "==> Extracting archive..."
case "$ARCHIVE_EXT" in
zip) unzip -qo "$ARCHIVE_FILE" -d "$STAGE" ;;
*) tar -xzf "$ARCHIVE_FILE" -C "$STAGE" ;;
esac
BIN_ABS="$STAGE/gelectron"
[[ -f "$STAGE/gelectron.exe" ]] && BIN_ABS="$STAGE/gelectron.exe"
if [[ ! -f "$BIN_ABS" ]]; then
echo "error: archive does not contain gelectron" >&2
ls -la "$STAGE" >&2
exit 1
fi
# ── Install ─────────────────────────────────────────────────────────────────
echo "==> Installing to $PREFIX"
mkdir -p "$PREFIX"
install -m 755 "$BIN_ABS" "$PREFIX/$(basename "$BIN_ABS")"
if [[ -d "$STAGE/compat" ]]; then
mkdir -p "$PREFIX/compat"
install -m 644 "$STAGE"/compat/*.js "$PREFIX/compat/"
fi
# Downloaded-from-GitHub binaries carry a quarantine attribute on macOS and
# Gatekeeper will block the first launch. Clear it — the binary is ad-hoc
# signed, so this is the only thing standing in the way.
if [[ "$PLATFORM" == "darwin" ]]; then
xattr -dr com.apple.quarantine "$PREFIX/$(basename "$BIN_ABS")" 2>/dev/null || true
fi
echo
echo " ✓ Installed: $PREFIX/$(basename "$BIN_ABS")"
echo " ✓ Installed: $PREFIX/compat/"
if ! command -v gelectron >/dev/null 2>&1 || [[ "$(command -v gelectron)" != "$PREFIX/gelectron" ]]; then
echo
echo " warning: $PREFIX is not on your PATH."
case "$SHELL" in
*zsh) RC="$HOME/.zshrc" ;;
*bash) RC="$HOME/.bashrc" ;;
*) RC="$HOME/.profile" ;;
esac
echo " Add this line to $RC:"
echo " export PATH=\"$PREFIX:\$PATH\""
fi
echo
"$PREFIX/$(basename "$BIN_ABS")" --version >/dev/null 2>&1 && \
echo " Run: gelectron /path/to/electron-app" || \
echo " Hint: $PREFIX/$(basename "$BIN_ABS") --version"
+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
+90
View File
@@ -0,0 +1,90 @@
; Gelectron Windows installer (NSIS)
;
; Build with makensis from a staging directory containing:
; gelectron.exe
; compat\*.js
;
; makensis /DVERSION=0.1.1 /DARCH=x64 Gelectron.nsi
;
; Installs to $PROGRAMFILES64\Gelectron, adds it to the machine PATH, creates
; Start Menu + Add/Remove Programs entries, and writes an uninstaller.
!include "MUI2.nsh"
!include "WordFunc.nsh"
!ifndef VERSION
!define VERSION "0.0.0"
!endif
!ifndef ARCH
!define ARCH "x64"
!endif
!define APPNAME "Gelectron"
!define COMPANY "gelectron"
!define ENV_KEY 'HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"'
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}"
Name "${APPNAME}"
OutFile "Gelectron-${VERSION}-${ARCH}.exe"
InstallDir "$PROGRAMFILES64\${APPNAME}"
InstallDirRegKey HKLM "Software\${APPNAME}" "InstallDir"
RequestExecutionLevel admin
; Modern UI
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
; Install section
Section "Install"
SetShellVarContext all
SetOutPath "$INSTDIR"
File "gelectron.exe"
File /r "compat"
WriteUninstaller "$INSTDIR\Uninstall.exe"
CreateDirectory "$SMPROGRAMS\${APPNAME}"
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" "$INSTDIR\gelectron.exe"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${APPNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${VERSION}"
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${COMPANY}"
WriteRegStr HKLM "${UNINST_KEY}" "InstallLocation" "$INSTDIR"
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" '"$INSTDIR\Uninstall.exe"'
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\gelectron.exe"
; Add $INSTDIR to the machine PATH (add-if-not-present via WordFunc)
ReadRegStr $0 ${ENV_KEY} "Path"
${WordAdd} $0 ";" "+$INSTDIR" $1
WriteRegExpandStr ${ENV_KEY} "Path" $1
SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment"
SectionEnd
; Uninstall section
Section "Uninstall"
SetShellVarContext all
; Remove $INSTDIR from the machine PATH
ReadRegStr $0 ${ENV_KEY} "Path"
${WordAdd} $0 ";" "-$INSTDIR" $1
WriteRegExpandStr ${ENV_KEY} "Path" $1
SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment"
Delete "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk"
RMDir "$SMPROGRAMS\${APPNAME}"
DeleteRegKey HKLM "${UNINST_KEY}"
DeleteRegKey HKLM "Software\${APPNAME}"
RMDir /r "$INSTDIR"
SectionEnd
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
#
# Build the Linux AppImage: bundles the gelectron binary, the compat layer
# and a Node.js runtime into a single self-contained, downloadable AppImage.
#
# Gelectron-<version>-x86_64.AppImage
#
# Requires: the gelectron binary (built against webkit2gtk-4.1), compat layer,
# and network access (downloads Node.js + appimagetool).
#
# Usage:
# scripts/pkg/make-appimage.sh -v VERSION [--binary PATH] [--compat DIR] [-o DIR]
set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
NODE_VERSION="20.18.1"
VERSION=""
BINARY=""
COMPAT="$REPO_DIR/src/electron"
OUT_DIR="$REPO_DIR/dist"
usage() {
cat <<'EOF'
Linux AppImage builder
Usage:
scripts/pkg/make-appimage.sh -v VERSION [--binary PATH] [--compat DIR] [-o DIR]
Options:
-v, --version VER Version string (e.g. 0.1.1)
-b, --binary PATH Path to the gelectron binary
-c, --compat DIR Path to the compat layer (default: <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 ;;
-b|--binary) BINARY="${2:-}"; shift 2 ;;
-c|--compat) COMPAT="${2:-}"; shift 2 ;;
-o|--out) OUT_DIR="${2:-}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "error: unknown option: $1" >&2; usage; exit 1 ;;
esac
done
VERSION="${VERSION#v}"
[[ -n "$VERSION" ]] || { echo "error: --version required" >&2; exit 1; }
if [[ -z "$BINARY" ]]; then
BINARY="$REPO_DIR/target/release/gelectron"
fi
[[ -f "$BINARY" ]] || { echo "error: binary not found: $BINARY" >&2; exit 1; }
[[ -d "$COMPAT" ]] || { echo "error: compat dir not found: $COMPAT" >&2; exit 1; }
log() { echo "==> $*"; }
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/gelectron-appimage.XXXXXX")"
trap 'rm -rf "$STAGE"' EXIT
APPDIR="$STAGE/Gelectron.AppDir"
mkdir -p "$APPDIR/usr/bin"
# ── Binaries ────────────────────────────────────────────────────────────────
log "Staging binary..."
install -m 755 "$BINARY" "$APPDIR/usr/bin/gelectron"
log "Staging compat layer (usr/bin/compat, resolved next to the binary)..."
mkdir -p "$APPDIR/usr/bin/compat"
install -m 644 "$COMPAT"/*.js "$APPDIR/usr/bin/compat/"
# Node.js runtime (gelectron's Node mode requires node on PATH)
NODE_DIR="$STAGE/node"
if [[ ! -d "$NODE_DIR" ]]; then
log "Downloading Node.js v$NODE_VERSION..."
mkdir -p "$NODE_DIR"
NODE_ARCHIVE="$STAGE/node.tar.xz"
curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" -o "$NODE_ARCHIVE"
tar -xJf "$NODE_ARCHIVE" -C "$NODE_DIR"
fi
install -m 755 "$NODE_DIR"/node-v${NODE_VERSION}-linux-x64/bin/node "$APPDIR/usr/bin/node"
install -m 644 "$NODE_DIR"/node-v${NODE_VERSION}-linux-x64/lib/libnode.so* "$APPDIR/usr/lib/" 2>/dev/null || true
mkdir -p "$APPDIR/usr/lib"
cp "$NODE_DIR"/node-v${NODE_VERSION}-linux-x64/lib/libnode.so* "$APPDIR/usr/lib/" 2>/dev/null || true
# ── AppImage metadata ───────────────────────────────────────────────────────
cat > "$APPDIR/AppRun" <<'EOF'
#!/bin/sh
SELF="$(dirname "$(readlink -f "$0")")"
export PATH="$SELF/usr/bin:$PATH"
export LD_LIBRARY_PATH="$SELF/usr/lib:${LD_LIBRARY_PATH:-}"
export GELECTRON_NATIVE=1
exec "$SELF/usr/bin/gelectron" "$@"
EOF
chmod +x "$APPDIR/AppRun"
cat > "$APPDIR/gelectron.desktop" <<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..."
curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" -o "$TOOL"
fi
chmod +x "$TOOL"
# ── Build ───────────────────────────────────────────────────────────────────
mkdir -p "$OUT_DIR"
APPIMAGE_NAME="Gelectron-$VERSION-x86_64.AppImage"
APPIMAGE_PATH="$OUT_DIR/$APPIMAGE_NAME"
export VERSION="$VERSION"
export ARCH="x86_64"
# 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
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
#
# Build the macOS installer: a .pkg (installs the gelectron runtime +
# compat layer to /usr/local/bin) wrapped in a .dmg for distribution.
#
# Gelectron-<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"
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/bin) ─────────────
PAYLOAD="$STAGE/gelectron"
mkdir -p "$PAYLOAD"
log "Staging payload..."
install -m 755 "$BINARY" "$PAYLOAD/gelectron"
mkdir -p "$PAYLOAD/compat"
install -m 644 "$COMPAT"/*.js "$PAYLOAD/compat/"
if command -v codesign >/dev/null 2>&1; then
log "Ad-hoc signing binary..."
codesign --force --sign - "$PAYLOAD/gelectron" 2>/dev/null || echo " (warning: codesign failed)"
fi
# ── Component package ───────────────────────────────────────────────────────
PKG="$STAGE/Install gelectron.pkg"
log "Building package (installs to /usr/local/bin)..."
pkgbuild \
--root "$PAYLOAD" \
--identifier "com.gelectron.runtime.$ARCH" \
--version "$VERSION" \
--ownership recommended \
--install-location /usr/local/bin \
"$PKG"
# ── DMG ────────────────────────────────────────────────────────────────────
mkdir -p "$OUT_DIR"
DMG_NAME="Gelectron-$VERSION-$ARCH.dmg"
DMG_PATH="$OUT_DIR/$DMG_NAME"
DMG_STAGE="$STAGE/dmg"
mkdir -p "$DMG_STAGE"
cp "$PKG" "$DMG_STAGE/"
log "Creating $DMG_NAME ..."
hdiutil create \
-volname "Gelectron $VERSION" \
-srcfolder "$DMG_STAGE" \
-format UDZO \
-ov \
"$DMG_PATH" >/dev/null
echo
log "Created installer: $DMG_PATH"
shasum -a 256 "$DMG_PATH"
+58
View File
@@ -0,0 +1,58 @@
# Gelectron Windows installer builder (NSIS)
#
# Stages the payload 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"
)
$ErrorActionPreference = "Stop"
if (-not (Get-Command makensis -ErrorAction SilentlyContinue)) {
throw "makensis not found. Install NSIS first (choco install nsis -y)."
}
$RealityVersion = $Version.TrimStart("v")
$Stage = Join-Path $env:TEMP ("gelectron-nsis-" + [Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $Stage -Force | Out-Null
try {
Copy-Item $Bin (Join-Path $Stage "gelectron.exe") -Force
Copy-Item -Recurse -Force $Compat (Join-Path $Stage "compat")
Copy-Item $NsiPath (Join-Path $Stage "gelectron.nsi") -Force
Write-Host "==> Compiling installer (makensis)..."
Push-Location $Stage
try {
& makensis "-DVERSION=$RealityVersion" "-DARCH=$Arch" gelectron.nsi
if ($LASTEXITCODE -ne 0) { throw "makensis failed with exit code $LASTEXITCODE" }
}
finally {
Pop-Location
}
$Installer = Get-ChildItem $Stage -Filter "Gelectron-*.exe" | Select-Object -First 1
if (-not $Installer) { throw "makensis produced no installer" }
New-Item -ItemType Directory -Path $Out -Force | Out-Null
Copy-Item $Installer.FullName (Join-Path $Out $Installer.Name) -Force
Write-Host ""
Write-Host "==> Created installer: $(Join-Path $Out $Installer.Name)"
}
finally {
Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue
}
+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 "═══════════════════════════════════════════════════"