mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
[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:
co-authored by
Copilot
TravisEz13
parent
0e83f60c0e
commit
ce725ce207
@@ -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/
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user