import { cpSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); /** User-facing Windows app entry point (Electrobun launcher). */ export const WINDOWS_APP_EXECUTABLE = "farmcontrol-server.exe"; /** * Bun runtime filename embedded in the Electrobun launcher binary. * Must stay exactly 7 characters (same length as "bun.exe") for binary patching. */ export const WINDOWS_RUNTIME_EXECUTABLE = "farm.exe"; const LAUNCHER_BINARY = "launcher.exe"; const LEGACY_RUNTIME_BINARY = "bun.exe"; const LAUNCHER_RUNTIME_REFERENCE = Buffer.from(`${LEGACY_RUNTIME_BINARY}\0`); const PATCHED_RUNTIME_REFERENCE = Buffer.from(`${WINDOWS_RUNTIME_EXECUTABLE}\0`); function ensureIconIco() { const iconPath = path.join(rootDir, "assets/icon.ico"); if (existsSync(iconPath)) { return iconPath; } const sources = [ path.join(rootDir, "assets/farmcontrolhosticon.png"), path.join(rootDir, "assets/icon.png"), ]; const source = sources.find((candidate) => existsSync(candidate)); if (!source) { throw new Error( "prepare-windows-app-binaries: assets/icon.ico not found and no source PNG to generate it", ); } console.log(`prepare-windows-app-binaries: generating icon.ico from ${source}`); const result = spawnSync( "bun", [path.join(rootDir, "scripts/prepare-app-icons.mjs"), source], { cwd: rootDir, stdio: "inherit", env: process.env }, ); if (result.status !== 0 || !existsSync(iconPath)) { throw new Error("prepare-windows-app-binaries: failed to generate assets/icon.ico"); } return iconPath; } function patchLauncherRuntimeReference(launcherPath) { const binary = readFileSync(launcherPath); const index = binary.indexOf(LAUNCHER_RUNTIME_REFERENCE); if (index === -1) { if (binary.includes(PATCHED_RUNTIME_REFERENCE)) { return; } throw new Error( `prepare-windows-app-binaries: could not find ${LEGACY_RUNTIME_BINARY} reference in ${launcherPath}`, ); } PATCHED_RUNTIME_REFERENCE.copy(binary, index); writeFileSync(launcherPath, binary); console.log( `prepare-windows-app-binaries: patched ${path.basename(launcherPath)} to spawn ${WINDOWS_RUNTIME_EXECUTABLE}`, ); } function renameWindowsRuntime(binDir) { const legacyRuntimePath = path.join(binDir, LEGACY_RUNTIME_BINARY); const runtimePath = path.join(binDir, WINDOWS_RUNTIME_EXECUTABLE); if (existsSync(runtimePath)) { return runtimePath; } if (!existsSync(legacyRuntimePath)) { throw new Error( `prepare-windows-app-binaries: neither ${LEGACY_RUNTIME_BINARY} nor ${WINDOWS_RUNTIME_EXECUTABLE} found in ${binDir}`, ); } renameSync(legacyRuntimePath, runtimePath); console.log( `prepare-windows-app-binaries: renamed ${LEGACY_RUNTIME_BINARY} -> ${WINDOWS_RUNTIME_EXECUTABLE}`, ); return runtimePath; } function createBrandedAppExecutable(binDir) { const launcherPath = path.join(binDir, LAUNCHER_BINARY); const appExecutablePath = path.join(binDir, WINDOWS_APP_EXECUTABLE); if (!existsSync(launcherPath)) { if (existsSync(appExecutablePath)) { patchLauncherRuntimeReference(appExecutablePath); return appExecutablePath; } throw new Error( `prepare-windows-app-binaries: ${LAUNCHER_BINARY} not found in ${binDir}`, ); } patchLauncherRuntimeReference(launcherPath); cpSync(launcherPath, appExecutablePath); rmSync(launcherPath, { force: true }); console.log( `prepare-windows-app-binaries: created ${WINDOWS_APP_EXECUTABLE} and removed ${LAUNCHER_BINARY}`, ); return appExecutablePath; } async function applyWindowsExecutableMetadata(exePath, iconPath) { const rcedit = (await import("rcedit")).default; await rcedit(exePath, { icon: iconPath, "version-string": { CompanyName: "Tom Butcher", FileDescription: "Farm Control Server", ProductName: "Farm Control Server", OriginalFilename: path.basename(exePath), }, }); } /** * Brand Windows app binaries for install: farmcontrol-server.exe entry point, * farm.exe runtime (patched launcher reference), and embedded icons. */ export async function prepareWindowsAppBinaries(appDir) { if (process.platform !== "win32") { console.log("prepare-windows-app-binaries: skipped (not Windows)"); return; } const binDir = path.join(appDir, "bin"); if (!existsSync(binDir)) { throw new Error(`prepare-windows-app-binaries: bin directory not found: ${binDir}`); } const iconPath = ensureIconIco(); const runtimePath = renameWindowsRuntime(binDir); const appExecutablePath = createBrandedAppExecutable(binDir); for (const exePath of [appExecutablePath, runtimePath]) { console.log(`prepare-windows-app-binaries: embedding icon into ${exePath}`); try { await applyWindowsExecutableMetadata(exePath, iconPath); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error( `prepare-windows-app-binaries: failed to update ${path.basename(exePath)}: ${message}`, ); } } }