Update Windows installer and build scripts for launcher executable and icon management
Some checks failed
farmcontrol/farmcontrol-server/pipeline/head There was a failure building this commit
Some checks failed
farmcontrol/farmcontrol-server/pipeline/head There was a failure building this commit
- Replaced references to `farmcontrol-server.exe` with `launcher.exe` in the NSIS installer script and build scripts for consistency with the new executable naming. - Updated the icon path in the configuration to use a PNG file instead of ICO, enhancing visual fidelity. - Removed the `prepare-windows-app-binaries.mjs` script to streamline the build process, as its functionality is now integrated into other scripts. - Enhanced error handling in the build script to ensure the correct executable is present before proceeding.
This commit is contained in:
parent
f04daa1998
commit
456e7904bf
@ -68,7 +68,7 @@ export default {
|
|||||||
},
|
},
|
||||||
win: {
|
win: {
|
||||||
bundleCEF: false,
|
bundleCEF: false,
|
||||||
icon: "assets/icon.ico",
|
icon: "assets/icon.iconset/icon_256x256.png",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scripts: {
|
scripts: {
|
||||||
|
|||||||
@ -46,7 +46,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
!macro createDesktopShortcut
|
!macro createDesktopShortcut
|
||||||
CreateShortCut "$DESKTOP\Farm Control Server.lnk" "$INSTDIR\bin\farmcontrol-server.exe"
|
CreateShortCut "$DESKTOP\Farm Control Server.lnk" "$INSTDIR\bin\launcher.exe"
|
||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
!macro createStartMenuShortcut
|
!macro createStartMenuShortcut
|
||||||
CreateDirectory "$SMPROGRAMS\Farm Control Server"
|
CreateDirectory "$SMPROGRAMS\Farm Control Server"
|
||||||
CreateShortCut "$SMPROGRAMS\Farm Control Server\Farm Control Server.lnk" "$INSTDIR\bin\farmcontrol-server.exe"
|
CreateShortCut "$SMPROGRAMS\Farm Control Server\Farm Control Server.lnk" "$INSTDIR\bin\launcher.exe"
|
||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
!macro removeDesktopShortcut
|
!macro removeDesktopShortcut
|
||||||
@ -21,10 +21,10 @@
|
|||||||
DeleteRegKey HKCR "farmcontrolserver"
|
DeleteRegKey HKCR "farmcontrolserver"
|
||||||
WriteRegStr HKCR "farmcontrolserver" "" "URL:farmcontrolserver"
|
WriteRegStr HKCR "farmcontrolserver" "" "URL:farmcontrolserver"
|
||||||
WriteRegStr HKCR "farmcontrolserver" "URL Protocol" ""
|
WriteRegStr HKCR "farmcontrolserver" "URL Protocol" ""
|
||||||
WriteRegStr HKCR "farmcontrolserver\DefaultIcon" "" "$INSTDIR\bin\farmcontrol-server.exe"
|
WriteRegStr HKCR "farmcontrolserver\DefaultIcon" "" "$INSTDIR\bin\launcher.exe"
|
||||||
WriteRegStr HKCR "farmcontrolserver\shell" "" ""
|
WriteRegStr HKCR "farmcontrolserver\shell" "" ""
|
||||||
WriteRegStr HKCR "farmcontrolserver\shell\Open" "" ""
|
WriteRegStr HKCR "farmcontrolserver\shell\Open" "" ""
|
||||||
WriteRegStr HKCR "farmcontrolserver\shell\Open\command" "" '"$INSTDIR\bin\farmcontrol-server.exe" "%1"'
|
WriteRegStr HKCR "farmcontrolserver\shell\Open\command" "" '"$INSTDIR\bin\launcher.exe" "%1"'
|
||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
!macro customUnInstall
|
!macro customUnInstall
|
||||||
|
|||||||
@ -67,9 +67,9 @@ if ($stagedAppBytes -lt 5242880) {
|
|||||||
throw "Staged application is only $([math]::Round($stagedAppBytes / 1KB, 1)) KiB. Expected a full Electrobun app bundle before building the installer."
|
throw "Staged application is only $([math]::Round($stagedAppBytes / 1KB, 1)) KiB. Expected a full Electrobun app bundle before building the installer."
|
||||||
}
|
}
|
||||||
|
|
||||||
$requiredExe = Join-Path $stagingAppDir "bin\farmcontrol-server.exe"
|
$requiredExe = Join-Path $stagingAppDir "bin\launcher.exe"
|
||||||
if (-not (Test-Path $requiredExe)) {
|
if (-not (Test-Path $requiredExe)) {
|
||||||
throw "Staged application is missing bin\farmcontrol-server.exe"
|
throw "Staged application is missing bin\launcher.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
Copy-Item -LiteralPath $nsiPath -Destination (Join-Path $workDir "farmcontrol-server.nsi")
|
Copy-Item -LiteralPath $nsiPath -Destination (Join-Path $workDir "farmcontrol-server.nsi")
|
||||||
@ -78,6 +78,7 @@ Copy-Item -LiteralPath $installerInclude -Destination (Join-Path $workDir "insta
|
|||||||
$iconPath = Join-Path $rootDir "assets\icon.ico"
|
$iconPath = Join-Path $rootDir "assets\icon.ico"
|
||||||
if (Test-Path $iconPath) {
|
if (Test-Path $iconPath) {
|
||||||
Copy-Item -LiteralPath $iconPath -Destination (Join-Path $workDir "icon.ico") -Force
|
Copy-Item -LiteralPath $iconPath -Destination (Join-Path $workDir "icon.ico") -Force
|
||||||
|
Write-Host "Using custom installer icon from $iconPath"
|
||||||
}
|
}
|
||||||
|
|
||||||
$makensis = Find-Makensis
|
$makensis = Find-Makensis
|
||||||
|
|||||||
@ -1,170 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -20,7 +20,6 @@ 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 { prepareWindowsAppBinaries } from "./prepare-windows-app-binaries.mjs";
|
|
||||||
|
|
||||||
const rootDir = path.resolve(
|
const rootDir = path.resolve(
|
||||||
path.dirname(fileURLToPath(import.meta.url)),
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
@ -581,7 +580,6 @@ async function main() {
|
|||||||
installerFiles.setupArchive,
|
installerFiles.setupArchive,
|
||||||
platformDir,
|
platformDir,
|
||||||
);
|
);
|
||||||
await prepareWindowsAppBinaries(appDir);
|
|
||||||
|
|
||||||
let published;
|
let published;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,124 +0,0 @@
|
|||||||
import {
|
|
||||||
cpSync,
|
|
||||||
existsSync,
|
|
||||||
readFileSync,
|
|
||||||
renameSync,
|
|
||||||
rmSync,
|
|
||||||
writeFileSync,
|
|
||||||
} from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
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)), "..");
|
|
||||||
|
|
||||||
/** User-facing Windows app entry point (Electrobun launcher). */
|
|
||||||
export const WINDOWS_APP_EXECUTABLE = "farmcontrol-server.exe";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bun runtime filename embedded in the Electrobun launcher binary.
|
|
||||||
* Must stay exactly 7 characters (same length as "bun.exe") for binary patching.
|
|
||||||
*/
|
|
||||||
export const WINDOWS_RUNTIME_EXECUTABLE = "farm.exe";
|
|
||||||
|
|
||||||
const LAUNCHER_BINARY = "launcher.exe";
|
|
||||||
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 patchLauncherRuntimeReference(launcherPath) {
|
|
||||||
const binary = readFileSync(launcherPath);
|
|
||||||
const index = binary.indexOf(LAUNCHER_RUNTIME_REFERENCE);
|
|
||||||
if (index === -1) {
|
|
||||||
if (binary.includes(PATCHED_RUNTIME_REFERENCE)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
`prepare-windows-app-binaries: could not find ${LEGACY_RUNTIME_BINARY} reference in ${launcherPath}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PATCHED_RUNTIME_REFERENCE.copy(binary, index);
|
|
||||||
writeFileSync(launcherPath, binary);
|
|
||||||
console.log(
|
|
||||||
`prepare-windows-app-binaries: patched ${path.basename(launcherPath)} to spawn ${WINDOWS_RUNTIME_EXECUTABLE}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renameWindowsRuntime(binDir) {
|
|
||||||
const legacyRuntimePath = path.join(binDir, LEGACY_RUNTIME_BINARY);
|
|
||||||
const runtimePath = path.join(binDir, WINDOWS_RUNTIME_EXECUTABLE);
|
|
||||||
|
|
||||||
if (existsSync(runtimePath)) {
|
|
||||||
return runtimePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(legacyRuntimePath)) {
|
|
||||||
throw new Error(
|
|
||||||
`prepare-windows-app-binaries: neither ${LEGACY_RUNTIME_BINARY} nor ${WINDOWS_RUNTIME_EXECUTABLE} found in ${binDir}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renameSync(legacyRuntimePath, runtimePath);
|
|
||||||
console.log(
|
|
||||||
`prepare-windows-app-binaries: renamed ${LEGACY_RUNTIME_BINARY} -> ${WINDOWS_RUNTIME_EXECUTABLE}`,
|
|
||||||
);
|
|
||||||
return runtimePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBrandedAppExecutable(binDir) {
|
|
||||||
const launcherPath = path.join(binDir, LAUNCHER_BINARY);
|
|
||||||
const appExecutablePath = path.join(binDir, WINDOWS_APP_EXECUTABLE);
|
|
||||||
|
|
||||||
if (!existsSync(launcherPath)) {
|
|
||||||
if (existsSync(appExecutablePath)) {
|
|
||||||
patchLauncherRuntimeReference(appExecutablePath);
|
|
||||||
return appExecutablePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
`prepare-windows-app-binaries: ${LAUNCHER_BINARY} not found in ${binDir}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
patchLauncherRuntimeReference(launcherPath);
|
|
||||||
cpSync(launcherPath, appExecutablePath);
|
|
||||||
rmSync(launcherPath, { force: true });
|
|
||||||
console.log(
|
|
||||||
`prepare-windows-app-binaries: created ${WINDOWS_APP_EXECUTABLE} and removed ${LAUNCHER_BINARY}`,
|
|
||||||
);
|
|
||||||
return appExecutablePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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") {
|
|
||||||
console.log("prepare-windows-app-binaries: skipped (not Windows)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const binDir = path.join(appDir, "bin");
|
|
||||||
if (!existsSync(binDir)) {
|
|
||||||
throw new Error(`prepare-windows-app-binaries: bin directory not found: ${binDir}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
await embedWindowsAppIcons([appExecutablePath, runtimePath], {
|
|
||||||
icon: DEFAULT_WINDOWS_ICON,
|
|
||||||
cleanupTemp: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user