-
Notifications
You must be signed in to change notification settings - Fork 8.1k
[release/v7.6] Replace fpm with native macOS packaging tools (pkgbuild/productbuild) #26495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[release/v7.6] Replace fpm with native macOS packaging tools (pkgbuild/productbuild) #26495
Conversation
…PowerShell#26268) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> Co-authored-by: Travis Plunk <travis.plunk@microsoft.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR backports the replacement of Ruby-based fpm packaging tool with native macOS packaging tools (pkgbuild/productbuild) from main branch to release/v7.6. This architectural change eliminates the external Ruby/fpm dependency on macOS and uses Apple's official packaging tools.
Key changes:
- Implements native macOS packaging using pkgbuild/productbuild instead of fpm
- Adds comprehensive Pester tests for macOS package validation
- Includes native RPM packaging with rpmbuild (eliminates fpm for RPM as well)
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/packaging/releaseTests/macOSPackage.tests.ps1 | New comprehensive Pester test suite for validating macOS package structure, contents, and permissions |
| tools/packaging/packaging.psm1 | Major refactoring: adds New-MacOSPackage and New-RpmSpec functions, refactors New-MacOsDistributionPackage, updates Test-Dependencies for native tools |
| .github/workflows/macos-ci.yml | Expands macOS CI workflow to build packages and run validation tests |
| docs/maintainers/releasing.md | Updates documentation to reflect new packaging approach for macOS (pkgbuild/productbuild) and Linux RPM (rpmbuild) |
| .github/instructions/start-native-execution.instructions.md | New instruction file documenting Start-NativeExecution usage patterns |
| .github/instructions/build-configuration-guide.md | Updates build configuration guidance with NuGet feed configuration details |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Use bash to properly handle rpmbuild arguments | ||
| # Add --target for cross-architecture builds | ||
| $targetArch = "" | ||
| if ($HostArchitecture -ne "x86_64" -and $HostArchitecture -ne "noarch") { | ||
| $targetArch = "--target $HostArchitecture" | ||
| } | ||
| $buildCmd = "rpmbuild -bb --quiet $targetArch --define '_topdir $rpmBuildRoot' --buildroot '$rpmBuildRoot/BUILDROOT' '$specFile'" | ||
| Write-Verbose "Running: $buildCmd" -Verbose | ||
| $Output = bash -c $buildCmd 2>&1 | ||
| $exitCode = $LASTEXITCODE |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of bash -c with string interpolation can be problematic if $targetArch or $rpmBuildRoot contain spaces or special characters. Consider using Start-NativeExecution directly with proper parameter passing instead of constructing a command string.
Alternative approach:
$rpmbuildArgs = @(
'-bb'
'--quiet'
'--define'
"_topdir $rpmBuildRoot"
'--buildroot'
"$rpmBuildRoot/BUILDROOT"
$specFile
)
if ($targetArch) {
$rpmbuildArgs = @('--target', $HostArchitecture) + $rpmbuildArgs
}
Start-NativeExecution -VerboseOutputOnError {
rpmbuild @rpmbuildArgs
}| # Use bash to properly handle rpmbuild arguments | |
| # Add --target for cross-architecture builds | |
| $targetArch = "" | |
| if ($HostArchitecture -ne "x86_64" -and $HostArchitecture -ne "noarch") { | |
| $targetArch = "--target $HostArchitecture" | |
| } | |
| $buildCmd = "rpmbuild -bb --quiet $targetArch --define '_topdir $rpmBuildRoot' --buildroot '$rpmBuildRoot/BUILDROOT' '$specFile'" | |
| Write-Verbose "Running: $buildCmd" -Verbose | |
| $Output = bash -c $buildCmd 2>&1 | |
| $exitCode = $LASTEXITCODE | |
| # Build argument array for rpmbuild | |
| $rpmbuildArgs = @( | |
| '-bb' | |
| '--quiet' | |
| '--define' | |
| "_topdir $rpmBuildRoot" | |
| '--buildroot' | |
| "$rpmBuildRoot/BUILDROOT" | |
| $specFile | |
| ) | |
| if ($HostArchitecture -ne "x86_64" -and $HostArchitecture -ne "noarch") { | |
| $rpmbuildArgs = @('--target', $HostArchitecture) + $rpmbuildArgs | |
| } | |
| Write-Verbose "Running: rpmbuild $($rpmbuildArgs -join ' ')" -Verbose | |
| $Output = Start-NativeExecution -VerboseOutputOnError { | |
| rpmbuild @rpmbuildArgs | |
| } |
| Push-Location $script:payloadDir | ||
| try { | ||
| $payloadFile = Join-Path $componentPkg.FullName "Payload" | ||
| Get-Content -Path $payloadFile -Raw -AsByteStream | & cpio -i 2>&1 | Out-Null |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cpio command's stderr output is redirected to Out-Null, which may hide important error messages if extraction fails. Consider capturing the output and only suppressing it on success, or using Start-NativeExecution for proper error handling:
Start-NativeExecution {
Get-Content -Path $payloadFile -Raw -AsByteStream | & cpio -i
}| Get-Content -Path $payloadFile -Raw -AsByteStream | & cpio -i 2>&1 | Out-Null | |
| Start-NativeExecution { | |
| Get-Content -Path $payloadFile -Raw -AsByteStream | & cpio -i | |
| } |
| } | ||
| Start-NativeExecution { |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The chmod commands are split into two separate Start-NativeExecution blocks. These can be combined into a single block for better efficiency:
Start-NativeExecution {
chmod 644 $plist
chmod 755 $shellscript
}| } | |
| Start-NativeExecution { |
| $specContent += "mkdir -p `$RPM_BUILD_ROOT$linkDir`n" | ||
| # For RPM, we copy the symlink itself (which fpm does by including it in the source) | ||
| # The symlink at $link.Source points to the actual target, so we'll copy it | ||
| # The -P flag preserves symlinks rather than copying their targets, which is critical for this operation. |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The comment mentions "The -P flag preserves symlinks rather than copying their targets, which is critical for this operation." However, this is valuable information but the variable name $linkDir and the logic could be clearer. Consider adding a brief inline comment explaining why -P is used at the point of use:
# Use -P to preserve symlinks instead of copying their targets
$specContent += "cp -P $($link.Source) `$RPM_BUILD_ROOT$($link.Destination)`n"| # The -P flag preserves symlinks rather than copying their targets, which is critical for this operation. | |
| # Use -P to preserve symlinks instead of copying their targets |
| $Output = bash -c $buildCmd 2>&1 | ||
| $exitCode = $LASTEXITCODE | ||
|
|
||
| if ($exitCode -ne 0) { | ||
| throw "rpmbuild failed with exit code $exitCode" |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling for bash -c command execution. If rpmbuild fails, the error output captured in $Output may not be displayed properly. Consider using Start-NativeExecution wrapper instead of manual exit code checking, which provides consistent error handling with caller context.
Example:
Start-NativeExecution -VerboseOutputOnError {
bash -c $buildCmd
}| $Output = bash -c $buildCmd 2>&1 | |
| $exitCode = $LASTEXITCODE | |
| if ($exitCode -ne 0) { | |
| throw "rpmbuild failed with exit code $exitCode" | |
| $Output = Start-NativeExecution -VerboseOutputOnError { | |
| bash -c $buildCmd |
| [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 |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter name $depenencyPath is misspelled - it should be $dependencyPath (missing 'd').
| [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 | |
| [string] $dependencyPath = $null | |
| $gemsPath = Get-ChildItem -Path /usr/lib64/ruby/gems | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName | |
| if ($gemsPath) { | |
| $dependencyPath = 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 +":" + $dependencyPath |
| [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 |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter name $depenencyPath is misspelled - it should be $dependencyPath (missing 'd').
| [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 | |
| [string] $dependencyPath = $null | |
| $gemsPath = Get-ChildItem -Path /usr/lib64/ruby/gems | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName | |
| if ($gemsPath) { | |
| $dependencyPath = 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 +":" + $dependencyPath |
| -HostArchitecture $HostArchitecture | ||
|
|
||
| $specFile = Join-Path $specsDir "$Name.spec" | ||
| $specContent | Out-File -FilePath $specFile -Encoding ascii |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The output encoding should be ascii instead of utf8 to match the existing pattern used elsewhere in the function (line 1530 in New-MacOsDistributionPackage still uses utf8, but spec files typically use ASCII encoding for better compatibility across systems).
| $package = Get-ChildItem -Path $packagePath -Filter "*.pkg" -ErrorAction SilentlyContinue | Select-Object -First 1 | ||
|
|
||
| if (-not $package) { | ||
| Write-Warning "No .pkg file found in $packagePath" |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Consider adding error handling for the case where $package is $null. While there's a warning message, the tests will still execute and fail with unclear errors. Consider using BeforeAll to skip all tests if the package is not found:
if (-not $package) {
Write-Warning "No .pkg file found in $packagePath"
Set-ItResult -Skipped -Because "Package file not found"
return
}Or add a guard test that other tests depend on.
| Write-Warning "No .pkg file found in $packagePath" | |
| Write-Warning "No .pkg file found in $packagePath" | |
| Set-ItResult -Skipped -Because "Package file not found" | |
| return |
Backport of #26268 to release/v7.6
Triggered by @TravisEz13 on behalf of @app/copilot-swe-agent
Original CL Label: CL-BuildPackaging
/cc @PowerShell/powershell-maintainers
Impact
REQUIRED: Choose either Tooling Impact or Customer Impact (or both). At least one checkbox must be selected.
Tooling Impact
This fundamentally changes the macOS packaging toolchain from fpm (Ruby gem) to native Xcode Command Line Tools (pkgbuild/productbuild). Build environments must have Xcode Command Line Tools installed. The change eliminates the Ruby/fpm dependency on macOS and uses tools that are standard on macOS developer systems.
Customer Impact
Regression
REQUIRED: Check exactly one box.
This is not a regression.
Testing
Verified that PR 26268 was tested in main branch with comprehensive macOS CI workflow including package creation and Pester validation tests. The backport applies cleanly after resolving merge conflicts in packaging.psm1 where the native macOS packaging approach (using pkgbuild/productbuild) was integrated with the release branch's packaging logic.
Risk
REQUIRED: Check exactly one box.
This is a significant architectural change that replaces the Ruby-based fpm tool with native macOS packaging tools (pkgbuild/productbuild) for creating macOS packages. However, the risk is mitigated by: (1) Already merged and validated in main branch with comprehensive CI testing, (2) Eliminates external Ruby dependency and fpm workarounds, (3) Uses Apple's official packaging tools that are better maintained, (4) Includes comprehensive Pester tests for package validation, (5) Only affects macOS packaging - other platforms (DEB, RPM) unchanged.
Merge Conflicts
Resolved merge conflict in tools/packaging/packaging.psm1 New-UnixPackage function. The conflict occurred because the release branch had simpler fpm-based packaging logic, while the incoming changes branch the logic: osxpkg type now calls New-MacOSPackage (native tools), while other types continue using fpm. Resolution: Accepted incoming changes that modernize macOS packaging while preserving fpm for Debian packages. Also added two new functions (New-MacOSPackage and New-RpmSpec) and updated Test-Dependencies to check for pkgbuild/productbuild on macOS.