Re-sign electrobun's CLI binary so the app actually launches
`bun run start` printed nothing and exited 0 without ever opening a window. electrobun's prebuilt CLI is a Bun single-file executable whose payload is appended after the linker ad-hoc signed it, so `codesign -v` reports "code or signature have been modified" and the macOS kernel SIGKILLs it at exec. The failure was invisible because bin/electrobun.cjs spawns the real binary and forwards `process.exit(code || 0)` — a signal death has `code === null`. Confirmed upstream, not local corruption: a fresh download from the GitHub release hashes identical to the shipped copy and fails the same check. Both the darwin-arm64 and darwin-x64 CLI tarballs are affected. scripts/fix-electrobun-cli.ts re-seals the binary with `codesign --force --sign -`. It runs from postinstall AND ahead of every electrobun invocation, because the wrapper downloads the CLI lazily on first use rather than at install time — postinstall alone would no-op on a fresh checkout. When the binary is missing it provokes that download first, so a clean tree self-heals. It also fixes .cache/electrobun, which the wrapper restores bin/ from, and release.ts re-signs the Rosetta x64 CLI on download and on cache reuse. Idempotent (~0.3s when already valid) and a no-op off macOS. Verified from a deleted-binary state: `bun run start` downloads, re-signs and launches. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -22,12 +22,20 @@ semver-shaped (`0.1.<n>`) for electrobun's config/updater; `BRANDING.buildLabel`
|
|||||||
- `scripts/node-version-shim.ts` (preloaded via `bunfig.toml`, root AND ui/)
|
- `scripts/node-version-shim.ts` (preloaded via `bunfig.toml`, root AND ui/)
|
||||||
fakes a Node version the Angular CLI accepts under Bun. Both preloads are
|
fakes a Node version the Angular CLI accepts under Bun. Both preloads are
|
||||||
load-bearing.
|
load-bearing.
|
||||||
|
- `scripts/fix-electrobun-cli.ts` re-signs electrobun's prebuilt CLI binary,
|
||||||
|
whose upstream ad-hoc signature is invalid — macOS SIGKILLs it and the app
|
||||||
|
silently never launches, with exit code 0 (see [gotchas.md](gotchas.md)). It
|
||||||
|
runs from `postinstall` **and** at the start of `dev`/`start`/`build`/
|
||||||
|
`release`, because the CLI is downloaded lazily on first use rather than at
|
||||||
|
install time; when the binary is missing it provokes that download first, so
|
||||||
|
a fresh checkout self-heals. Equally load-bearing — removing it means
|
||||||
|
`bun run start` does nothing at all.
|
||||||
|
|
||||||
## Release builds
|
## Release builds
|
||||||
|
|
||||||
`bun scripts/release.ts [--target mac-arm64|mac-x64|win-x64]` (default: host).
|
`bun scripts/release.ts [--target mac-arm64|mac-x64|win-x64]` (default: host).
|
||||||
Steps: gen-version → gen-icons → Angular build → `electrobun build
|
Steps: gen-version → fix-electrobun-cli → gen-icons → Angular build →
|
||||||
--env=stable` → copy/rename artifacts into
|
`electrobun build --env=stable` → copy/rename artifacts into
|
||||||
`release/<target>/miniSCP-Build<n>-<target>.*`.
|
`release/<target>/miniSCP-Build<n>-<target>.*`.
|
||||||
|
|
||||||
Facts the script encodes:
|
Facts the script encodes:
|
||||||
@@ -46,6 +54,10 @@ Facts the script encodes:
|
|||||||
`dist-macos-x64` core into the electrobun package dir on first use, and
|
`dist-macos-x64` core into the electrobun package dir on first use, and
|
||||||
packages a genuine x64 app. Note: Bun's `fetch`→`Bun.write` streaming hung
|
packages a genuine x64 app. Note: Bun's `fetch`→`Bun.write` streaming hung
|
||||||
on the GitHub redirect chain; the script uses `curl -fsSL` deliberately.
|
on the GitHub redirect chain; the script uses `curl -fsSL` deliberately.
|
||||||
|
- **Both CLI binaries need re-signing.** The x64 CLI fetched for the Rosetta
|
||||||
|
path carries the same broken upstream signature as the arm64 one, so
|
||||||
|
`x64Cli()` re-signs it after download *and* on cache reuse; the native path
|
||||||
|
is covered by the `fix-electrobun-cli.ts` step up front.
|
||||||
- **Core binary downloads**: the installed npm package ships mac-arm64
|
- **Core binary downloads**: the installed npm package ships mac-arm64
|
||||||
binaries only; other `dist-<os>-<arch>` cores are fetched from
|
binaries only; other `dist-<os>-<arch>` cores are fetched from
|
||||||
`github.com/blackboardsh/electrobun/releases` at build time — network to
|
`github.com/blackboardsh/electrobun/releases` at build time — network to
|
||||||
|
|||||||
@@ -31,6 +31,21 @@ touching the adjacent code.
|
|||||||
|
|
||||||
## Electrobun
|
## Electrobun
|
||||||
|
|
||||||
|
- **The prebuilt CLI binary ships with a broken ad-hoc signature**, so macOS
|
||||||
|
SIGKILLs it at exec and `bun run start` prints nothing and exits **0** —
|
||||||
|
`bin/electrobun.cjs` spawns the real binary and forwards
|
||||||
|
`process.exit(code || 0)`, and a signal death has `code === null`. The binary
|
||||||
|
is a Bun single-file executable whose payload is appended *after* the linker
|
||||||
|
signed it, so `codesign -v` reports "code or signature have been modified";
|
||||||
|
the arm64 and x64 CLI tarballs are both affected upstream (confirmed by
|
||||||
|
hashing a fresh download against the shipped copy — it is not local
|
||||||
|
corruption, and re-installing does not help). `scripts/fix-electrobun-cli.ts`
|
||||||
|
re-seals it with `codesign --force --sign -`. It runs from `postinstall`
|
||||||
|
**and** ahead of every electrobun invocation, because the wrapper downloads
|
||||||
|
the binary **lazily on first use**, not at install time — postinstall alone
|
||||||
|
would no-op on a fresh checkout. It also fixes `.cache/electrobun`, which the
|
||||||
|
wrapper restores `bin/` from. Never read a silent `exit 0` from the
|
||||||
|
electrobun CLI as "nothing happened": run `codesign -v` on it first.
|
||||||
- **Window `close` is not cancellable** (response type `{}`; only the
|
- **Window `close` is not cancellable** (response type `{}`; only the
|
||||||
app-level `before-quit` supports `event.response = {allow:false}`). All
|
app-level `before-quit` supports `event.response = {allow:false}`). All
|
||||||
unsaved-state guards must either intercept app-controlled close paths
|
unsaved-state guards must either intercept app-controlled close paths
|
||||||
|
|||||||
+4
-4
@@ -7,12 +7,12 @@
|
|||||||
"ui"
|
"ui"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun scripts/gen-version.ts && bun scripts/dev.ts",
|
"dev": "bun scripts/gen-version.ts && bun scripts/fix-electrobun-cli.ts && bun scripts/dev.ts",
|
||||||
"dev:ui": "bun run --cwd ui start",
|
"dev:ui": "bun run --cwd ui start",
|
||||||
"build:ui": "bun run --cwd ui build",
|
"build:ui": "bun run --cwd ui build",
|
||||||
"build": "bun scripts/gen-version.ts && bun run build:ui && bunx electrobun build",
|
"build": "bun scripts/gen-version.ts && bun scripts/fix-electrobun-cli.ts && bun run build:ui && bunx electrobun build",
|
||||||
"start": "bun scripts/gen-version.ts && bun run build:ui && bunx electrobun dev",
|
"start": "bun scripts/gen-version.ts && bun scripts/fix-electrobun-cli.ts && bun run build:ui && bunx electrobun dev",
|
||||||
"postinstall": "bun scripts/gen-version.ts",
|
"postinstall": "bun scripts/gen-version.ts && bun scripts/fix-electrobun-cli.ts",
|
||||||
"release": "bun scripts/release.ts"
|
"release": "bun scripts/release.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Re-signs electrobun's prebuilt CLI binary, which ships with a BROKEN ad-hoc
|
||||||
|
// signature: it is a Bun single-file executable whose payload is appended after
|
||||||
|
// the linker signed it, so `codesign -v` reports "code or signature have been
|
||||||
|
// modified" and the macOS kernel SIGKILLs it at exec. The failure is invisible:
|
||||||
|
// electrobun's bin/electrobun.cjs wrapper spawns the real binary and forwards
|
||||||
|
// `process.exit(code || 0)` — a signal death has code === null, so `bun run
|
||||||
|
// start` printed nothing and exited 0 while never launching the app.
|
||||||
|
// `codesign --force --sign -` re-seals it ad-hoc and it runs normally.
|
||||||
|
//
|
||||||
|
// The binary is downloaded LAZILY by that wrapper on first use, not by install,
|
||||||
|
// so this runs both from postinstall and ahead of every electrobun invocation.
|
||||||
|
// Idempotent, and a no-op off macOS.
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const ROOT = path.join(import.meta.dir, "..");
|
||||||
|
|
||||||
|
/** true if `bin` has (or now has) a signature the kernel will accept. */
|
||||||
|
export function ensureSigned(bin: string): boolean {
|
||||||
|
if (process.platform !== "darwin" || !existsSync(bin)) return true;
|
||||||
|
const valid = () => Bun.spawnSync(["codesign", "-v", bin], { stderr: "ignore" }).success;
|
||||||
|
if (valid()) return true;
|
||||||
|
const signed = Bun.spawnSync(["codesign", "--force", "--sign", "-", bin], { stderr: "pipe" });
|
||||||
|
if (!signed.success || !valid()) {
|
||||||
|
console.warn(`[electrobun-cli] could not re-sign ${path.relative(ROOT, bin)} — the app will not launch`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.log(`[electrobun-cli] re-signed ${path.relative(ROOT, bin)} (upstream ad-hoc signature is invalid)`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
const pkgDir = path.join(ROOT, "node_modules", "electrobun");
|
||||||
|
const bin = path.join(pkgDir, "bin", "electrobun");
|
||||||
|
|
||||||
|
// The wrapper downloads the CLI on first run and only then execs it, so one
|
||||||
|
// throwaway invocation leaves a downloaded (still unsigned) binary behind
|
||||||
|
// even though that exec is killed.
|
||||||
|
if (process.platform === "darwin" && existsSync(pkgDir) && !existsSync(bin)) {
|
||||||
|
Bun.spawnSync(["bunx", "electrobun", "--version"], { cwd: ROOT, stdout: "ignore", stderr: "ignore" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// .cache is what the wrapper restores bin/ from, so both copies need fixing.
|
||||||
|
const ok = [bin, path.join(pkgDir, ".cache", "electrobun")].map(ensureSigned).every(Boolean);
|
||||||
|
if (!ok) process.exit(1);
|
||||||
|
}
|
||||||
+9
-1
@@ -9,6 +9,7 @@
|
|||||||
import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { ensureSigned } from "./fix-electrobun-cli";
|
||||||
|
|
||||||
const ROOT = path.join(import.meta.dir, "..");
|
const ROOT = path.join(import.meta.dir, "..");
|
||||||
const TARGETS = ["mac-arm64", "mac-x64", "win-x64"] as const;
|
const TARGETS = ["mac-arm64", "mac-x64", "win-x64"] as const;
|
||||||
@@ -51,6 +52,7 @@ const viaRosetta = requested === "mac-x64" && process.arch === "arm64";
|
|||||||
// ---- prepare: version, icons, UI bundle ----
|
// ---- prepare: version, icons, UI bundle ----
|
||||||
|
|
||||||
run(["bun", "scripts/gen-version.ts"]);
|
run(["bun", "scripts/gen-version.ts"]);
|
||||||
|
run(["bun", "scripts/fix-electrobun-cli.ts"]);
|
||||||
run(["bun", "scripts/gen-icons.ts"]);
|
run(["bun", "scripts/gen-icons.ts"]);
|
||||||
run(["bun", "run", "--cwd", "ui", "build"]);
|
run(["bun", "run", "--cwd", "ui", "build"]);
|
||||||
|
|
||||||
@@ -64,7 +66,12 @@ async function x64Cli(): Promise<string> {
|
|||||||
const pkg = await Bun.file(path.join(ROOT, "node_modules", "electrobun", "package.json")).json();
|
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 dir = path.join(os.homedir(), ".cache", "miniscp", `electrobun-cli-x64-${pkg.version}`);
|
||||||
const bin = path.join(dir, "electrobun");
|
const bin = path.join(dir, "electrobun");
|
||||||
if (existsSync(bin)) return bin;
|
// 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)…`);
|
console.log(`[release] downloading electrobun x64 CLI ${pkg.version} (one-time)…`);
|
||||||
mkdirSync(dir, { recursive: true });
|
mkdirSync(dir, { recursive: true });
|
||||||
const url = `https://github.com/blackboardsh/electrobun/releases/download/v${pkg.version}/electrobun-cli-darwin-x64.tar.gz`;
|
const url = `https://github.com/blackboardsh/electrobun/releases/download/v${pkg.version}/electrobun-cli-darwin-x64.tar.gz`;
|
||||||
@@ -73,6 +80,7 @@ async function x64Cli(): Promise<string> {
|
|||||||
run(["tar", "-xzf", tarball, "-C", dir]);
|
run(["tar", "-xzf", tarball, "-C", dir]);
|
||||||
if (!existsSync(bin)) fail(`x64 CLI tarball did not contain "electrobun" (${dir})`);
|
if (!existsSync(bin)) fail(`x64 CLI tarball did not contain "electrobun" (${dir})`);
|
||||||
run(["chmod", "+x", bin]);
|
run(["chmod", "+x", bin]);
|
||||||
|
if (!ensureSigned(bin)) fail(`x64 CLI at ${bin} has an unusable code signature`);
|
||||||
return bin;
|
return bin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user