From 90da336696824a3e03954628ccb774d969f5e4f5 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 1 Aug 2026 20:01:55 +0100 Subject: [PATCH] Update macOS build process and enhance artifact finalization - Replaced the macOS build command in package.json to use a new script for better architecture handling. - Added a new script for building macOS applications that supports both arm64 and x64 architectures. - Enhanced the finalize-desktop-artifacts script to improve artifact discovery and cleanup, ensuring proper handling of macOS DMG and app bundles. - Updated functions to streamline the process of finding and publishing artifacts, improving overall reliability. --- package.json | 2 +- scripts/build-macos.mjs | 53 ++++++++++++++ scripts/finalize-desktop-artifacts.mjs | 97 +++++++++++++++++--------- 3 files changed, 117 insertions(+), 35 deletions(-) create mode 100644 scripts/build-macos.mjs diff --git a/package.json b/package.json index e079b3b..7e1219a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build:server": "bun run scripts/build-server.mjs", "build:renderer": "vite build src/app", "build:app": "bun run build && electrobun build --env=stable", - "build:app:mac": "bun run build && electrobun build --env=stable", + "build:app:mac": "bun scripts/build-macos.mjs", "build:linux": "bun run cleanBuild && bun run build:server && bun run build:linux-binary && bun run build:linux-packages", "build:linux-binary": "bun scripts/build-linux-binary.mjs", "build:linux-packages": "bash scripts/build-linux-packages.sh", diff --git a/scripts/build-macos.mjs b/scripts/build-macos.mjs new file mode 100644 index 0000000..037e49a --- /dev/null +++ b/scripts/build-macos.mjs @@ -0,0 +1,53 @@ +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const hostArch = process.arch === "arm64" ? "arm64" : "x64"; +const targetArchs = ["arm64", "x64"]; + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: rootDir, + stdio: "inherit", + env: process.env, + ...options, + }); + + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function getElectrobunCommand(targetArch) { + const electrobunArgs = ["electrobun", "build", "--env=stable"]; + + if (targetArch === hostArch) { + return { command: "bun", args: electrobunArgs }; + } + + if (hostArch === "arm64" && targetArch === "x64") { + return { command: "arch", args: ["-x86_64", "bun", ...electrobunArgs] }; + } + + if (hostArch === "x64" && targetArch === "arm64") { + return { command: "arch", args: ["-arm64", "bun", ...electrobunArgs] }; + } + + return null; +} + +run("bun", ["run", "build"]); + +for (const targetArch of targetArchs) { + const invocation = getElectrobunCommand(targetArch); + if (!invocation) { + console.warn( + `Skipping macOS ${targetArch} build: unsupported cross-compile from ${hostArch}.`, + ); + continue; + } + + console.log(`\n=== Building macOS ${targetArch} ===\n`); + run(invocation.command, invocation.args); +} diff --git a/scripts/finalize-desktop-artifacts.mjs b/scripts/finalize-desktop-artifacts.mjs index 61e6897..c83c738 100644 --- a/scripts/finalize-desktop-artifacts.mjs +++ b/scripts/finalize-desktop-artifacts.mjs @@ -23,6 +23,7 @@ const packageJson = JSON.parse( const version = getReleaseVersion(packageJson); const artifactDir = path.join(rootDir, "app_dist"); const identifier = "com.tombutcher.farmcontrolserver"; +const artifactPrefix = `farmcontrol-server-${version}-`; function walkFiles(dir) { const files = []; @@ -49,28 +50,42 @@ function findByExtension(root, extension) { ); } -function findMacAppBundle() { - const buildDir = path.join(rootDir, "build"); - if (!existsSync(buildDir)) { +function findMacAppBundle(arch) { + const platformDir = path.join(rootDir, "build", `stable-macos-${arch}`); + if (!existsSync(platformDir)) { return null; } - for (const entry of readdirSync(buildDir)) { - if (!entry.startsWith("stable-macos-")) { - continue; - } - - const platformDir = path.join(buildDir, entry); - for (const child of readdirSync(platformDir)) { - if (child.endsWith(".app")) { - return path.join(platformDir, child); - } + for (const child of readdirSync(platformDir)) { + if (child.endsWith(".app")) { + return path.join(platformDir, child); } } return null; } +function findMacDmgSource(arch) { + const prefixedArtifact = walkFiles(artifactDir).find( + (filePath) => + filePath.includes(`stable-macos-${arch}`) && + filePath.toLowerCase().endsWith(".dmg"), + ); + if (prefixedArtifact) { + return prefixedArtifact; + } + + const buildDmg = findByExtension( + path.join(rootDir, "build", `stable-macos-${arch}`), + ".dmg", + ); + if (buildDmg) { + return buildDmg; + } + + return findByExtension(artifactDir, ".dmg"); +} + function findWindowsSetupExe() { const buildDir = path.join(rootDir, "build"); if (!existsSync(buildDir)) { @@ -96,7 +111,9 @@ function findWindowsSetupExe() { function publishArtifact(sourcePath, arch, ext) { if (!sourcePath || !existsSync(sourcePath)) { - throw new Error(`Missing source artifact for ${arch}.${ext}: ${sourcePath ?? "not found"}`); + throw new Error( + `Missing source artifact for ${arch}.${ext}: ${sourcePath ?? "not found"}`, + ); } mkdirSync(artifactDir, { recursive: true }); @@ -109,17 +126,24 @@ function publishArtifact(sourcePath, arch, ext) { return destination; } -function cleanArtifactDir(keepNames) { +function cleanStagingArtifacts(keepNames) { if (!existsSync(artifactDir)) { return; } for (const entry of readdirSync(artifactDir)) { - if (keepNames.includes(entry)) { + if (keepNames.includes(entry) || entry.startsWith(artifactPrefix)) { continue; } - rmSync(path.join(artifactDir, entry), { recursive: true, force: true }); + if ( + entry.includes("stable-macos-") || + entry.includes("stable-win-") || + entry.endsWith("-update.json") || + entry.endsWith(".tar.gz") + ) { + rmSync(path.join(artifactDir, entry), { recursive: true, force: true }); + } } } @@ -196,23 +220,26 @@ function buildWindowsMsi(setupExePath, arch) { return msiPath; } -const published = []; - if (process.platform === "darwin") { - const arch = getReleaseArch(); - const dmgSource = - findByExtension(artifactDir, ".dmg") ?? findByExtension(path.join(rootDir, "build"), ".dmg"); - const appBundle = findMacAppBundle(); + const buildArch = getReleaseArch( + process.env.ELECTROBUN_ARCH || process.arch, + ); + const dmgSource = findMacDmgSource(buildArch); + const appBundle = findMacAppBundle(buildArch); - if (!dmgSource) { - throw new Error("Could not find a macOS DMG artifact to publish"); - } - if (!appBundle) { - throw new Error("Could not find a macOS .app bundle to build a PKG"); + if (!dmgSource || !appBundle) { + console.log( + `finalize-desktop-artifacts: no macOS ${buildArch} release artifacts found, skipping`, + ); + process.exit(0); } - published.push(publishArtifact(dmgSource, arch, "dmg")); - published.push(buildMacPkg(appBundle, arch)); + const published = [ + publishArtifact(dmgSource, buildArch, "dmg"), + buildMacPkg(appBundle, buildArch), + ]; + + cleanStagingArtifacts(published.map((filePath) => path.basename(filePath))); } else if (process.platform === "win32") { const arch = "x64"; const setupExe = findWindowsSetupExe(); @@ -221,11 +248,13 @@ if (process.platform === "darwin") { throw new Error("Could not find the Windows setup executable to publish"); } - published.push(publishArtifact(setupExe, arch, "exe")); - published.push(buildWindowsMsi(setupExe, arch)); + const published = [ + publishArtifact(setupExe, arch, "exe"), + buildWindowsMsi(setupExe, arch), + ]; + + cleanStagingArtifacts(published.map((filePath) => path.basename(filePath))); } else { console.log("finalize-desktop-artifacts: skipping unsupported platform", process.platform); process.exit(0); } - -cleanArtifactDir(published.map((filePath) => path.basename(filePath)));