Migrate to Electrobun 2.0.2 (Hutch)
Electrobun 2.x replaces the vendored CLI with Hutch: the npm package is now a
bootstrap whose exports are a throwing stub, and the SDK is projected into
.hutch/devkit. The app stays on the Bun main process — `mainProcess: "bun"` is
load-bearing, since the 2.x default (Cottontail) ignores build.bun and with it
the ssh2 externals — and the bundle now ships the pinned Bun 1.4.0.
- electrobun 2.0.2 as an exact devDependency. `electrobun prepare` takes the
old CLI re-sign step's slot in postinstall and every dev/build/start/release
script: tsc (root tsconfig extends the devkit's) and the Angular build
(electrobun/view mapped into the devkit) resolve the SDK there.
- Imports move to electrobun/main. The CLI re-sign script and the v1 "three"
type shim are gone (Hutch's binaries are validly signed).
- UI: Electroview's bunSocket is hostSocket; noImplicitReturns is off because
the devkit's raw-TS browser SDK compiles inside the UI program.
- Updater.applyUpdate() now consults before-quit, so the transfers/unsaved
guard's "Quit Anyway" re-runs the apply instead of quitting without it.
- 2.x applies trafficLightOffset from macOS's default button position (1.18.1
clamped it inside the titlebar); {12, 4} centres the lights in the header.
- Releases: mac-x64 dropped (2.x publishes no macOS x64 core, so no Rosetta
path). artifacts/ is emptied before each build and published verbatim.
Installers lost the stable- prefix (the update feed kept it), so release.ts,
deploy.sh and the site links follow. Linux ships Electrobun's Setup
installer instead of the portable tarball and its miniSCP.sh wrapper.
- Docs cover all of the above, plus re-checks against 2.0.2: still no native
modal windows; window close is now cancellable through will-close.
Verified: tsc, build:ui, a stable mac-arm64 release (dmg + the v1-compatible
stable-macos-arm64 feed) and dev launches (RPC, Settings window, traffic
lights). Windows/Linux builds and the v1 -> 2.x Windows update bridge still
need their first run on native machines.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+38
-97
@@ -1,24 +1,21 @@
|
||||
// Build one release artifact for one target, on the matching host:
|
||||
// bun scripts/release.ts [--target mac-arm64|mac-x64|win-x64|linux-x64]
|
||||
// Default target = the host. electrobun cannot cross-compile OSes (its
|
||||
// packaging needs host tools: codesign/hdiutil on mac, rcedit/PowerShell on
|
||||
// Windows), so win-x64 must run on a Windows machine. mac-x64 IS buildable on
|
||||
// an arm64 Mac: we fetch electrobun's x64 CLI binary and run it under
|
||||
// Rosetta 2 — it sees an x64 host, pulls the x64 core, and packages mac-x64.
|
||||
// Artifacts land in release/<target>/miniSCP-Build<N>-<target>.<ext>.
|
||||
import { cpSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
// bun scripts/release.ts [--target mac-arm64|win-x64|linux-x64]
|
||||
// Default target = the host. electrobun cannot cross-compile (Hutch builds for
|
||||
// its host OS and arch only; packaging needs host tools: codesign/hdiutil on
|
||||
// mac, rcedit/PowerShell on Windows), so win-x64 must run on a Windows machine.
|
||||
// There is no mac-x64: Electrobun 2.x publishes no macOS x64 core at all.
|
||||
// Artifacts land in release/<target>/miniSCP-Build<N>-<target><ext>.
|
||||
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { ensureSigned } from "./fix-electrobun-cli";
|
||||
|
||||
const ROOT = path.join(import.meta.dir, "..");
|
||||
const TARGETS = ["mac-arm64", "mac-x64", "win-x64", "linux-x64"] as const;
|
||||
const TARGETS = ["mac-arm64", "win-x64", "linux-x64"] as const;
|
||||
type Target = (typeof TARGETS)[number];
|
||||
|
||||
function hostTarget(): Target {
|
||||
if (process.platform === "win32") return "win-x64";
|
||||
if (process.platform === "linux") return "linux-x64";
|
||||
return process.arch === "arm64" ? "mac-arm64" : "mac-x64";
|
||||
return "mac-arm64";
|
||||
}
|
||||
|
||||
function fail(message: string): never {
|
||||
@@ -39,8 +36,10 @@ function run(cmd: string[], opts: { cwd?: string } = {}): void {
|
||||
// ---- target selection + host guards ----
|
||||
|
||||
const argIndex = process.argv.indexOf("--target");
|
||||
const requested = (argIndex >= 0 ? process.argv[argIndex + 1] : hostTarget()) as Target;
|
||||
if (!TARGETS.includes(requested)) fail(`unknown target "${requested}" (${TARGETS.join(", ")})`);
|
||||
const arg = argIndex >= 0 ? process.argv[argIndex + 1] : hostTarget();
|
||||
if (arg === "mac-x64") fail("mac-x64 was dropped — Electrobun 2.x publishes no macOS x64 core.");
|
||||
if (!TARGETS.includes(arg as Target)) fail(`unknown target "${arg}" (${TARGETS.join(", ")})`);
|
||||
const requested = arg as Target;
|
||||
|
||||
if (requested === "win-x64" && process.platform !== "win32") {
|
||||
fail("win-x64 must be built on a Windows machine/runner — electrobun cannot cross-compile.");
|
||||
@@ -48,15 +47,14 @@ if (requested === "win-x64" && process.platform !== "win32") {
|
||||
if (requested === "linux-x64" && process.platform !== "linux") {
|
||||
fail("linux-x64 must be built on a Linux machine/runner — electrobun cannot cross-compile.");
|
||||
}
|
||||
if (requested.startsWith("mac") && process.platform !== "darwin") {
|
||||
fail(`${requested} must be built on macOS.`);
|
||||
if (requested === "mac-arm64" && (process.platform !== "darwin" || process.arch !== "arm64")) {
|
||||
fail("mac-arm64 must be built on an Apple Silicon Mac (Electrobun 2.x has no macOS x64 build).");
|
||||
}
|
||||
const viaRosetta = requested === "mac-x64" && process.arch === "arm64";
|
||||
|
||||
// ---- prepare: version, icons, UI bundle ----
|
||||
// ---- prepare: version, devkit, icons, UI bundle ----
|
||||
|
||||
run(["bun", "scripts/gen-version.ts"]);
|
||||
run(["bun", "scripts/fix-electrobun-cli.ts"]);
|
||||
run(["bunx", "electrobun", "prepare"]);
|
||||
run(["bun", "scripts/gen-icons.ts"]);
|
||||
run(["bun", "run", "--cwd", "ui", "build"]);
|
||||
|
||||
@@ -64,104 +62,47 @@ const { BUILD } = await import("../src/shared/version.gen");
|
||||
const { BRANDING } = await import("../src/shared/branding");
|
||||
const buildName = `Build${BUILD.number}`;
|
||||
|
||||
// ---- electrobun build (native, or x64 CLI under Rosetta) ----
|
||||
// ---- electrobun build ----
|
||||
|
||||
async function x64Cli(): Promise<string> {
|
||||
const pkg = await Bun.file(path.join(ROOT, "node_modules", "electrobun", "package.json")).json();
|
||||
const dir = path.join(os.homedir(), ".cache", "miniscp", `electrobun-cli-x64-${pkg.version}`);
|
||||
const bin = path.join(dir, "electrobun");
|
||||
// Same broken upstream ad-hoc signature as the arm64 CLI (see
|
||||
// scripts/fix-electrobun-cli.ts) — unsigned it is SIGKILLed on exec.
|
||||
if (existsSync(bin)) {
|
||||
if (!ensureSigned(bin)) fail(`x64 CLI at ${bin} has an unusable code signature`);
|
||||
return bin;
|
||||
}
|
||||
console.log(`[release] downloading electrobun x64 CLI ${pkg.version} (one-time)…`);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const url = `https://github.com/blackboardsh/electrobun/releases/download/v${pkg.version}/electrobun-cli-darwin-x64.tar.gz`;
|
||||
const tarball = path.join(dir, "cli.tar.gz");
|
||||
run(["curl", "-fsSL", "-o", tarball, url]);
|
||||
run(["tar", "-xzf", tarball, "-C", dir]);
|
||||
if (!existsSync(bin)) fail(`x64 CLI tarball did not contain "electrobun" (${dir})`);
|
||||
run(["chmod", "+x", bin]);
|
||||
if (!ensureSigned(bin)) fail(`x64 CLI at ${bin} has an unusable code signature`);
|
||||
return bin;
|
||||
}
|
||||
|
||||
if (viaRosetta) {
|
||||
// x64 Mach-O runs under Rosetta transparently; it sees process.arch === x64
|
||||
const rosetta = Bun.spawnSync(["arch", "-x86_64", "true"]);
|
||||
if (!rosetta.success) {
|
||||
fail("Rosetta 2 is not installed — run: softwareupdate --install-rosetta --agree-to-license");
|
||||
}
|
||||
run([await x64Cli(), "build", "--env=stable"]);
|
||||
} else {
|
||||
run(["bunx", "electrobun", "build", "--env=stable"]);
|
||||
}
|
||||
// Start from an empty artifacts/: site/deploy.sh publishes it verbatim (the
|
||||
// updater contract), so an earlier build's files must not ride along.
|
||||
const artifactsDir = path.join(ROOT, "artifacts");
|
||||
rmSync(artifactsDir, { recursive: true, force: true });
|
||||
run(["bunx", "electrobun", "build", "--env=stable"]);
|
||||
|
||||
// ---- collect artifacts ----
|
||||
|
||||
const [osName, archName] =
|
||||
requested === "win-x64"
|
||||
? ["win", "x64"]
|
||||
: requested === "linux-x64"
|
||||
? ["linux", "x64"]
|
||||
: ["macos", requested.slice(4)];
|
||||
const prefix = `stable-${osName}-${archName}-`;
|
||||
const artifactsDir = path.join(ROOT, "artifacts");
|
||||
requested === "win-x64" ? ["win", "x64"] : requested === "linux-x64" ? ["linux", "x64"] : ["macos", "arm64"];
|
||||
// 2.x installers carry no channel prefix (macos-arm64-miniSCP.dmg); only the
|
||||
// update feed (stable-<os>-<arch>-*) keeps it, for v1-compatible clients.
|
||||
const prefix = `${osName}-${archName}-`;
|
||||
const outDir = path.join(ROOT, "release", requested);
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const produced = readdirSync(artifactsDir).filter(
|
||||
(f) => f.startsWith(prefix) && f.includes(BRANDING.appName),
|
||||
);
|
||||
if (!produced.length) fail(`no ${prefix}* artifacts found in artifacts/ — did the build run?`);
|
||||
if (!produced.length) fail(`no ${prefix}* installer found in artifacts/ — did the build run?`);
|
||||
|
||||
const out: string[] = [];
|
||||
for (const file of produced) {
|
||||
const ext = file.endsWith(".dmg") ? ".dmg" : file.endsWith("-Setup.zip") ? "-Setup.zip" : null;
|
||||
if (!ext) continue; // skip the raw .app.tar.zst + update.json
|
||||
const ext = [".dmg", "-Setup.zip", "-Setup.tar.gz"].find((e) => file.endsWith(e));
|
||||
if (!ext) continue;
|
||||
const name = `${BRANDING.appName}-${buildName}-${requested}${ext}`;
|
||||
cpSync(path.join(artifactsDir, file), path.join(outDir, name));
|
||||
out.push(name);
|
||||
}
|
||||
|
||||
// Windows / Linux: package the assembled app folder directly. On Windows that
|
||||
// complements the Setup.exe installer (portable = unzip anywhere and run); on
|
||||
// Linux it is the ONLY artifact, since electrobun emits just the .tar.zst
|
||||
// updater blob there — no installer, AppImage or .deb. Best-effort: the folder
|
||||
// layout is confirmed by the first real build on each OS.
|
||||
if (requested === "win-x64" || requested === "linux-x64") {
|
||||
const appDir = path.join(ROOT, "build", `stable-${osName}-${archName}`, BRANDING.appName);
|
||||
// Windows: also zip the assembled app folder — the portable build (unzip
|
||||
// anywhere and run) next to the Setup installer. Best-effort: the folder
|
||||
// layout is confirmed by the first real 2.x Windows build.
|
||||
if (requested === "win-x64") {
|
||||
const appDir = path.join(ROOT, "build", "stable-win-x64", BRANDING.appName);
|
||||
if (existsSync(appDir)) {
|
||||
if (requested === "win-x64") {
|
||||
const portable = path.join(outDir, `${BRANDING.appName}-${buildName}-win-x64-portable.zip`);
|
||||
run(["powershell", "-Command", `Compress-Archive -Force -Path '${appDir}\\*' -DestinationPath '${portable}'`]);
|
||||
out.push(path.basename(portable));
|
||||
} else {
|
||||
// Ship a wrapper beside the launcher: the packaged app never goes
|
||||
// through scripts/start.ts, and the NVIDIA/DMABUF workaround has to be
|
||||
// in the environment BEFORE the process starts (see scripts/linux-gpu.ts).
|
||||
const wrapper = path.join(appDir, `${BRANDING.appName}.sh`);
|
||||
writeFileSync(
|
||||
wrapper,
|
||||
[
|
||||
"#!/bin/sh",
|
||||
"# WebKitGTK's DMABUF renderer cannot allocate GBM buffers on the NVIDIA",
|
||||
"# proprietary driver — the window renders black. Disable it there only.",
|
||||
'if [ -z "$WEBKIT_DISABLE_DMABUF_RENDERER" ] && [ -e /proc/driver/nvidia ]; then',
|
||||
" export WEBKIT_DISABLE_DMABUF_RENDERER=1",
|
||||
"fi",
|
||||
'exec "$(dirname "$(readlink -f "$0")")/bin/launcher" "$@"',
|
||||
"",
|
||||
].join("\n"),
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
const tarball = path.join(outDir, `${BRANDING.appName}-${buildName}-linux-x64.tar.gz`);
|
||||
// -C the parent so the archive unpacks into one named folder
|
||||
run(["tar", "-czf", tarball, "-C", path.dirname(appDir), path.basename(appDir)]);
|
||||
out.push(path.basename(tarball));
|
||||
}
|
||||
const portable = path.join(outDir, `${BRANDING.appName}-${buildName}-win-x64-portable.zip`);
|
||||
run(["powershell", "-Command", `Compress-Archive -Force -Path '${appDir}\\*' -DestinationPath '${portable}'`]);
|
||||
out.push(path.basename(portable));
|
||||
} else {
|
||||
console.warn(`[release] WARN: portable skipped — app folder not found at ${appDir}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user