import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, } from "node:fs"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { getReleaseArch, getReleaseArtifactName, getReleaseVersion, } from "./release-artifact-utils.mjs"; 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"; function walkFiles(dir) { const files = []; if (!existsSync(dir)) { return files; } for (const entry of readdirSync(dir)) { const fullPath = path.join(dir, entry); const stats = statSync(fullPath); if (stats.isDirectory()) { files.push(...walkFiles(fullPath)); } else { files.push(fullPath); } } return files; } function findByExtension(root, extension) { return walkFiles(root).find((filePath) => filePath.toLowerCase().endsWith(extension.toLowerCase()), ); } function findMacAppBundle() { const buildDir = path.join(rootDir, "build"); if (!existsSync(buildDir)) { 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); } } } return null; } function findWindowsSetupExe() { const buildDir = path.join(rootDir, "build"); if (!existsSync(buildDir)) { return null; } for (const entry of readdirSync(buildDir)) { if (!entry.startsWith("stable-win-")) { continue; } const platformDir = path.join(buildDir, entry); const setupExe = walkFiles(platformDir).find((filePath) => filePath.toLowerCase().endsWith("-setup.exe"), ); if (setupExe) { return setupExe; } } return findByExtension(buildDir, "-Setup.exe"); } function publishArtifact(sourcePath, arch, ext) { if (!sourcePath || !existsSync(sourcePath)) { throw new Error(`Missing source artifact for ${arch}.${ext}: ${sourcePath ?? "not found"}`); } mkdirSync(artifactDir, { recursive: true }); const destination = path.join( artifactDir, getReleaseArtifactName(version, arch, ext), ); cpSync(sourcePath, destination); console.log(`Published ${destination}`); return destination; } function cleanArtifactDir(keepNames) { if (!existsSync(artifactDir)) { return; } for (const entry of readdirSync(artifactDir)) { if (keepNames.includes(entry)) { continue; } rmSync(path.join(artifactDir, entry), { recursive: true, force: true }); } } function buildMacPkg(appBundlePath, arch) { const pkgPath = path.join( artifactDir, getReleaseArtifactName(version, arch, "pkg"), ); const result = spawnSync( "pkgbuild", [ "--component", appBundlePath, "--install-location", "/Applications", "--identifier", identifier, "--version", version, pkgPath, ], { stdio: "inherit" }, ); if (result.status !== 0) { throw new Error(`pkgbuild failed with exit code ${result.status ?? 1}`); } console.log(`Published ${pkgPath}`); return pkgPath; } function buildWindowsMsi(setupExePath, arch) { const scriptPath = path.join(rootDir, "scripts/build-windows-msi.ps1"); const msiPath = path.join( artifactDir, getReleaseArtifactName(version, arch, "msi"), ); const powershell = process.env.SystemRoot ? path.join( process.env.SystemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe", ) : "powershell.exe"; const result = spawnSync( powershell, [ "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath, "-SetupExe", setupExePath, "-OutputMsi", msiPath, "-Version", version, ], { stdio: "inherit" }, ); if (result.status !== 0) { throw new Error(`build-windows-msi.ps1 failed with exit code ${result.status ?? 1}`); } console.log(`Published ${msiPath}`); 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(); 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"); } published.push(publishArtifact(dmgSource, arch, "dmg")); published.push(buildMacPkg(appBundle, arch)); } else if (process.platform === "win32") { const arch = "x64"; const setupExe = findWindowsSetupExe(); if (!setupExe) { throw new Error("Could not find the Windows setup executable to publish"); } published.push(publishArtifact(setupExe, arch, "exe")); published.push(buildWindowsMsi(setupExe, arch)); } else { console.log("finalize-desktop-artifacts: skipping unsupported platform", process.platform); process.exit(0); } cleanArtifactDir(published.map((filePath) => path.basename(filePath)));