From cda0286db7cf8982aa7101d56b3427de17418a6f Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 1 Aug 2026 21:38:05 +0100 Subject: [PATCH] Add postWrap script for macOS bundle expansion and implement expand-macos-bundle script - Updated electrobun.config.ts to include a new postWrap script for expanding macOS bundles. - Introduced expand-macos-bundle.mjs script to handle the extraction and setup of macOS application bundles, including metadata validation and resource management. - Enhanced error handling and logging for better visibility during the expansion process. --- electrobun.config.ts | 1 + scripts/expand-macos-bundle.mjs | 138 ++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 scripts/expand-macos-bundle.mjs diff --git a/electrobun.config.ts b/electrobun.config.ts index 3c1070e..0838b20 100644 --- a/electrobun.config.ts +++ b/electrobun.config.ts @@ -54,6 +54,7 @@ export default { }, scripts: { preBuild: "scripts/pre-build.mjs", + postWrap: "scripts/expand-macos-bundle.mjs", postPackage: "scripts/finalize-desktop-artifacts.mjs", }, release: { diff --git a/scripts/expand-macos-bundle.mjs b/scripts/expand-macos-bundle.mjs new file mode 100644 index 0000000..3246b78 --- /dev/null +++ b/scripts/expand-macos-bundle.mjs @@ -0,0 +1,138 @@ +import { + cpSync, + existsSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, +} from "node:fs"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { fileURLToPath } from "node:url"; + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const buildEnv = process.env.ELECTROBUN_BUILD_ENV || "dev"; +const targetOs = process.env.ELECTROBUN_OS || "macos"; +const wrapperPath = process.env.ELECTROBUN_WRAPPER_BUNDLE_PATH; + +if (buildEnv === "dev" || targetOs !== "macos") { + console.log("expand-macos-bundle: skipping (dev or non-macOS build)"); + process.exit(0); +} + +if (!wrapperPath || !existsSync(wrapperPath)) { + console.error("expand-macos-bundle: ELECTROBUN_WRAPPER_BUNDLE_PATH not set or missing"); + process.exit(1); +} + +const resolvedWrapperPath = path.resolve(wrapperPath); +const contentsPath = path.join(resolvedWrapperPath, "Contents"); +const resourcesPath = path.join(contentsPath, "Resources"); +const metadataPath = path.join(resourcesPath, "metadata.json"); + +if (!existsSync(metadataPath)) { + console.log("expand-macos-bundle: no extractor metadata, assuming already expanded"); + process.exit(0); +} + +const metadata = JSON.parse(readFileSync(metadataPath, "utf8")); +const hash = metadata.hash; +const appName = metadata.name || "Farm Control Server"; +const tarZstPath = path.resolve(resourcesPath, `${hash}.tar.zst`); + +if (!existsSync(tarZstPath)) { + console.log(`expand-macos-bundle: ${hash}.tar.zst not found, skipping`); + process.exit(0); +} + +function findZstd() { + const fromPath = spawnSync("zstd", ["--version"], { encoding: "utf8" }); + if (fromPath.status === 0) { + return "zstd"; + } + + const hostArch = process.arch === "arm64" ? "arm64" : "x64"; + const electrobunZstd = path.join( + rootDir, + "node_modules/electrobun/dist-macos-" + hostArch, + "zig-zstd", + ); + if (existsSync(electrobunZstd)) { + return electrobunZstd; + } + + throw new Error( + "expand-macos-bundle: zstd not found (install zstd or use electrobun dist binaries)", + ); +} + +const workDir = path.join(tmpdir(), `farmcontrol-server-expand-${hash}`); +const tarPath = path.join(workDir, `${hash}.tar`); +rmSync(workDir, { recursive: true, force: true }); +mkdirSync(workDir, { recursive: true }); + +const zstd = findZstd(); +const decompress = spawnSync(zstd, ["-d", "-f", "-o", tarPath, tarZstPath], { + stdio: "inherit", +}); + +if (decompress.status !== 0) { + console.error("expand-macos-bundle: zstd decompression failed"); + process.exit(decompress.status ?? 1); +} + +const extractTar = spawnSync("tar", ["-xf", tarPath, "-C", workDir], { + stdio: "inherit", +}); + +if (extractTar.status !== 0) { + console.error("expand-macos-bundle: tar extraction failed"); + process.exit(extractTar.status ?? 1); +} + +const innerAppPath = path.join(workDir, `${appName}.app`); +if (!existsSync(innerAppPath)) { + const appBundle = readdirSync(workDir).find((entry) => entry.endsWith(".app")); + if (!appBundle) { + console.error("expand-macos-bundle: extracted app bundle not found"); + process.exit(1); + } +} + +const resolvedInnerApp = existsSync(innerAppPath) + ? innerAppPath + : path.join(workDir, readdirSync(workDir).find((e) => e.endsWith(".app"))); +const innerContentsPath = path.join(resolvedInnerApp, "Contents"); + +function replaceDirectory(sourceDir, destinationDir) { + rmSync(destinationDir, { recursive: true, force: true }); + cpSync(sourceDir, destinationDir, { recursive: true, dereference: true }); +} + +replaceDirectory( + path.join(innerContentsPath, "MacOS"), + path.join(contentsPath, "MacOS"), +); + +rmSync(resourcesPath, { recursive: true, force: true }); +cpSync(path.join(innerContentsPath, "Resources"), resourcesPath, { + recursive: true, + dereference: true, +}); + +const innerFrameworks = path.join(innerContentsPath, "Frameworks"); +const wrapperFrameworks = path.join(contentsPath, "Frameworks"); +if (existsSync(innerFrameworks)) { + replaceDirectory(innerFrameworks, wrapperFrameworks); +} + +cpSync(path.join(innerContentsPath, "Info.plist"), path.join(contentsPath, "Info.plist")); + +rmSync(workDir, { recursive: true, force: true }); + +if (process.platform === "darwin") { + spawnSync("xattr", ["-cr", resolvedWrapperPath], { stdio: "inherit" }); +} + +console.log(`expand-macos-bundle: pre-expanded ${resolvedWrapperPath}`);