Add rcedit dependency and implement Windows launcher icon embedding
All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good

- Added rcedit dependency in package.json and bun.lock for embedding icons in Windows executables.
- Introduced a new script to generate and apply the application icon for Windows launcher binaries, improving visual consistency.
- Updated the NSIS installer script to support dynamic icon paths, enhancing flexibility in icon management during installation.
- Integrated the icon application process into the finalization of desktop artifacts, ensuring the icon is embedded in the generated binaries.
This commit is contained in:
Tom Butcher 2026-08-01 23:16:22 +01:00
parent eb49ba7ce3
commit 29444e2ed4
6 changed files with 104 additions and 3 deletions

View File

@ -35,6 +35,7 @@
"jimp": "^1.6.1", "jimp": "^1.6.1",
"png-to-ico": "^2.1.8", "png-to-ico": "^2.1.8",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"rcedit": "^4.0.1",
"react": "^19.2.6", "react": "^19.2.6",
"react-dom": "^19.2.6", "react-dom": "^19.2.6",
"supertest": "^7.2.2", "supertest": "^7.2.2",

View File

@ -46,6 +46,7 @@
"jest": "^30.4.2", "jest": "^30.4.2",
"jimp": "^1.6.1", "jimp": "^1.6.1",
"png-to-ico": "^2.1.8", "png-to-ico": "^2.1.8",
"rcedit": "^4.0.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^19.2.6", "react": "^19.2.6",
"react-dom": "^19.2.6", "react-dom": "^19.2.6",

View File

@ -21,8 +21,16 @@ InstallDirRegKey HKLM "Software\Tom Butcher\Farm Control Server" "InstallDir"
RequestExecutionLevel admin RequestExecutionLevel admin
!define MUI_ABORTWARNING !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_DIRECTORY
!insertmacro MUI_PAGE_COMPONENTS !insertmacro MUI_PAGE_COMPONENTS

View File

@ -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}`,
);
}
}

View File

@ -53,14 +53,20 @@ if (-not (Test-Path $outputDir)) {
New-Item -ItemType Directory -Path $outputDir | Out-Null New-Item -ItemType Directory -Path $outputDir | Out-Null
} }
$iconPath = Join-Path $rootDir "assets\icon.ico"
$makensisArgs = @( $makensisArgs = @(
"/NOCD" "/NOCD"
"/DOUTFILE=$outputExePath" "/DOUTFILE=$outputExePath"
"/DVERSION=$Version" "/DVERSION=$Version"
"/DAPP_SOURCE_DIR=$appDirPath" "/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 Push-Location $workDir
try { try {
& $makensis @makensisArgs & $makensis @makensisArgs

View File

@ -20,6 +20,7 @@ import {
expandWindowsAppFromArchive, expandWindowsAppFromArchive,
} from "./expand-windows-installer.mjs"; } from "./expand-windows-installer.mjs";
import { codesignMacAppBundle } from "./codesign-macos-app.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 rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const packageJson = JSON.parse( const packageJson = JSON.parse(
@ -577,6 +578,7 @@ async function main() {
installerFiles.setupArchive, installerFiles.setupArchive,
platformDir, platformDir,
); );
await applyWindowsLauncherIcon(appDir);
let published; let published;
try { try {