(build): install windows setup per-machine under maksit\cluster console

This commit is contained in:
Maksym Sadovnychyy 2026-08-30 19:00:45 +02:00
parent d09fe2aada
commit 7da30fd459
5 changed files with 92 additions and 20 deletions

View File

@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
## [Unreleased] ## [Unreleased]
## [0.6.1] - 2026-08-30
### Changed
- Windows setup is per-machine: install path `C:\Program Files\MaksIT\Cluster Console`, Start Menu folder **MaksIT**, shortcuts named **Cluster Console**. The bootstrapper install-folder box expands Program Files instead of showing a raw `[ProgramFiles6432Folder]` token.
## [0.6.0] - 2026-08-30 ## [0.6.0] - 2026-08-30
### Fixed ### Fixed

View File

@ -3,7 +3,7 @@
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Version>0.6.0</Version> <Version>0.6.1</Version>
<Product>MaksIT.ClusterConsole</Product> <Product>MaksIT.ClusterConsole</Product>
<AssemblyTitle>MaksIT.ClusterConsole</AssemblyTitle> <AssemblyTitle>MaksIT.ClusterConsole</AssemblyTitle>
</PropertyGroup> </PropertyGroup>

View File

@ -56,10 +56,11 @@
"enabled": true, "enabled": true,
"appName": "MaksIT Cluster Console", "appName": "MaksIT Cluster Console",
"manufacturer": "MaksIT", "manufacturer": "MaksIT",
"installFolderName": "Cluster Console",
"upgradeCode": "3e45e88a-6524-49cc-88e9-d522d1261b3a", "upgradeCode": "3e45e88a-6524-49cc-88e9-d522d1261b3a",
"executableName": "MaksIT.ClusterConsole.UI.exe", "executableName": "MaksIT.ClusterConsole.UI.exe",
"runtimeIdentifier": "win-x64", "runtimeIdentifier": "win-x64",
"installScope": "perUser", "installScope": "perMachine",
"iconPath": "..\\..\\..\\src\\MaksIT.ClusterConsole.UI\\Assets\\icon.ico", "iconPath": "..\\..\\..\\src\\MaksIT.ClusterConsole.UI\\Assets\\icon.ico",
"exeNamePattern": "maksit-cluster-console-{version}.exe" "exeNamePattern": "maksit-cluster-console-{version}.exe"
}, },

View File

@ -27,6 +27,34 @@ function ConvertTo-WixIdentifier {
return ($Prefix + $hash.Substring(0, 16)) return ($Prefix + $hash.Substring(0, 16))
} }
function Get-DesktopInstallFolderName {
param(
[Parameter(Mandatory = $true)]
[string]$AppName,
[Parameter(Mandatory = $false)]
[string]$Manufacturer,
[Parameter(Mandatory = $false)]
[string]$InstallFolderName
)
if (-not [string]::IsNullOrWhiteSpace($InstallFolderName)) {
return $InstallFolderName.Trim()
}
$name = $AppName.Trim()
$mfr = if ([string]::IsNullOrWhiteSpace($Manufacturer)) { '' } else { $Manufacturer.Trim() }
if ($mfr.Length -gt 0 -and $name.StartsWith($mfr, [System.StringComparison]::OrdinalIgnoreCase)) {
$rest = $name.Substring($mfr.Length).TrimStart([char[]]@(' ', '-', '.'))
if (-not [string]::IsNullOrWhiteSpace($rest)) {
return $rest
}
}
return $name
}
function Get-MsiProductVersion { function Get-MsiProductVersion {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@ -215,7 +243,10 @@ function New-WixPackageXml {
[string]$ExecutablePath, [string]$ExecutablePath,
[Parameter(Mandatory = $false)] [Parameter(Mandatory = $false)]
[string]$InstallScope = 'perUser', [string]$InstallScope = 'perMachine',
[Parameter(Mandatory = $false)]
[string]$InstallFolderName,
[Parameter(Mandatory = $false)] [Parameter(Mandatory = $false)]
[string]$IconPath [string]$IconPath
@ -260,27 +291,37 @@ function New-WixPackageXml {
$null = $desktopProp.SetAttribute('Value', '0') $null = $desktopProp.SetAttribute('Value', '0')
$null = $package.AppendChild($desktopProp) $null = $package.AppendChild($desktopProp)
$productFolder = Get-DesktopInstallFolderName `
-AppName $AppName `
-Manufacturer $Manufacturer `
-InstallFolderName $InstallFolderName
$stdLocal = $xml.CreateElement('StandardDirectory', $ns) $stdLocal = $xml.CreateElement('StandardDirectory', $ns)
$rootFolderId = if ($scope -eq 'perMachine') { 'ProgramFiles6432Folder' } else { 'LocalAppDataFolder' } $rootFolderId = if ($scope -eq 'perMachine') { 'ProgramFiles6432Folder' } else { 'LocalAppDataFolder' }
$null = $stdLocal.SetAttribute('Id', $rootFolderId) $null = $stdLocal.SetAttribute('Id', $rootFolderId)
$null = $package.AppendChild($stdLocal) $null = $package.AppendChild($stdLocal)
$programs = $xml.CreateElement('Directory', $ns) $installParent = $stdLocal
$null = $programs.SetAttribute('Id', 'LocalProgramsFolder') if (-not [string]::IsNullOrWhiteSpace($Manufacturer)) {
$null = $programs.SetAttribute('Name', 'Programs') $mfrDir = $xml.CreateElement('Directory', $ns)
$null = $stdLocal.AppendChild($programs) $null = $mfrDir.SetAttribute('Id', 'ManufacturerFolder')
$null = $mfrDir.SetAttribute('Name', $Manufacturer)
$null = $stdLocal.AppendChild($mfrDir)
$installParent = $mfrDir
}
$installFolder = $xml.CreateElement('Directory', $ns) $installFolder = $xml.CreateElement('Directory', $ns)
$null = $installFolder.SetAttribute('Id', 'INSTALLFOLDER') $null = $installFolder.SetAttribute('Id', 'INSTALLFOLDER')
$null = $installFolder.SetAttribute('Name', $AppName) $null = $installFolder.SetAttribute('Name', $productFolder)
$null = $programs.AppendChild($installFolder) $null = $installParent.AppendChild($installFolder)
$stdMenu = $xml.CreateElement('StandardDirectory', $ns) $stdMenu = $xml.CreateElement('StandardDirectory', $ns)
$null = $stdMenu.SetAttribute('Id', 'ProgramMenuFolder') $null = $stdMenu.SetAttribute('Id', 'ProgramMenuFolder')
$null = $package.AppendChild($stdMenu) $null = $package.AppendChild($stdMenu)
$menuDir = $xml.CreateElement('Directory', $ns) $menuDir = $xml.CreateElement('Directory', $ns)
$null = $menuDir.SetAttribute('Id', 'AppShortcutFolder') $null = $menuDir.SetAttribute('Id', 'AppShortcutFolder')
$null = $menuDir.SetAttribute('Name', $AppName) $menuName = if ([string]::IsNullOrWhiteSpace($Manufacturer)) { $productFolder } else { $Manufacturer }
$null = $menuDir.SetAttribute('Name', $menuName)
$null = $stdMenu.AppendChild($menuDir) $null = $stdMenu.AppendChild($menuDir)
$stdDesktop = $xml.CreateElement('StandardDirectory', $ns) $stdDesktop = $xml.CreateElement('StandardDirectory', $ns)
@ -344,7 +385,7 @@ function New-WixPackageXml {
$null = $shortcut.SetAttribute('Guid', '*') $null = $shortcut.SetAttribute('Guid', '*')
$shortcutNode = $xml.CreateElement('Shortcut', $ns) $shortcutNode = $xml.CreateElement('Shortcut', $ns)
$null = $shortcutNode.SetAttribute('Id', 'AppStartMenuShortcut') $null = $shortcutNode.SetAttribute('Id', 'AppStartMenuShortcut')
$null = $shortcutNode.SetAttribute('Name', $AppName) $null = $shortcutNode.SetAttribute('Name', $productFolder)
$null = $shortcutNode.SetAttribute('Target', "[INSTALLFOLDER]$exeName") $null = $shortcutNode.SetAttribute('Target', "[INSTALLFOLDER]$exeName")
$null = $shortcutNode.SetAttribute('WorkingDirectory', 'INSTALLFOLDER') $null = $shortcutNode.SetAttribute('WorkingDirectory', 'INSTALLFOLDER')
if (-not [string]::IsNullOrWhiteSpace($IconPath) -and (Test-Path -LiteralPath $IconPath -PathType Leaf)) { if (-not [string]::IsNullOrWhiteSpace($IconPath) -and (Test-Path -LiteralPath $IconPath -PathType Leaf)) {
@ -373,7 +414,7 @@ function New-WixPackageXml {
$null = $desktop.SetAttribute('Condition', 'INSTALLDESKTOPSHORTCUT = 1') $null = $desktop.SetAttribute('Condition', 'INSTALLDESKTOPSHORTCUT = 1')
$desktopShortcut = $xml.CreateElement('Shortcut', $ns) $desktopShortcut = $xml.CreateElement('Shortcut', $ns)
$null = $desktopShortcut.SetAttribute('Id', 'AppDesktopShortcut') $null = $desktopShortcut.SetAttribute('Id', 'AppDesktopShortcut')
$null = $desktopShortcut.SetAttribute('Name', $AppName) $null = $desktopShortcut.SetAttribute('Name', $productFolder)
$null = $desktopShortcut.SetAttribute('Target', "[INSTALLFOLDER]$exeName") $null = $desktopShortcut.SetAttribute('Target', "[INSTALLFOLDER]$exeName")
$null = $desktopShortcut.SetAttribute('WorkingDirectory', 'INSTALLFOLDER') $null = $desktopShortcut.SetAttribute('WorkingDirectory', 'INSTALLFOLDER')
if (-not [string]::IsNullOrWhiteSpace($IconPath) -and (Test-Path -LiteralPath $IconPath -PathType Leaf)) { if (-not [string]::IsNullOrWhiteSpace($IconPath) -and (Test-Path -LiteralPath $IconPath -PathType Leaf)) {
@ -846,13 +887,20 @@ function New-WixBundleXml {
[string]$ThemePath, [string]$ThemePath,
[Parameter(Mandatory = $false)] [Parameter(Mandatory = $false)]
[string]$InstallScope = 'perUser' [string]$InstallScope = 'perMachine',
[Parameter(Mandatory = $false)]
[string]$InstallFolderName
) )
$escapedName = [System.Security.SecurityElement]::Escape($AppName) $escapedName = [System.Security.SecurityElement]::Escape($AppName)
$escapedMfr = [System.Security.SecurityElement]::Escape($Manufacturer) $escapedMfr = [System.Security.SecurityElement]::Escape($Manufacturer)
$escapedMsi = [System.Security.SecurityElement]::Escape($MsiPath) $escapedMsi = [System.Security.SecurityElement]::Escape($MsiPath)
$bundleUpgrade = Get-DerivedBundleUpgradeCode -UpgradeCode $UpgradeCode $bundleUpgrade = Get-DerivedBundleUpgradeCode -UpgradeCode $UpgradeCode
$productFolder = Get-DesktopInstallFolderName `
-AppName $AppName `
-Manufacturer $Manufacturer `
-InstallFolderName $InstallFolderName
$iconAttr = '' $iconAttr = ''
if (-not [string]::IsNullOrWhiteSpace($IconPath)) { if (-not [string]::IsNullOrWhiteSpace($IconPath)) {
$iconAttr = " IconSourceFile=`"$([System.Security.SecurityElement]::Escape($IconPath))`"" $iconAttr = " IconSourceFile=`"$([System.Security.SecurityElement]::Escape($IconPath))`""
@ -874,20 +922,33 @@ function New-WixBundleXml {
$logoAttrs += " ThemeFile=`"$([System.Security.SecurityElement]::Escape($ThemePath))`"" $logoAttrs += " ThemeFile=`"$([System.Security.SecurityElement]::Escape($ThemePath))`""
} }
$folderRoot = if ($InstallScope -eq 'perMachine') { # Type=formatted so WixStdBA expands well-known folders in the InstallFolder edit box.
'[ProgramFiles64Folder]Programs\' # Type=string shows the raw token, e.g. [ProgramFiles6432Folder]MaksIT\Cluster Console.
# Burn CSIDL folders already end with a backslash, so do not insert another one.
# Layout is {ProgramFiles|LocalAppData}\{Manufacturer}\{product} — product folder is the
# internal name (appName with manufacturer prefix stripped, or installFolderName).
$folderRoot = if ($InstallScope -eq 'perUser') {
'[LocalAppDataFolder]'
} }
else { else {
'[LocalAppDataFolder]Programs\' '[ProgramFiles6432Folder]'
} }
$escapedFolder = [System.Security.SecurityElement]::Escape($folderRoot + $AppName) $folderPath = if ([string]::IsNullOrWhiteSpace($Manufacturer)) {
$folderRoot + $productFolder
}
else {
$folderRoot + $Manufacturer + '\' + $productFolder
}
$escapedFolder = [System.Security.SecurityElement]::Escape($folderPath)
# WiX v7 Bundle has no Scope attribute (WIX0004). MSI Package/@Scope plus
# InstallFolder tokens decide per-machine vs per-user; Burn infers bundle scope.
return @" return @"
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:bal="http://wixtoolset.org/schemas/v4/wxs/bal"> <Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:bal="http://wixtoolset.org/schemas/v4/wxs/bal">
<Bundle Name="$escapedName" Manufacturer="$escapedMfr" Version="$ProductVersion" UpgradeCode="$($bundleUpgrade.ToString('D'))"$iconAttr> <Bundle Name="$escapedName" Manufacturer="$escapedMfr" Version="$ProductVersion" UpgradeCode="$($bundleUpgrade.ToString('D'))"$iconAttr>
<Variable Name="InstallFolder" Type="string" Value="$escapedFolder" bal:Overridable="yes" /> <Variable Name="InstallFolder" Type="formatted" Value="$escapedFolder" bal:Overridable="yes" />
<Variable Name="InstallDesktopShortcut" Type="numeric" Value="0" bal:Overridable="yes" /> <Variable Name="InstallDesktopShortcut" Type="numeric" Value="0" bal:Overridable="yes" />
<BootstrapperApplication> <BootstrapperApplication>
<bal:WixStandardBootstrapperApplication Theme="$theme" LicenseUrl=""$logoAttrs /> <bal:WixStandardBootstrapperApplication Theme="$theme" LicenseUrl=""$logoAttrs />
@ -906,6 +967,7 @@ function New-WixBundleXml {
Export-ModuleMember -Function ` Export-ModuleMember -Function `
ConvertTo-WixIdentifier, ` ConvertTo-WixIdentifier, `
Get-MsiProductVersion, ` Get-MsiProductVersion, `
Get-DesktopInstallFolderName, `
Get-PluginPropertyValue, ` Get-PluginPropertyValue, `
Resolve-DesktopPublishDirectory, ` Resolve-DesktopPublishDirectory, `
Resolve-DesktopExecutablePath, ` Resolve-DesktopExecutablePath, `

View File

@ -102,7 +102,8 @@ function Invoke-Plugin {
$upgradeCode = [guid]$upgradeCodeRaw $upgradeCode = [guid]$upgradeCodeRaw
$manufacturer = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'manufacturer' -Default 'MaksIT') $manufacturer = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'manufacturer' -Default 'MaksIT')
$runtimeIdentifier = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtimeIdentifier' -Default 'win-x64') $runtimeIdentifier = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtimeIdentifier' -Default 'win-x64')
$installScope = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'installScope' -Default 'perUser') $installScope = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'installScope' -Default 'perMachine')
$installFolderName = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'installFolderName')
$executableName = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'executableName') $executableName = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'executableName')
$publishDirSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'publishDir') $publishDirSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'publishDir')
$iconSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'iconPath') $iconSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'iconPath')
@ -217,6 +218,7 @@ function Invoke-Plugin {
-PublishDirectory $publishDirectory ` -PublishDirectory $publishDirectory `
-ExecutablePath $executablePath ` -ExecutablePath $executablePath `
-InstallScope $installScope ` -InstallScope $installScope `
-InstallFolderName $installFolderName `
-IconPath $iconPath -IconPath $iconPath
$xml.Save($wxsPath) $xml.Save($wxsPath)
@ -262,7 +264,8 @@ function Invoke-Plugin {
-LogoPath $logoPath ` -LogoPath $logoPath `
-LogoSidePath $logoSidePath ` -LogoSidePath $logoSidePath `
-ThemePath $themePath ` -ThemePath $themePath `
-InstallScope $installScope -InstallScope $installScope `
-InstallFolderName $installFolderName
[System.IO.File]::WriteAllText($bundleWxsPath, $bundleXml, [System.Text.UTF8Encoding]::new($false)) [System.IO.File]::WriteAllText($bundleWxsPath, $bundleXml, [System.Text.UTF8Encoding]::new($false))
if (Test-Path -LiteralPath $exePath -PathType Leaf) { if (Test-Path -LiteralPath $exePath -PathType Leaf) {