diff --git a/packaging/windows/farmcontrol-server.nsi b/packaging/windows/farmcontrol-server.nsi index 6a742d1..6aa5ce1 100644 --- a/packaging/windows/farmcontrol-server.nsi +++ b/packaging/windows/farmcontrol-server.nsi @@ -9,16 +9,8 @@ !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" +!ifndef APP_SOURCE_DIR + !define APP_SOURCE_DIR "." !endif Name "Farm Control Server" @@ -39,20 +31,8 @@ RequestExecutionLevel admin Section "Farm Control Server" SecMain SetOutPath $INSTDIR + File /r "${APP_SOURCE_DIR}\*" - 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 @@ -75,12 +55,7 @@ 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" diff --git a/packaging/windows/installer.nsh b/packaging/windows/installer.nsh index 62ab3fc..dbe3458 100644 --- a/packaging/windows/installer.nsh +++ b/packaging/windows/installer.nsh @@ -3,10 +3,10 @@ DeleteRegKey HKCR "farmcontrolserver" WriteRegStr HKCR "farmcontrolserver" "" "URL:farmcontrolserver" WriteRegStr HKCR "farmcontrolserver" "URL Protocol" "" - WriteRegStr HKCR "farmcontrolserver\DefaultIcon" "" "$INSTDIR\launcher.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\launcher.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 64f6438..0a80e3e 100644 --- a/scripts/build-windows-nsis.ps1 +++ b/scripts/build-windows-nsis.ps1 @@ -1,12 +1,6 @@ param( [Parameter(Mandatory = $true)] - [string]$SetupExe, - - [Parameter(Mandatory = $true)] - [string]$SetupArchive, - - [Parameter(Mandatory = $true)] - [string]$SetupMetadata, + [string]$AppDir, [Parameter(Mandatory = $true)] [string]$OutputExe, @@ -47,17 +41,8 @@ if (Test-Path $workDir) { } 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 +$appDirPath = (Resolve-Path -LiteralPath $AppDir).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") @@ -72,9 +57,7 @@ $makensisArgs = @( "/NOCD" "/DOUTFILE=$outputExePath" "/DVERSION=$Version" - "/DSETUP_EXE_NAME=$setupExeName" - "/DSETUP_ARCHIVE_NAME=$setupArchiveName" - "/DSETUP_METADATA_NAME=$setupMetadataName" + "/DAPP_SOURCE_DIR=$appDirPath" (Join-Path $workDir "farmcontrol-server.nsi") ) diff --git a/scripts/expand-windows-installer.mjs b/scripts/expand-windows-installer.mjs new file mode 100644 index 0000000..124de26 --- /dev/null +++ b/scripts/expand-windows-installer.mjs @@ -0,0 +1,123 @@ +import { + existsSync, + mkdirSync, + readdirSync, + rmSync, + statSync, +} from "node:fs"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); + +function findZstdDecompressCommand() { + const systemZstd = spawnSync("zstd", ["--version"], { encoding: "utf8" }); + if (systemZstd.status === 0) { + return { command: "zstd", args: (inputPath, outputPath) => [ + "-d", + "-f", + "-o", + outputPath, + inputPath, + ] }; + } + + const hostArch = process.arch === "arm64" ? "arm64" : "x64"; + const candidates = [ + path.join( + rootDir, + "node_modules/electrobun/dist-win-x64/zig-zstd", + hostArch, + "zig-zstd.exe", + ), + path.join( + rootDir, + "node_modules/electrobun/dist-win-x64/zig-zstd", + "x64", + "zig-zstd.exe", + ), + path.join(rootDir, "node_modules/electrobun/dist-win-x64", "zig-zstd"), + ]; + + for (const candidate of candidates) { + if (existsSync(candidate)) { + return { + command: candidate, + args: (inputPath, outputPath) => [ + "decompress", + "-i", + inputPath, + "-o", + outputPath, + "--no-timing", + ], + }; + } + } + + return null; +} + +function decompressTarZst(inputPath, outputPath) { + const zstd = findZstdDecompressCommand(); + if (!zstd) { + throw new Error( + "expand-windows-installer: zstd not found (install zstd or use electrobun dist binaries)", + ); + } + + const result = spawnSync(zstd.command, zstd.args(inputPath, outputPath), { + stdio: "inherit", + }); + + if (result.status !== 0) { + throw new Error( + `expand-windows-installer: decompression failed (exit ${result.status ?? 1})`, + ); + } +} + +export function expandWindowsAppFromArchive(setupArchivePath, parentDir) { + const resolvedArchive = path.resolve(setupArchivePath); + if (!existsSync(resolvedArchive)) { + throw new Error(`expand-windows-installer: archive not found: ${resolvedArchive}`); + } + + const workDir = path.join(path.resolve(parentDir), ".expanded-app"); + const tarPath = path.join(workDir, "app.tar"); + + rmSync(workDir, { recursive: true, force: true }); + mkdirSync(workDir, { recursive: true }); + + decompressTarZst(resolvedArchive, tarPath); + + const extractTar = spawnSync("tar", ["-xf", tarPath, "-C", workDir], { + stdio: "inherit", + }); + + if (extractTar.status !== 0) { + rmSync(workDir, { recursive: true, force: true }); + throw new Error( + `expand-windows-installer: tar extraction failed (exit ${extractTar.status ?? 1})`, + ); + } + + const appDir = readdirSync(workDir) + .filter((entry) => entry !== "app.tar" && entry !== "__MACOSX") + .map((entry) => path.join(workDir, entry)) + .find((entryPath) => statSync(entryPath).isDirectory()); + + if (!appDir) { + rmSync(workDir, { recursive: true, force: true }); + throw new Error("expand-windows-installer: app folder not found in archive"); + } + + console.log(`expand-windows-installer: expanded ${appDir}`); + return appDir; +} + +export function cleanExpandedWindowsApp(parentDir) { + const workDir = path.join(path.resolve(parentDir), ".expanded-app"); + rmSync(workDir, { recursive: true, force: true }); +} diff --git a/scripts/finalize-desktop-artifacts.mjs b/scripts/finalize-desktop-artifacts.mjs index c0f196a..224a3ac 100644 --- a/scripts/finalize-desktop-artifacts.mjs +++ b/scripts/finalize-desktop-artifacts.mjs @@ -16,6 +16,10 @@ import { getReleaseArtifactName, getReleaseVersion, } from "./release-artifact-utils.mjs"; +import { + cleanExpandedWindowsApp, + expandWindowsAppFromArchive, +} from "./expand-windows-installer.mjs"; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const packageJson = JSON.parse( @@ -299,7 +303,15 @@ function buildMacPkg(appBundlePath, arch) { return pkgPath; } -function buildWindowsNsis(installerFiles, arch) { +function cleanWinBuildDir(arch) { + const platformDir = path.join(getBuildRoot(), `stable-win-${arch}`); + if (existsSync(platformDir)) { + rmSync(platformDir, { recursive: true, force: true }); + console.log(`Removed build output ${platformDir}`); + } +} + +function buildWindowsNsis(appDir, arch) { const scriptPath = path.join(rootDir, "scripts/build-windows-nsis.ps1"); const exePath = path.join( artifactDir, @@ -324,12 +336,8 @@ function buildWindowsNsis(installerFiles, arch) { "Bypass", "-File", scriptPath, - "-SetupExe", - installerFiles.setupExe, - "-SetupArchive", - installerFiles.setupArchive, - "-SetupMetadata", - installerFiles.setupMetadata, + "-AppDir", + appDir, "-OutputExe", exePath, "-Version", @@ -376,19 +384,31 @@ if (targetOs === "macos") { const arch = getReleaseArch(process.env.ELECTROBUN_ARCH || "x64"); const installerFiles = findWindowsInstallerFiles(); - if (!installerFiles) { + if (!installerFiles?.setupArchive) { throw new Error( - "Could not find the Windows installer files (setup exe, archive, and metadata)", + "Could not find the Windows setup archive (.tar.zst) to expand", ); } - const published = [buildWindowsNsis(installerFiles, arch)]; + const platformDir = path.dirname(installerFiles.setupArchive); + const appDir = expandWindowsAppFromArchive( + installerFiles.setupArchive, + platformDir, + ); - if (installerFiles.setupZip) { - published.push(publishArtifact(installerFiles.setupZip, arch, "zip")); + let published; + try { + published = [buildWindowsNsis(appDir, arch)]; + + if (installerFiles.setupZip) { + published.push(publishArtifact(installerFiles.setupZip, arch, "zip")); + } + } finally { + cleanExpandedWindowsApp(platformDir); } cleanStagingArtifacts(published.map((filePath) => path.basename(filePath))); + cleanWinBuildDir(arch); } else { console.log( "finalize-desktop-artifacts: no desktop packaging configured for",