From d0d43aebb78698bfc6030ed69e0c538e05cc39ba Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 1 Aug 2026 23:22:43 +0100 Subject: [PATCH] Refactor Windows installer and update launcher executable - Updated the NSIS installer script to replace references from `launcher.exe` to `farmcontrol-server.exe`, ensuring consistency with the new executable naming. - Removed the `apply-windows-launcher-icon.mjs` script and integrated its functionality into a new script, `prepare-windows-app-binaries.mjs`, which handles branding and icon embedding for Windows app binaries. - Enhanced the finalization process of desktop artifacts to utilize the new script for preparing Windows app binaries, improving the overall build workflow. --- packaging/windows/installer.nsh | 8 +- scripts/apply-windows-launcher-icon.mjs | 83 ----------- scripts/finalize-desktop-artifacts.mjs | 4 +- scripts/prepare-windows-app-binaries.mjs | 168 +++++++++++++++++++++++ 4 files changed, 174 insertions(+), 89 deletions(-) delete mode 100644 scripts/apply-windows-launcher-icon.mjs create mode 100644 scripts/prepare-windows-app-binaries.mjs diff --git a/packaging/windows/installer.nsh b/packaging/windows/installer.nsh index 9200c40..22e304f 100644 --- a/packaging/windows/installer.nsh +++ b/packaging/windows/installer.nsh @@ -1,10 +1,10 @@ !macro createDesktopShortcut - CreateShortCut "$DESKTOP\Farm Control Server.lnk" "$INSTDIR\bin\launcher.exe" + CreateShortCut "$DESKTOP\Farm Control Server.lnk" "$INSTDIR\bin\farmcontrol-server.exe" !macroend !macro createStartMenuShortcut CreateDirectory "$SMPROGRAMS\Farm Control Server" - CreateShortCut "$SMPROGRAMS\Farm Control Server\Farm Control Server.lnk" "$INSTDIR\bin\launcher.exe" + CreateShortCut "$SMPROGRAMS\Farm Control Server\Farm Control Server.lnk" "$INSTDIR\bin\farmcontrol-server.exe" !macroend !macro removeDesktopShortcut @@ -21,10 +21,10 @@ DeleteRegKey HKCR "farmcontrolserver" WriteRegStr HKCR "farmcontrolserver" "" "URL:farmcontrolserver" WriteRegStr HKCR "farmcontrolserver" "URL Protocol" "" - WriteRegStr HKCR "farmcontrolserver\DefaultIcon" "" "$INSTDIR\bin\launcher.exe" + WriteRegStr HKCR "farmcontrolserver\DefaultIcon" "" "$INSTDIR\bin\farmcontrol-server.exe" WriteRegStr HKCR "farmcontrolserver\shell" "" "" WriteRegStr HKCR "farmcontrolserver\shell\Open" "" "" - WriteRegStr HKCR "farmcontrolserver\shell\Open\command" "" '"$INSTDIR\bin\launcher.exe" "%1"' + WriteRegStr HKCR "farmcontrolserver\shell\Open\command" "" '"$INSTDIR\bin\farmcontrol-server.exe" "%1"' !macroend !macro customUnInstall diff --git a/scripts/apply-windows-launcher-icon.mjs b/scripts/apply-windows-launcher-icon.mjs deleted file mode 100644 index 1f041bf..0000000 --- a/scripts/apply-windows-launcher-icon.mjs +++ /dev/null @@ -1,83 +0,0 @@ -import { existsSync } 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)), ".."); - -const WINDOWS_BIN_FILES = ["launcher.exe", "bun.exe"]; - -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( - "apply-windows-launcher-icon: assets/icon.ico not found and no source PNG to generate it", - ); - } - - console.log(`apply-windows-launcher-icon: 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("apply-windows-launcher-icon: failed to generate assets/icon.ico"); - } - - return iconPath; -} - -/** - * Embed the Farm Control icon into Windows launcher binaries. - * Electrobun's built-in rcedit step often fails in CI (see electrobun#429). - */ -export async function applyWindowsLauncherIcon(appDir) { - if (process.platform !== "win32") { - console.log("apply-windows-launcher-icon: skipped (not Windows)"); - return; - } - - const iconPath = ensureIconIco(); - const binDir = path.join(appDir, "bin"); - if (!existsSync(binDir)) { - throw new Error(`apply-windows-launcher-icon: bin directory not found: ${binDir}`); - } - - const rcedit = (await import("rcedit")).default; - let patched = 0; - - for (const fileName of WINDOWS_BIN_FILES) { - const exePath = path.join(binDir, fileName); - if (!existsSync(exePath)) { - continue; - } - - console.log(`apply-windows-launcher-icon: embedding icon into ${exePath}`); - try { - await rcedit(exePath, { icon: iconPath }); - patched += 1; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error( - `apply-windows-launcher-icon: failed to embed icon into ${fileName}: ${message}`, - ); - } - } - - if (patched === 0) { - throw new Error( - `apply-windows-launcher-icon: no launcher binaries found in ${binDir}`, - ); - } -} diff --git a/scripts/finalize-desktop-artifacts.mjs b/scripts/finalize-desktop-artifacts.mjs index 473b609..3931e8f 100644 --- a/scripts/finalize-desktop-artifacts.mjs +++ b/scripts/finalize-desktop-artifacts.mjs @@ -20,7 +20,7 @@ import { expandWindowsAppFromArchive, } from "./expand-windows-installer.mjs"; import { codesignMacAppBundle } from "./codesign-macos-app.mjs"; -import { applyWindowsLauncherIcon } from "./apply-windows-launcher-icon.mjs"; +import { prepareWindowsAppBinaries } from "./prepare-windows-app-binaries.mjs"; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const packageJson = JSON.parse( @@ -578,7 +578,7 @@ async function main() { installerFiles.setupArchive, platformDir, ); - await applyWindowsLauncherIcon(appDir); + await prepareWindowsAppBinaries(appDir); let published; try { diff --git a/scripts/prepare-windows-app-binaries.mjs b/scripts/prepare-windows-app-binaries.mjs new file mode 100644 index 0000000..43a58bd --- /dev/null +++ b/scripts/prepare-windows-app-binaries.mjs @@ -0,0 +1,168 @@ +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}`, + ); + } + } +}