All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good
- Added a new script to embed icons into Windows executables, improving visual consistency across application binaries. - Updated the NSIS installer script to conditionally embed an icon if provided, enhancing flexibility in icon management. - Refactored the Windows app binary preparation process to utilize the new icon embedding functionality, ensuring icons are correctly applied to the launcher and runtime executables.
160 lines
4.9 KiB
JavaScript
160 lines
4.9 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 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);
|
|
});
|
|
}
|