diff --git a/Jenkinsfile b/Jenkinsfile index e3a88df..624b963 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -86,37 +86,61 @@ def buildLinux() { } } +def prepareMacBuildWorkspace() { + if (isUnix()) { + sh ''' + df -h . + for vol in /Volumes/Farm\\ Control\\ Server*; do + if [ -d "$vol" ]; then + hdiutil detach "$vol" -force || true + fi + done + rm -rf build/stable-macos-*/.dmg-staging build/stable-macos-*/.finalize-dmg-staging 2>/dev/null || true + ''' + } +} + def buildOnLabel(label, buildCommand) { return { node(label) { - stage("Checkout (${label})") { - checkout scm - } - - withBun { - stage("Check Bun (${label})") { - checkBun() + try { + stage("Checkout (${label})") { + checkout scm } - stage("Install Dependencies (${label})") { - if (isUnix()) { - sh 'bun install --frozen-lockfile' - } else { - bat 'bun install --frozen-lockfile' + withBun { + stage("Check Bun (${label})") { + checkBun() + } + + if (label.startsWith('macos')) { + stage("Prepare macOS workspace (${label})") { + prepareMacBuildWorkspace() + } + } + + stage("Install Dependencies (${label})") { + if (isUnix()) { + sh 'bun install --frozen-lockfile' + } else { + bat 'bun install --frozen-lockfile' + } + } + + stage("Write Build Metadata (${label})") { + writeBuildMetadata() + } + + stage("Build (${label})") { + runBuild(buildCommand) } } - stage("Write Build Metadata (${label})") { - writeBuildMetadata() + stage("Archive Artifacts (${label})") { + archiveArtifacts artifacts: 'app_dist/farmcontrol-server-*', fingerprint: true } - - stage("Build (${label})") { - runBuild(buildCommand) - } - } - - stage("Archive Artifacts (${label})") { - archiveArtifacts artifacts: 'app_dist/farmcontrol-server-*', fingerprint: true + } finally { + cleanWs() } } } diff --git a/electrobun.config.ts b/electrobun.config.ts index f8bd558..f7114a0 100644 --- a/electrobun.config.ts +++ b/electrobun.config.ts @@ -56,6 +56,8 @@ export default { watchIgnore: ["dist/**", "build/**", "app_dist/**"], mac: { bundleCEF: false, + // DMG is built in finalize-desktop-artifacts (symlink staging saves disk). + createDmg: false, codesign: isStable && canCodesign, notarize: isStable && canCodesign, }, diff --git a/scripts/finalize-desktop-artifacts.mjs b/scripts/finalize-desktop-artifacts.mjs index e8f445a..f18663f 100644 --- a/scripts/finalize-desktop-artifacts.mjs +++ b/scripts/finalize-desktop-artifacts.mjs @@ -6,6 +6,7 @@ import { readFileSync, rmSync, statSync, + symlinkSync, } from "node:fs"; import path from "node:path"; import { spawnSync } from "node:child_process"; @@ -184,6 +185,54 @@ function cleanStagingArtifacts(keepNames) { } } +function buildMacDmg(appBundlePath, arch) { + const dmgPath = path.join( + artifactDir, + getReleaseArtifactName(version, arch, "dmg"), + ); + const stagingDir = path.join(path.dirname(appBundlePath), ".finalize-dmg-staging"); + + rmSync(stagingDir, { recursive: true, force: true }); + mkdirSync(stagingDir, { recursive: true }); + + const appName = path.basename(appBundlePath); + symlinkSync(appBundlePath, path.join(stagingDir, appName), "dir"); + symlinkSync("/Applications", path.join(stagingDir, "Applications")); + + const result = spawnSync( + "hdiutil", + [ + "create", + "-volname", + "Farm Control Server", + "-srcfolder", + stagingDir, + "-ov", + "-format", + "UDZO", + dmgPath, + ], + { stdio: "inherit" }, + ); + + rmSync(stagingDir, { recursive: true, force: true }); + + if (result.status !== 0) { + throw new Error(`hdiutil create failed with exit code ${result.status ?? 1}`); + } + + console.log(`Published ${dmgPath}`); + return dmgPath; +} + +function cleanMacBuildDir(arch) { + const platformDir = path.join(getBuildRoot(), `stable-macos-${arch}`); + if (existsSync(platformDir)) { + rmSync(platformDir, { recursive: true, force: true }); + console.log(`Removed build output ${platformDir}`); + } +} + function buildMacPkg(appBundlePath, arch) { const pkgPath = path.join( artifactDir, @@ -269,22 +318,24 @@ if (buildEnv === "dev") { } if (targetOs === "macos") { - const dmgSource = findMacDmgSource(buildArch); const appBundle = findMacAppBundle(buildArch); - if (!dmgSource || !appBundle) { + if (!appBundle) { console.log( - `finalize-desktop-artifacts: no macOS ${buildArch} release artifacts found, skipping`, + `finalize-desktop-artifacts: no macOS ${buildArch} app bundle found, skipping`, ); process.exit(0); } - const published = [ - publishArtifact(dmgSource, buildArch, "dmg"), - buildMacPkg(appBundle, buildArch), - ]; + const existingDmg = findMacDmgSource(buildArch); + const dmgPath = existingDmg + ? publishArtifact(existingDmg, buildArch, "dmg") + : buildMacDmg(appBundle, buildArch); + + const published = [dmgPath, buildMacPkg(appBundle, buildArch)]; cleanStagingArtifacts(published.map((filePath) => path.basename(filePath))); + cleanMacBuildDir(buildArch); } else if (targetOs === "win") { const arch = getReleaseArch(process.env.ELECTROBUN_ARCH || "x64"); const installerFiles = findWindowsInstallerFiles();