[release/v7.5] Replace fpm with native macOS packaging tools (pkgbuild/productbuild)#26801
Conversation
…PowerShell#26268) Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: TravisEz13 <[email protected]> Co-authored-by: Travis Plunk <[email protected]>
There was a problem hiding this comment.
Pull request overview
Backport to release/v7.5 that replaces macOS packaging via fpm with native macOS tooling (pkgbuild/productbuild), and updates CI + tests/docs accordingly.
Changes:
- Add native macOS package creation flow in
tools/packaging/packaging.psm1and routeosxpkgbuilds through it. - Add Pester-based validation for macOS
.pkgcontents and wire it into the macOS CI workflow. - Update maintainer documentation and add Copilot instruction docs for
Start-NativeExecution.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/packaging/packaging.psm1 | Adds New-MacOSPackage, refactors distribution package creation, updates dependency checks, and switches osxpkg to native tools. |
| tools/packaging/releaseTests/macOSPackage.tests.ps1 | Adds Pester validation that expands/extracts .pkg and verifies required contents/permissions. |
| .github/workflows/macos-ci.yml | Adds a macOS packaging + validation job, including test result publishing and artifact upload. |
| docs/maintainers/releasing.md | Updates packaging tooling description per-platform and fixes manpage link. |
| .github/instructions/start-native-execution.instructions.md | Adds guidance for using Start-NativeExecution for native command execution. |
| .github/instructions/build-configuration-guide.md | Updates release build guidance to include Switch-PSNugetConfig -Source Public. |
| ### For Release/Packaging | ||
|
|
||
| **Use: Release with version tag** | ||
| **Use: Release with version tag and public NuGet feeds** | ||
|
|
||
| ```yaml | ||
| - name: Build for Release |
There was a problem hiding this comment.
This file is under .github/instructions/ but it doesn’t follow the repo’s instruction-file requirements (must be named *.instructions.md and include YAML frontmatter with applyTo). As-is, it may not be picked up as a Copilot instruction file. Rename to build-configuration-guide.instructions.md and add the required frontmatter (see custom-instructions/repo/.github/instructions/instruction-file-format.instructions.md).
| # 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 |
There was a problem hiding this comment.
In macOS packaging, $Destination is an absolute path (e.g. /usr/local/...). Join-Path $pkgRoot $Destination will ignore $pkgRoot and resolve to the real system path, causing the packaging step to create/copy files under /usr/local/... on the build machine instead of inside the package root. Strip the leading / (or otherwise convert to a relative path) before joining so all writes stay under $pkgRoot.
| $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 |
There was a problem hiding this comment.
$ManDestination is an absolute install path (/usr/local/share/...). Using it with Join-Path $pkgRoot ... will escape the temp package root for the same reason as $Destination, writing to the host filesystem. Convert $ManDestination to a relative path under $pkgRoot before creating directories / copying the gzipped man page.
| $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 | |
| $relativeManDestination = if ([System.IO.Path]::IsPathRooted($ManDestination)) { | |
| $root = [System.IO.Path]::GetPathRoot($ManDestination) | |
| $ManDestination.Substring($root.Length).TrimStart('\', '/') | |
| } else { | |
| $ManDestination | |
| } | |
| $manDir = Join-Path $pkgRoot (Split-Path $relativeManDestination -Parent) | |
| New-Item -ItemType Directory -Path $manDir -Force | Out-Null | |
| Copy-Item -Path $ManGzipFile -Destination (Join-Path $pkgRoot $relativeManDestination) -Force |
| $linkDestDir = Join-Path $pkgRoot (Split-Path $link.Destination -Parent) | ||
| New-Item -ItemType Directory -Path $linkDestDir -Force | Out-Null | ||
| $finalLinkPath = Join-Path $pkgRoot $link.Destination |
There was a problem hiding this comment.
$link.Destination values are absolute (e.g. /usr/local/bin/pwsh). Join-Path $pkgRoot $link.Destination will ignore $pkgRoot, so symlinks may be created on the build host instead of in the package root. Ensure link destinations are made relative to $pkgRoot (for example by trimming the leading /) before joining/creating items.
| $linkDestDir = Join-Path $pkgRoot (Split-Path $link.Destination -Parent) | |
| New-Item -ItemType Directory -Path $linkDestDir -Force | Out-Null | |
| $finalLinkPath = Join-Path $pkgRoot $link.Destination | |
| # $link.Destination may be an absolute path (e.g. /usr/local/bin/pwsh). | |
| # Trim leading directory separators so paths are relative to $pkgRoot. | |
| $relativeLinkDestination = $link.Destination.TrimStart('\', '/') | |
| $linkDestDir = Join-Path $pkgRoot (Split-Path $relativeLinkDestination -Parent) | |
| New-Item -ItemType Directory -Path $linkDestDir -Force | Out-Null | |
| $finalLinkPath = Join-Path $pkgRoot $relativeLinkDestination |
| [Parameter(Mandatory)] | ||
| [string]$Iteration, | ||
|
|
||
| [Parameter(Mandatory)] | ||
| [string]$Staging, |
There was a problem hiding this comment.
New-MacOSPackage declares Iteration as a mandatory parameter but the function never uses it. Either remove it (and stop passing it) or incorporate it into the package version/filename so the parameter has an effect.
| - uses: actions/setup-dotnet@v4 | ||
| with: | ||
| global-json-file: ./global.json | ||
|
|
||
| if: success() | ||
| run: |- | ||
| import-module ./build.psm1 | ||
| start-psbootstrap -Scenario package |
There was a problem hiding this comment.
This job step is syntactically invalid: a single GitHub Actions step can't have both uses: (setup-dotnet) and run:. It looks like the bootstrap command lost its own - name: entry / indentation. Split bootstrap into a separate step (with run:/shell:) after the setup-dotnet step.
| - uses: actions/setup-dotnet@v4 | |
| with: | |
| global-json-file: ./global.json | |
| if: success() | |
| run: |- | |
| import-module ./build.psm1 | |
| start-psbootstrap -Scenario package | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| global-json-file: ./global.json | |
| - name: Bootstrap | |
| if: success() | |
| run: |- | |
| Import-Module ./build.psm1 | |
| 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 |
There was a problem hiding this comment.
The fallback PATH-augmentation block has typos (re-tesing, $depenencyPath) and references OpenSUSE even though it only runs under $Environment.IsDebianFamily. Please fix the typos and update the comment/logic so it accurately reflects the platform/path it is trying to handle.
| $payloadFile = Join-Path $componentPkg.FullName "Payload" | ||
| Get-Content -Path $payloadFile -Raw -AsByteStream | & cpio -i 2>&1 | Out-Null | ||
| } finally { |
There was a problem hiding this comment.
Payload extraction pipes the raw Payload blob directly into cpio -i. For pkgbuild-created component packages the Payload is typically a gzip-compressed cpio archive, so this can fail silently (especially since output is discarded). Consider explicitly decompressing (e.g., via gzip -dc/gunzip -dc) and run it under Start-NativeExecution so failures surface with a non-zero exit code and useful diagnostics.
Backport of #26268 to release/v7.5
Triggered by @daxian-dbw 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 change replaces the Ruby-based fpm tool with native macOS packaging tools (pkgbuild/productbuild). It eliminates the Ruby dependency and uses Apple-supported tools for better maintainability and integration with macOS.
Customer Impact
Regression
REQUIRED: Check exactly one box.
This is not a regression.
Testing
Comprehensive testing via macOS CI workflow with Pester tests validating package creation and contents. Already validated in master, 7.4, and 7.6 branches.
Risk
REQUIRED: Check exactly one box.
This is a significant change to macOS packaging infrastructure that replaces fpm with native macOS tools. However, it has been tested in master, 7.4, and 7.6 branches with improved reliability and maintainability. The change eliminates Ruby dependency and uses Apple-supported tools.
Merge Conflicts
Resolved conflict in macos-ci.yml: updated checkout action to v5 and added setup-dotnet step.