Enhance Windows app binary preparation and icon embedding
All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good
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.
This commit is contained in:
parent
c711bd9863
commit
d4b22e3734
@ -78,4 +78,9 @@ if (-not (Test-Path $outputExePath)) {
|
||||
throw "NSIS installer was not created at $outputExePath"
|
||||
}
|
||||
|
||||
if (Test-Path $iconPath) {
|
||||
$embedIconScript = Join-Path $rootDir "scripts/embed-windows-exe-icon.mjs"
|
||||
& bun $embedIconScript $outputExePath --icon $iconPath
|
||||
}
|
||||
|
||||
Write-Host "Created NSIS installer at $outputExePath"
|
||||
|
||||
159
scripts/embed-windows-exe-icon.mjs
Normal file
159
scripts/embed-windows-exe-icon.mjs
Normal file
@ -0,0 +1,159 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
@ -170,6 +170,36 @@ if (!cliSource.includes("hookBunBinary")) {
|
||||
"console.error(\"Tried to run with bun at:\", hookBunBinary);",
|
||||
);
|
||||
writeFileSync(cliPath, cliSource);
|
||||
cliSource = readFileSync(cliPath, "utf8");
|
||||
}
|
||||
|
||||
if (!cliSource.includes("resolveProjectRceditPkgPath")) {
|
||||
cliSource = cliSource.replaceAll(
|
||||
"const rceditPkgPath = require.resolve(\"rcedit/package.json\");",
|
||||
"const rceditPkgPath = resolveProjectRceditPkgPath(projectRoot);",
|
||||
);
|
||||
cliSource = cliSource.replace(
|
||||
"function getPlatformPaths(",
|
||||
`function resolveProjectRceditPkgPath(projectRoot) {
|
||||
const projectRcedit = join(projectRoot, "node_modules/rcedit/package.json");
|
||||
if (existsSync(projectRcedit)) {
|
||||
return projectRcedit;
|
||||
}
|
||||
|
||||
const nestedRcedit = join(
|
||||
projectRoot,
|
||||
"node_modules/electrobun/node_modules/rcedit/package.json",
|
||||
);
|
||||
if (existsSync(nestedRcedit)) {
|
||||
return nestedRcedit;
|
||||
}
|
||||
|
||||
return require.resolve("rcedit/package.json");
|
||||
}
|
||||
|
||||
function getPlatformPaths(`,
|
||||
);
|
||||
writeFileSync(cliPath, cliSource);
|
||||
}
|
||||
|
||||
const markerPath = path.join(electrobunDir, ".farmcontrol-electrobun-patched");
|
||||
|
||||
@ -52,6 +52,14 @@ if (prepareIcons.status !== 0) {
|
||||
process.exit(prepareIcons.status ?? 1);
|
||||
}
|
||||
|
||||
const windowsIconPath = path.join(rootDir, "assets/icon.ico");
|
||||
if (!existsSync(windowsIconPath)) {
|
||||
console.error(
|
||||
"pre-build: assets/icon.ico not found after prepare-app-icons (required for build.win.icon)",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const writeBuildInfo = spawnSync(
|
||||
"bun",
|
||||
[path.join(rootDir, "scripts/write-build-info.mjs")],
|
||||
|
||||
@ -7,8 +7,11 @@ import {
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
DEFAULT_WINDOWS_ICON,
|
||||
embedWindowsAppIcons,
|
||||
} from "./embed-windows-exe-icon.mjs";
|
||||
|
||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
@ -26,37 +29,6 @@ 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);
|
||||
@ -122,22 +94,9 @@ function createBrandedAppExecutable(binDir) {
|
||||
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.
|
||||
* Brand Windows app binaries and embed icons per Electrobun application-icons docs:
|
||||
* launcher executable, Bun runtime executable, then NSIS installer (separately).
|
||||
*/
|
||||
export async function prepareWindowsAppBinaries(appDir) {
|
||||
if (process.platform !== "win32") {
|
||||
@ -150,19 +109,16 @@ export async function prepareWindowsAppBinaries(appDir) {
|
||||
throw new Error(`prepare-windows-app-binaries: bin directory not found: ${binDir}`);
|
||||
}
|
||||
|
||||
const iconPath = ensureIconIco();
|
||||
const launcherPath = path.join(binDir, LAUNCHER_BINARY);
|
||||
if (existsSync(launcherPath)) {
|
||||
await embedWindowsAppIcons([launcherPath], { icon: DEFAULT_WINDOWS_ICON });
|
||||
}
|
||||
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
await embedWindowsAppIcons([appExecutablePath, runtimePath], {
|
||||
icon: DEFAULT_WINDOWS_ICON,
|
||||
cleanupTemp: true,
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user