diff --git a/Jenkinsfile b/Jenkinsfile index be83a4d..ea74858 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -70,6 +70,9 @@ def buildLinux() { } } +def releaseBaseUrl = + "https://dist.farmcontrol.app/jenkins/farmcontrol/farmcontrol-server" + def buildOnLabel(label, buildCommand) { return { node(label) { @@ -96,9 +99,9 @@ def buildOnLabel(label, buildCommand) { stage("Build (${label})") { if (isUnix()) { - sh "VITE_BUILD_NUMBER=${env.BUILD_NUMBER} NODE_ENV=production ${buildCommand}" + sh "VITE_BUILD_NUMBER=${env.BUILD_NUMBER} NODE_ENV=production ELECTROBUN_RELEASE_BASE_URL=${releaseBaseUrl} ${buildCommand}" } else { - bat "set VITE_BUILD_NUMBER=${env.BUILD_NUMBER} && set NODE_ENV=production && ${buildCommand}" + bat "set VITE_BUILD_NUMBER=${env.BUILD_NUMBER} && set NODE_ENV=production && set ELECTROBUN_RELEASE_BASE_URL=${releaseBaseUrl} && ${buildCommand}" } } } diff --git a/electrobun.config.ts b/electrobun.config.ts index 35e9b46..6eaa547 100644 --- a/electrobun.config.ts +++ b/electrobun.config.ts @@ -1,11 +1,18 @@ -// Electrobun requires this filename; application code remains JavaScript. +import type { ElectrobunConfig } from "electrobun"; +import { readFileSync } from "node:fs"; + +const packageJson = JSON.parse(readFileSync("./package.json", "utf8")); + export default { app: { name: "Farm Control Server", identifier: "com.tombutcher.farmcontrolserver", - version: "1.0.0", + version: packageJson.version, description: "Farm Control Server desktop host application", }, + runtime: { + exitOnLastWindowClosed: false, + }, build: { buildFolder: "build", artifactFolder: "app_dist", @@ -17,7 +24,8 @@ export default { "dist/app/assets": "views/mainview/assets", "config.json": "config.json", }, - watchIgnore: ["dist/**"], + watch: ["scripts"], + watchIgnore: ["dist/**", "app_dist/**"], mac: { bundleCEF: false, }, @@ -28,10 +36,14 @@ export default { bundleCEF: false, }, }, - runtime: { - exitOnLastWindowClosed: false, - }, scripts: { + preBuild: "scripts/pre-build.mjs", postPackage: "scripts/finalize-desktop-artifacts.mjs", }, -}; + release: { + baseUrl: + process.env.ELECTROBUN_RELEASE_BASE_URL || + process.env.RELEASE_BASE_URL || + "", + }, +} satisfies ElectrobunConfig; diff --git a/scripts/build-macos.mjs b/scripts/build-macos.mjs index 037e49a..35613b2 100644 --- a/scripts/build-macos.mjs +++ b/scripts/build-macos.mjs @@ -3,7 +3,6 @@ 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 = {}) { @@ -19,35 +18,15 @@ function run(command, args, options = {}) { } } -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); + + run("bun", ["electrobun", "build", "--env=stable"], { + env: { + ...process.env, + ELECTROBUN_ARCH: targetArch, + }, + }); } diff --git a/scripts/finalize-desktop-artifacts.mjs b/scripts/finalize-desktop-artifacts.mjs index 078e9c9..2eee80f 100644 --- a/scripts/finalize-desktop-artifacts.mjs +++ b/scripts/finalize-desktop-artifacts.mjs @@ -20,11 +20,41 @@ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") const packageJson = JSON.parse( readFileSync(path.join(rootDir, "package.json"), "utf8"), ); -const version = getReleaseVersion(packageJson); -const artifactDir = path.join(rootDir, "app_dist"); -const identifier = "com.tombutcher.farmcontrolserver"; +const buildEnv = process.env.ELECTROBUN_BUILD_ENV || "stable"; +const targetOs = + process.env.ELECTROBUN_OS || + (process.platform === "darwin" + ? "macos" + : process.platform === "win32" + ? "win" + : process.platform === "linux" + ? "linux" + : null); +const buildArch = getReleaseArch( + process.env.ELECTROBUN_ARCH || process.arch, +); +const version = + process.env.ELECTROBUN_APP_VERSION || getReleaseVersion(packageJson); +const artifactDir = + process.env.ELECTROBUN_ARTIFACT_DIR || path.join(rootDir, "app_dist"); +const identifier = + process.env.ELECTROBUN_APP_IDENTIFIER || "com.tombutcher.farmcontrolserver"; const artifactPrefix = `farmcontrol-server-${version}-`; +function getBuildRoot() { + const electrobunBuildDir = process.env.ELECTROBUN_BUILD_DIR; + if (!electrobunBuildDir) { + return path.join(rootDir, "build"); + } + + const baseName = path.basename(electrobunBuildDir); + if (baseName.startsWith("stable-")) { + return path.dirname(electrobunBuildDir); + } + + return electrobunBuildDir; +} + function walkFiles(dir) { const files = []; if (!existsSync(dir)) { @@ -51,7 +81,7 @@ function findByExtension(root, extension) { } function findMacAppBundle(arch) { - const platformDir = path.join(rootDir, "build", `stable-macos-${arch}`); + const platformDir = path.join(getBuildRoot(), `stable-macos-${arch}`); if (!existsSync(platformDir)) { return null; } @@ -76,7 +106,7 @@ function findMacDmgSource(arch) { } const buildDmg = findByExtension( - path.join(rootDir, "build", `stable-macos-${arch}`), + path.join(getBuildRoot(), `stable-macos-${arch}`), ".dmg", ); if (buildDmg) { @@ -87,30 +117,26 @@ function findMacDmgSource(arch) { } function findWindowsInstallerFiles() { - const buildDir = path.join(rootDir, "build"); - if (!existsSync(buildDir)) { + const buildRoot = getBuildRoot(); + if (!existsSync(buildRoot)) { return null; } - for (const entry of readdirSync(buildDir)) { + for (const entry of readdirSync(buildRoot)) { if (!entry.startsWith("stable-win-")) { continue; } - const platformDir = path.join(buildDir, entry); + const platformDir = path.join(buildRoot, entry); const files = walkFiles(platformDir); - const setupExe = files.find((filePath) => - /-setup\.exe$/i.test(filePath), - ); + const setupExe = files.find((filePath) => /-setup\.exe$/i.test(filePath)); const setupArchive = files.find((filePath) => /-setup\.tar\.zst$/i.test(filePath), ); const setupMetadata = files.find((filePath) => /-setup\.metadata\.json$/i.test(filePath), ); - const setupZip = files.find((filePath) => - /-setup\.zip$/i.test(filePath), - ); + const setupZip = files.find((filePath) => /-setup\.zip$/i.test(filePath)); if (setupExe && setupArchive && setupMetadata) { return { setupExe, setupArchive, setupMetadata, setupZip }; @@ -235,10 +261,12 @@ function buildWindowsMsi(installerFiles, arch) { return msiPath; } -if (process.platform === "darwin") { - const buildArch = getReleaseArch( - process.env.ELECTROBUN_ARCH || process.arch, - ); +if (buildEnv === "dev") { + console.log("finalize-desktop-artifacts: skipping dev build"); + process.exit(0); +} + +if (targetOs === "macos") { const dmgSource = findMacDmgSource(buildArch); const appBundle = findMacAppBundle(buildArch); @@ -255,8 +283,8 @@ if (process.platform === "darwin") { ]; cleanStagingArtifacts(published.map((filePath) => path.basename(filePath))); -} else if (process.platform === "win32") { - const arch = "x64"; +} else if (targetOs === "win") { + const arch = getReleaseArch(process.env.ELECTROBUN_ARCH || "x64"); const installerFiles = findWindowsInstallerFiles(); if (!installerFiles) { @@ -273,6 +301,9 @@ if (process.platform === "darwin") { cleanStagingArtifacts(published.map((filePath) => path.basename(filePath))); } else { - console.log("finalize-desktop-artifacts: skipping unsupported platform", process.platform); + console.log( + "finalize-desktop-artifacts: no desktop packaging configured for", + targetOs ?? "unknown target", + ); process.exit(0); } diff --git a/scripts/pre-build.mjs b/scripts/pre-build.mjs new file mode 100644 index 0000000..35b9d80 --- /dev/null +++ b/scripts/pre-build.mjs @@ -0,0 +1,38 @@ +import { existsSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const buildEnv = process.env.ELECTROBUN_BUILD_ENV || "dev"; + +const requiredFiles = [ + "src/bun/index.js", + "electrobun.config.ts", + "package.json", +]; + +for (const relativePath of requiredFiles) { + const filePath = path.join(rootDir, relativePath); + if (!existsSync(filePath)) { + console.error(`pre-build: required file not found: ${relativePath}`); + process.exit(1); + } +} + +if (buildEnv === "stable" && process.env.ELECTROBUN_OS === "macos") { + const codesignVars = [ + "ELECTROBUN_DEVELOPER_ID", + "ELECTROBUN_APPLEID", + "ELECTROBUN_APPLEIDPASS", + "ELECTROBUN_TEAMID", + ]; + const missing = codesignVars.filter((name) => !process.env[name]); + + if (missing.length > 0) { + console.warn( + `pre-build: stable macOS build without code signing credentials (${missing.join(", ")}); Electrobun will skip signing.`, + ); + } +} + +console.log(`pre-build: validation passed (${buildEnv})`);