Add Windows MSI build script and integrate into artifact finalization
Some checks failed
farmcontrol/farmcontrol-server/pipeline/head There was a failure building this commit

- Introduced build-windows-msi.ps1 script for generating MSI installers, enhancing the Windows packaging process.
- Updated finalize-desktop-artifacts.mjs to include a new function for building MSI packages, ensuring proper integration with existing artifact generation workflows.
- Enhanced error handling and logging during the MSI build process for better feedback on failures.
This commit is contained in:
Tom Butcher 2026-08-01 22:40:09 +01:00
parent 3aec10c87f
commit 8783caa93c
2 changed files with 488 additions and 1 deletions

View File

@ -0,0 +1,441 @@
param(
[Parameter(Mandatory = $true)]
[string]$SetupExe,
[Parameter(Mandatory = $true)]
[string]$OutputMsi,
[Parameter(Mandatory = $true)]
[string]$Version,
[string]$UpgradeCode = "194198A0-0A65-52A3-9E49-CCDE26A5BF65",
[string]$ProductId = "F2AECE98-63E9-5A32-9B4B-9D7C6254CBA8"
)
$ErrorActionPreference = "Stop"
function Get-MsiVersion {
param([string]$InputVersion)
$parts = $InputVersion.Split(".")
while ($parts.Count -lt 4) {
$parts += "0"
}
return ($parts[0..3] -join ".")
}
function Invoke-InstallerExecute {
param(
[object]$Database,
[string]$Sql
)
$null = $Database.GetType().InvokeMember(
"Execute",
"InvokeMethod",
$null,
$Database,
@($Sql)
)
}
function Invoke-InstallerViewInsert {
param(
[object]$Installer,
[object]$Database,
[string]$Sql,
[object[]]$Values
)
$view = $Database.GetType().InvokeMember(
"OpenView",
"InvokeMethod",
$null,
$Database,
@($Sql)
)
$record = $Installer.GetType().InvokeMember(
"CreateRecord",
"InvokeMethod",
$null,
$Installer,
@($Values.Count)
)
for ($index = 0; $index -lt $Values.Count; $index++) {
$value = $Values[$index]
if ($value -is [int]) {
$record.GetType().InvokeMember(
"IntegerData",
"SetProperty",
$null,
$record,
@($index + 1, $value)
)
} else {
$record.GetType().InvokeMember(
"StringData",
"SetProperty",
$null,
$record,
@($index + 1, [string]$value)
)
}
}
$view.GetType().InvokeMember("Execute", "InvokeMethod", $null, $view, @($record))
$view.GetType().InvokeMember("Close", "InvokeMethod", $null, $view, $null)
}
function Set-InstallerBinaryStream {
param(
[object]$Installer,
[object]$Database,
[string]$Name,
[string]$FilePath
)
$deleteSql = "DELETE FROM `Binary` WHERE `Name` = '$Name'"
Invoke-InstallerExecute -Database $Database -Sql $deleteSql
$insertSql = "INSERT INTO `Binary` (`Name`, `Data`) VALUES (?, ?)"
$view = $Database.GetType().InvokeMember(
"OpenView",
"InvokeMethod",
$null,
$Database,
@($insertSql)
)
$record = $Installer.GetType().InvokeMember(
"CreateRecord",
"InvokeMethod",
$null,
$Installer,
@(2)
)
$record.GetType().InvokeMember(
"StringData",
"SetProperty",
$null,
$record,
@(1, $Name)
)
$record.GetType().InvokeMember(
"SetStream",
"InvokeMethod",
$null,
$record,
@(2, (Resolve-Path -LiteralPath $FilePath).Path)
)
$view.GetType().InvokeMember("Execute", "InvokeMethod", $null, $view, @($record))
$view.GetType().InvokeMember("Close", "InvokeMethod", $null, $view, $null)
}
function New-WrapperMsiDatabase {
param(
[string]$OutputPath,
[string]$SetupExePath,
[string]$MsiVersion,
[string]$ProductCode,
[string]$UpgradeCodeValue
)
if (Test-Path $OutputPath) {
Remove-Item -LiteralPath $OutputPath -Force
}
$outputDir = Split-Path $OutputPath -Parent
if ($outputDir -and -not (Test-Path $outputDir)) {
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
}
$installer = New-Object -ComObject WindowsInstaller.Installer
$database = $installer.GetType().InvokeMember(
"OpenDatabase",
"InvokeMethod",
$null,
$installer,
@($OutputPath, 1)
)
Invoke-InstallerExecute -Database $database -Sql @"
CREATE TABLE `Property` (
`Property` CHAR(72) NOT NULL PRIMARY KEY,
`Value` CHAR(0) NOT NULL LOCALIZABLE
)
"@
Invoke-InstallerExecute -Database $database -Sql @"
CREATE TABLE `Directory` (
`Directory` CHAR(72) NOT NULL PRIMARY KEY,
`Directory_Parent` CHAR(72),
`DefaultDir` CHAR(255) NOT NULL
)
"@
Invoke-InstallerExecute -Database $database -Sql @"
CREATE TABLE `Component` (
`Component` CHAR(72) NOT NULL PRIMARY KEY,
`ComponentId` CHAR(38) NOT NULL,
`Directory_` CHAR(72) NOT NULL,
`Attributes` SHORT NOT NULL
)
"@
Invoke-InstallerExecute -Database $database -Sql @"
CREATE TABLE `Feature` (
`Feature` CHAR(72) NOT NULL PRIMARY KEY,
`Feature_Parent` CHAR(72),
`Title` CHAR(64) NOT NULL LOCALIZABLE,
`Description` CHAR(255) LOCALIZABLE,
`Display` SHORT,
`Level` SHORT NOT NULL,
`Directory_` CHAR(72)
)
"@
Invoke-InstallerExecute -Database $database -Sql @"
CREATE TABLE `FeatureComponents` (
`Feature_` CHAR(72) NOT NULL,
`Component_` CHAR(72) NOT NULL PRIMARY KEY
)
"@
Invoke-InstallerExecute -Database $database -Sql @"
CREATE TABLE `Binary` (
`Name` CHAR(72) NOT NULL PRIMARY KEY,
`Data` OBJECT NOT NULL
)
"@
Invoke-InstallerExecute -Database $database -Sql @"
CREATE TABLE `CustomAction` (
`Action` CHAR(72) NOT NULL PRIMARY KEY,
`Type` SHORT NOT NULL,
`Source` CHAR(72),
`Target` CHAR(255)
)
"@
Invoke-InstallerExecute -Database $database -Sql @"
CREATE TABLE `InstallExecuteSequence` (
`Action` CHAR(72) NOT NULL,
`Condition` CHAR(255),
`Sequence` SHORT
)
"@
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `Property` (`Property`, `Value`) VALUES (?, ?)" `
-Values @("Manufacturer", "Tom Butcher")
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `Property` (`Property`, `Value`) VALUES (?, ?)" `
-Values @("ProductName", "Farm Control Server")
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `Property` (`Property`, `Value`) VALUES (?, ?)" `
-Values @("ProductVersion", $MsiVersion)
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `Property` (`Property`, `Value`) VALUES (?, ?)" `
-Values @("ProductCode", $ProductCode)
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `Property` (`Property`, `Value`) VALUES (?, ?)" `
-Values @("UpgradeCode", $UpgradeCodeValue)
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `Property` (`Property`, `Value`) VALUES (?, ?)" `
-Values @("ProductLanguage", "1033")
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `Property` (`Property`, `Value`) VALUES (?, ?)" `
-Values @("ALLUSERS", "1")
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `Property` (`Property`, `Value`) VALUES (?, ?)" `
-Values @("MSIINSTALLPERUSER", "1")
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `Property` (`Property`, `Value`) VALUES (?, ?)" `
-Values @("DISABLEADVTSHORTCUTS", "1")
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `Directory` (`Directory`, `Directory_Parent`, `DefaultDir`) VALUES (?, ?, ?)" `
-Values @("TARGETDIR", "", "SourceDir")
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `Directory` (`Directory`, `Directory_Parent`, `DefaultDir`) VALUES (?, ?, ?)" `
-Values @("TempFolder", "TARGETDIR", "Temp")
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `Component` (`Component`, `ComponentId`, `Directory_`, `Attributes`) VALUES (?, ?, ?, ?)" `
-Values @(
"EmptyComponent",
"7145262D-7149-4F1A-9A69-F377E38F04DA",
"TempFolder",
256
)
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `Feature` (`Feature`, `Feature_Parent`, `Title`, `Description`, `Display`, `Level`, `Directory_`) VALUES (?, ?, ?, ?, ?, ?, ?)" `
-Values @("EmptyFeature", "", "Empty", "", 0, 0, "")
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `FeatureComponents` (`Feature_`, `Component_`) VALUES (?, ?)" `
-Values @("EmptyFeature", "EmptyComponent")
Set-InstallerBinaryStream -Installer $installer -Database $database `
-Name "WrappedExe" -FilePath $SetupExePath
# EXE from Binary, deferred, no impersonate, check return code.
$customActionType = 2 + 64 + 1024 + 2048
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `CustomAction` (`Action`, `Type`, `Source`, `Target`) VALUES (?, ?, ?, ?)" `
-Values @("RunInstaller", $customActionType, "WrappedExe", "/S")
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `InstallExecuteSequence` (`Action`, `Condition`, `Sequence`) VALUES (?, ?, ?)" `
-Values @("InstallValidate", "", 1400)
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `InstallExecuteSequence` (`Action`, `Condition`, `Sequence`) VALUES (?, ?, ?)" `
-Values @("InstallInitialize", "", 1500)
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `InstallExecuteSequence` (`Action`, `Condition`, `Sequence`) VALUES (?, ?, ?)" `
-Values @("ProcessComponents", "", 1600)
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `InstallExecuteSequence` (`Action`, `Condition`, `Sequence`) VALUES (?, ?, ?)" `
-Values @("RunInstaller", "", 1601)
Invoke-InstallerViewInsert -Installer $installer -Database $database `
-Sql "INSERT INTO `InstallExecuteSequence` (`Action`, `Condition`, `Sequence`) VALUES (?, ?, ?)" `
-Values @("InstallFinalize", "", 6600)
$summary = $database.GetType().InvokeMember(
"GetSummaryInformation",
"InvokeMethod",
$null,
$database,
@(20)
)
$summary.GetType().InvokeMember(
"Property",
"SetProperty",
$null,
$summary,
@(1, ";\1033")
)
$summary.GetType().InvokeMember(
"Property",
"SetProperty",
$null,
$summary,
@(2, "Farm Control Server")
)
$summary.GetType().InvokeMember(
"Property",
"SetProperty",
$null,
$summary,
@(3, [Guid]::NewGuid().ToString().ToUpper())
)
$summary.GetType().InvokeMember(
"Property",
"SetProperty",
$null,
$summary,
@(4, "Farm Control Server")
)
$summary.GetType().InvokeMember(
"Property",
"SetProperty",
$null,
$summary,
@(7, "x64;1033")
)
$summary.GetType().InvokeMember(
"Property",
"SetProperty",
$null,
$summary,
@(9, "Farm Control Server")
)
$summary.GetType().InvokeMember(
"Property",
"SetProperty",
$null,
$summary,
@(14, "200")
)
$summary.GetType().InvokeMember(
"Property",
"SetProperty",
$null,
$summary,
@(15, "2")
)
$summary.GetType().InvokeMember(
"Property",
"SetProperty",
$null,
$summary,
@(19, "2")
)
$summary.GetType().InvokeMember(
"Persist",
"InvokeMethod",
$null,
$summary,
$null
)
$database.GetType().InvokeMember(
"Commit",
"InvokeMethod",
$null,
$database,
$null
)
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($summary) | Out-Null
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($database) | Out-Null
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($installer) | Out-Null
}
$setupExePath = (Resolve-Path -LiteralPath $SetupExe).Path
$outputMsiPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputMsi)
$msiVersion = Get-MsiVersion $Version
New-WrapperMsiDatabase `
-OutputPath $outputMsiPath `
-SetupExePath $setupExePath `
-MsiVersion $msiVersion `
-ProductCode $ProductId `
-UpgradeCodeValue $UpgradeCode
if (-not (Test-Path $outputMsiPath)) {
throw "MSI installer was not created at $outputMsiPath"
}
Write-Host "Created MSI installer at $outputMsiPath"

View File

@ -386,6 +386,51 @@ function buildWindowsNsis(appDir, arch) {
return exePath; return exePath;
} }
function buildWindowsMsi(setupExePath, arch) {
const scriptPath = path.join(rootDir, "scripts/build-windows-msi.ps1");
const msiPath = path.join(
artifactDir,
getReleaseArtifactName(version, arch, "msi"),
);
const powershell =
process.env.SystemRoot
? path.join(
process.env.SystemRoot,
"System32",
"WindowsPowerShell",
"v1.0",
"powershell.exe",
)
: "powershell.exe";
const result = spawnSync(
powershell,
[
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
scriptPath,
"-SetupExe",
setupExePath,
"-OutputMsi",
msiPath,
"-Version",
version,
],
{ stdio: "inherit" },
);
if (result.status !== 0) {
throw new Error(
`build-windows-msi.ps1 failed with exit code ${result.status ?? 1}`,
);
}
console.log(`Published ${msiPath}`);
return msiPath;
}
if (buildEnv === "dev") { if (buildEnv === "dev") {
console.log("finalize-desktop-artifacts: skipping dev build"); console.log("finalize-desktop-artifacts: skipping dev build");
process.exit(0); process.exit(0);
@ -430,7 +475,8 @@ if (targetOs === "macos") {
let published; let published;
try { try {
published = [buildWindowsNsis(appDir, arch)]; const setupExe = buildWindowsNsis(appDir, arch);
published = [setupExe, buildWindowsMsi(setupExe, arch)];
if (installerFiles.setupZip) { if (installerFiles.setupZip) {
published.push(publishArtifact(installerFiles.setupZip, arch, "zip")); published.push(publishArtifact(installerFiles.setupZip, arch, "zip"));