Skip to content

Conversation

@TravisEz13
Copy link
Member

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

  • Required tooling change
  • Optional tooling change (include reasoning)

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

  • Customer reported
  • Found internally

Regression

REQUIRED: Check exactly one box.

  • Yes
  • No

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.

  • High
  • Medium
  • Low

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.

…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>
@TravisEz13 TravisEz13 added the CL-BuildPackaging Indicates that a PR should be marked as a build or packaging change in the Change Log label Nov 20, 2025
Copilot AI review requested due to automatic review settings November 20, 2025 01:58
@TravisEz13 TravisEz13 requested a review from a team as a code owner November 20, 2025 01:58
@TravisEz13 TravisEz13 closed this Nov 20, 2025
Copy link
Contributor

Copilot AI left a 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.

Comment on lines +1285 to +1294
# 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
Copy link

Copilot AI Nov 20, 2025

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
}
Suggested change
# 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
}

Copilot uses AI. Check for mistakes.
Push-Location $script:payloadDir
try {
$payloadFile = Join-Path $componentPkg.FullName "Payload"
Get-Content -Path $payloadFile -Raw -AsByteStream | & cpio -i 2>&1 | Out-Null
Copy link

Copilot AI Nov 20, 2025

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
}
Suggested change
Get-Content -Path $payloadFile -Raw -AsByteStream | & cpio -i 2>&1 | Out-Null
Start-NativeExecution {
Get-Content -Path $payloadFile -Raw -AsByteStream | & cpio -i
}

Copilot uses AI. Check for mistakes.
Comment on lines +2291 to +2292
}
Start-NativeExecution {
Copy link

Copilot AI Nov 20, 2025

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
}
Suggested change
}
Start-NativeExecution {

Copilot uses AI. Check for mistakes.
$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.
Copy link

Copilot AI Nov 20, 2025

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"
Suggested change
# 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

Copilot uses AI. Check for mistakes.
Comment on lines +1293 to +1297
$Output = bash -c $buildCmd 2>&1
$exitCode = $LASTEXITCODE

if ($exitCode -ne 0) {
throw "rpmbuild failed with exit code $exitCode"
Copy link

Copilot AI Nov 20, 2025

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
}
Suggested change
$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

Copilot uses AI. Check for mistakes.
Comment on lines +2122 to +2127
[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
Copy link

Copilot AI Nov 20, 2025

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').

Suggested change
[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

Copilot uses AI. Check for mistakes.
Comment on lines +2122 to +2127
[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
Copy link

Copilot AI Nov 20, 2025

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').

Suggested change
[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

Copilot uses AI. Check for mistakes.
-HostArchitecture $HostArchitecture

$specFile = Join-Path $specsDir "$Name.spec"
$specContent | Out-File -FilePath $specFile -Encoding ascii
Copy link

Copilot AI Nov 20, 2025

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).

Copilot uses AI. Check for mistakes.
$package = Get-ChildItem -Path $packagePath -Filter "*.pkg" -ErrorAction SilentlyContinue | Select-Object -First 1

if (-not $package) {
Write-Warning "No .pkg file found in $packagePath"
Copy link

Copilot AI Nov 20, 2025

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-BuildPackaging Indicates that a PR should be marked as a build or packaging change in the Change Log

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant