All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good
- 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.
171 lines
5.2 KiB
JavaScript
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);
|
|
});
|
|
}
|