uscheduler/utils/plugins/Desktop/FlatpakPack.psm1

446 lines
18 KiB
PowerShell

#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Linux Flatpak pack plugin for published .NET desktop apps (Community).
.DESCRIPTION
Stages a linux-* DotNetPublish folder into a Flatpak layout (manifest,
desktop file, metainfo, launch script). Prefers Flathub org.flatpak.Builder
(current builder) over distro flatpak-builder. On Windows, native builder
is tried first; if missing, the plugin builds via a pinned WSL distro
(default Debian) on the Linux filesystem, then copies the bundle back.
Docker Desktop WSL distros are never used. whenBuilderMissing skip|fail
applies when neither native nor WSL builder is available.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Get-FlatpakWhenBuilderMissing {
param($PluginSettings)
$value = [string](Get-PluginPropertyValue -PluginSettings $PluginSettings -Name 'whenBuilderMissing' -Default 'skip')
if ($value -notin @('skip', 'fail')) {
throw "FlatpakPack whenBuilderMissing must be 'skip' or 'fail'."
}
return $value
}
function Test-FlatpakBuilderMissingException {
param(
[Parameter(Mandatory = $true)]
[System.Management.Automation.ErrorRecord]$ErrorRecord
)
$message = [string]$ErrorRecord.Exception.Message
if ($message -match '(?i)bash: line \d+') {
return $false
}
if ($ErrorRecord.Exception -is [System.Management.Automation.CommandNotFoundException]) {
return $true
}
if ($message -match '(?i)(not recognized|command not found|The term .+ is not recognized|marked unavailable)') {
return $true
}
return $false
}
function Test-WslFallbackSkippable {
param(
[Parameter(Mandatory = $true)]
[System.Management.Automation.ErrorRecord]$ErrorRecord
)
if (Test-FlatpakBuilderMissingException -ErrorRecord $ErrorRecord) {
return $true
}
$message = [string]$ErrorRecord.Exception.Message
if ($message -match "(?i)WSL distro '.+' is not installed") {
return $true
}
return $false
}
function Get-WslInstalledDistroNames {
$output = Invoke-ExternalCommand -Name wsl -ArgumentList @('-l', '-q') -ThrowOnError:$false
return @(ConvertFrom-WslListOutput -InputObject $output)
}
function Invoke-FlatpakBundleViaWsl {
param(
[Parameter(Mandatory = $true)]
[string]$Distro,
[Parameter(Mandatory = $true)]
[string]$WindowsStageRoot,
[Parameter(Mandatory = $true)]
[string]$WindowsBundlePath,
[Parameter(Mandatory = $true)]
[string]$AppId
)
Assert-WslDistroAllowedForFlatpak -Distro $Distro
$installed = @(Get-WslInstalledDistroNames)
if (-not (Test-WslDistroInstalled -Distro $Distro -InstalledNames $installed)) {
throw "WSL distro '$Distro' is not installed. Install Debian with: wsl --install Debian --no-launch --web-download"
}
$script = ConvertTo-UnixLineEndings -Value (
New-WslFlatpakBuildScript `
-WindowsStageRoot $WindowsStageRoot `
-WindowsBundlePath $WindowsBundlePath `
-AppId $AppId
)
if (-not $script.EndsWith("`n")) {
$script += "`n"
}
$tempScript = Join-Path ([System.IO.Path]::GetTempPath()) ("maksit-flatpak-" + [guid]::NewGuid().ToString('N') + ".sh")
try {
[System.IO.File]::WriteAllBytes($tempScript, [System.Text.UTF8Encoding]::new($false).GetBytes($script))
$wslScript = ConvertTo-WslMountPath -WindowsPath $tempScript
Invoke-ExternalCommand -Name wsl -ArgumentList @('-d', $Distro, '--', 'bash', $wslScript) | Out-Null
}
finally {
if (Test-Path -LiteralPath $tempScript -PathType Leaf) {
Remove-Item -LiteralPath $tempScript -Force -ErrorAction SilentlyContinue
}
}
}
function Invoke-HostFlatpakBuilder {
param(
[Parameter(Mandatory = $true)]
[string]$RepoDir,
[Parameter(Mandatory = $true)]
[string]$BuildDir,
[Parameter(Mandatory = $true)]
[string]$ManifestPath
)
$hasFlathubBuilder = $false
try {
Invoke-ExternalCommand -Name flatpak -ArgumentList @('info', 'org.flatpak.Builder') -ThrowOnError:$false | Out-Null
$hasFlathubBuilder = ($global:LASTEXITCODE -eq 0)
}
catch {
if (-not (Test-FlatpakBuilderMissingException -ErrorRecord $_)) {
throw
}
}
if ($hasFlathubBuilder) {
$stageRoot = [System.IO.Path]::GetDirectoryName($ManifestPath)
Invoke-ExternalCommand -Name flatpak -ArgumentList @(
'run',
'--filesystem=host',
'--share=network',
"--cwd=$stageRoot",
'org.flatpak.Builder',
'--disable-rofiles-fuse',
"--repo=$RepoDir",
'--force-clean',
$BuildDir,
$ManifestPath
) | Out-Null
return
}
Invoke-ExternalCommand -Name flatpak-builder -ArgumentList @(
'--disable-rofiles-fuse',
"--repo=$RepoDir",
'--force-clean',
$BuildDir,
$ManifestPath
) | Out-Null
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Add-ReleaseAssetPaths"
Import-PluginDependency -ModuleName "ExternalCommandSupport" -RequiredCommand "Invoke-ExternalCommand"
Import-PluginDependency -ModuleName "DesktopPackSupport" -RequiredCommand "New-WslFlatpakBuildScript"
$pluginSettings = $Settings
$sharedSettings = $Settings.context
$scriptDir = $sharedSettings.scriptDir
$version = [string]$sharedSettings.version
$artifactsDirectory = $sharedSettings.artifactsDirectory
$appId = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'appId')
$appName = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'appName')
if ([string]::IsNullOrWhiteSpace($appId)) {
throw "FlatpakPack plugin requires appId (reverse-DNS, e.g. com.maks_it.Wvc210)."
}
Assert-FlatpakAppId -AppId $appId
if ([string]::IsNullOrWhiteSpace($appName)) {
throw "FlatpakPack plugin requires appName."
}
$command = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'command')
$executableName = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'executableName')
$runtimeIdentifier = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtimeIdentifier' -Default 'linux-x64')
$publishDirSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'publishDir')
$iconSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'iconPath')
$summary = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'summary' -Default $appName)
$projectLicense = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'projectLicense' -Default 'MIT')
$categories = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'categories' -Default 'Utility;')
$runtime = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtime' -Default 'org.freedesktop.Platform')
$runtimeVersion = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtimeVersion' -Default '24.08')
$sdk = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'sdk' -Default 'org.freedesktop.Sdk')
$whenBuilderMissing = Get-FlatpakWhenBuilderMissing -PluginSettings $pluginSettings
$wslDistro = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'wslDistro' -Default 'Debian')
$useWslSetting = Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'useWsl' -Default $null
$useWsl = if ($null -eq $useWslSetting) { [bool]$IsWindows } else { [bool]$useWslSetting }
$buildBundle = $true
if ($pluginSettings.PSObject.Properties['buildBundle'] -and $null -ne $pluginSettings.buildBundle) {
$buildBundle = [bool]$pluginSettings.buildBundle
}
$attachSourceZip = $false
if ($pluginSettings.PSObject.Properties['attachSourceZip'] -and $null -ne $pluginSettings.attachSourceZip) {
$attachSourceZip = [bool]$pluginSettings.attachSourceZip
}
$finishArgs = @(Get-DefaultFlatpakFinishArgs)
if ($pluginSettings.PSObject.Properties['finishArgs'] -and $null -ne $pluginSettings.finishArgs) {
$finishArgs = @($pluginSettings.finishArgs | ForEach-Object { [string]$_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
}
if ([string]::IsNullOrWhiteSpace([string]$artifactsDirectory)) {
throw "FlatpakPack plugin requires an artifacts directory in the shared context."
}
if (-not (Test-Path -LiteralPath $artifactsDirectory -PathType Container)) {
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
}
$publishDirectory = Resolve-DesktopPublishDirectory `
-Context $sharedSettings `
-RuntimeIdentifier $runtimeIdentifier `
-PublishDir $publishDirSetting `
-ScriptDir $scriptDir
$executablePath = Resolve-DesktopExecutablePath `
-PublishDirectory $publishDirectory `
-ExecutableName $executableName
$executableFileName = [System.IO.Path]::GetFileName($executablePath)
if ([string]::IsNullOrWhiteSpace($command)) {
$command = $executableFileName
}
$moduleName = ($appId.Split('.') | Select-Object -Last 1).ToLowerInvariant()
$stageRoot = Join-Path $artifactsDirectory "flatpak-$moduleName"
if (Test-Path -LiteralPath $stageRoot) {
Remove-Item -LiteralPath $stageRoot -Recurse -Force
}
$filesRoot = Join-Path $stageRoot 'files'
$libDir = Join-Path $filesRoot "lib\$moduleName"
$binDir = Join-Path $filesRoot 'bin'
$shareAppDir = Join-Path $filesRoot 'share\applications'
$shareMetaDir = Join-Path $filesRoot 'share\metainfo'
$shareIconDir = Join-Path $filesRoot 'share\icons\hicolor\256x256\apps'
New-Item -ItemType Directory -Path $libDir, $binDir, $shareAppDir, $shareMetaDir, $shareIconDir -Force | Out-Null
Write-Log -Level "STEP" -Message "Staging Flatpak layout from $publishDirectory"
Copy-Item -Path (Join-Path $publishDirectory '*') -Destination $libDir -Recurse -Force
$launchScript = New-FlatpakLaunchScript -ModuleName $moduleName -ExecutableFileName $executableFileName
$launchPath = Join-Path $binDir $command
[System.IO.File]::WriteAllText($launchPath, $launchScript.Replace("`r`n", "`n"), [System.Text.UTF8Encoding]::new($false))
$desktopPath = Join-Path $shareAppDir "$appId.desktop"
[System.IO.File]::WriteAllText(
$desktopPath,
(New-FlatpakDesktopEntry -AppId $appId -AppName $appName -Command $command -Categories $categories).Replace("`r`n", "`n"),
[System.Text.UTF8Encoding]::new($false)
)
$metaPath = Join-Path $shareMetaDir "$appId.metainfo.xml"
[System.IO.File]::WriteAllText(
$metaPath,
(New-FlatpakMetainfoXml -AppId $appId -AppName $appName -Summary $summary -ProjectLicense $projectLicense).Replace("`r`n", "`n"),
[System.Text.UTF8Encoding]::new($false)
)
if (-not [string]::IsNullOrWhiteSpace($iconSetting)) {
$iconPath = if ([System.IO.Path]::IsPathRooted($iconSetting)) {
$iconSetting
}
else {
[System.IO.Path]::GetFullPath((Join-Path $scriptDir $iconSetting))
}
if (-not (Test-Path -LiteralPath $iconPath -PathType Leaf)) {
throw "FlatpakPack iconPath not found: $iconPath"
}
if ([System.IO.Path]::GetExtension($iconPath) -ine '.png') {
Write-Log -Level "WARN" -Message " Flatpak icon should be PNG; skipping copy of $iconPath"
}
else {
Copy-Item -LiteralPath $iconPath -Destination (Join-Path $shareIconDir "$appId.png") -Force
}
}
$buildCommands = @(
'mkdir -p /app/lib /app/share',
"cp -a lib/$moduleName /app/lib/$moduleName",
'cp -a share /app/share',
"install -Dm755 bin/$command /app/bin/$command",
"chmod +x /app/lib/$moduleName/$executableFileName"
)
$manifest = New-FlatpakManifestObject `
-AppId $appId `
-Command $command `
-ModuleName $moduleName `
-Runtime $runtime `
-RuntimeVersion $runtimeVersion `
-Sdk $sdk `
-FinishArgs $finishArgs `
-BuildCommands $buildCommands
$manifestPath = Join-Path $stageRoot 'manifest.json'
$manifestJson = $manifest | ConvertTo-Json -Depth 8
[System.IO.File]::WriteAllText($manifestPath, $manifestJson, [System.Text.UTF8Encoding]::new($false))
$bundleNamePattern = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'bundleNamePattern' -Default '{name}-{version}.flatpak')
$safeName = ($appName -replace '[^A-Za-z0-9._-]', '-').Trim('-')
$bundleFileName = $bundleNamePattern.Replace('{version}', $version).Replace('{id}', $appId).Replace('{name}', $safeName)
$bundlePath = Join-Path $artifactsDirectory $bundleFileName
$builderBlock = (Get-FlatpakBuilderShellLines) -join "`n"
$buildScript = @"
#!/bin/sh
set -e
cd "`$(dirname "`$0")"
$builderBlock
flatpak build-bundle "`$ROOT/repo" "../$bundleFileName" "$appId"
"@
[System.IO.File]::WriteAllText((Join-Path $stageRoot 'build-flatpak.sh'), $buildScript.Replace("`r`n", "`n"), [System.Text.UTF8Encoding]::new($false))
$assetPaths = [System.Collections.Generic.List[string]]::new()
if ($attachSourceZip) {
$sourceZipPattern = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'sourceZipNamePattern' -Default '{id}-{version}-flatpak-src.zip')
$sourceZipName = $sourceZipPattern.Replace('{version}', $version).Replace('{id}', $appId).Replace('{name}', $safeName)
$sourceZipPath = Join-Path $artifactsDirectory $sourceZipName
if (Test-Path -LiteralPath $sourceZipPath -PathType Leaf) {
Remove-Item -LiteralPath $sourceZipPath -Force
}
Compress-Archive -Path $stageRoot -DestinationPath $sourceZipPath -CompressionLevel Optimal -Force
Write-Log -Level "OK" -Message " Flatpak source archive: $sourceZipPath"
$assetPaths.Add($sourceZipPath)
Set-EngineFact -Context $sharedSettings -Namespace 'desktop' -Name 'flatpakSourcePath' -Value $sourceZipPath -Overwrite Replace
}
if ($buildBundle) {
Write-Log -Level "STEP" -Message "Building Flatpak bundle..."
$buildDir = Join-Path $stageRoot 'build-dir'
$repoDir = Join-Path $stageRoot 'repo'
$built = $false
try {
Invoke-HostFlatpakBuilder -RepoDir $repoDir -BuildDir $buildDir -ManifestPath $manifestPath
if (Test-Path -LiteralPath $bundlePath -PathType Leaf) {
Remove-Item -LiteralPath $bundlePath -Force
}
Invoke-ExternalCommand -Name flatpak -ArgumentList @(
'build-bundle',
$repoDir,
$bundlePath,
$appId
) | Out-Null
if (-not (Test-Path -LiteralPath $bundlePath -PathType Leaf)) {
throw "flatpak build-bundle completed but bundle was not produced: $bundlePath"
}
$built = $true
}
catch {
if (-not (Test-FlatpakBuilderMissingException -ErrorRecord $_)) {
throw
}
Write-Log -Level "INFO" -Message " Native Flatpak builder not available (Flathub org.flatpak.Builder or distro flatpak-builder)."
}
if (-not $built -and $useWsl -and $IsWindows) {
try {
Write-Log -Level "STEP" -Message "Building Flatpak bundle via WSL distro $wslDistro (Linux filesystem, not Docker Desktop)..."
if (Test-Path -LiteralPath $bundlePath -PathType Leaf) {
Remove-Item -LiteralPath $bundlePath -Force
}
Invoke-FlatpakBundleViaWsl `
-Distro $wslDistro `
-WindowsStageRoot $stageRoot `
-WindowsBundlePath $bundlePath `
-AppId $appId
if (-not (Test-Path -LiteralPath $bundlePath -PathType Leaf)) {
throw "WSL flatpak build-bundle completed but bundle was not produced: $bundlePath"
}
$built = $true
}
catch {
if ($whenBuilderMissing -eq 'skip' -and (Test-WslFallbackSkippable -ErrorRecord $_)) {
Write-Log -Level "WARN" -Message " WSL Flatpak builder not available ($($_.Exception.Message)); skipped bundle."
}
else {
throw
}
}
}
if ($built) {
Write-Log -Level "OK" -Message " Flatpak bundle ready: $bundlePath"
$assetPaths.Add($bundlePath)
Set-EngineFact -Context $sharedSettings -Namespace 'desktop' -Name 'flatpakBundlePath' -Value $bundlePath -Overwrite Replace
}
elseif ($whenBuilderMissing -eq 'fail') {
throw "Flatpak builder is not available (Flathub org.flatpak.Builder, distro flatpak-builder, or WSL distro $wslDistro)."
}
elseif (-not $useWsl -or -not $IsWindows) {
Write-Log -Level "WARN" -Message " Flatpak builder not available; skipped bundle. Install Flathub org.flatpak.Builder (preferred) or distro flatpak-builder."
}
}
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
if ($assetPaths.Count -gt 0) {
Add-ReleaseAssetPaths -Context $sharedSettings -Path @($assetPaths)
}
}
Export-ModuleMember -Function Invoke-Plugin