farmcontrol-server/scripts/embed-windows-exe-icon.mjs
Tom Butcher f04daa1998
All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good
Update NSIS installer and build script for improved file handling and icon management
- Changed the application source directory definition in the NSIS installer script to point to "app" for better clarity.
- Updated the file inclusion pattern to ensure all files are correctly packaged in the installer.
- Enhanced the build script to define a local installer name and manage the icon file more effectively, ensuring it is copied to the working directory if present.
- Improved error handling to provide clearer messages regarding the installer creation process.
2026-08-01 23:59:59 +01:00

171 lines
5.2 KiB
JavaScript

import { createRequire } from "node:module";
import { execFileSync } from "node:child_process";
import {
existsSync,
mkdirSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import pngToIco from "png-to-ico";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const require = createRequire(path.join(rootDir, "package.json"));
/** Default icon path from electrobun.config.ts build.win.icon */
export const DEFAULT_WINDOWS_ICON = "assets/icon.ico";
function resolveRceditExe() {
const projectRcedit = path.join(rootDir, "node_modules/rcedit/package.json");
const rceditPkgPath = existsSync(projectRcedit)
? projectRcedit
: require.resolve("rcedit/package.json");
const rceditDir = path.dirname(rceditPkgPath);
const rceditX64 = path.join(rceditDir, "bin", "rcedit-x64.exe");
if (existsSync(rceditX64)) {
return rceditX64;
}
const rceditExe = path.join(rceditDir, "bin", "rcedit.exe");
if (!existsSync(rceditExe)) {
throw new Error(`embed-windows-exe-icon: rcedit not found under ${rceditDir}`);
}
return rceditExe;
}
/**
* Resolve the Windows app icon path per Electrobun docs:
* https://framework.blackboard.sh/electrobun/apis/application-icons/
*/
export function resolveWindowsIconSource(iconConfigPath = DEFAULT_WINDOWS_ICON) {
const candidates = [];
if (iconConfigPath) {
const configured =
iconConfigPath.startsWith("/") || /^[a-zA-Z]:/.test(iconConfigPath)
? iconConfigPath
: path.join(rootDir, iconConfigPath);
candidates.push(configured);
}
candidates.push(
path.join(rootDir, "assets/icon.ico"),
path.join(rootDir, "assets/icon.iconset/icon_256x256.png"),
path.join(rootDir, "assets/icon.png"),
);
const found = candidates.find((candidate) => existsSync(candidate));
if (!found) {
throw new Error(
"embed-windows-exe-icon: Windows icon not found. Set build.win.icon to assets/icon.ico or run scripts/prepare-app-icons.mjs",
);
}
return found;
}
async function materializeIco(iconSourcePath, workDir) {
if (iconSourcePath.toLowerCase().endsWith(".ico")) {
return { icoPath: iconSourcePath, temp: false };
}
if (!iconSourcePath.toLowerCase().endsWith(".png")) {
throw new Error(
`embed-windows-exe-icon: unsupported icon format: ${iconSourcePath}`,
);
}
mkdirSync(workDir, { recursive: true });
const icoPath = path.join(workDir, "app-icon.ico");
const icoBuffer = await pngToIco(iconSourcePath);
writeFileSync(icoPath, new Uint8Array(icoBuffer));
console.log(`embed-windows-exe-icon: converted PNG to ICO (${icoPath})`);
return { icoPath, temp: true };
}
/**
* Embed an icon into a Windows executable using rcedit, matching Electrobun's
* build.win.icon behavior (launcher, runtime, and installer executables).
*/
export async function embedWindowsExeIcon(exePath, options = {}) {
if (process.platform !== "win32") {
console.log(`embed-windows-exe-icon: skipped ${path.basename(exePath)} (not Windows)`);
return;
}
if (!existsSync(exePath)) {
throw new Error(`embed-windows-exe-icon: executable not found: ${exePath}`);
}
const basename = path.basename(exePath).toLowerCase();
if (
basename.includes("farmcontrol-server-") &&
basename.endsWith(".exe") &&
!basename.endsWith("-setup.exe")
) {
throw new Error(
`embed-windows-exe-icon: refusing to patch NSIS installer ${basename}; use NSIS Icon/MUI_ICON instead`,
);
}
const iconSourcePath = resolveWindowsIconSource(options.icon);
const workDir =
options.workDir || path.join(rootDir, "build", ".windows-icon-work");
const { icoPath, temp } = await materializeIco(iconSourcePath, workDir);
const rceditExe = resolveRceditExe();
const resolvedExe = path.resolve(exePath);
const resolvedIcon = path.resolve(icoPath);
console.log(
`embed-windows-exe-icon: embedding icon into ${path.basename(resolvedExe)}`,
);
try {
execFileSync(rceditExe, [resolvedExe, "--set-icon", resolvedIcon], {
stdio: "inherit",
windowsHide: true,
});
} finally {
if (temp && options.cleanupTemp) {
unlinkSync(icoPath);
}
}
}
/**
* Embed icons into the launcher and Bun runtime executables (Electrobun docs).
*/
export async function embedWindowsAppIcons(exePaths, options = {}) {
for (const exePath of exePaths) {
await embedWindowsExeIcon(exePath, options);
}
}
async function main() {
const args = process.argv.slice(2);
const exePath = args.find((arg) => !arg.startsWith("-"));
if (!exePath) {
console.error("embed-windows-exe-icon: usage: bun scripts/embed-windows-exe-icon.mjs <exe> [--icon path]");
process.exit(1);
}
const iconArgIndex = args.indexOf("--icon");
const icon =
iconArgIndex >= 0 && args[iconArgIndex + 1]
? args[iconArgIndex + 1]
: DEFAULT_WINDOWS_ICON;
await embedWindowsExeIcon(exePath, { icon, cleanupTemp: true });
}
const invokedPath = path.resolve(process.argv[1] ?? "");
const modulePath = path.resolve(fileURLToPath(import.meta.url));
if (invokedPath === modulePath) {
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
}