diff --git a/electrobun.config.ts b/electrobun.config.ts index 0108c8c..7f8c458 100644 --- a/electrobun.config.ts +++ b/electrobun.config.ts @@ -68,7 +68,7 @@ export default { }, win: { bundleCEF: false, - icon: "assets/icon.ico", + icon: "assets/icon.iconset/icon_256x256.png", }, }, scripts: { diff --git a/package.json b/package.json index 1047f0e..ad85ba0 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "jest": "^30.4.2", "jimp": "^1.6.1", "png-to-ico": "^2.1.8", - "rcedit": "^4.0.1", "prop-types": "^15.8.1", "react": "^19.2.6", "react-dom": "^19.2.6", diff --git a/packaging/windows/installer.nsh b/packaging/windows/installer.nsh index 22e304f..9200c40 100644 --- a/packaging/windows/installer.nsh +++ b/packaging/windows/installer.nsh @@ -1,10 +1,10 @@ !macro createDesktopShortcut - CreateShortCut "$DESKTOP\Farm Control Server.lnk" "$INSTDIR\bin\farmcontrol-server.exe" + CreateShortCut "$DESKTOP\Farm Control Server.lnk" "$INSTDIR\bin\launcher.exe" !macroend !macro createStartMenuShortcut 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 !macro removeDesktopShortcut @@ -21,10 +21,10 @@ DeleteRegKey HKCR "farmcontrolserver" WriteRegStr HKCR "farmcontrolserver" "" "URL:farmcontrolserver" 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\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 !macro customUnInstall diff --git a/scripts/build-windows-nsis.ps1 b/scripts/build-windows-nsis.ps1 index 0f21449..854c79e 100644 --- a/scripts/build-windows-nsis.ps1 +++ b/scripts/build-windows-nsis.ps1 @@ -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." } -$requiredExe = Join-Path $stagingAppDir "bin\farmcontrol-server.exe" +$requiredExe = Join-Path $stagingAppDir "bin\launcher.exe" 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") @@ -78,6 +78,7 @@ Copy-Item -LiteralPath $installerInclude -Destination (Join-Path $workDir "insta $iconPath = Join-Path $rootDir "assets\icon.ico" if (Test-Path $iconPath) { Copy-Item -LiteralPath $iconPath -Destination (Join-Path $workDir "icon.ico") -Force + Write-Host "Using custom installer icon from $iconPath" } $makensis = Find-Makensis diff --git a/scripts/embed-windows-exe-icon.mjs b/scripts/embed-windows-exe-icon.mjs deleted file mode 100644 index 1ecfe9d..0000000 --- a/scripts/embed-windows-exe-icon.mjs +++ /dev/null @@ -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 [--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); - }); -} diff --git a/scripts/finalize-desktop-artifacts.mjs b/scripts/finalize-desktop-artifacts.mjs index 9857487..7c64d37 100644 --- a/scripts/finalize-desktop-artifacts.mjs +++ b/scripts/finalize-desktop-artifacts.mjs @@ -20,7 +20,6 @@ import { expandWindowsAppFromArchive, } from "./expand-windows-installer.mjs"; import { codesignMacAppBundle } from "./codesign-macos-app.mjs"; -import { prepareWindowsAppBinaries } from "./prepare-windows-app-binaries.mjs"; const rootDir = path.resolve( path.dirname(fileURLToPath(import.meta.url)), @@ -581,7 +580,6 @@ async function main() { installerFiles.setupArchive, platformDir, ); - await prepareWindowsAppBinaries(appDir); let published; try { diff --git a/scripts/prepare-windows-app-binaries.mjs b/scripts/prepare-windows-app-binaries.mjs deleted file mode 100644 index 9adfe63..0000000 --- a/scripts/prepare-windows-app-binaries.mjs +++ /dev/null @@ -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, - }); -}