Merge origin/main into feat/7css-skin

Brings in 6c05e39 (re-sign electrobun's CLI binary so the app launches).

Two conflicts, both resolved to keep both sides:
- package.json: start keeps scripts/start.ts (injects the Linux NVIDIA env)
  behind main's fix-electrobun-cli re-sign; build and postinstall take
  main's re-sign step (the resize watchdog is already gone).
- docs/build-and-release.md: the release usage keeps the linux-x64 target
  and gains main's fix-electrobun-cli step.

scripts/release.ts and docs/gotchas.md auto-merged with both sides intact:
the Linux target + tarball wrapper, and the arm64/x64 CLI re-sign.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-23 16:25:34 +02:00
co-authored by Claude
5 changed files with 88 additions and 7 deletions
+46
View File
@@ -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
View File
@@ -9,6 +9,7 @@
import { cpSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
import os from "node:os";
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;
@@ -55,6 +56,7 @@ const viaRosetta = requested === "mac-x64" && process.arch === "arm64";
// ---- prepare: version, icons, UI bundle ----
run(["bun", "scripts/gen-version.ts"]);
run(["bun", "scripts/fix-electrobun-cli.ts"]);
run(["bun", "scripts/gen-icons.ts"]);
run(["bun", "run", "--cwd", "ui", "build"]);
@@ -68,7 +70,12 @@ 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");
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)…`);
mkdirSync(dir, { recursive: true });
const url = `https://github.com/blackboardsh/electrobun/releases/download/v${pkg.version}/electrobun-cli-darwin-x64.tar.gz`;
@@ -77,6 +84,7 @@ async function x64Cli(): Promise<string> {
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;
}