`bun run start` died on Linux with ERR_DLOPEN_FAILED (missing libayatana-appindicator3.so.1). Fixing the environment exposed the real gap: electrobun's ContextMenu.showContextMenu is a documented no-op on Linux, and since the UI suppresses the webview's own right-click menu, EVERY panel context menu was silently dead there — Send to Other Panel, Open, Rename, Permissions, Delete, Copy Path, Reveal and the column picker all unreachable. Electrobun itself logs "Application menus are not supported on Linux. Implement menu UI in your webview HTML instead." at boot. - Context menus are now HTML on EVERY platform (ui/src/app/context-menu.ts + context-menu.service.ts), not just as a Linux fallback: one implementation cannot drift from a second one. The item builders are ported verbatim from the old bun menu, so ordering, per-platform labels and the Windows-local chmod hide are unchanged, and picks are emitted as the same `ctx-*` / `col:*` strings through RpcService.emitMenuAction — App.handleMenuAction is untouched and still the single dispatcher for both the macOS menubar and right-clicks. Adds what the native menu gave for free: arrow/Enter/Escape keys, viewport edge-flip, close on blur/resize/scroll. The Edit item now shows the real F4 hint instead of the native menu's "e" mnemonic. - New shellItemAction RPC replaces showRowMenu/showColumnsMenu — the only part the webview can't do itself (reveal, open-with-default, copy path). Deletes src/bun/context-menu.ts entirely. - release.ts gains a linux-x64 target. Electrobun emits only the .tar.zst updater blob on Linux, so the artifact is a .tar.gz of the assembled app folder, mirroring the Windows portable zip. - Docs: platform-notes gets a Linux runtime-dependency table (AppIndicator is linked HARD — a missing lib is a boot failure, not a degraded tray), the ldd diagnostic, GNOME tray caveat, known issues and a first-boot smoke test; gotchas gets the ERR_DLOPEN_FAILED trail and the benign GLX/GBM startup noise; architecture/build-and-release/README updated accordingly. Verified: backend `tsc --noEmit` clean, `bun run build:ui` clean (399 kB initial), app boots on Linux with no errors beyond the documented benign warnings. NOT verified on macOS/Windows — the native menu is retired on those platforms too, so the menus need eyeballing on a Mac before the next release. Co-Authored-By: Claude <noreply@anthropic.com>
147 lines
6.2 KiB
TypeScript
147 lines
6.2 KiB
TypeScript
// 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 } from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
const ROOT = path.join(import.meta.dir, "..");
|
|
const TARGETS = ["mac-arm64", "mac-x64", "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";
|
|
}
|
|
|
|
function fail(message: string): never {
|
|
console.error(`\n[release] ERROR: ${message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function run(cmd: string[], opts: { cwd?: string } = {}): void {
|
|
console.log(`[release] $ ${cmd.join(" ")}`);
|
|
const proc = Bun.spawnSync(cmd, {
|
|
cwd: opts.cwd ?? ROOT,
|
|
stdout: "inherit",
|
|
stderr: "inherit",
|
|
});
|
|
if (!proc.success) fail(`command failed: ${cmd.join(" ")}`);
|
|
}
|
|
|
|
// ---- 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(", ")})`);
|
|
|
|
if (requested === "win-x64" && process.platform !== "win32") {
|
|
fail("win-x64 must be built on a Windows machine/runner — electrobun cannot cross-compile.");
|
|
}
|
|
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.`);
|
|
}
|
|
const viaRosetta = requested === "mac-x64" && process.arch === "arm64";
|
|
|
|
// ---- prepare: version, icons, UI bundle ----
|
|
|
|
run(["bun", "scripts/gen-version.ts"]);
|
|
run(["bun", "scripts/gen-icons.ts"]);
|
|
run(["bun", "run", "--cwd", "ui", "build"]);
|
|
|
|
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) ----
|
|
|
|
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;
|
|
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]);
|
|
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"]);
|
|
}
|
|
|
|
// ---- 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");
|
|
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?`);
|
|
|
|
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 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);
|
|
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 {
|
|
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));
|
|
}
|
|
} else {
|
|
console.warn(`[release] WARN: portable skipped — app folder not found at ${appDir}`);
|
|
}
|
|
}
|
|
|
|
if (!out.length) fail("artifacts were found but none matched a known package extension.");
|
|
console.log(`\n[release] ${BRANDING.appName} ${BRANDING.buildLabel} (${BUILD.commit}) — ${requested}`);
|
|
for (const name of out) console.log(`[release] release/${requested}/${name}`);
|