Refactor Windows installer scripts and enhance Electron navigation

- Added macros for uninstalling previous Farm Control installations in the NSIS installer.
- Updated the MSI wrapped configuration to include custom actions for handling previous installations.
- Improved the Electron navigation by modifying CSS classes for better drag-and-drop functionality.
- Enhanced build scripts to include the WixUtilExtension for improved MSI packaging.
This commit is contained in:
Tom Butcher 2026-08-02 01:49:37 +01:00
parent 37fe489b07
commit f90ef10b78
8 changed files with 308 additions and 209 deletions

View File

@ -104,14 +104,8 @@
line-height: 0.7; line-height: 0.7;
} }
.electron-navigation-wrapper { .electron-navigation-wrapper.electrobun-webkit-app-region-drag {
-webkit-app-region: drag; user-select: none;
}
.electron-navigation-wrapper li,
.electron-navigation-wrapper button,
.electron-navigation-wrapper .ant-tag {
-webkit-app-region: no-drag;
} }
.electron-navigation { .electron-navigation {

View File

@ -43,6 +43,7 @@ Function .onInit
${If} ${Silent} ${If} ${Silent}
SetAutoClose true SetAutoClose true
${EndIf} ${EndIf}
!insertmacro uninstallPreviousFarmControl
FunctionEnd FunctionEnd
Section "Farm Control" SecMain Section "Farm Control" SecMain

View File

@ -1,3 +1,33 @@
!macro quitFarmControl
DetailPrint "Stopping running Farm Control processes..."
ExecWait 'taskkill /F /IM launcher.exe /T' $R0
!macroend
!macro uninstallPreviousFarmControl
ClearErrors
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" "UninstallString"
StrCmp $R0 "" check_install_dir run_uninstall
check_install_dir:
ReadRegStr $R0 HKLM "Software\Tom Butcher\Farm Control" "InstallDir"
StrCmp $R0 "" check_default_dir
StrCpy $R0 "$R0\Uninstall.exe"
Goto run_uninstall
check_default_dir:
StrCpy $R0 "$PROGRAMFILES64\Farm Control\Uninstall.exe"
IfFileExists $R0 run_uninstall done_uninstall
run_uninstall:
IfFileExists $R0 0 done_uninstall
DetailPrint "Removing previous Farm Control installation..."
!insertmacro quitFarmControl
ExecWait '$R0 /S' $R1
DetailPrint "Previous installation removed (exit code: $R1)"
done_uninstall:
!macroend
!macro createDesktopShortcut !macro createDesktopShortcut
CreateShortCut "$DESKTOP\Farm Control.lnk" "$INSTDIR\bin\launcher.exe" CreateShortCut "$DESKTOP\Farm Control.lnk" "$INSTDIR\bin\launcher.exe"
!macroend !macroend

View File

@ -23,8 +23,42 @@
<Property Id="DISABLEADVTSHORTCUTS" Value="1" /> <Property Id="DISABLEADVTSHORTCUTS" Value="1" />
<Property Id="MSIINSTALLPERUSER" Value="1" /> <Property Id="MSIINSTALLPERUSER" Value="1" />
<Property Id="PREVIOUS_UNINSTALL_CMD">
<RegistrySearch
Id="PreviousUninstallCmdSearch"
Root="HKLM"
Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control"
Name="UninstallString"
Type="raw" />
</Property>
<Property Id="PREVIOUS_INSTALL_DIR">
<RegistrySearch
Id="PreviousInstallDirSearch"
Root="HKLM"
Key="Software\Tom Butcher\Farm Control"
Name="InstallDir"
Type="directory" />
</Property>
<Binary Id="WrappedExe" SourceFile="__SETUP_EXE__" /> <Binary Id="WrappedExe" SourceFile="__SETUP_EXE__" />
<CustomAction
Id="KillFarmControl"
BinaryKey="WixCA"
DllEntry="WixQuietExec"
Execute="deferred"
Impersonate="no"
Return="ignore" />
<CustomAction
Id="UninstallPrevious"
BinaryKey="WixCA"
DllEntry="WixQuietExec"
Execute="deferred"
Impersonate="no"
Return="ignore" />
<CustomAction <CustomAction
Id="RunInstaller" Id="RunInstaller"
BinaryKey="WrappedExe" BinaryKey="WrappedExe"
@ -45,8 +79,33 @@
<ComponentRef Id="EmptyComponent" /> <ComponentRef Id="EmptyComponent" />
</Feature> </Feature>
<CustomAction
Id="SetKillFarmControl"
Property="QtKillFarmControl"
Value="taskkill.exe /F /IM launcher.exe /T"
Execute="immediate" />
<CustomAction
Id="SetUninstallPreviousFromRegistry"
Property="QtUninstallPrevious"
Value="&quot;[PREVIOUS_UNINSTALL_CMD]&quot; /S"
Execute="immediate" />
<CustomAction
Id="SetUninstallPreviousFromInstallDir"
Property="QtUninstallPrevious"
Value="&quot;[PREVIOUS_INSTALL_DIR]Uninstall.exe&quot; /S"
Execute="immediate" />
<InstallExecuteSequence> <InstallExecuteSequence>
<Custom Action="RunInstaller" After="ProcessComponents">NOT Installed</Custom> <Custom Action="SetKillFarmControl" Before="KillFarmControl">NOT Installed</Custom>
<Custom Action="KillFarmControl" After="SetKillFarmControl">NOT Installed</Custom>
<Custom Action="SetUninstallPreviousFromRegistry" After="KillFarmControl">NOT Installed AND PREVIOUS_UNINSTALL_CMD</Custom>
<Custom Action="SetUninstallPreviousFromInstallDir" After="KillFarmControl">NOT Installed AND NOT PREVIOUS_UNINSTALL_CMD AND PREVIOUS_INSTALL_DIR</Custom>
<Custom Action="UninstallPrevious" After="SetUninstallPreviousFromRegistry">NOT Installed AND (PREVIOUS_UNINSTALL_CMD OR PREVIOUS_INSTALL_DIR)</Custom>
<Custom Action="UninstallPrevious" After="SetUninstallPreviousFromInstallDir">NOT Installed AND (PREVIOUS_UNINSTALL_CMD OR PREVIOUS_INSTALL_DIR)</Custom>
<Custom Action="RunInstaller" After="UninstallPrevious">NOT Installed AND (PREVIOUS_UNINSTALL_CMD OR PREVIOUS_INSTALL_DIR)</Custom>
<Custom Action="RunInstaller" After="KillFarmControl">NOT Installed AND NOT PREVIOUS_UNINSTALL_CMD AND NOT PREVIOUS_INSTALL_DIR</Custom>
</InstallExecuteSequence> </InstallExecuteSequence>
</Product> </Product>
</Wix> </Wix>

View File

@ -103,12 +103,12 @@ $wixObj = Join-Path $workDir "project.wixobj"
Push-Location $workDir Push-Location $workDir
try { try {
& $wix.Candle "-arch" "x64" $projectWxs & $wix.Candle "-arch" "x64" "-ext" "WixUtilExtension" $projectWxs
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "candle.exe failed with exit code $LASTEXITCODE" throw "candle.exe failed with exit code $LASTEXITCODE"
} }
& $wix.Light "-out" $outputMsiPath "-spdb" "-sw1076" $wixObj & $wix.Light "-out" $outputMsiPath "-spdb" "-sw1076" "-ext" "WixUtilExtension" $wixObj
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "light.exe failed with exit code $LASTEXITCODE" throw "light.exe failed with exit code $LASTEXITCODE"
} }

View File

@ -3,34 +3,34 @@ import {
existsSync, existsSync,
mkdirSync, mkdirSync,
readFileSync, readFileSync,
writeFileSync, writeFileSync
} from "node:fs"; } from 'node:fs'
import path from "node:path"; import path from 'node:path'
import { fileURLToPath } from "node:url"; import { fileURLToPath } from 'node:url'
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const electrobunDir = path.join(rootDir, "node_modules/electrobun"); const electrobunDir = path.join(rootDir, 'node_modules/electrobun')
const sharedSrcDir = path.join(electrobunDir, "src/shared"); const sharedSrcDir = path.join(electrobunDir, 'src/shared')
const sharedDistDir = path.join(electrobunDir, "dist/api/shared"); const sharedDistDir = path.join(electrobunDir, 'dist/api/shared')
if (!existsSync(sharedDistDir)) { if (!existsSync(sharedDistDir)) {
console.error("patch-electrobun-src: electrobun dist/api/shared not found"); console.error('patch-electrobun-src: electrobun dist/api/shared not found')
process.exit(1); process.exit(1)
} }
mkdirSync(sharedSrcDir, { recursive: true }); mkdirSync(sharedSrcDir, { recursive: true })
for (const fileName of [ for (const fileName of [
"cef-version.ts", 'cef-version.ts',
"bun-version.ts", 'bun-version.ts',
"electrobun-version.ts", 'electrobun-version.ts',
"naming.ts", 'naming.ts',
"rpc.ts", 'rpc.ts'
]) { ]) {
const source = path.join(sharedDistDir, fileName); const source = path.join(sharedDistDir, fileName)
const destination = path.join(sharedSrcDir, fileName); const destination = path.join(sharedSrcDir, fileName)
if (existsSync(source)) { if (existsSync(source)) {
cpSync(source, destination); cpSync(source, destination)
} }
} }
@ -109,14 +109,14 @@ export function getPlatformOS(): SupportedOS {
export function getPlatformArch(): SupportedArch { export function getPlatformArch(): SupportedArch {
return ARCH; return ARCH;
} }
`; `
writeFileSync(path.join(sharedSrcDir, "platform.ts"), platformPatch); writeFileSync(path.join(sharedSrcDir, 'platform.ts'), platformPatch)
const templatesDir = path.join(electrobunDir, "src/cli/templates"); const templatesDir = path.join(electrobunDir, 'src/cli/templates')
mkdirSync(templatesDir, { recursive: true }); mkdirSync(templatesDir, { recursive: true })
writeFileSync( writeFileSync(
path.join(templatesDir, "embedded.ts"), path.join(templatesDir, 'embedded.ts'),
`export function getTemplateNames() { `export function getTemplateNames() {
return []; return [];
} }
@ -124,8 +124,8 @@ writeFileSync(
export function getTemplate(name: string) { export function getTemplate(name: string) {
throw new Error(\`Template not available in patched electrobun CLI: \${name}\`); throw new Error(\`Template not available in patched electrobun CLI: \${name}\`);
} }
`, `
); )
const RCEDIT_RESOLVER_FN = `function resolveProjectRceditPkgPath(projectRoot) { const RCEDIT_RESOLVER_FN = `function resolveProjectRceditPkgPath(projectRoot) {
const candidates = [ const candidates = [
@ -140,35 +140,41 @@ const RCEDIT_RESOLVER_FN = `function resolveProjectRceditPkgPath(projectRoot) {
throw new Error( throw new Error(
"rcedit not found under " + projectRoot + ". Install rcedit in the project (bun add -d rcedit).", "rcedit not found under " + projectRoot + ". Install rcedit in the project (bun add -d rcedit).",
); );
}`; }`
const cliPath = path.join(electrobunDir, "src/cli/index.ts"); const cliPath = path.join(electrobunDir, 'src/cli/index.ts')
let cliSource = readFileSync(cliPath, "utf8"); let cliSource = readFileSync(cliPath, 'utf8')
if (!cliSource.includes("HOST_ARCH")) { if (!cliSource.includes('HOST_ARCH')) {
cliSource = cliSource.replace( cliSource = cliSource.replace(
'import { OS, ARCH } from "../shared/platform";', 'import { OS, ARCH } from "../shared/platform";',
'import { OS, ARCH, HOST_ARCH } from "../shared/platform";', 'import { OS, ARCH, HOST_ARCH } from "../shared/platform";'
); )
cliSource = cliSource.replace( cliSource = cliSource.replace(
"const hostPaths = getPlatformPaths(OS, ARCH);", 'const hostPaths = getPlatformPaths(OS, ARCH);',
"const hostPaths = getPlatformPaths(OS, HOST_ARCH);", 'const hostPaths = getPlatformPaths(OS, HOST_ARCH);'
); )
} }
if (!cliSource.includes("toolPaths")) { if (!cliSource.includes('toolPaths')) {
cliSource = cliSource.replace( cliSource = cliSource.replace(
"const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);", 'const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);',
"const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);\n\t\tconst toolPaths = getPlatformPaths(currentTarget.os, ARCH !== HOST_ARCH ? HOST_ARCH : ARCH);", 'const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);\n\t\tconst toolPaths = getPlatformPaths(currentTarget.os, ARCH !== HOST_ARCH ? HOST_ARCH : ARCH);'
); )
cliSource = cliSource.replaceAll("const zstdPath = targetPaths.ZSTD;", "const zstdPath = toolPaths.ZSTD;"); cliSource = cliSource.replaceAll(
cliSource = cliSource.replaceAll("const bsdiffpath = targetPaths.BSDIFF;", "const bsdiffpath = toolPaths.BSDIFF;"); 'const zstdPath = targetPaths.ZSTD;',
'const zstdPath = toolPaths.ZSTD;'
)
cliSource = cliSource.replaceAll(
'const bsdiffpath = targetPaths.BSDIFF;',
'const bsdiffpath = toolPaths.BSDIFF;'
)
cliSource = cliSource.replace( cliSource = cliSource.replace(
"zigAsarCli = join(targetPaths.BSPATCH).replace(\"bspatch\", \"zig-asar\");", 'zigAsarCli = join(targetPaths.BSPATCH).replace("bspatch", "zig-asar");',
"zigAsarCli = join(toolPaths.BSPATCH).replace(\"bspatch\", \"zig-asar\");", 'zigAsarCli = join(toolPaths.BSPATCH).replace("bspatch", "zig-asar");'
); )
writeFileSync(cliPath, cliSource); writeFileSync(cliPath, cliSource)
cliSource = readFileSync(cliPath, "utf8"); cliSource = readFileSync(cliPath, 'utf8')
} }
if (!cliSource.includes("hookBunBinary")) { if (!cliSource.includes('hookBunBinary')) {
cliSource = cliSource.replace( cliSource = cliSource.replace(
`const hostPaths = getPlatformPaths(OS, HOST_ARCH); `const hostPaths = getPlatformPaths(OS, HOST_ARCH);
@ -178,41 +184,41 @@ if (!cliSource.includes("hookBunBinary")) {
? hostPaths.BUN_BINARY ? hostPaths.BUN_BINARY
: process.execPath; : process.execPath;
const result = Bun.spawnSync([hookBunBinary, hookScript],`, const result = Bun.spawnSync([hookBunBinary, hookScript],`
); )
cliSource = cliSource.replace( cliSource = cliSource.replace(
"console.error(\"Tried to run with bun at:\", hostPaths.BUN_BINARY);", 'console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);',
"console.error(\"Tried to run with bun at:\", hookBunBinary);", 'console.error("Tried to run with bun at:", hookBunBinary);'
); )
writeFileSync(cliPath, cliSource); writeFileSync(cliPath, cliSource)
cliSource = readFileSync(cliPath, "utf8"); cliSource = readFileSync(cliPath, 'utf8')
} }
if (!cliSource.includes("resolveProjectRceditPkgPath")) { if (!cliSource.includes('resolveProjectRceditPkgPath')) {
cliSource = cliSource.replaceAll( cliSource = cliSource.replaceAll(
"const rceditPkgPath = require.resolve(\"rcedit/package.json\");", 'const rceditPkgPath = require.resolve("rcedit/package.json");',
"const rceditPkgPath = resolveProjectRceditPkgPath(projectRoot);", 'const rceditPkgPath = resolveProjectRceditPkgPath(projectRoot);'
); )
cliSource = cliSource.replace( cliSource = cliSource.replace(
"function getPlatformPaths(", 'function getPlatformPaths(',
`${RCEDIT_RESOLVER_FN} `${RCEDIT_RESOLVER_FN}
function getPlatformPaths(`, function getPlatformPaths(`
); )
writeFileSync(cliPath, cliSource); writeFileSync(cliPath, cliSource)
cliSource = readFileSync(cliPath, "utf8"); cliSource = readFileSync(cliPath, 'utf8')
} else { } else {
cliSource = cliSource.replace( cliSource = cliSource.replace(
/function resolveProjectRceditPkgPath\(projectRoot\) \{[\s\S]*?\n\}/m, /function resolveProjectRceditPkgPath\(projectRoot\) \{[\s\S]*?\n\}/m,
RCEDIT_RESOLVER_FN, RCEDIT_RESOLVER_FN
); )
writeFileSync(cliPath, cliSource); writeFileSync(cliPath, cliSource)
} }
const electrobunCjsPath = path.join(electrobunDir, "bin/electrobun.cjs"); const electrobunCjsPath = path.join(electrobunDir, 'bin/electrobun.cjs')
let electrobunCjs = readFileSync(electrobunCjsPath, "utf8"); let electrobunCjs = readFileSync(electrobunCjsPath, 'utf8')
if (!electrobunCjs.includes("farmcontrol-use-patched-cli")) { if (!electrobunCjs.includes('farmcontrol-use-patched-cli')) {
electrobunCjs = electrobunCjs.replace( electrobunCjs = electrobunCjs.replace(
`async function main() { `async function main() {
try { try {
@ -259,19 +265,21 @@ if (!electrobunCjs.includes("farmcontrol-use-patched-cli")) {
const child = spawn(cliPath, args, { const child = spawn(cliPath, args, {
stdio: 'inherit', stdio: 'inherit',
cwd: process.cwd() cwd: process.cwd()
});`, });`
); )
writeFileSync(electrobunCjsPath, electrobunCjs); writeFileSync(electrobunCjsPath, electrobunCjs)
} }
const rceditSrc = path.join(rootDir, "node_modules/rcedit"); const rceditSrc = path.join(rootDir, 'node_modules/rcedit')
const rceditDest = path.join(electrobunDir, "node_modules/rcedit"); const rceditDest = path.join(electrobunDir, 'node_modules/rcedit')
if (existsSync(rceditSrc)) { if (existsSync(rceditSrc)) {
mkdirSync(path.join(electrobunDir, "node_modules"), { recursive: true }); mkdirSync(path.join(electrobunDir, 'node_modules'), { recursive: true })
cpSync(rceditSrc, rceditDest, { recursive: true, force: true }); cpSync(rceditSrc, rceditDest, { recursive: true, force: true })
} }
const markerPath = path.join(electrobunDir, ".farmcontrol-electrobun-patched"); const markerPath = path.join(electrobunDir, '.farmcontrol-electrobun-patched')
writeFileSync(markerPath, `patched-at=${new Date().toISOString()}\n`); writeFileSync(markerPath, `patched-at=${new Date().toISOString()}\n`)
console.log("patch-electrobun-src: electrobun src/shared ready for cross-arch builds"); console.log(
'patch-electrobun-src: electrobun src/shared ready for cross-arch builds'
)

View File

@ -199,6 +199,7 @@ const DashboardNavigation = () => {
</Text> </Text>
</Flex> </Flex>
)} )}
<div className='electrobun-webkit-app-region-no-drag'>
<Menu <Menu
mode='horizontal' mode='horizontal'
className={isElectron ? 'electron-navigation' : null} className={isElectron ? 'electron-navigation' : null}
@ -219,7 +220,9 @@ const DashboardNavigation = () => {
/> />
} }
/> />
</div>
{isMobile && <div style={{ flexGrow: 1 }} />} {isMobile && <div style={{ flexGrow: 1 }} />}
<div className='electrobun-webkit-app-region-no-drag'>
<Flex <Flex
gap={'small'} gap={'small'}
align='center' align='center'
@ -327,6 +330,7 @@ const DashboardNavigation = () => {
</Tag> </Tag>
) : null} ) : null}
</Flex> </Flex>
</div>
{isOtherApp ? <DashboardWindowButtons /> : null} {isOtherApp ? <DashboardWindowButtons /> : null}
</Flex> </Flex>
) )
@ -335,7 +339,7 @@ const DashboardNavigation = () => {
<> <>
{isElectron ? ( {isElectron ? (
<Flex <Flex
className={`ant-menu-horizontal ant-menu-light${!notificationCenterVisible ? ' electron-navigation-wrapper' : ''}`} className={`ant-menu-horizontal ant-menu-light${!notificationCenterVisible ? ' electron-navigation-wrapper electrobun-webkit-app-region-drag' : ''}`}
style={{ lineHeight: '40px', padding: '0 2px 0 2px' }} style={{ lineHeight: '40px', padding: '0 2px 0 2px' }}
> >
{navigationContents} {navigationContents}

View File

@ -39,7 +39,10 @@ const DashboardWindowButtons = () => {
<div style={{ width: '80px' }} /> <div style={{ width: '80px' }} />
) : null ) : null
) : ( ) : (
<div style={{ width: '95px', marginRight: '2.5px' }}> <div
className='electrobun-webkit-app-region-no-drag'
style={{ width: '95px', marginRight: '2.5px' }}
>
<Flex style={{ position: 'relative', zIndex: 9999 }}> <Flex style={{ position: 'relative', zIndex: 9999 }}>
{maximizeButton} {maximizeButton}
{minimizeButton} {minimizeButton}