From 6f3c312f6d8bce7e70f3a470cfb550875b308dcb Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 1 Aug 2026 21:30:11 +0100 Subject: [PATCH] Implement NSIS installer for Farm Control Server and update build scripts - Added a new NSIS installer script (farmcontrol-server.nsi) for packaging the Farm Control Server on Windows, replacing the previous MSI approach. - Updated the build process to utilize the new NSIS script, including changes in the finalize-desktop-artifacts.mjs to reflect the new installer format. - Removed the obsolete MSI build script (build-windows-msi.ps1) and related files to streamline the packaging process. - Updated the README to reflect changes in the post-package process for Windows installers. --- packaging/windows/farmcontrol-server.nsi | 88 ++++++++++++++++++++++ packaging/windows/farmcontrol-server.wxs | 56 -------------- packaging/windows/installer.nsh | 14 ++++ readme.md | 2 +- scripts/build-windows-msi.ps1 | 95 ------------------------ scripts/build-windows-nsis.ps1 | 92 +++++++++++++++++++++++ scripts/finalize-desktop-artifacts.mjs | 22 +++--- 7 files changed, 207 insertions(+), 162 deletions(-) create mode 100644 packaging/windows/farmcontrol-server.nsi delete mode 100644 packaging/windows/farmcontrol-server.wxs create mode 100644 packaging/windows/installer.nsh delete mode 100644 scripts/build-windows-msi.ps1 create mode 100644 scripts/build-windows-nsis.ps1 diff --git a/packaging/windows/farmcontrol-server.nsi b/packaging/windows/farmcontrol-server.nsi new file mode 100644 index 0000000..6a742d1 --- /dev/null +++ b/packaging/windows/farmcontrol-server.nsi @@ -0,0 +1,88 @@ +!include "MUI2.nsh" +!include "installer.nsh" + +!ifndef OUTFILE + !define OUTFILE "farmcontrol-server-installer.exe" +!endif + +!ifndef VERSION + !define VERSION "1.0.0" +!endif + +!ifndef SETUP_EXE_NAME + !define SETUP_EXE_NAME "Farm Control Server-Setup.exe" +!endif + +!ifndef SETUP_ARCHIVE_NAME + !define SETUP_ARCHIVE_NAME "Farm Control Server-Setup.tar.zst" +!endif + +!ifndef SETUP_METADATA_NAME + !define SETUP_METADATA_NAME "Farm Control Server-Setup.metadata.json" +!endif + +Name "Farm Control Server" +OutFile "${OUTFILE}" +InstallDir "$PROGRAMFILES64\Farm Control Server" +InstallDirRegKey HKLM "Software\Tom Butcher\Farm Control Server" "InstallDir" +RequestExecutionLevel admin + +!define MUI_ABORTWARNING +!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico" +!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico" + +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES +!insertmacro MUI_LANGUAGE "English" + +Section "Farm Control Server" SecMain + SetOutPath $INSTDIR + + File "${SETUP_EXE_NAME}" + File "${SETUP_ARCHIVE_NAME}" + File "${SETUP_METADATA_NAME}" + + DetailPrint "Running Farm Control Server setup..." + ExecWait '"$INSTDIR\${SETUP_EXE_NAME}" /S' $0 + StrCmp $0 "0" setup_ok setup_failed + + setup_failed: + MessageBox MB_ICONSTOP "Farm Control Server setup failed (exit code $0)." + Abort + + setup_ok: + !insertmacro customInstall + + WriteRegStr HKLM "Software\Tom Butcher\Farm Control Server" "InstallDir" $INSTDIR + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control Server" \ + "DisplayName" "Farm Control Server" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control Server" \ + "DisplayVersion" "${VERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control Server" \ + "Publisher" "Tom Butcher" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control Server" \ + "UninstallString" "$INSTDIR\Uninstall.exe" + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control Server" \ + "NoModify" 1 + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control Server" \ + "NoRepair" 1 + + WriteUninstaller "$INSTDIR\Uninstall.exe" +SectionEnd + +Section "Uninstall" + !insertmacro customUnInstall + + Delete "$INSTDIR\${SETUP_EXE_NAME}" + Delete "$INSTDIR\${SETUP_ARCHIVE_NAME}" + Delete "$INSTDIR\${SETUP_METADATA_NAME}" + Delete "$INSTDIR\launcher.exe" + Delete "$INSTDIR\Uninstall.exe" + + RMDir /r "$INSTDIR" + + DeleteRegKey HKLM "Software\Tom Butcher\Farm Control Server" + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control Server" +SectionEnd diff --git a/packaging/windows/farmcontrol-server.wxs b/packaging/windows/farmcontrol-server.wxs deleted file mode 100644 index 185408b..0000000 --- a/packaging/windows/farmcontrol-server.wxs +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NOT Installed - - - diff --git a/packaging/windows/installer.nsh b/packaging/windows/installer.nsh new file mode 100644 index 0000000..62ab3fc --- /dev/null +++ b/packaging/windows/installer.nsh @@ -0,0 +1,14 @@ +!macro customInstall + DetailPrint "Register farmcontrolserver URI Handler" + DeleteRegKey HKCR "farmcontrolserver" + WriteRegStr HKCR "farmcontrolserver" "" "URL:farmcontrolserver" + WriteRegStr HKCR "farmcontrolserver" "URL Protocol" "" + WriteRegStr HKCR "farmcontrolserver\DefaultIcon" "" "$INSTDIR\launcher.exe" + WriteRegStr HKCR "farmcontrolserver\shell" "" "" + WriteRegStr HKCR "farmcontrolserver\shell\Open" "" "" + WriteRegStr HKCR "farmcontrolserver\shell\Open\command" "" '"$INSTDIR\launcher.exe" "%1"' +!macroend + +!macro customUnInstall + DeleteRegKey HKCR "farmcontrolserver" +!macroend diff --git a/readme.md b/readme.md index 4d9b2f8..01496ae 100644 --- a/readme.md +++ b/readme.md @@ -40,7 +40,7 @@ A Bun application that bridges communication between Farm Control cloud services Build configuration lives in `electrobun.config.ts` and follows the [Electrobun build configuration](https://framework.blackboard.sh/electrobun/apis/cli/build-configuration/) model: - **preBuild** — validates sources, writes build metadata, and runs the Vite renderer build -- **postPackage** — normalizes release artifacts (DMG/PKG on macOS, MSI on Windows) +- **postPackage** — normalizes release artifacts (DMG/PKG on macOS, NSIS installer on Windows) Commands: diff --git a/scripts/build-windows-msi.ps1 b/scripts/build-windows-msi.ps1 deleted file mode 100644 index 0859fd4..0000000 --- a/scripts/build-windows-msi.ps1 +++ /dev/null @@ -1,95 +0,0 @@ -param( - [Parameter(Mandatory = $true)] - [string]$SetupExe, - - [Parameter(Mandatory = $true)] - [string]$SetupArchive, - - [Parameter(Mandatory = $true)] - [string]$SetupMetadata, - - [Parameter(Mandatory = $true)] - [string]$OutputMsi, - - [Parameter(Mandatory = $true)] - [string]$Version -) - -$ErrorActionPreference = "Stop" - -function Find-WixTool($toolName) { - $command = Get-Command $toolName -ErrorAction SilentlyContinue - if ($command) { - return $command.Source - } - - $searchRoots = @( - "${env:ProgramFiles(x86)}\WiX Toolset v3.14\bin", - "${env:ProgramFiles(x86)}\WiX Toolset v3.11\bin", - "${env:ProgramFiles}\WiX Toolset v3.14\bin", - "${env:ProgramFiles}\WiX Toolset v3.11\bin" - ) - - foreach ($root in $searchRoots) { - $candidate = Join-Path $root "$toolName.exe" - if (Test-Path $candidate) { - return $candidate - } - } - - throw "Could not find $toolName. Install WiX Toolset v3.11 or v3.14 on the Windows build agent." -} - -function ConvertTo-WixVersion([string]$rawVersion) { - $parts = $rawVersion.Split('.') - while ($parts.Count -lt 4) { - $parts += '0' - } - return ($parts[0..3] -join '.') -} - -$rootDir = Split-Path -Parent $PSScriptRoot -$wxsPath = Join-Path $rootDir "packaging/windows/farmcontrol-server.wxs" -$workDir = Join-Path $env:TEMP "farmcontrol-server-msi" -$wixObj = Join-Path $workDir "farmcontrol-server.wixobj" - -if (Test-Path $workDir) { - Remove-Item $workDir -Recurse -Force -} -New-Item -ItemType Directory -Path $workDir | Out-Null - -$candle = Find-WixTool "candle" -$light = Find-WixTool "light" - -$wixVersion = ConvertTo-WixVersion $Version -$setupExePath = (Resolve-Path -LiteralPath $SetupExe).Path -$setupArchivePath = (Resolve-Path -LiteralPath $SetupArchive).Path -$setupMetadataPath = (Resolve-Path -LiteralPath $SetupMetadata).Path - -$candleArgs = @( - '-nologo' - '-out' - $wixObj - "-dVersion=$wixVersion" - "-dSetupExe=$setupExePath" - "-dSetupArchive=$setupArchivePath" - "-dSetupMetadata=$setupMetadataPath" - $wxsPath -) - -& $candle @candleArgs - -$lightArgs = @( - '-nologo' - '-out' - $OutputMsi - $wixObj -) - -& $light @lightArgs - -if (-not (Test-Path $OutputMsi)) { - throw "MSI was not created at $OutputMsi" -} - -Write-Host "Created MSI at $OutputMsi" diff --git a/scripts/build-windows-nsis.ps1 b/scripts/build-windows-nsis.ps1 new file mode 100644 index 0000000..64f6438 --- /dev/null +++ b/scripts/build-windows-nsis.ps1 @@ -0,0 +1,92 @@ +param( + [Parameter(Mandatory = $true)] + [string]$SetupExe, + + [Parameter(Mandatory = $true)] + [string]$SetupArchive, + + [Parameter(Mandatory = $true)] + [string]$SetupMetadata, + + [Parameter(Mandatory = $true)] + [string]$OutputExe, + + [Parameter(Mandatory = $true)] + [string]$Version +) + +$ErrorActionPreference = "Stop" + +function Find-Makensis() { + $command = Get-Command makensis -ErrorAction SilentlyContinue + if ($command) { + return $command.Source + } + + $searchRoots = @( + "${env:ProgramFiles(x86)}\NSIS\makensis.exe", + "${env:ProgramFiles}\NSIS\makensis.exe" + ) + + foreach ($candidate in $searchRoots) { + if (Test-Path $candidate) { + return $candidate + } + } + + throw "Could not find makensis. Install NSIS 3.x on the Windows build agent." +} + +$rootDir = Split-Path -Parent $PSScriptRoot +$nsiPath = Join-Path $rootDir "packaging/windows/farmcontrol-server.nsi" +$installerInclude = Join-Path $rootDir "packaging/windows/installer.nsh" +$workDir = Join-Path $env:TEMP "farmcontrol-server-nsis" + +if (Test-Path $workDir) { + Remove-Item $workDir -Recurse -Force +} +New-Item -ItemType Directory -Path $workDir | Out-Null + +$setupExePath = (Resolve-Path -LiteralPath $SetupExe).Path +$setupArchivePath = (Resolve-Path -LiteralPath $SetupArchive).Path +$setupMetadataPath = (Resolve-Path -LiteralPath $SetupMetadata).Path + +$setupExeName = Split-Path $setupExePath -Leaf +$setupArchiveName = Split-Path $setupArchivePath -Leaf +$setupMetadataName = Split-Path $setupMetadataPath -Leaf + +Copy-Item -LiteralPath $setupExePath -Destination (Join-Path $workDir $setupExeName) +Copy-Item -LiteralPath $setupArchivePath -Destination (Join-Path $workDir $setupArchiveName) +Copy-Item -LiteralPath $setupMetadataPath -Destination (Join-Path $workDir $setupMetadataName) +Copy-Item -LiteralPath $nsiPath -Destination (Join-Path $workDir "farmcontrol-server.nsi") +Copy-Item -LiteralPath $installerInclude -Destination (Join-Path $workDir "installer.nsh") + +$makensis = Find-Makensis +$outputExePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputExe) +$outputDir = Split-Path $outputExePath -Parent +if (-not (Test-Path $outputDir)) { + New-Item -ItemType Directory -Path $outputDir | Out-Null +} + +$makensisArgs = @( + "/NOCD" + "/DOUTFILE=$outputExePath" + "/DVERSION=$Version" + "/DSETUP_EXE_NAME=$setupExeName" + "/DSETUP_ARCHIVE_NAME=$setupArchiveName" + "/DSETUP_METADATA_NAME=$setupMetadataName" + (Join-Path $workDir "farmcontrol-server.nsi") +) + +Push-Location $workDir +try { + & $makensis @makensisArgs +} finally { + Pop-Location +} + +if (-not (Test-Path $outputExePath)) { + throw "NSIS installer was not created at $outputExePath" +} + +Write-Host "Created NSIS installer at $outputExePath" diff --git a/scripts/finalize-desktop-artifacts.mjs b/scripts/finalize-desktop-artifacts.mjs index 2eee80f..e8f445a 100644 --- a/scripts/finalize-desktop-artifacts.mjs +++ b/scripts/finalize-desktop-artifacts.mjs @@ -214,11 +214,11 @@ function buildMacPkg(appBundlePath, arch) { return pkgPath; } -function buildWindowsMsi(installerFiles, arch) { - const scriptPath = path.join(rootDir, "scripts/build-windows-msi.ps1"); - const msiPath = path.join( +function buildWindowsNsis(installerFiles, arch) { + const scriptPath = path.join(rootDir, "scripts/build-windows-nsis.ps1"); + const exePath = path.join( artifactDir, - getReleaseArtifactName(version, arch, "msi"), + getReleaseArtifactName(version, arch, "exe"), ); const powershell = process.env.SystemRoot @@ -245,8 +245,8 @@ function buildWindowsMsi(installerFiles, arch) { installerFiles.setupArchive, "-SetupMetadata", installerFiles.setupMetadata, - "-OutputMsi", - msiPath, + "-OutputExe", + exePath, "-Version", version, ], @@ -254,11 +254,13 @@ function buildWindowsMsi(installerFiles, arch) { ); if (result.status !== 0) { - throw new Error(`build-windows-msi.ps1 failed with exit code ${result.status ?? 1}`); + throw new Error( + `build-windows-nsis.ps1 failed with exit code ${result.status ?? 1}`, + ); } - console.log(`Published ${msiPath}`); - return msiPath; + console.log(`Published ${exePath}`); + return exePath; } if (buildEnv === "dev") { @@ -293,7 +295,7 @@ if (targetOs === "macos") { ); } - const published = [buildWindowsMsi(installerFiles, arch)]; + const published = [buildWindowsNsis(installerFiles, arch)]; if (installerFiles.setupZip) { published.push(publishArtifact(installerFiles.setupZip, arch, "zip"));