[release/v7.6] Replace fpm with native macOS packaging tools (pkgbuild/productbuild) (#26501)

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com>
This commit is contained in:
Travis Plunk
2025-11-20 15:50:38 -06:00
committed by GitHub
co-authored by Copilot TravisEz13
parent 0e83f60c0e
commit ce725ce207
6 changed files with 706 additions and 87 deletions
@@ -27,13 +27,15 @@
### For Release/Packaging
**Use: Release with version tag**
**Use: Release with version tag and public NuGet feeds**
```yaml
- name: Build for Release
shell: pwsh
run: |
Import-Module ./build.psm1
Import-Module ./tools/ci.psm1
Switch-PSNugetConfig -Source Public
$releaseTag = Get-ReleaseTag
Start-PSBuild -Configuration 'Release' -ReleaseTag $releaseTag
```
@@ -43,6 +45,11 @@
- No debug symbols (smaller size)
- Production-ready
**Why Switch-PSNugetConfig -Source Public:**
- Switches NuGet package sources to public feeds (nuget.org and public Azure DevOps feeds)
- Required for CI/CD environments that don't have access to private feeds
- Uses publicly available packages instead of Microsoft internal feeds
### For Code Coverage
**Use: CodeCoverage configuration**
@@ -93,3 +100,42 @@ src/powershell-win-core/bin/Debug/<netversion>/<runtime>/publish/
3. Match configuration to purpose
4. Use `-CI` only when needed
5. Always specify `-ReleaseTag` for release or packaging builds
6. Use `Switch-PSNugetConfig -Source Public` in CI/CD for release builds
## NuGet Feed Configuration
### Switch-PSNugetConfig
The `Switch-PSNugetConfig` function in `build.psm1` manages NuGet package source configuration.
**Available Sources:**
- **Public**: Uses public feeds (nuget.org and public Azure DevOps feeds)
- Required for: CI/CD environments, public builds, packaging
- Does not require authentication
- **Private**: Uses internal PowerShell team feeds
- Required for: Internal development with preview packages
- Requires authentication credentials
- **NuGetOnly**: Uses only nuget.org
- Required for: Minimal dependency scenarios
**Usage:**
```powershell
# Switch to public feeds (most common for CI/CD)
Switch-PSNugetConfig -Source Public
# Switch to private feeds with authentication
Switch-PSNugetConfig -Source Private -UserName $userName -ClearTextPAT $pat
# Switch to nuget.org only
Switch-PSNugetConfig -Source NuGetOnly
```
**When to Use:**
- **Always use `-Source Public`** before building in CI/CD workflows
- Use before any build that will create packages for distribution
- Use in forks or environments without access to Microsoft internal feeds
@@ -0,0 +1,149 @@
---
applyTo:
- "**/*.ps1"
- "**/*.psm1"
---
# Using Start-NativeExecution for Native Command Execution
## Purpose
`Start-NativeExecution` is the standard function for executing native commands (external executables) in PowerShell scripts within this repository. It provides consistent error handling and better diagnostics when native commands fail.
## When to Use
Use `Start-NativeExecution` whenever you need to:
- Execute external commands (e.g., `git`, `dotnet`, `pkgbuild`, `productbuild`, `fpm`, `rpmbuild`)
- Ensure proper exit code checking
- Get better error messages with caller information
- Handle verbose output on error
## Basic Usage
```powershell
Start-NativeExecution {
git clone https://github.com/PowerShell/PowerShell.git
}
```
## With Parameters
Use backticks for line continuation within the script block:
```powershell
Start-NativeExecution {
pkgbuild --root $pkgRoot `
--identifier $pkgIdentifier `
--version $Version `
--scripts $scriptsDir `
$outputPath
}
```
## Common Parameters
### -VerboseOutputOnError
Captures command output and displays it only if the command fails:
```powershell
Start-NativeExecution -VerboseOutputOnError {
dotnet build --configuration Release
}
```
### -IgnoreExitcode
Allows the command to fail without throwing an exception:
```powershell
Start-NativeExecution -IgnoreExitcode {
git diff --exit-code # Returns 1 if differences exist
}
```
## Availability
The function is defined in `tools/buildCommon/startNativeExecution.ps1` and is available in:
- `build.psm1` (dot-sourced automatically)
- `tools/packaging/packaging.psm1` (dot-sourced automatically)
- Test modules that include `HelpersCommon.psm1`
To use in other scripts, dot-source the function:
```powershell
. "$PSScriptRoot/../buildCommon/startNativeExecution.ps1"
```
## Error Handling
When a native command fails (non-zero exit code), `Start-NativeExecution`:
1. Captures the exit code
2. Identifies the calling location (file and line number)
3. Throws a descriptive error with full context
Example error message:
```
Execution of {git clone ...} by /path/to/script.ps1: line 42 failed with exit code 1
```
## Examples from the Codebase
### Git Operations
```powershell
Start-NativeExecution {
git fetch --tags --quiet upstream
}
```
### Build Operations
```powershell
Start-NativeExecution -VerboseOutputOnError {
dotnet publish --configuration Release
}
```
### Packaging Operations
```powershell
Start-NativeExecution -VerboseOutputOnError {
pkgbuild --root $pkgRoot --identifier $pkgId --version $version $outputPath
}
```
### Permission Changes
```powershell
Start-NativeExecution {
find $staging -type d | xargs chmod 755
find $staging -type f | xargs chmod 644
}
```
## Anti-Patterns
**Don't do this:**
```powershell
& somecommand $args
if ($LASTEXITCODE -ne 0) {
throw "Command failed"
}
```
**Do this instead:**
```powershell
Start-NativeExecution {
somecommand $args
}
```
## Best Practices
1. **Always use Start-NativeExecution** for native commands to ensure consistent error handling
2. **Use -VerboseOutputOnError** for commands with useful diagnostic output
3. **Use backticks for readability** when commands have multiple arguments
4. **Don't capture output unnecessarily** - let the function handle it
5. **Use -IgnoreExitcode sparingly** - only when non-zero exit codes are expected and acceptable
## Related Documentation
- Source: `tools/buildCommon/startNativeExecution.ps1`
- Blog post: https://mnaoumov.wordpress.com/2015/01/11/execution-of-external-commands-in-powershell-done-right/
+53 -2
View File
@@ -157,7 +157,7 @@ jobs:
runner_os: macos-15-large
test_results_artifact_name: testResults-xunit
PackageMac-macos_packaging:
name: macOS packaging (bootstrap only)
name: macOS packaging and testing
needs:
- changes
if: ${{ needs.changes.outputs.source == 'true' }}
@@ -166,12 +166,63 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v5
with:
fetch-depth: 1000
- uses: actions/setup-dotnet@v4
with:
global-json-file: ./global.json
- name: Bootstrap packaging
if: success() || failure()
if: success()
run: |-
import-module ./build.psm1
start-psbootstrap -Scenario package
shell: pwsh
- name: Build PowerShell and Create macOS package
if: success()
run: |-
import-module ./build.psm1
import-module ./tools/ci.psm1
import-module ./tools/packaging/packaging.psm1
Switch-PSNugetConfig -Source Public
Sync-PSTags -AddRemoteIfMissing
$releaseTag = Get-ReleaseTag
Start-PSBuild -Configuration Release -PSModuleRestore -ReleaseTag $releaseTag
$macOSRuntime = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'osx-arm64' } else { 'osx-x64' }
Start-PSPackage -Type osxpkg -ReleaseTag $releaseTag -MacOSRuntime $macOSRuntime -SkipReleaseChecks
shell: pwsh
- name: Test package contents
if: success()
run: |-
$env:PACKAGE_FOLDER = Get-Location
$testResultsPath = Join-Path $env:RUNNER_WORKSPACE "testResults"
if (-not (Test-Path $testResultsPath)) {
New-Item -ItemType Directory -Path $testResultsPath -Force | Out-Null
}
Import-Module Pester
$pesterConfig = New-PesterConfiguration
$pesterConfig.Run.Path = './tools/packaging/releaseTests/macOSPackage.tests.ps1'
$pesterConfig.Run.PassThru = $true
$pesterConfig.Output.Verbosity = 'Detailed'
$pesterConfig.TestResult.Enabled = $true
$pesterConfig.TestResult.OutputFormat = 'NUnitXml'
$pesterConfig.TestResult.OutputPath = Join-Path $testResultsPath "macOSPackage.xml"
$result = Invoke-Pester -Configuration $pesterConfig
if ($result.FailedCount -gt 0) {
throw "Package validation failed with $($result.FailedCount) failed test(s)"
}
shell: pwsh
- name: Publish and Upload Pester Test Results
if: always()
uses: "./.github/actions/test/process-pester-results"
with:
name: "macOSPackage"
testResultsFolder: "${{ runner.workspace }}/testResults"
- name: Upload package artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: macos-package
path: "*.pkg"
ready_to_merge:
name: macos ready to merge
needs:
+12 -5
View File
@@ -72,11 +72,18 @@ The output of `Start-PSBuild` includes a `powershell.exe` executable which can s
#### Linux / macOS
The `Start-PSPackage` function delegates to `New-UnixPackage`.
It relies on the [Effing Package Management][fpm] project,
which makes building packages for any (non-Windows) platform a breeze.
Similarly, the PowerShell man-page is generated from the Markdown-like file
For **Linux** (Debian-based distributions), it relies on the [Effing Package Management][fpm] project,
which makes building packages a breeze.
For **macOS**, it uses native packaging tools (`pkgbuild` and `productbuild`) from Xcode Command Line Tools,
eliminating the need for Ruby or fpm.
For **Linux** (Red Hat-based distributions), it uses `rpmbuild` directly.
The PowerShell man-page is generated from the Markdown-like file
[`assets/pwsh.1.ronn`][man] using [Ronn][].
The function `Start-PSBootstrap -Package` will install both these tools.
The function `Start-PSBootstrap -Package` will install these tools.
To modify any property of the packages, edit the `New-UnixPackage` function.
Please also refer to the function for details on the package properties
@@ -131,7 +138,7 @@ Without `-Name` specified, the primary `powershell`
package will instead be created.
[fpm]: https://github.com/jordansissel/fpm
[man]: ../../assets/pwsh.1.ronn
[man]: ../../assets/manpage/pwsh.1.ronn
[ronn]: https://github.com/rtomayko/ronn
### Build and Packaging Examples
+283 -79
View File
@@ -1,6 +1,8 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
. "$PSScriptRoot\..\buildCommon\startNativeExecution.ps1"
$Environment = Get-EnvironmentInformation
$RepoRoot = (Resolve-Path -Path "$PSScriptRoot/../..").Path
@@ -1197,19 +1199,7 @@ function New-UnixPackage {
# Generate After Install and After Remove scripts
$AfterScriptInfo = New-AfterScripts -Link $Link -Distribution $DebDistro -Destination $Destination
# there is a weird bug in fpm
# if the target of the powershell symlink exists, `fpm` aborts
# with a `utime` error on macOS.
# so we move it to make symlink broken
# refers to executable, does not vary by channel
$symlink_dest = "$Destination/pwsh"
$hack_dest = "./_fpm_symlink_hack_powershell"
if ($Environment.IsMacOS) {
if (Test-Path $symlink_dest) {
Write-Warning "Move $symlink_dest to $hack_dest (fpm utime bug)"
Start-NativeExecution ([ScriptBlock]::Create("$sudo mv $symlink_dest $hack_dest"))
}
}
# Note: The fpm symlink workaround is no longer needed with native macOS packaging tools
# Generate gzip of man file
$ManGzipInfo = New-ManGzip -IsPreview:$IsPreview -IsLTS:$LTS
@@ -1329,8 +1319,38 @@ function New-UnixPackage {
throw
}
}
} elseif ($Type -eq 'osxpkg') {
# Use native macOS packaging tools
if ($PSCmdlet.ShouldProcess("Create macOS package with pkgbuild/productbuild")) {
Write-Log "Creating macOS package with native tools..."
$macPkgArgs = @{
Name = $Name
Version = $packageVersion
Iteration = $Iteration
Staging = $Staging
Destination = $Destination
ManGzipFile = $ManGzipInfo.GzipFile
ManDestination = $ManGzipInfo.ManFile
LinkInfo = $Links
AfterInstallScript = $AfterScriptInfo.AfterInstallScript
AppsFolder = $AppsFolder
HostArchitecture = $HostArchitecture
CurrentLocation = $CurrentLocation
}
try {
$packageFile = New-MacOSPackage @macPkgArgs
$Output = @("Created package {:path=>""$($packageFile.Name)""}")
}
catch {
Write-Verbose -Message "!!!Handling error in macOS packaging!!!" -Verbose -ErrorAction SilentlyContinue
Get-Error -InputObject $_
throw
}
}
} else {
# Use fpm for DEB and macOS packages
# Use fpm for DEB packages
$Arguments = @()
$Arguments += Get-FpmArguments `
@@ -1372,12 +1392,6 @@ function New-UnixPackage {
{
Clear-MacOSLauncher
}
# this is continuation of a fpm hack for a weird bug
if (Test-Path $hack_dest) {
Write-Warning "Move $hack_dest to $symlink_dest (fpm utime bug)"
Start-NativeExecution -sb ([ScriptBlock]::Create("$sudo mv $hack_dest $symlink_dest")) -VerboseOutputOnError
}
}
# Clean up rpmbuild directory if it was created
@@ -1401,12 +1415,9 @@ function New-UnixPackage {
# Magic to get path output
$createdPackage = Get-Item (Join-Path $CurrentLocation (($Output[-1] -split ":path=>")[-1] -replace '["{}]'))
if ($Environment.IsMacOS) {
if ($PSCmdlet.ShouldProcess("Add distribution information and Fix PackageName"))
{
$createdPackage = New-MacOsDistributionPackage -FpmPackage $createdPackage -HostArchitecture $HostArchitecture -IsPreview:$IsPreview
}
}
# For macOS with native tools, the package is already in the correct format
# For macOS with fpm (no longer used), we would need New-MacOsDistributionPackage
# For other platforms, the package name from fpm/rpmbuild is sufficient
if (Test-Path $createdPackage)
{
@@ -1451,14 +1462,27 @@ Function New-LinkInfo
function New-MacOsDistributionPackage
{
[CmdletBinding(SupportsShouldProcess=$true)]
param(
[Parameter(Mandatory,HelpMessage='The FileInfo of the file created by FPM')]
[System.IO.FileInfo]$FpmPackage,
[Parameter(Mandatory,HelpMessage='The FileInfo of the component package')]
[System.IO.FileInfo]$ComponentPackage,
[Parameter(Mandatory,HelpMessage='Package name for the output file')]
[string]$PackageName,
[Parameter(Mandatory,HelpMessage='Package version')]
[string]$Version,
[Parameter(Mandatory,HelpMessage='Output directory for the final package')]
[string]$OutputDirectory,
[Parameter(HelpMessage='x86_64 for Intel or arm64 for Apple Silicon')]
[ValidateSet("x86_64", "arm64")]
[string] $HostArchitecture = "x86_64",
[Parameter(HelpMessage='Package identifier')]
[string]$PackageIdentifier,
[Switch] $IsPreview
)
@@ -1467,64 +1491,81 @@ function New-MacOsDistributionPackage
throw 'New-MacOsDistributionPackage is only supported on macOS!'
}
$packageName = Split-Path -Leaf -Path $FpmPackage
# Create a temp directory to store the needed files
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
New-Item -ItemType Directory -Path $tempDir -Force > $null
$resourcesDir = Join-Path -Path $tempDir -ChildPath 'resources'
New-Item -ItemType Directory -Path $resourcesDir -Force > $null
#Copy background file to temp directory
# Copy background file to temp directory
$backgroundFile = "$RepoRoot/assets/macDialog.png"
Copy-Item -Path $backgroundFile -Destination $resourcesDir
# Move the current package to the temp directory
$tempPackagePath = Join-Path -Path $tempDir -ChildPath $packageName
Move-Item -Path $FpmPackage -Destination $tempPackagePath -Force
# Add the OS information to the macOS package file name.
$packageExt = [System.IO.Path]::GetExtension($FpmPackage.Name)
# get the package name from fpm without the extension, but replace powershell-preview at the beginning of the name with powershell.
$packageNameWithoutExt = [System.IO.Path]::GetFileNameWithoutExtension($FpmPackage.Name) -replace '^powershell\-preview' , 'powershell'
$newPackageName = "{0}-{1}{2}" -f $packageNameWithoutExt, $script:Options.Runtime, $packageExt
$newPackagePath = Join-Path $FpmPackage.DirectoryName $newPackageName
# -Force is not deleting the NewName if it exists, so delete it if it does
if ($Force -and (Test-Path -Path $newPackagePath))
{
Remove-Item -Force $newPackagePath
if (Test-Path $backgroundFile) {
Copy-Item -Path $backgroundFile -Destination $resourcesDir -Force
}
# Copy the component package to temp directory
$componentFileName = Split-Path -Leaf -Path $ComponentPackage
$tempComponentPath = Join-Path -Path $tempDir -ChildPath $componentFileName
Copy-Item -Path $ComponentPackage -Destination $tempComponentPath -Force
# Create the distribution xml
$distributionXmlPath = Join-Path -Path $tempDir -ChildPath 'powershellDistribution.xml'
$packageId = Get-MacOSPackageId -IsPreview:$IsPreview.IsPresent
# Get package ID if not provided
if (-not $PackageIdentifier) {
$PackageIdentifier = Get-MacOSPackageId -IsPreview:$IsPreview.IsPresent
}
# Minimum OS version
$minOSVersion = "11.0" # macOS Big Sur minimum
# format distribution template with:
# 0 - title
# 1 - version
# 2 - package path
# 2 - package path (component package filename)
# 3 - minimum os version
# 4 - Package Identifier
# 5 - host architecture (x86_64 for Intel or arm64 for Apple Silicon)
$PackagingStrings.OsxDistributionTemplate -f "PowerShell - $packageVersion", $packageVersion, $packageName, '10.14', $packageId, $HostArchitecture | Out-File -Encoding ascii -FilePath $distributionXmlPath -Force
$PackagingStrings.OsxDistributionTemplate -f $PackageName, $Version, $componentFileName, $minOSVersion, $PackageIdentifier, $HostArchitecture | Out-File -Encoding utf8 -FilePath $distributionXmlPath -Force
Write-Log "Applying distribution.xml to package..."
Push-Location $tempDir
try
{
# productbuild is an xcode command line tool, and those tools are installed when you install brew
Start-NativeExecution -sb {productbuild --distribution $distributionXmlPath --resources $resourcesDir $newPackagePath} -VerboseOutputOnError
}
finally
{
Pop-Location
Remove-Item -Path $tempDir -Recurse -Force
# Build final package path
$finalPackagePath = Join-Path $OutputDirectory "$PackageName-$Version-osx-$HostArchitecture.pkg"
# Remove existing package if it exists
if (Test-Path $finalPackagePath) {
Write-Warning "Removing existing package: $finalPackagePath"
Remove-Item $finalPackagePath -Force
}
return (Get-Item $newPackagePath)
if ($PSCmdlet.ShouldProcess("Build product package with productbuild")) {
Write-Log "Applying distribution.xml to package..."
Push-Location $tempDir
try
{
# productbuild is an xcode command line tool
Start-NativeExecution -VerboseOutputOnError {
productbuild --distribution $distributionXmlPath `
--package-path $tempDir `
--resources $resourcesDir `
$finalPackagePath
}
if (Test-Path $finalPackagePath) {
Write-Log "Successfully created macOS package: $finalPackagePath"
}
else {
throw "Package was not created at expected location: $finalPackagePath"
}
}
finally
{
Pop-Location
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
return (Get-Item $finalPackagePath)
}
Class LinkInfo
@@ -1692,6 +1733,157 @@ cp $ManGzipFile `$RPM_BUILD_ROOT$ManDestination
return $specContent
}
function New-MacOSPackage
{
[CmdletBinding(SupportsShouldProcess=$true)]
param(
[Parameter(Mandatory)]
[string]$Name,
[Parameter(Mandatory)]
[string]$Version,
[Parameter(Mandatory)]
[string]$Iteration,
[Parameter(Mandatory)]
[string]$Staging,
[Parameter(Mandatory)]
[string]$Destination,
[Parameter(Mandatory)]
[string]$ManGzipFile,
[Parameter(Mandatory)]
[string]$ManDestination,
[Parameter(Mandatory)]
[LinkInfo[]]$LinkInfo,
[Parameter(Mandatory)]
[string]$AfterInstallScript,
[Parameter(Mandatory)]
[string]$AppsFolder,
[Parameter(Mandatory)]
[string]$HostArchitecture,
[string]$CurrentLocation = (Get-Location)
)
Write-Log "Creating macOS package using pkgbuild and productbuild..."
# Create a temporary directory for package building
$tempRoot = New-TempFolder
$componentPkgPath = Join-Path $tempRoot "component.pkg"
$scriptsDir = Join-Path $tempRoot "scripts"
$resourcesDir = Join-Path $tempRoot "resources"
$distributionFile = Join-Path $tempRoot "distribution.xml"
try {
# Create scripts directory
New-Item -ItemType Directory -Path $scriptsDir -Force | Out-Null
# Copy and prepare the postinstall script
$postInstallPath = Join-Path $scriptsDir "postinstall"
Copy-Item -Path $AfterInstallScript -Destination $postInstallPath -Force
Start-NativeExecution {
chmod 755 $postInstallPath
}
# Create a temporary directory for the package root
$pkgRoot = Join-Path $tempRoot "pkgroot"
New-Item -ItemType Directory -Path $pkgRoot -Force | Out-Null
# Copy staging files to destination path in package root
$destInPkg = Join-Path $pkgRoot $Destination
New-Item -ItemType Directory -Path $destInPkg -Force | Out-Null
Write-Verbose "Copying staging files from $Staging to $destInPkg" -Verbose
Copy-Item -Path "$Staging/*" -Destination $destInPkg -Recurse -Force
# Create man page directory structure
$manDir = Join-Path $pkgRoot (Split-Path $ManDestination -Parent)
New-Item -ItemType Directory -Path $manDir -Force | Out-Null
Copy-Item -Path $ManGzipFile -Destination (Join-Path $pkgRoot $ManDestination) -Force
# Create symlinks in package root
# The LinkInfo contains Source (a temp file that IS a symlink) and Destination (where to install it)
foreach ($link in $LinkInfo) {
$linkDestDir = Join-Path $pkgRoot (Split-Path $link.Destination -Parent)
New-Item -ItemType Directory -Path $linkDestDir -Force | Out-Null
$finalLinkPath = Join-Path $pkgRoot $link.Destination
Write-Verbose "Creating symlink at $finalLinkPath" -Verbose
# Remove if exists
if (Test-Path $finalLinkPath) {
Remove-Item $finalLinkPath -Force
}
# Get the target of the original symlink and recreate it in the package root
if (Test-Path $link.Source) {
$linkTarget = (Get-Item $link.Source).Target
if ($linkTarget) {
Write-Verbose "Creating symlink to target: $linkTarget" -Verbose
New-Item -ItemType SymbolicLink -Path $finalLinkPath -Target $linkTarget -Force | Out-Null
} else {
Write-Warning "Could not determine target for symlink at $($link.Source), copying file instead"
Copy-Item -Path $link.Source -Destination $finalLinkPath -Force
}
} else {
Write-Warning "Source symlink $($link.Source) does not exist"
}
}
# Copy launcher app folder if provided
if ($AppsFolder) {
$appsInPkg = Join-Path $pkgRoot "Applications"
New-Item -ItemType Directory -Path $appsInPkg -Force | Out-Null
Write-Verbose "Copying launcher app from $AppsFolder to $appsInPkg" -Verbose
Copy-Item -Path "$AppsFolder/*" -Destination $appsInPkg -Recurse -Force
}
# Build the component package using pkgbuild
$pkgIdentifier = Get-MacOSPackageId -IsPreview:($Name -like '*-preview')
if ($PSCmdlet.ShouldProcess("Build component package with pkgbuild")) {
Write-Log "Running pkgbuild to create component package..."
Start-NativeExecution -VerboseOutputOnError {
pkgbuild --root $pkgRoot `
--identifier $pkgIdentifier `
--version $Version `
--scripts $scriptsDir `
--install-location "/" `
$componentPkgPath
}
Write-Verbose "Component package created: $componentPkgPath" -Verbose
}
# Create the final distribution package using the refactored function
$distributionPackage = New-MacOsDistributionPackage `
-ComponentPackage (Get-Item $componentPkgPath) `
-PackageName $Name `
-Version $Version `
-OutputDirectory $CurrentLocation `
-HostArchitecture $HostArchitecture `
-PackageIdentifier $pkgIdentifier `
-IsPreview:($Name -like '*-preview')
return $distributionPackage
}
finally {
# Clean up temporary directory
if (Test-Path $tempRoot) {
Write-Verbose "Cleaning up temporary directory: $tempRoot" -Verbose
Remove-Item -Path $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
function Get-FpmArguments
{
param(
@@ -1905,6 +2097,7 @@ function Get-PackageDependencies
function Test-Dependencies
{
# Note: RPM packages no longer require fpm; they use rpmbuild directly
# macOS packages use pkgbuild and productbuild from Xcode Command Line Tools
# DEB packages still use fpm
$Dependencies = @()
@@ -1913,22 +2106,31 @@ function Test-Dependencies
$Dependencies += "fpm"
}
# Check for macOS packaging tools
if ($Environment.IsMacOS) {
$Dependencies += "pkgbuild"
$Dependencies += "productbuild"
}
foreach ($Dependency in $Dependencies) {
if (!(precheck $Dependency "Package dependency '$Dependency' not found. Run Start-PSBootstrap -Scenario Package")) {
# These tools are not added to the path automatically on OpenSUSE 13.2
# try adding them to the path and re-tesing first
[string] $gemsPath = $null
[string] $depenencyPath = $null
$gemsPath = Get-ChildItem -Path /usr/lib64/ruby/gems | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName
if ($gemsPath) {
$depenencyPath = Get-ChildItem -Path (Join-Path -Path $gemsPath -ChildPath "gems" -AdditionalChildPath $Dependency) -Recurse | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty DirectoryName
$originalPath = $env:PATH
$env:PATH = $ENV:PATH +":" + $depenencyPath
if ((precheck $Dependency "Package dependency '$Dependency' not found. Run Start-PSBootstrap -Scenario Package")) {
continue
}
else {
$env:PATH = $originalPath
# For Debian systems, try adding ruby gems to the path
if ($Environment.IsDebianFamily) {
# These tools are not added to the path automatically on OpenSUSE 13.2
# try adding them to the path and re-tesing first
[string] $gemsPath = $null
[string] $depenencyPath = $null
$gemsPath = Get-ChildItem -Path /usr/lib64/ruby/gems | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName
if ($gemsPath) {
$depenencyPath = Get-ChildItem -Path (Join-Path -Path $gemsPath -ChildPath "gems" -AdditionalChildPath $Dependency) -Recurse | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty DirectoryName
$originalPath = $env:PATH
$env:PATH = $ENV:PATH +":" + $depenencyPath
if ((precheck $Dependency "Package dependency '$Dependency' not found. Run Start-PSBootstrap -Scenario Package")) {
continue
}
else {
$env:PATH = $originalPath
}
}
}
@@ -2086,10 +2288,12 @@ function New-MacOSLauncher
# Set permissions for plist and shell script.
Start-NativeExecution {
chmod 644 $plist
}
Start-NativeExecution {
chmod 755 $shellscript
}
# Add app folder to fpm paths.
# Return the app folder path for packaging
$appsfolder = (Resolve-Path -Path "$macosapp/..").Path
return $appsfolder
@@ -0,0 +1,162 @@
Describe "Verify macOS Package" {
BeforeAll {
Write-Verbose "In Describe BeforeAll" -Verbose
Import-Module $PSScriptRoot/../../../build.psm1
# Find the macOS package
$packagePath = $env:PACKAGE_FOLDER
if (-not $packagePath) {
$packagePath = Get-Location
}
Write-Verbose "Looking for package in: $packagePath" -Verbose
$package = Get-ChildItem -Path $packagePath -Filter "*.pkg" -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $package) {
Write-Warning "No .pkg file found in $packagePath"
} else {
Write-Verbose "Found package: $($package.FullName)" -Verbose
}
# Set up test directories
$script:package = $package
$script:expandDir = $null
$script:payloadDir = $null
$script:extractedFiles = @()
if ($package) {
# Use TestDrive for temporary directories - pkgutil will create the expand directory
$script:expandDir = Join-Path "TestDrive:" -ChildPath "package-contents-test"
$expandDirResolved = (Resolve-Path "TestDrive:").ProviderPath
$script:expandDir = Join-Path $expandDirResolved -ChildPath "package-contents-test"
Write-Verbose "Expanding package to: $($script:expandDir)" -Verbose
# pkgutil will create the directory itself, so don't pre-create it
Start-NativeExecution {
pkgutil --expand $package.FullName $script:expandDir
}
# Extract the payload to verify files
$script:payloadDir = Join-Path "TestDrive:" -ChildPath "package-payload-test"
$payloadDirResolved = (Resolve-Path "TestDrive:").ProviderPath
$script:payloadDir = Join-Path $payloadDirResolved -ChildPath "package-payload-test"
# Create payload directory since cpio needs it
if (-not (Test-Path $script:payloadDir)) {
$null = New-Item -ItemType Directory -Path $script:payloadDir -Force
}
$componentPkg = Get-ChildItem -Path $script:expandDir -Filter "*.pkg" -Recurse | Select-Object -First 1
if ($componentPkg) {
Write-Verbose "Extracting payload from: $($componentPkg.FullName)" -Verbose
Push-Location $script:payloadDir
try {
$payloadFile = Join-Path $componentPkg.FullName "Payload"
Get-Content -Path $payloadFile -Raw -AsByteStream | & cpio -i 2>&1 | Out-Null
} finally {
Pop-Location
}
}
# Get all extracted files for verification
$script:extractedFiles = Get-ChildItem -Path $script:payloadDir -Recurse -ErrorAction SilentlyContinue
Write-Verbose "Extracted $($script:extractedFiles.Count) files" -Verbose
}
}
AfterAll {
# TestDrive automatically cleans up, but we can ensure cleanup happens
# No manual cleanup needed as TestDrive handles it
}
Context "Package existence and structure" {
It "Package file should exist" {
$script:package | Should -Not -BeNullOrEmpty -Because "A .pkg file should be created"
$script:package.Extension | Should -Be ".pkg"
}
It "Package should expand successfully" {
$script:expandDir | Should -Exist
Get-ChildItem -Path $script:expandDir | Should -Not -BeNullOrEmpty
}
It "Package should have a component package" {
$componentPkg = Get-ChildItem -Path $script:expandDir -Filter "*.pkg" -Recurse -ErrorAction SilentlyContinue
$componentPkg | Should -Not -BeNullOrEmpty -Because "Package should contain a component.pkg"
}
It "Payload should extract successfully" {
$script:payloadDir | Should -Exist
$script:extractedFiles | Should -Not -BeNullOrEmpty -Because "Package payload should contain files"
}
}
Context "Required files in package" {
BeforeAll {
$expectedFilePatterns = @{
"PowerShell executable" = "usr/local/microsoft/powershell/*/pwsh"
"PowerShell symlink in /usr/local/bin" = "usr/local/bin/pwsh*"
"Man page" = "usr/local/share/man/man1/pwsh*.gz"
"Launcher application plist" = "Applications/PowerShell*.app/Contents/Info.plist"
}
$testCases = @()
foreach ($key in $expectedFilePatterns.Keys) {
$testCases += @{
Description = $key
Pattern = $expectedFilePatterns[$key]
}
}
$script:testCases = $testCases
}
It "Should contain <Description>" -TestCases $script:testCases {
param($Description, $Pattern)
$found = $script:extractedFiles | Where-Object { $_.FullName -like "*$Pattern*" }
$found | Should -Not -BeNullOrEmpty -Because "$Description should exist in the package at path matching '$Pattern'"
}
}
Context "PowerShell binary verification" {
It "PowerShell executable should be executable" {
$pwshBinary = $script:extractedFiles | Where-Object { $_.FullName -like "*/pwsh" -and $_.FullName -like "*/microsoft/powershell/*" }
$pwshBinary | Should -Not -BeNullOrEmpty
# Check if file has executable permissions (on Unix-like systems)
if ($IsLinux -or $IsMacOS) {
$permissions = (Get-Item $pwshBinary[0].FullName).UnixFileMode
# Executable bit should be set
$permissions.ToString() | Should -Match 'x' -Because "pwsh binary should have execute permissions"
}
}
}
Context "Launcher application" {
It "Launcher app should have proper bundle structure" {
$plistFile = $script:extractedFiles | Where-Object { $_.FullName -like "*PowerShell*.app/Contents/Info.plist" }
$plistFile | Should -Not -BeNullOrEmpty
# Verify the bundle has required components
$appPath = Split-Path (Split-Path $plistFile[0].FullName -Parent) -Parent
$macOSDir = Join-Path $appPath "Contents/MacOS"
$resourcesDir = Join-Path $appPath "Contents/Resources"
Test-Path $macOSDir | Should -Be $true -Because "App bundle should have Contents/MacOS directory"
Test-Path $resourcesDir | Should -Be $true -Because "App bundle should have Contents/Resources directory"
}
It "Launcher script should exist and be executable" {
$launcherScript = $script:extractedFiles | Where-Object {
$_.FullName -like "*PowerShell*.app/Contents/MacOS/PowerShell.sh"
}
$launcherScript | Should -Not -BeNullOrEmpty -Because "Launcher script should exist"
if ($IsLinux -or $IsMacOS) {
$permissions = (Get-Item $launcherScript[0].FullName).UnixFileMode
$permissions.ToString() | Should -Match 'x' -Because "Launcher script should have execute permissions"
}
}
}
}