diff --git a/bun.lock b/bun.lock index 9311b1e..8ffe372 100644 --- a/bun.lock +++ b/bun.lock @@ -35,6 +35,7 @@ "jimp": "^1.6.1", "png-to-ico": "^2.1.8", "prop-types": "^15.8.1", + "rcedit": "^4.0.1", "react": "^19.2.6", "react-dom": "^19.2.6", "supertest": "^7.2.2", diff --git a/package.json b/package.json index ad85ba0..1047f0e 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "jest": "^30.4.2", "jimp": "^1.6.1", "png-to-ico": "^2.1.8", + "rcedit": "^4.0.1", "prop-types": "^15.8.1", "react": "^19.2.6", "react-dom": "^19.2.6", diff --git a/packaging/windows/farmcontrol-server.nsi b/packaging/windows/farmcontrol-server.nsi index f69e4d5..76f1d0f 100644 --- a/packaging/windows/farmcontrol-server.nsi +++ b/packaging/windows/farmcontrol-server.nsi @@ -21,8 +21,16 @@ InstallDirRegKey HKLM "Software\Tom Butcher\Farm Control Server" "InstallDir" RequestExecutionLevel admin !define MUI_ABORTWARNING -!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico" -!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico" + +!ifndef INSTALLER_ICON + !define INSTALLER_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico" +!endif + +!define MUI_ICON "${INSTALLER_ICON}" +!define MUI_UNICON "${INSTALLER_ICON}" + +Icon "${INSTALLER_ICON}" +UninstallIcon "${INSTALLER_ICON}" !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_COMPONENTS diff --git a/scripts/apply-windows-launcher-icon.mjs b/scripts/apply-windows-launcher-icon.mjs new file mode 100644 index 0000000..1f041bf --- /dev/null +++ b/scripts/apply-windows-launcher-icon.mjs @@ -0,0 +1,83 @@ +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/build-windows-nsis.ps1 b/scripts/build-windows-nsis.ps1 index 0a80e3e..b6e7a3a 100644 --- a/scripts/build-windows-nsis.ps1 +++ b/scripts/build-windows-nsis.ps1 @@ -53,14 +53,20 @@ if (-not (Test-Path $outputDir)) { New-Item -ItemType Directory -Path $outputDir | Out-Null } +$iconPath = Join-Path $rootDir "assets\icon.ico" $makensisArgs = @( "/NOCD" "/DOUTFILE=$outputExePath" "/DVERSION=$Version" "/DAPP_SOURCE_DIR=$appDirPath" - (Join-Path $workDir "farmcontrol-server.nsi") ) +if (Test-Path $iconPath) { + $makensisArgs += "/DINSTALLER_ICON=$iconPath" +} + +$makensisArgs += (Join-Path $workDir "farmcontrol-server.nsi") + Push-Location $workDir try { & $makensis @makensisArgs diff --git a/scripts/finalize-desktop-artifacts.mjs b/scripts/finalize-desktop-artifacts.mjs index 152bec1..473b609 100644 --- a/scripts/finalize-desktop-artifacts.mjs +++ b/scripts/finalize-desktop-artifacts.mjs @@ -20,6 +20,7 @@ import { expandWindowsAppFromArchive, } from "./expand-windows-installer.mjs"; import { codesignMacAppBundle } from "./codesign-macos-app.mjs"; +import { applyWindowsLauncherIcon } from "./apply-windows-launcher-icon.mjs"; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const packageJson = JSON.parse( @@ -577,6 +578,7 @@ async function main() { installerFiles.setupArchive, platformDir, ); + await applyWindowsLauncherIcon(appDir); let published; try {