#!/usr/bin/env pwsh <# .SYNOPSIS Creates a .deb or .rpm package from a pre-built binary directory. .DESCRIPTION Builds a native Linux package without requiring Ruby/fpm. .deb is assembled with tar and ar (binutils). .rpm is built with rpmbuild (rpm-build). .PARAMETER PackageType Package format to produce: 'deb' or 'rpm'. .PARAMETER SourceDir Directory containing the pre-built binaries to package. .PARAMETER OutputPath Full path to the output package file (e.g. output/unigetui.deb). .PARAMETER Version Package version string, already formatted for the target format: deb: "2026.1.0" or "2026.1.0~beta1" rpm: "2026.1.0" .PARAMETER Architecture Target CPU architecture in the format expected by the package type: deb: amd64 | arm64 rpm: x86_64 | aarch64 .PARAMETER Iteration RPM Release/iteration field (default: 1). Ignored for .deb. .PARAMETER PackageName Package name (default: unigetui). .PARAMETER InstallPrefix Absolute install directory on the target system (default: /opt/unigetui). .PARAMETER Description One-line package description. .PARAMETER Maintainer Maintainer field value, e.g. "Name ". .PARAMETER Url Homepage / upstream URL. .PARAMETER AppExecutableName Executable filename inside InstallPrefix. .PARAMETER LauncherName Public command name exposed in PATH. .PARAMETER IconSourcePath Source path for the installed desktop icon. #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('deb', 'rpm')] [string] $PackageType, [Parameter(Mandatory)] [string] $SourceDir, [Parameter(Mandatory)] [string] $OutputPath, [Parameter(Mandatory)] [string] $Version, [Parameter(Mandatory)] [string] $Architecture, [string] $Iteration = '1', [string] $PackageName = 'unigetui', [string] $InstallPrefix = '/opt/unigetui', [string] $Description = 'UniGetUI - GUI for package managers', [string] $Maintainer = 'Devolutions Inc. ', [string] $Url = 'https://github.com/Devolutions/UniGetUI', [string] $AppExecutableName = 'UniGetUI', [string] $LauncherName = 'unigetui', [string] $IconSourcePath = (Join-Path $PSScriptRoot '..' 'src' 'SharedAssets' 'Assets' 'Images' 'icon.png') ) $ErrorActionPreference = 'Stop' $SourceDir = (Resolve-Path $SourceDir).Path $OutputPath = [System.IO.Path]::GetFullPath($OutputPath) $IconSourcePath = (Resolve-Path $IconSourcePath).Path $OutDir = Split-Path $OutputPath if ($OutDir) { New-Item -ItemType Directory -Path $OutDir -Force | Out-Null } $TmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "pkg-$(New-Guid)" New-Item -ItemType Directory -Path $TmpDir | Out-Null $IconInstallDir = '/usr/share/icons/hicolor/512x512/apps' $IconTargetName = "$LauncherName.png" $IconInstallPath = "$IconInstallDir/$IconTargetName" $DesktopFilePath = "/usr/share/applications/$LauncherName.desktop" $LauncherPath = "/usr/bin/$LauncherName" function New-LinuxIntegrationAssets { param( [Parameter(Mandatory)] [string] $StageRoot ) $payloadDir = Join-Path $StageRoot ($InstallPrefix.TrimStart('/')) New-Item -ItemType Directory -Path $payloadDir -Force | Out-Null & /bin/cp -a "$SourceDir/." $payloadDir if ($LASTEXITCODE -ne 0) { throw "cp (payload staging) exited $LASTEXITCODE" } $appExecutableFullPath = Join-Path $payloadDir $AppExecutableName if (-not (Test-Path $appExecutableFullPath -PathType Leaf)) { throw "App executable '$AppExecutableName' was not found in package payload '$payloadDir'" } $launcherFullPath = Join-Path $StageRoot $LauncherPath.TrimStart('/') New-Item -ItemType Directory -Path (Split-Path $launcherFullPath -Parent) -Force | Out-Null $launcherCommand = 'exec {0}/{1} "$@"' -f $InstallPrefix, $AppExecutableName $launcherScript = @( '#!/bin/sh', $launcherCommand, '' ) -join "`n" [System.IO.File]::WriteAllText($launcherFullPath, $launcherScript) & chmod 755 $launcherFullPath if ($LASTEXITCODE -ne 0) { throw "chmod (launcher) exited $LASTEXITCODE" } $desktopEntryFullPath = Join-Path $StageRoot $DesktopFilePath.TrimStart('/') New-Item -ItemType Directory -Path (Split-Path $desktopEntryFullPath -Parent) -Force | Out-Null $desktopEntry = @( '[Desktop Entry]', 'Version=1.0', 'Type=Application', 'Name=UniGetUI', "Comment=$Description", "Exec=$LauncherPath", "Icon=$LauncherName", 'Terminal=false', 'Categories=System;Utility;', 'StartupNotify=true', '' ) -join "`n" [System.IO.File]::WriteAllText($desktopEntryFullPath, $desktopEntry) & chmod 644 $desktopEntryFullPath if ($LASTEXITCODE -ne 0) { throw "chmod (desktop entry) exited $LASTEXITCODE" } $iconFullPath = Join-Path $StageRoot $IconInstallPath.TrimStart('/') New-Item -ItemType Directory -Path (Split-Path $iconFullPath -Parent) -Force | Out-Null Copy-Item -Path $IconSourcePath -Destination $iconFullPath -Force & chmod 644 $iconFullPath if ($LASTEXITCODE -ne 0) { throw "chmod (icon) exited $LASTEXITCODE" } } try { # ------------------------------------------------------------------ # .deb # Structure: ar archive containing debian-binary, control.tar.gz, data.tar.gz # Tools required: tar, ar (binutils – pre-installed on Ubuntu runners) # ------------------------------------------------------------------ if ($PackageType -eq 'deb') { # 1. debian-binary (must be the first ar member, content is "2.0\n") $DebianBinaryPath = Join-Path $TmpDir 'debian-binary' [System.IO.File]::WriteAllText($DebianBinaryPath, "2.0`n") # 2. data.tar.gz – payload and Linux integration assets staged under target paths $DataStage = Join-Path $TmpDir 'data' New-LinuxIntegrationAssets -StageRoot $DataStage $InstalledSizeKb = [long][System.Math]::Ceiling( (Get-ChildItem -Recurse -File $DataStage | Measure-Object -Property Length -Sum).Sum / 1024 ) # 3. control.tar.gz $ControlDir = Join-Path $TmpDir 'control' New-Item -ItemType Directory -Path $ControlDir | Out-Null # dpkg requires LF line endings and a trailing newline in control files $ControlLines = @( "Package: $PackageName", "Version: $Version", "Architecture: $Architecture", "Maintainer: $Maintainer", "Installed-Size: $InstalledSizeKb", "Homepage: $Url", "Description: $Description", "Priority: optional", "" ) [System.IO.File]::WriteAllText( (Join-Path $ControlDir 'control'), ($ControlLines -join "`n") ) $ControlTarPath = Join-Path $TmpDir 'control.tar.gz' & tar -czf $ControlTarPath -C $ControlDir . if ($LASTEXITCODE -ne 0) { throw "tar (control) exited $LASTEXITCODE" } $DataTarPath = Join-Path $TmpDir 'data.tar.gz' & tar -czf $DataTarPath -C $DataStage . if ($LASTEXITCODE -ne 0) { throw "tar (data) exited $LASTEXITCODE" } # 4. Assemble .deb with ar # Member names in the archive must be bare filenames (not paths), # so Push-Location into TmpDir before invoking ar. Push-Location $TmpDir try { & ar rc $OutputPath 'debian-binary' 'control.tar.gz' 'data.tar.gz' if ($LASTEXITCODE -ne 0) { throw "ar exited $LASTEXITCODE" } } finally { Pop-Location } } # ------------------------------------------------------------------ # .rpm # Built with rpmbuild. %install copies from the absolute SourceDir # into the buildroot so no pre-staged directory tricks are needed. # Tools required: rpmbuild (rpm-build apt package) # ------------------------------------------------------------------ elseif ($PackageType -eq 'rpm') { $DataStage = Join-Path $TmpDir 'data' New-LinuxIntegrationAssets -StageRoot $DataStage $RpmTop = Join-Path $TmpDir 'rpmbuild' foreach ($d in 'BUILD', 'BUILDROOT', 'RPMS', 'SOURCES', 'SPECS', 'SRPMS') { New-Item -ItemType Directory -Path (Join-Path $RpmTop $d) | Out-Null } $SpecPath = Join-Path $RpmTop 'SPECS' "$PackageName.spec" # Spec uses LF line endings; here-string keeps them on Linux/macOS $SpecText = @" Name: $PackageName Version: $Version Release: $Iteration Summary: $Description License: GPL-3.0-or-later URL: $Url # The self-contained .NET runtime links against liblttng-ust.so.0, which was # renamed to .so.1 in newer distros (Fedora 38+). Exclude it so the package # installs on both old and new releases; .NET ships its own tracing fallback. %global __requires_exclude ^liblttng-ust\.so %description $Description %install cp -rp $DataStage/. %{buildroot}/ %files %defattr(-,root,root,-) $InstallPrefix $LauncherPath $DesktopFilePath $IconInstallPath %changelog "@ [System.IO.File]::WriteAllText($SpecPath, $SpecText) & rpmbuild -bb ` --define "_topdir $RpmTop" ` --define "_build_name_fmt %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm" ` --define "__strip /bin/true" ` --define "__debug_install_post %{nil}" ` --define "debug_package %{nil}" ` --target "$Architecture-unknown-linux" ` $SpecPath if ($LASTEXITCODE -ne 0) { throw "rpmbuild exited $LASTEXITCODE" } $BuiltRpm = Get-ChildItem -Path (Join-Path $RpmTop 'RPMS') -Recurse -Filter '*.rpm' | Select-Object -First 1 if (-not $BuiltRpm) { throw "rpmbuild produced no .rpm file" } Move-Item -Path $BuiltRpm.FullName -Destination $OutputPath -Force } } finally { Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue } Write-Host "Created: $OutputPath"