mirror of
https://github.com/MAKS-IT-COM/maksit-postclient.git
synced 2026-09-29 19:08:09 +02:00
1324 lines
43 KiB
PowerShell
1324 lines
43 KiB
PowerShell
#requires -Version 7.0
|
|
#requires -PSEdition Core
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Helpers for Community desktop pack plugins (Windows MSI, Linux Flatpak).
|
|
#>
|
|
|
|
function ConvertTo-WixIdentifier {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Value,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Prefix = 'id'
|
|
)
|
|
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Value)
|
|
$sha = [System.Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$hash = [System.BitConverter]::ToString($sha.ComputeHash($bytes)).Replace('-', '')
|
|
}
|
|
finally {
|
|
$sha.Dispose()
|
|
}
|
|
|
|
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-WixArchitectureFromRuntimeIdentifier {
|
|
param(
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$RuntimeIdentifier = ''
|
|
)
|
|
|
|
$rid = [string]$RuntimeIdentifier
|
|
if ($rid -match '(?i)arm64') {
|
|
return 'arm64'
|
|
}
|
|
|
|
if ($rid -match '(?i)(^|-)x86($|-)') {
|
|
return 'x86'
|
|
}
|
|
|
|
return 'x64'
|
|
}
|
|
|
|
function Get-WixPerMachineProgramFilesFolderId {
|
|
param(
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Architecture = 'x64'
|
|
)
|
|
|
|
if ($Architecture -eq 'x86') {
|
|
return 'ProgramFilesFolder'
|
|
}
|
|
|
|
return 'ProgramFiles64Folder'
|
|
}
|
|
|
|
function Get-MsiProductVersion {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Version
|
|
)
|
|
|
|
$numeric = $Version
|
|
$plusIndex = $numeric.IndexOf('+')
|
|
if ($plusIndex -ge 0) {
|
|
$numeric = $numeric.Substring(0, $plusIndex)
|
|
}
|
|
|
|
$hyphenIndex = $numeric.IndexOf('-')
|
|
if ($hyphenIndex -ge 0) {
|
|
$numeric = $numeric.Substring(0, $hyphenIndex)
|
|
}
|
|
|
|
$parts = @($numeric.Split('.') | Where-Object { $_ -match '^\d+$' })
|
|
if ($parts.Count -lt 2) {
|
|
throw "Cannot derive an MSI ProductVersion from '$Version'. Expected semver with at least major.minor."
|
|
}
|
|
|
|
while ($parts.Count -lt 3) {
|
|
$parts += '0'
|
|
}
|
|
|
|
if ($parts.Count -gt 4) {
|
|
$parts = $parts[0..3]
|
|
}
|
|
|
|
return ($parts -join '.')
|
|
}
|
|
|
|
function Get-PluginPropertyValue {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
$PluginSettings,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Name,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
$Default = $null
|
|
)
|
|
|
|
if ($PluginSettings.PSObject.Properties[$Name] -and $null -ne $PluginSettings.$Name) {
|
|
$value = $PluginSettings.$Name
|
|
if ($value -is [string] -and [string]::IsNullOrWhiteSpace($value)) {
|
|
return $Default
|
|
}
|
|
|
|
return $value
|
|
}
|
|
|
|
return $Default
|
|
}
|
|
|
|
function Resolve-DesktopPublishDirectory {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[psobject]$Context,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$RuntimeIdentifier,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$PublishDir,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$ScriptDir
|
|
)
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($PublishDir)) {
|
|
$resolved = $PublishDir
|
|
if (-not [System.IO.Path]::IsPathRooted($resolved) -and -not [string]::IsNullOrWhiteSpace($ScriptDir)) {
|
|
$resolved = [System.IO.Path]::GetFullPath((Join-Path $ScriptDir $PublishDir))
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $resolved -PathType Container)) {
|
|
throw "Desktop publish directory not found: $resolved"
|
|
}
|
|
|
|
return $resolved
|
|
}
|
|
|
|
$outputs = @(Get-EngineFact -Context $Context -Namespace 'dotnet' -Name 'publishOutputs' -Default @())
|
|
if ($outputs.Count -gt 0) {
|
|
$exact = @(
|
|
$outputs |
|
|
Where-Object { [string]$_.runtimeIdentifier -eq $RuntimeIdentifier }
|
|
)
|
|
if ($exact.Count -gt 0) {
|
|
return [string]$exact[0].directory
|
|
}
|
|
|
|
$family = $RuntimeIdentifier.Split('-')[0]
|
|
if (-not [string]::IsNullOrWhiteSpace($family)) {
|
|
$matchedFamily = @(
|
|
$outputs |
|
|
Where-Object { ([string]$_.runtimeIdentifier).StartsWith("$family-", [System.StringComparison]::OrdinalIgnoreCase) }
|
|
)
|
|
if ($matchedFamily.Count -gt 0) {
|
|
return [string]$matchedFamily[0].directory
|
|
}
|
|
}
|
|
}
|
|
|
|
$archiveInputs = @(Get-EngineFact -Context $Context -Namespace 'release' -Name 'archiveInputs' -Default @() -LegacyProperty @('releaseArchiveInputs'))
|
|
$dirs = @(
|
|
$archiveInputs |
|
|
Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) -and (Test-Path -LiteralPath ([string]$_) -PathType Container) }
|
|
)
|
|
if ($dirs.Count -eq 1) {
|
|
return [string]$dirs[0]
|
|
}
|
|
|
|
throw "Could not resolve a '$RuntimeIdentifier' publish directory. Run DotNetPublish with runtimeIdentifiers (or set publishDir)."
|
|
}
|
|
|
|
function Resolve-DesktopExecutablePath {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$PublishDirectory,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$ExecutableName,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[switch]$Windows
|
|
)
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($ExecutableName)) {
|
|
$direct = Join-Path $PublishDirectory $ExecutableName
|
|
if (Test-Path -LiteralPath $direct -PathType Leaf) {
|
|
return $direct
|
|
}
|
|
|
|
$withExe = Join-Path $PublishDirectory ($ExecutableName + '.exe')
|
|
if ($Windows -and (Test-Path -LiteralPath $withExe -PathType Leaf)) {
|
|
return $withExe
|
|
}
|
|
|
|
throw "Executable '$ExecutableName' was not found in: $PublishDirectory"
|
|
}
|
|
|
|
$candidates = @()
|
|
if ($Windows) {
|
|
$candidates = @(Get-ChildItem -LiteralPath $PublishDirectory -File -Filter '*.exe' | Where-Object { $_.Name -notin @('createdump.exe') })
|
|
}
|
|
else {
|
|
$candidates = @(
|
|
Get-ChildItem -LiteralPath $PublishDirectory -File |
|
|
Where-Object { [string]::IsNullOrWhiteSpace($_.Extension) }
|
|
)
|
|
}
|
|
|
|
if ($candidates.Count -eq 1) {
|
|
return $candidates[0].FullName
|
|
}
|
|
|
|
if ($candidates.Count -gt 1) {
|
|
throw "Multiple executables found in '$PublishDirectory'. Set executableName."
|
|
}
|
|
|
|
throw "No executable found in '$PublishDirectory'. Set executableName."
|
|
}
|
|
|
|
function New-WixPackageXml {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AppName,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Manufacturer,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ProductVersion,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[guid]$UpgradeCode,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$PublishDirectory,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ExecutablePath,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$InstallScope = 'perMachine',
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$InstallFolderName,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Architecture = 'x64',
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$IconPath
|
|
)
|
|
|
|
$ns = 'http://wixtoolset.org/schemas/v4/wxs'
|
|
$xml = New-Object System.Xml.XmlDocument
|
|
$null = $xml.AppendChild($xml.CreateXmlDeclaration('1.0', 'utf-8', $null))
|
|
$wix = $xml.CreateElement('Wix', $ns)
|
|
$null = $xml.AppendChild($wix)
|
|
|
|
$scope = if ($InstallScope -eq 'perMachine') { 'perMachine' } else { 'perUser' }
|
|
$package = $xml.CreateElement('Package', $ns)
|
|
$null = $package.SetAttribute('Name', $AppName)
|
|
$null = $package.SetAttribute('Manufacturer', $Manufacturer)
|
|
$null = $package.SetAttribute('Version', $ProductVersion)
|
|
$null = $package.SetAttribute('UpgradeCode', $UpgradeCode.ToString('D'))
|
|
$null = $package.SetAttribute('Scope', $scope)
|
|
$null = $wix.AppendChild($package)
|
|
|
|
$major = $xml.CreateElement('MajorUpgrade', $ns)
|
|
$null = $major.SetAttribute('DowngradeErrorMessage', 'A newer version is already installed.')
|
|
$null = $package.AppendChild($major)
|
|
|
|
$media = $xml.CreateElement('MediaTemplate', $ns)
|
|
$null = $media.SetAttribute('EmbedCab', 'yes')
|
|
$null = $package.AppendChild($media)
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($IconPath) -and (Test-Path -LiteralPath $IconPath -PathType Leaf)) {
|
|
$icon = $xml.CreateElement('Icon', $ns)
|
|
$null = $icon.SetAttribute('Id', 'AppIcon')
|
|
$null = $icon.SetAttribute('SourceFile', $IconPath)
|
|
$null = $package.AppendChild($icon)
|
|
$prop = $xml.CreateElement('Property', $ns)
|
|
$null = $prop.SetAttribute('Id', 'ARPPRODUCTICON')
|
|
$null = $prop.SetAttribute('Value', 'AppIcon')
|
|
$null = $package.AppendChild($prop)
|
|
}
|
|
|
|
$desktopProp = $xml.CreateElement('Property', $ns)
|
|
$null = $desktopProp.SetAttribute('Id', 'INSTALLDESKTOPSHORTCUT')
|
|
$null = $desktopProp.SetAttribute('Value', '0')
|
|
$null = $desktopProp.SetAttribute('Secure', 'yes')
|
|
$null = $package.AppendChild($desktopProp)
|
|
|
|
# Present when a previous install wrote the desktop shortcut marker.
|
|
# Burn also searches HKCU as the user and passes DESKTOPSHORTCUTFOUND,
|
|
# because a per-machine execute sequence runs as SYSTEM.
|
|
$foundProp = $xml.CreateElement('Property', $ns)
|
|
$null = $foundProp.SetAttribute('Id', 'DESKTOPSHORTCUTFOUND')
|
|
$null = $foundProp.SetAttribute('Secure', 'yes')
|
|
$foundSearch = $xml.CreateElement('RegistrySearch', $ns)
|
|
$null = $foundSearch.SetAttribute('Id', 'DesktopShortcutFoundSearch')
|
|
$null = $foundSearch.SetAttribute('Root', 'HKCU')
|
|
$null = $foundSearch.SetAttribute('Key', "Software\$Manufacturer\$AppName")
|
|
$null = $foundSearch.SetAttribute('Name', 'desktop')
|
|
$null = $foundSearch.SetAttribute('Type', 'raw')
|
|
$null = $foundProp.AppendChild($foundSearch)
|
|
$null = $package.AppendChild($foundProp)
|
|
|
|
# Checkbox off on reinstall must not drop an icon that is already there.
|
|
# Major upgrade removes the previous shortcut; this installs that one again.
|
|
$keepShortcut = $xml.CreateElement('SetProperty', $ns)
|
|
$null = $keepShortcut.SetAttribute('Id', 'INSTALLDESKTOPSHORTCUT')
|
|
$null = $keepShortcut.SetAttribute('Value', '1')
|
|
$null = $keepShortcut.SetAttribute('Before', 'CostFinalize')
|
|
$null = $keepShortcut.SetAttribute('Condition', 'DESKTOPSHORTCUTFOUND = "1"')
|
|
$null = $package.AppendChild($keepShortcut)
|
|
|
|
$productFolder = Get-DesktopInstallFolderName `
|
|
-AppName $AppName `
|
|
-Manufacturer $Manufacturer `
|
|
-InstallFolderName $InstallFolderName
|
|
|
|
$stdLocal = $xml.CreateElement('StandardDirectory', $ns)
|
|
$rootFolderId = if ($scope -eq 'perMachine') {
|
|
Get-WixPerMachineProgramFilesFolderId -Architecture $Architecture
|
|
}
|
|
else {
|
|
'LocalAppDataFolder'
|
|
}
|
|
$null = $stdLocal.SetAttribute('Id', $rootFolderId)
|
|
$null = $package.AppendChild($stdLocal)
|
|
|
|
$installParent = $stdLocal
|
|
if (-not [string]::IsNullOrWhiteSpace($Manufacturer)) {
|
|
$mfrDir = $xml.CreateElement('Directory', $ns)
|
|
$null = $mfrDir.SetAttribute('Id', 'ManufacturerFolder')
|
|
$null = $mfrDir.SetAttribute('Name', $Manufacturer)
|
|
$null = $stdLocal.AppendChild($mfrDir)
|
|
$installParent = $mfrDir
|
|
}
|
|
|
|
$installFolder = $xml.CreateElement('Directory', $ns)
|
|
$null = $installFolder.SetAttribute('Id', 'INSTALLFOLDER')
|
|
$null = $installFolder.SetAttribute('Name', $productFolder)
|
|
$null = $installParent.AppendChild($installFolder)
|
|
|
|
$stdMenu = $xml.CreateElement('StandardDirectory', $ns)
|
|
$null = $stdMenu.SetAttribute('Id', 'ProgramMenuFolder')
|
|
$null = $package.AppendChild($stdMenu)
|
|
$menuDir = $xml.CreateElement('Directory', $ns)
|
|
$null = $menuDir.SetAttribute('Id', 'AppShortcutFolder')
|
|
$menuName = if ([string]::IsNullOrWhiteSpace($Manufacturer)) { $productFolder } else { $Manufacturer }
|
|
$null = $menuDir.SetAttribute('Name', $menuName)
|
|
$null = $stdMenu.AppendChild($menuDir)
|
|
|
|
$stdDesktop = $xml.CreateElement('StandardDirectory', $ns)
|
|
$null = $stdDesktop.SetAttribute('Id', 'DesktopFolder')
|
|
$null = $package.AppendChild($stdDesktop)
|
|
|
|
$dirNodes = @{
|
|
'' = $installFolder
|
|
}
|
|
$componentIds = [System.Collections.Generic.List[string]]::new()
|
|
|
|
$publishRoot = [System.IO.Path]::GetFullPath($PublishDirectory)
|
|
$files = @(Get-ChildItem -LiteralPath $publishRoot -Recurse -File)
|
|
if ($files.Count -eq 0) {
|
|
throw "WindowsInstaller found no files to harvest in: $publishRoot"
|
|
}
|
|
|
|
foreach ($file in $files) {
|
|
$relative = $file.FullName.Substring($publishRoot.Length).TrimStart('\', '/')
|
|
$relativeDir = [System.IO.Path]::GetDirectoryName($relative)
|
|
if ($null -eq $relativeDir) {
|
|
$relativeDir = ''
|
|
}
|
|
|
|
$parent = $installFolder
|
|
if (-not [string]::IsNullOrWhiteSpace($relativeDir)) {
|
|
$parts = $relativeDir.Split([char[]]@('\', '/'), [System.StringSplitOptions]::RemoveEmptyEntries)
|
|
$walk = ''
|
|
foreach ($part in $parts) {
|
|
$walk = if ($walk) { Join-Path $walk $part } else { $part }
|
|
if (-not $dirNodes.ContainsKey($walk)) {
|
|
$child = $xml.CreateElement('Directory', $ns)
|
|
$null = $child.SetAttribute('Id', (ConvertTo-WixIdentifier -Value $walk -Prefix 'd'))
|
|
$null = $child.SetAttribute('Name', $part)
|
|
$null = $parent.AppendChild($child)
|
|
$dirNodes[$walk] = $child
|
|
}
|
|
|
|
$parent = $dirNodes[$walk]
|
|
}
|
|
}
|
|
|
|
$component = $xml.CreateElement('Component', $ns)
|
|
$componentId = ConvertTo-WixIdentifier -Value $relative -Prefix 'c'
|
|
$null = $component.SetAttribute('Id', $componentId)
|
|
$null = $component.SetAttribute('Guid', '*')
|
|
$componentIds.Add($componentId)
|
|
$fileNode = $xml.CreateElement('File', $ns)
|
|
$null = $fileNode.SetAttribute('Id', (ConvertTo-WixIdentifier -Value $relative -Prefix 'f'))
|
|
$null = $fileNode.SetAttribute('Source', $file.FullName)
|
|
$null = $fileNode.SetAttribute('Name', $file.Name)
|
|
$null = $fileNode.SetAttribute('KeyPath', 'yes')
|
|
$null = $component.AppendChild($fileNode)
|
|
$null = $parent.AppendChild($component)
|
|
}
|
|
|
|
$exeName = [System.IO.Path]::GetFileName($ExecutablePath)
|
|
$shortcut = $xml.CreateElement('Component', $ns)
|
|
$null = $shortcut.SetAttribute('Id', 'StartMenuShortcut')
|
|
$null = $shortcut.SetAttribute('Directory', 'AppShortcutFolder')
|
|
$null = $shortcut.SetAttribute('Guid', '*')
|
|
$shortcutNode = $xml.CreateElement('Shortcut', $ns)
|
|
$null = $shortcutNode.SetAttribute('Id', 'AppStartMenuShortcut')
|
|
$null = $shortcutNode.SetAttribute('Name', $productFolder)
|
|
$null = $shortcutNode.SetAttribute('Target', "[INSTALLFOLDER]$exeName")
|
|
$null = $shortcutNode.SetAttribute('WorkingDirectory', 'INSTALLFOLDER')
|
|
if (-not [string]::IsNullOrWhiteSpace($IconPath) -and (Test-Path -LiteralPath $IconPath -PathType Leaf)) {
|
|
$null = $shortcutNode.SetAttribute('Icon', 'AppIcon')
|
|
}
|
|
|
|
$null = $shortcut.AppendChild($shortcutNode)
|
|
$remove = $xml.CreateElement('RemoveFolder', $ns)
|
|
$null = $remove.SetAttribute('Id', 'AppShortcutFolder')
|
|
$null = $remove.SetAttribute('On', 'uninstall')
|
|
$null = $shortcut.AppendChild($remove)
|
|
$reg = $xml.CreateElement('RegistryValue', $ns)
|
|
$null = $reg.SetAttribute('Root', 'HKCU')
|
|
$null = $reg.SetAttribute('Key', "Software\$Manufacturer\$AppName")
|
|
$null = $reg.SetAttribute('Name', 'installed')
|
|
$null = $reg.SetAttribute('Type', 'integer')
|
|
$null = $reg.SetAttribute('Value', '1')
|
|
$null = $reg.SetAttribute('KeyPath', 'yes')
|
|
$null = $shortcut.AppendChild($reg)
|
|
$null = $package.AppendChild($shortcut)
|
|
|
|
$desktop = $xml.CreateElement('Component', $ns)
|
|
$null = $desktop.SetAttribute('Id', 'DesktopShortcut')
|
|
$null = $desktop.SetAttribute('Directory', 'DesktopFolder')
|
|
$shortcutGuid = Get-DesktopShortcutComponentGuid -Manufacturer $Manufacturer -AppName $AppName
|
|
$null = $desktop.SetAttribute('Guid', $shortcutGuid.ToString('D'))
|
|
$null = $desktop.SetAttribute('Condition', 'INSTALLDESKTOPSHORTCUT = 1')
|
|
$desktopShortcut = $xml.CreateElement('Shortcut', $ns)
|
|
$null = $desktopShortcut.SetAttribute('Id', 'AppDesktopShortcut')
|
|
$null = $desktopShortcut.SetAttribute('Name', $productFolder)
|
|
$null = $desktopShortcut.SetAttribute('Target', "[INSTALLFOLDER]$exeName")
|
|
$null = $desktopShortcut.SetAttribute('WorkingDirectory', 'INSTALLFOLDER')
|
|
if (-not [string]::IsNullOrWhiteSpace($IconPath) -and (Test-Path -LiteralPath $IconPath -PathType Leaf)) {
|
|
$null = $desktopShortcut.SetAttribute('Icon', 'AppIcon')
|
|
}
|
|
|
|
$null = $desktop.AppendChild($desktopShortcut)
|
|
# Same file name is replaced. A second ".lnk" (and the "(2)" copy) is removed
|
|
# before CreateShortcuts so a checked box does not leave two desktop icons.
|
|
$cleanupNames = [System.Collections.Generic.List[string]]::new()
|
|
$cleanupNames.Add($productFolder)
|
|
if (-not $cleanupNames.Contains($AppName)) {
|
|
$cleanupNames.Add($AppName)
|
|
}
|
|
|
|
$cleanupIndex = 0
|
|
foreach ($shortcutName in $cleanupNames) {
|
|
foreach ($suffix in @('', ' (2)')) {
|
|
$cleanupIndex++
|
|
$removeLnk = $xml.CreateElement('RemoveFile', $ns)
|
|
$null = $removeLnk.SetAttribute('Id', "DesktopShortcutCleanup$cleanupIndex")
|
|
$null = $removeLnk.SetAttribute('Name', "$shortcutName$suffix.lnk")
|
|
$null = $removeLnk.SetAttribute('On', 'install')
|
|
$null = $desktop.AppendChild($removeLnk)
|
|
}
|
|
}
|
|
|
|
$desktopReg = $xml.CreateElement('RegistryValue', $ns)
|
|
$null = $desktopReg.SetAttribute('Root', 'HKCU')
|
|
$null = $desktopReg.SetAttribute('Key', "Software\$Manufacturer\$AppName")
|
|
$null = $desktopReg.SetAttribute('Name', 'desktop')
|
|
$null = $desktopReg.SetAttribute('Type', 'integer')
|
|
$null = $desktopReg.SetAttribute('Value', '1')
|
|
$null = $desktopReg.SetAttribute('KeyPath', 'yes')
|
|
$null = $desktop.AppendChild($desktopReg)
|
|
$null = $package.AppendChild($desktop)
|
|
|
|
$feature = $xml.CreateElement('Feature', $ns)
|
|
$null = $feature.SetAttribute('Id', 'Main')
|
|
$null = $feature.SetAttribute('Title', $AppName)
|
|
$null = $feature.SetAttribute('Level', '1')
|
|
$refShortcut = $xml.CreateElement('ComponentRef', $ns)
|
|
$null = $refShortcut.SetAttribute('Id', 'StartMenuShortcut')
|
|
$null = $feature.AppendChild($refShortcut)
|
|
$refDesktop = $xml.CreateElement('ComponentRef', $ns)
|
|
$null = $refDesktop.SetAttribute('Id', 'DesktopShortcut')
|
|
$null = $feature.AppendChild($refDesktop)
|
|
foreach ($id in $componentIds) {
|
|
$cref = $xml.CreateElement('ComponentRef', $ns)
|
|
$null = $cref.SetAttribute('Id', $id)
|
|
$null = $feature.AppendChild($cref)
|
|
}
|
|
|
|
$null = $package.AppendChild($feature)
|
|
|
|
return $xml
|
|
}
|
|
|
|
function Get-DefaultFlatpakFinishArgs {
|
|
return @(
|
|
'--share=ipc',
|
|
'--share=network',
|
|
'--socket=x11',
|
|
'--socket=wayland',
|
|
'--device=dri',
|
|
'--socket=pulseaudio',
|
|
'--filesystem=home'
|
|
)
|
|
}
|
|
|
|
function New-FlatpakDesktopEntry {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AppId,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AppName,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Command,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Categories = 'Utility;'
|
|
)
|
|
|
|
$lines = @(
|
|
'[Desktop Entry]',
|
|
'Type=Application',
|
|
"Name=$AppName",
|
|
"Exec=$Command",
|
|
"Icon=$AppId",
|
|
"StartupWMClass=$AppId",
|
|
'StartupNotify=true',
|
|
'Terminal=false',
|
|
"Categories=$Categories"
|
|
)
|
|
return (($lines -join "`n") + "`n")
|
|
}
|
|
|
|
function Copy-FlatpakHicolorIcons {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AppId,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$HicolorRoot,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$IconPath,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$SvgIconPath
|
|
)
|
|
|
|
$pngSource = $null
|
|
$svgSource = $null
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($IconPath) -and (Test-Path -LiteralPath $IconPath -PathType Leaf)) {
|
|
$ext = [System.IO.Path]::GetExtension($IconPath)
|
|
if ($ext -ieq '.png') {
|
|
$pngSource = $IconPath
|
|
}
|
|
elseif ($ext -ieq '.svg') {
|
|
$svgSource = $IconPath
|
|
}
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($SvgIconPath) -and (Test-Path -LiteralPath $SvgIconPath -PathType Leaf)) {
|
|
$svgSource = $SvgIconPath
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($svgSource) -and -not [string]::IsNullOrWhiteSpace($pngSource)) {
|
|
$siblingSvg = [System.IO.Path]::ChangeExtension($pngSource, '.svg')
|
|
if (Test-Path -LiteralPath $siblingSvg -PathType Leaf) {
|
|
$svgSource = $siblingSvg
|
|
}
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($pngSource) -and -not [string]::IsNullOrWhiteSpace($svgSource)) {
|
|
$siblingPng = [System.IO.Path]::ChangeExtension($svgSource, '.png')
|
|
if (Test-Path -LiteralPath $siblingPng -PathType Leaf) {
|
|
$pngSource = $siblingPng
|
|
}
|
|
}
|
|
|
|
$installed = $false
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($pngSource)) {
|
|
foreach ($size in @('48x48', '64x64', '128x128', '256x256')) {
|
|
$pngDir = Join-Path $HicolorRoot "$size\apps"
|
|
New-Item -ItemType Directory -Path $pngDir -Force | Out-Null
|
|
Copy-Item -LiteralPath $pngSource -Destination (Join-Path $pngDir "$AppId.png") -Force
|
|
}
|
|
|
|
$installed = $true
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($svgSource)) {
|
|
$svgDir = Join-Path $HicolorRoot 'scalable\apps'
|
|
New-Item -ItemType Directory -Path $svgDir -Force | Out-Null
|
|
Copy-Item -LiteralPath $svgSource -Destination (Join-Path $svgDir "$AppId.svg") -Force
|
|
$installed = $true
|
|
}
|
|
|
|
return $installed
|
|
}
|
|
|
|
function ConvertTo-FlatpakMetainfoDescriptionXml {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AppName,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[AllowEmptyString()]
|
|
[string]$Summary,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[AllowEmptyString()]
|
|
[string]$Description
|
|
)
|
|
|
|
$text = $Description
|
|
if ([string]::IsNullOrWhiteSpace($text)) {
|
|
$text = "$AppName is a desktop application. $Summary".Trim()
|
|
}
|
|
|
|
$paragraphs = [System.Collections.Generic.List[string]]::new()
|
|
foreach ($chunk in @($text -split '(\r?\n){2,}')) {
|
|
$trimmed = $chunk.Trim()
|
|
if ([string]::IsNullOrWhiteSpace($trimmed)) {
|
|
continue
|
|
}
|
|
|
|
$paragraphs.Add(' <p>' + [System.Security.SecurityElement]::Escape($trimmed) + '</p>')
|
|
}
|
|
|
|
if ($paragraphs.Count -eq 0) {
|
|
$paragraphs.Add(' <p>' + [System.Security.SecurityElement]::Escape("$AppName is a desktop application.") + '</p>')
|
|
}
|
|
|
|
return ($paragraphs -join "`n")
|
|
}
|
|
|
|
function New-FlatpakMetainfoXml {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AppId,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AppName,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Summary,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[AllowEmptyString()]
|
|
[string]$Description = '',
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$ProjectLicense = 'MIT',
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$MetadataLicense = 'CC0-1.0',
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Version = ''
|
|
)
|
|
|
|
$escapedName = [System.Security.SecurityElement]::Escape($AppName)
|
|
$escapedSummary = [System.Security.SecurityElement]::Escape($Summary)
|
|
$descriptionXml = ConvertTo-FlatpakMetainfoDescriptionXml -AppName $AppName -Summary $Summary -Description $Description
|
|
$xml = @"
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<component type="desktop-application">
|
|
<id>$AppId</id>
|
|
<name>$escapedName</name>
|
|
<summary>$escapedSummary</summary>
|
|
<description>
|
|
$descriptionXml
|
|
</description>
|
|
<launchable type="desktop-id">$AppId.desktop</launchable>
|
|
<metadata_license>$MetadataLicense</metadata_license>
|
|
<project_license>$ProjectLicense</project_license>
|
|
</component>
|
|
"@
|
|
if ([string]::IsNullOrWhiteSpace($Version)) {
|
|
return $xml
|
|
}
|
|
|
|
return Set-FlatpakMetainfoRelease -XmlText $xml -Version $Version
|
|
}
|
|
|
|
function Set-FlatpakMetainfoRelease {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$XmlText,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Version,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Date = ''
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Version)) {
|
|
throw "Set-FlatpakMetainfoRelease requires Version."
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Date)) {
|
|
$Date = [datetime]::UtcNow.ToString('yyyy-MM-dd')
|
|
}
|
|
|
|
$doc = New-Object System.Xml.XmlDocument
|
|
$doc.PreserveWhitespace = $false
|
|
$doc.LoadXml($XmlText)
|
|
$component = $doc.SelectSingleNode('/component')
|
|
if ($null -eq $component) {
|
|
throw "Flatpak metainfo has no <component> root."
|
|
}
|
|
|
|
$releases = $component.SelectSingleNode('releases')
|
|
if ($null -eq $releases) {
|
|
$releases = $doc.CreateElement('releases')
|
|
$null = $component.AppendChild($releases)
|
|
}
|
|
|
|
$match = $null
|
|
foreach ($node in @($releases.SelectNodes('release'))) {
|
|
if ([string]$node.GetAttribute('version') -eq $Version) {
|
|
$match = $node
|
|
break
|
|
}
|
|
}
|
|
|
|
$first = $releases.SelectSingleNode('release')
|
|
if ($null -ne $match) {
|
|
$match.SetAttribute('date', $Date)
|
|
if (-not [object]::ReferenceEquals($match, $first)) {
|
|
$null = $releases.InsertBefore($match, $first)
|
|
}
|
|
}
|
|
else {
|
|
$release = $doc.CreateElement('release')
|
|
$release.SetAttribute('version', $Version)
|
|
$release.SetAttribute('date', $Date)
|
|
if ($null -eq $first) {
|
|
$null = $releases.AppendChild($release)
|
|
}
|
|
else {
|
|
$null = $releases.InsertBefore($release, $first)
|
|
}
|
|
}
|
|
|
|
$settings = New-Object System.Xml.XmlWriterSettings
|
|
$settings.Indent = $true
|
|
$settings.OmitXmlDeclaration = $false
|
|
$settings.Encoding = [System.Text.UTF8Encoding]::new($false)
|
|
$stream = New-Object System.IO.MemoryStream
|
|
try {
|
|
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
|
try {
|
|
$doc.Save($writer)
|
|
}
|
|
finally {
|
|
$writer.Dispose()
|
|
}
|
|
|
|
return [System.Text.Encoding]::UTF8.GetString($stream.ToArray())
|
|
}
|
|
finally {
|
|
$stream.Dispose()
|
|
}
|
|
}
|
|
|
|
function New-FlatpakManifestObject {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AppId,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Command,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ModuleName,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Runtime = 'org.freedesktop.Platform',
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$RuntimeVersion = '24.08',
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Sdk = 'org.freedesktop.Sdk',
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string[]]$FinishArgs,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string[]]$BuildCommands
|
|
)
|
|
|
|
if ($null -eq $FinishArgs -or $FinishArgs.Count -eq 0) {
|
|
$FinishArgs = @(Get-DefaultFlatpakFinishArgs)
|
|
}
|
|
|
|
if ($null -eq $BuildCommands -or $BuildCommands.Count -eq 0) {
|
|
$BuildCommands = @(
|
|
'mkdir -p /app/lib /app/share',
|
|
'cp -a lib/. /app/lib/',
|
|
'cp -a share/. /app/share/',
|
|
"install -Dm755 bin/$Command /app/bin/$Command",
|
|
"chmod +x /app/lib/$ModuleName/$Command"
|
|
)
|
|
}
|
|
|
|
return [ordered]@{
|
|
'app-id' = $AppId
|
|
runtime = $Runtime
|
|
'runtime-version' = $RuntimeVersion
|
|
sdk = $Sdk
|
|
command = $Command
|
|
'finish-args' = @($FinishArgs)
|
|
modules = @(
|
|
[ordered]@{
|
|
name = $ModuleName
|
|
buildsystem = 'simple'
|
|
'build-commands' = @($BuildCommands)
|
|
sources = @(
|
|
[ordered]@{
|
|
type = 'dir'
|
|
path = 'files'
|
|
}
|
|
)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
function New-FlatpakLaunchScript {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ModuleName,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ExecutableFileName
|
|
)
|
|
|
|
$lib = "/app/lib/$ModuleName"
|
|
return @"
|
|
#!/bin/sh
|
|
LIB='$lib'
|
|
if [ -f "`$LIB/flatpak-native.env" ]; then
|
|
. "`$LIB/flatpak-native.env"
|
|
fi
|
|
exec `$LIB/$ExecutableFileName "`$`@"
|
|
"@
|
|
}
|
|
|
|
function Assert-FlatpakAppId {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AppId
|
|
)
|
|
|
|
$hint = "Domain hyphens become underscores (maks-it.com → com.maks_it.App). Only the last segment may contain '-'."
|
|
$segments = @($AppId.Split('.'))
|
|
if ($segments.Count -lt 3) {
|
|
throw "Flatpak appId '$AppId' is invalid: need at least three reverse-DNS segments (e.g. com.maks_it.Wvc210). $hint"
|
|
}
|
|
|
|
for ($i = 0; $i -lt $segments.Count; $i++) {
|
|
$segment = $segments[$i]
|
|
$isLast = ($i -eq ($segments.Count - 1))
|
|
$pattern = if ($isLast) { '^[A-Za-z_][A-Za-z0-9_-]*$' } else { '^[A-Za-z_][A-Za-z0-9_]*$' }
|
|
if ($segment -notmatch $pattern) {
|
|
throw "Flatpak appId '$AppId' is invalid: Only last name segment can contain '-'. $hint"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-DesktopShortcutComponentGuid {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Manufacturer,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AppName
|
|
)
|
|
|
|
$identity = "maksit-desktop-shortcut|$Manufacturer|$AppName"
|
|
$md5 = [System.Security.Cryptography.MD5]::Create()
|
|
try {
|
|
$hash = $md5.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($identity))
|
|
}
|
|
finally {
|
|
$md5.Dispose()
|
|
}
|
|
|
|
return [guid]::new($hash)
|
|
}
|
|
|
|
function Get-DerivedBundleUpgradeCode {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[guid]$UpgradeCode
|
|
)
|
|
|
|
$bytes = $UpgradeCode.ToByteArray()
|
|
$bytes[0] = $bytes[0] -bxor 0x5A
|
|
return [guid]::new($bytes)
|
|
}
|
|
|
|
function ConvertTo-ShSingleQuoted {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[AllowEmptyString()]
|
|
[string]$Value
|
|
)
|
|
|
|
return "'" + ($Value -replace "'", "'\''") + "'"
|
|
}
|
|
|
|
function ConvertTo-UnixLineEndings {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[AllowEmptyString()]
|
|
[string]$Value
|
|
)
|
|
|
|
return (($Value -replace "`r`n", "`n") -replace "`r", "`n")
|
|
}
|
|
|
|
function ConvertTo-WslMountPath {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$WindowsPath
|
|
)
|
|
|
|
$full = [System.IO.Path]::GetFullPath($WindowsPath)
|
|
if ($full.StartsWith('\\')) {
|
|
throw "UNC paths cannot be mapped to /mnt for WSL Flatpak builds: $WindowsPath"
|
|
}
|
|
|
|
$root = [System.IO.Path]::GetPathRoot($full)
|
|
$drive = $root.Substring(0, 1).ToLowerInvariant()
|
|
$rest = $full.Substring($root.Length).Replace('\', '/')
|
|
return "/mnt/$drive/$rest"
|
|
}
|
|
|
|
function ConvertFrom-WslMountPath {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$UnixPath
|
|
)
|
|
|
|
if ($UnixPath -notmatch '^/mnt/([A-Za-z])/(.*)$') {
|
|
return $null
|
|
}
|
|
|
|
$drive = $Matches[1].ToUpperInvariant()
|
|
$rest = $Matches[2].Replace('/', '\')
|
|
return "${drive}:\$rest"
|
|
}
|
|
|
|
function ConvertFrom-WslText {
|
|
param($InputObject)
|
|
|
|
$parts = foreach ($item in @($InputObject)) {
|
|
if ($null -eq $item) {
|
|
continue
|
|
}
|
|
|
|
[string]$item
|
|
}
|
|
|
|
$text = ($parts -join "`n").Replace("`0", '')
|
|
$lines = [System.Collections.Generic.List[string]]::new()
|
|
foreach ($line in ($text -split "`r?`n")) {
|
|
$trimmed = $line.Trim()
|
|
if ([string]::IsNullOrWhiteSpace($trimmed)) {
|
|
continue
|
|
}
|
|
|
|
$utf16Spaced = $trimmed.Length -ge 3 -and ($trimmed.Length % 2) -eq 1
|
|
if ($utf16Spaced) {
|
|
for ($i = 1; $i -lt $trimmed.Length; $i += 2) {
|
|
if ($trimmed[$i] -ne [char]' ') {
|
|
$utf16Spaced = $false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($utf16Spaced) {
|
|
$chars = for ($i = 0; $i -lt $trimmed.Length; $i += 2) { $trimmed[$i] }
|
|
$trimmed = (-join $chars).Trim()
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($trimmed)) {
|
|
$lines.Add($trimmed)
|
|
}
|
|
}
|
|
|
|
return @($lines)
|
|
}
|
|
|
|
function ConvertFrom-WslListOutput {
|
|
param($InputObject)
|
|
|
|
return @(
|
|
ConvertFrom-WslText -InputObject $InputObject |
|
|
Where-Object { $_ -notmatch '(?i)^(Windows Subsystem|Distributions:|NAME\s+STATE|The operation completed)' } |
|
|
ForEach-Object { ($_ -replace '\s+\(.*Default.*\)$', '').Trim() } |
|
|
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
|
)
|
|
}
|
|
|
|
function Test-WslDistroBlockedForFlatpak {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Distro
|
|
)
|
|
|
|
$blocked = @('docker-desktop', 'docker-desktop-data')
|
|
return $blocked -contains $Distro.Trim().ToLowerInvariant()
|
|
}
|
|
|
|
function Assert-WslDistroAllowedForFlatpak {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Distro
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Distro)) {
|
|
throw "FlatpakPack wslDistro is required on Windows (pin Debian; do not use the default WSL distro)."
|
|
}
|
|
|
|
if (Test-WslDistroBlockedForFlatpak -Distro $Distro) {
|
|
throw "FlatpakPack wslDistro cannot be '$Distro' (Docker Desktop). Install Debian and set wslDistro to Debian."
|
|
}
|
|
}
|
|
|
|
function Test-WslDistroInstalled {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Distro,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[AllowEmptyCollection()]
|
|
[string[]]$InstalledNames
|
|
)
|
|
|
|
foreach ($name in @($InstalledNames)) {
|
|
if ([string]::Equals($name, $Distro, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
return $true
|
|
}
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
function Get-FlatpakBuilderShellLines {
|
|
param(
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Repo = 'repo',
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$BuildDir = 'build-dir',
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Manifest = 'manifest.json'
|
|
)
|
|
|
|
# org.flatpak.Builder's sandbox cwd is $HOME, so relative manifest.json
|
|
# resolves to /home/<user>/manifest.json. Always pass absolute paths.
|
|
# --disable-rofiles-fuse: WSL/bwrap has no working rofiles FUSE overlay.
|
|
$runLine = ' flatpak run --filesystem=host --filesystem="$ROOT" --share=network --cwd="$ROOT" org.flatpak.Builder --disable-rofiles-fuse --repo="$ROOT/{0}" --force-clean "$ROOT/{1}" "$ROOT/{2}"' -f $Repo, $BuildDir, $Manifest
|
|
$cliLine = ' flatpak-builder --disable-rofiles-fuse --repo="$ROOT/{0}" --force-clean "$ROOT/{1}" "$ROOT/{2}"' -f $Repo, $BuildDir, $Manifest
|
|
|
|
return @(
|
|
'ROOT=$(pwd)',
|
|
'if flatpak info org.flatpak.Builder >/dev/null 2>&1; then',
|
|
$runLine,
|
|
'elif command -v flatpak-builder >/dev/null 2>&1; then',
|
|
$cliLine,
|
|
'else',
|
|
' echo "flatpak builder missing; install Flathub org.flatpak.Builder" >&2',
|
|
' exit 127',
|
|
'fi'
|
|
)
|
|
}
|
|
|
|
function New-WslFlatpakBuildScript {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$WindowsStageRoot,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$WindowsBundlePath,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AppId
|
|
)
|
|
|
|
$stageQuoted = ConvertTo-ShSingleQuoted -Value $WindowsStageRoot
|
|
$bundleQuoted = ConvertTo-ShSingleQuoted -Value $WindowsBundlePath
|
|
$appQuoted = ConvertTo-ShSingleQuoted -Value $AppId
|
|
$lines = @(
|
|
'set -euo pipefail',
|
|
"STAGE_WIN=$stageQuoted",
|
|
"BUNDLE_WIN=$bundleQuoted",
|
|
"APP_ID=$appQuoted",
|
|
'STAGE=$(wslpath -a "$STAGE_WIN")',
|
|
'BUNDLE=$(wslpath -a "$BUNDLE_WIN")',
|
|
'mkdir -p "$(dirname "$BUNDLE")"',
|
|
'CACHE="${XDG_CACHE_HOME:-$HOME/.cache}"',
|
|
'mkdir -p "$CACHE"',
|
|
'WORK=$(mktemp -d "$CACHE/maksit-flatpak.XXXXXX")',
|
|
'cleanup() { rm -rf "$WORK"; }',
|
|
'trap cleanup EXIT',
|
|
'cp -a "$STAGE"/. "$WORK"/',
|
|
'cd "$WORK"',
|
|
'if [ -d files/bin ]; then',
|
|
' find files/bin -type f -print0 | xargs -0 -r sed -i "s/\r$//"',
|
|
' chmod +x files/bin/* || true',
|
|
'fi'
|
|
)
|
|
$lines += @(Get-FlatpakBuilderShellLines)
|
|
$lines += @(
|
|
'flatpak build-bundle "$ROOT/repo" "$BUNDLE" "$APP_ID"',
|
|
'test -f "$BUNDLE"'
|
|
)
|
|
|
|
return (($lines -join "`n") + "`n")
|
|
}
|
|
|
|
function New-WixBundleXml {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AppName,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Manufacturer,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ProductVersion,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[guid]$UpgradeCode,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$MsiPath,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$IconPath,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$LogoPath,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$LogoSidePath,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$ThemePath,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$InstallScope = 'perMachine',
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$InstallFolderName,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Architecture = 'x64'
|
|
)
|
|
|
|
$escapedName = [System.Security.SecurityElement]::Escape($AppName)
|
|
$escapedMfr = [System.Security.SecurityElement]::Escape($Manufacturer)
|
|
$escapedMsi = [System.Security.SecurityElement]::Escape($MsiPath)
|
|
$bundleUpgrade = Get-DerivedBundleUpgradeCode -UpgradeCode $UpgradeCode
|
|
$productFolder = Get-DesktopInstallFolderName `
|
|
-AppName $AppName `
|
|
-Manufacturer $Manufacturer `
|
|
-InstallFolderName $InstallFolderName
|
|
$iconAttr = ''
|
|
if (-not [string]::IsNullOrWhiteSpace($IconPath)) {
|
|
$iconAttr = " IconSourceFile=`"$([System.Security.SecurityElement]::Escape($IconPath))`""
|
|
}
|
|
|
|
$theme = 'hyperlinkLicense'
|
|
$logoAttrs = ''
|
|
if (-not [string]::IsNullOrWhiteSpace($LogoPath)) {
|
|
$logoAttrs += " LogoFile=`"$([System.Security.SecurityElement]::Escape($LogoPath))`""
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($LogoSidePath)) {
|
|
$theme = 'hyperlinkSidebarLicense'
|
|
$logoAttrs += " LogoSideFile=`"$([System.Security.SecurityElement]::Escape($LogoSidePath))`""
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($ThemePath)) {
|
|
$theme = 'hyperlinkSidebarLicense'
|
|
$logoAttrs += " ThemeFile=`"$([System.Security.SecurityElement]::Escape($ThemePath))`""
|
|
}
|
|
|
|
# Type=formatted so WixStdBA expands well-known folders in the InstallFolder edit box.
|
|
# Type=string shows the raw token, e.g. [ProgramFiles64Folder]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 {
|
|
'[' + (Get-WixPerMachineProgramFilesFolderId -Architecture $Architecture) + ']'
|
|
}
|
|
|
|
$folderPath = if ([string]::IsNullOrWhiteSpace($Manufacturer)) {
|
|
$folderRoot + $productFolder
|
|
}
|
|
else {
|
|
$folderRoot + $Manufacturer + '\' + $productFolder
|
|
}
|
|
$escapedFolder = [System.Security.SecurityElement]::Escape($folderPath)
|
|
$escapedShortcutKey = [System.Security.SecurityElement]::Escape("Software\$Manufacturer\$AppName")
|
|
|
|
# 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 @"
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:bal="http://wixtoolset.org/schemas/v4/wxs/bal" xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
|
|
<Bundle Name="$escapedName" Manufacturer="$escapedMfr" Version="$ProductVersion" UpgradeCode="$($bundleUpgrade.ToString('D'))"$iconAttr>
|
|
<Variable Name="InstallFolder" Type="formatted" Value="$escapedFolder" bal:Overridable="yes" />
|
|
<Variable Name="InstallDesktopShortcut" Type="numeric" Value="0" bal:Overridable="yes" />
|
|
<Variable Name="DesktopShortcutFound" Type="string" Value="" bal:Overridable="yes" />
|
|
<util:RegistrySearch Id="DesktopShortcutFoundSearch" Variable="DesktopShortcutFound" Root="HKCU" Key="$escapedShortcutKey" Value="desktop" Result="exists" />
|
|
<BootstrapperApplication>
|
|
<bal:WixStandardBootstrapperApplication Theme="$theme" LicenseUrl=""$logoAttrs />
|
|
</BootstrapperApplication>
|
|
<Chain>
|
|
<MsiPackage SourceFile="$escapedMsi" Compressed="yes" Vital="yes">
|
|
<MsiProperty Name="INSTALLFOLDER" Value="[InstallFolder]" />
|
|
<MsiProperty Name="INSTALLDESKTOPSHORTCUT" Value="[InstallDesktopShortcut]" />
|
|
<MsiProperty Name="DESKTOPSHORTCUTFOUND" Value="[DesktopShortcutFound]" />
|
|
</MsiPackage>
|
|
</Chain>
|
|
</Bundle>
|
|
</Wix>
|
|
"@
|
|
}
|
|
|
|
Export-ModuleMember -Function `
|
|
ConvertTo-WixIdentifier, `
|
|
Get-MsiProductVersion, `
|
|
Get-DesktopInstallFolderName, `
|
|
Get-WixArchitectureFromRuntimeIdentifier, `
|
|
Get-WixPerMachineProgramFilesFolderId, `
|
|
Get-PluginPropertyValue, `
|
|
Resolve-DesktopPublishDirectory, `
|
|
Resolve-DesktopExecutablePath, `
|
|
New-WixPackageXml, `
|
|
Get-DerivedBundleUpgradeCode, `
|
|
New-WixBundleXml, `
|
|
Get-DefaultFlatpakFinishArgs, `
|
|
New-FlatpakDesktopEntry, `
|
|
Copy-FlatpakHicolorIcons, `
|
|
New-FlatpakMetainfoXml, `
|
|
Set-FlatpakMetainfoRelease, `
|
|
New-FlatpakManifestObject, `
|
|
New-FlatpakLaunchScript, `
|
|
Assert-FlatpakAppId, `
|
|
ConvertTo-ShSingleQuoted, `
|
|
ConvertTo-UnixLineEndings, `
|
|
ConvertTo-WslMountPath, `
|
|
ConvertFrom-WslMountPath, `
|
|
ConvertFrom-WslText, `
|
|
ConvertFrom-WslListOutput, `
|
|
Test-WslDistroBlockedForFlatpak, `
|
|
Assert-WslDistroAllowedForFlatpak, `
|
|
Test-WslDistroInstalled, `
|
|
Get-FlatpakBuilderShellLines, `
|
|
New-WslFlatpakBuildScript
|