Skip to content

Commit 83250d0

Browse files
CopilotTravisEz13
andcommitted
Replace fpm with native macOS packaging tools (pkgbuild/productbuild) (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>
1 parent ea0bfb3 commit 83250d0

6 files changed

Lines changed: 999 additions & 118 deletions

File tree

.github/instructions/build-configuration-guide.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@
2727

2828
### For Release/Packaging
2929

30-
**Use: Release with version tag**
30+
**Use: Release with version tag and public NuGet feeds**
3131

3232
```yaml
3333
- name: Build for Release
3434
shell: pwsh
3535
run: |
36+
Import-Module ./build.psm1
3637
Import-Module ./tools/ci.psm1
38+
Switch-PSNugetConfig -Source Public
3739
$releaseTag = Get-ReleaseTag
3840
Start-PSBuild -Configuration 'Release' -ReleaseTag $releaseTag
3941
```
@@ -43,6 +45,11 @@
4345
- No debug symbols (smaller size)
4446
- Production-ready
4547

48+
**Why Switch-PSNugetConfig -Source Public:**
49+
- Switches NuGet package sources to public feeds (nuget.org and public Azure DevOps feeds)
50+
- Required for CI/CD environments that don't have access to private feeds
51+
- Uses publicly available packages instead of Microsoft internal feeds
52+
4653
### For Code Coverage
4754

4855
**Use: CodeCoverage configuration**
@@ -93,3 +100,42 @@ src/powershell-win-core/bin/Debug/<netversion>/<runtime>/publish/
93100
3. Match configuration to purpose
94101
4. Use `-CI` only when needed
95102
5. Always specify `-ReleaseTag` for release or packaging builds
103+
6. Use `Switch-PSNugetConfig -Source Public` in CI/CD for release builds
104+
105+
## NuGet Feed Configuration
106+
107+
### Switch-PSNugetConfig
108+
109+
The `Switch-PSNugetConfig` function in `build.psm1` manages NuGet package source configuration.
110+
111+
**Available Sources:**
112+
113+
- **Public**: Uses public feeds (nuget.org and public Azure DevOps feeds)
114+
- Required for: CI/CD environments, public builds, packaging
115+
- Does not require authentication
116+
117+
- **Private**: Uses internal PowerShell team feeds
118+
- Required for: Internal development with preview packages
119+
- Requires authentication credentials
120+
121+
- **NuGetOnly**: Uses only nuget.org
122+
- Required for: Minimal dependency scenarios
123+
124+
**Usage:**
125+
126+
```powershell
127+
# Switch to public feeds (most common for CI/CD)
128+
Switch-PSNugetConfig -Source Public
129+
130+
# Switch to private feeds with authentication
131+
Switch-PSNugetConfig -Source Private -UserName $userName -ClearTextPAT $pat
132+
133+
# Switch to nuget.org only
134+
Switch-PSNugetConfig -Source NuGetOnly
135+
```
136+
137+
**When to Use:**
138+
139+
- **Always use `-Source Public`** before building in CI/CD workflows
140+
- Use before any build that will create packages for distribution
141+
- Use in forks or environments without access to Microsoft internal feeds
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
---
2+
applyTo:
3+
- "**/*.ps1"
4+
- "**/*.psm1"
5+
---
6+
7+
# Using Start-NativeExecution for Native Command Execution
8+
9+
## Purpose
10+
11+
`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.
12+
13+
## When to Use
14+
15+
Use `Start-NativeExecution` whenever you need to:
16+
- Execute external commands (e.g., `git`, `dotnet`, `pkgbuild`, `productbuild`, `fpm`, `rpmbuild`)
17+
- Ensure proper exit code checking
18+
- Get better error messages with caller information
19+
- Handle verbose output on error
20+
21+
## Basic Usage
22+
23+
```powershell
24+
Start-NativeExecution {
25+
git clone https://github.com/PowerShell/PowerShell.git
26+
}
27+
```
28+
29+
## With Parameters
30+
31+
Use backticks for line continuation within the script block:
32+
33+
```powershell
34+
Start-NativeExecution {
35+
pkgbuild --root $pkgRoot `
36+
--identifier $pkgIdentifier `
37+
--version $Version `
38+
--scripts $scriptsDir `
39+
$outputPath
40+
}
41+
```
42+
43+
## Common Parameters
44+
45+
### -VerboseOutputOnError
46+
47+
Captures command output and displays it only if the command fails:
48+
49+
```powershell
50+
Start-NativeExecution -VerboseOutputOnError {
51+
dotnet build --configuration Release
52+
}
53+
```
54+
55+
### -IgnoreExitcode
56+
57+
Allows the command to fail without throwing an exception:
58+
59+
```powershell
60+
Start-NativeExecution -IgnoreExitcode {
61+
git diff --exit-code # Returns 1 if differences exist
62+
}
63+
```
64+
65+
## Availability
66+
67+
The function is defined in `tools/buildCommon/startNativeExecution.ps1` and is available in:
68+
- `build.psm1` (dot-sourced automatically)
69+
- `tools/packaging/packaging.psm1` (dot-sourced automatically)
70+
- Test modules that include `HelpersCommon.psm1`
71+
72+
To use in other scripts, dot-source the function:
73+
74+
```powershell
75+
. "$PSScriptRoot/../buildCommon/startNativeExecution.ps1"
76+
```
77+
78+
## Error Handling
79+
80+
When a native command fails (non-zero exit code), `Start-NativeExecution`:
81+
1. Captures the exit code
82+
2. Identifies the calling location (file and line number)
83+
3. Throws a descriptive error with full context
84+
85+
Example error message:
86+
```
87+
Execution of {git clone ...} by /path/to/script.ps1: line 42 failed with exit code 1
88+
```
89+
90+
## Examples from the Codebase
91+
92+
### Git Operations
93+
```powershell
94+
Start-NativeExecution {
95+
git fetch --tags --quiet upstream
96+
}
97+
```
98+
99+
### Build Operations
100+
```powershell
101+
Start-NativeExecution -VerboseOutputOnError {
102+
dotnet publish --configuration Release
103+
}
104+
```
105+
106+
### Packaging Operations
107+
```powershell
108+
Start-NativeExecution -VerboseOutputOnError {
109+
pkgbuild --root $pkgRoot --identifier $pkgId --version $version $outputPath
110+
}
111+
```
112+
113+
### Permission Changes
114+
```powershell
115+
Start-NativeExecution {
116+
find $staging -type d | xargs chmod 755
117+
find $staging -type f | xargs chmod 644
118+
}
119+
```
120+
121+
## Anti-Patterns
122+
123+
**Don't do this:**
124+
```powershell
125+
& somecommand $args
126+
if ($LASTEXITCODE -ne 0) {
127+
throw "Command failed"
128+
}
129+
```
130+
131+
**Do this instead:**
132+
```powershell
133+
Start-NativeExecution {
134+
somecommand $args
135+
}
136+
```
137+
138+
## Best Practices
139+
140+
1. **Always use Start-NativeExecution** for native commands to ensure consistent error handling
141+
2. **Use -VerboseOutputOnError** for commands with useful diagnostic output
142+
3. **Use backticks for readability** when commands have multiple arguments
143+
4. **Don't capture output unnecessarily** - let the function handle it
144+
5. **Use -IgnoreExitcode sparingly** - only when non-zero exit codes are expected and acceptable
145+
146+
## Related Documentation
147+
148+
- Source: `tools/buildCommon/startNativeExecution.ps1`
149+
- Blog post: https://mnaoumov.wordpress.com/2015/01/11/execution-of-external-commands-in-powershell-done-right/

.github/workflows/macos-ci.yml

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ jobs:
157157
runner_os: macos-15-large
158158
test_results_artifact_name: testResults-xunit
159159
PackageMac-macos_packaging:
160-
name: macOS packaging (bootstrap only)
160+
name: macOS packaging and testing
161161
needs:
162162
- changes
163163
if: ${{ needs.changes.outputs.source == 'true' }}
@@ -166,12 +166,63 @@ jobs:
166166
steps:
167167
- name: checkout
168168
uses: actions/checkout@v5
169+
with:
170+
fetch-depth: 1000
171+
- uses: actions/setup-dotnet@v4
172+
with:
173+
global-json-file: ./global.json
169174
- name: Bootstrap packaging
170-
if: success() || failure()
175+
if: success()
171176
run: |-
172177
import-module ./build.psm1
173178
start-psbootstrap -Scenario package
174179
shell: pwsh
180+
- name: Build PowerShell and Create macOS package
181+
if: success()
182+
run: |-
183+
import-module ./build.psm1
184+
import-module ./tools/ci.psm1
185+
import-module ./tools/packaging/packaging.psm1
186+
Switch-PSNugetConfig -Source Public
187+
Sync-PSTags -AddRemoteIfMissing
188+
$releaseTag = Get-ReleaseTag
189+
Start-PSBuild -Configuration Release -PSModuleRestore -ReleaseTag $releaseTag
190+
$macOSRuntime = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'osx-arm64' } else { 'osx-x64' }
191+
Start-PSPackage -Type osxpkg -ReleaseTag $releaseTag -MacOSRuntime $macOSRuntime -SkipReleaseChecks
192+
shell: pwsh
193+
- name: Test package contents
194+
if: success()
195+
run: |-
196+
$env:PACKAGE_FOLDER = Get-Location
197+
$testResultsPath = Join-Path $env:RUNNER_WORKSPACE "testResults"
198+
if (-not (Test-Path $testResultsPath)) {
199+
New-Item -ItemType Directory -Path $testResultsPath -Force | Out-Null
200+
}
201+
Import-Module Pester
202+
$pesterConfig = New-PesterConfiguration
203+
$pesterConfig.Run.Path = './tools/packaging/releaseTests/macOSPackage.tests.ps1'
204+
$pesterConfig.Run.PassThru = $true
205+
$pesterConfig.Output.Verbosity = 'Detailed'
206+
$pesterConfig.TestResult.Enabled = $true
207+
$pesterConfig.TestResult.OutputFormat = 'NUnitXml'
208+
$pesterConfig.TestResult.OutputPath = Join-Path $testResultsPath "macOSPackage.xml"
209+
$result = Invoke-Pester -Configuration $pesterConfig
210+
if ($result.FailedCount -gt 0) {
211+
throw "Package validation failed with $($result.FailedCount) failed test(s)"
212+
}
213+
shell: pwsh
214+
- name: Publish and Upload Pester Test Results
215+
if: always()
216+
uses: "./.github/actions/test/process-pester-results"
217+
with:
218+
name: "macOSPackage"
219+
testResultsFolder: "${{ runner.workspace }}/testResults"
220+
- name: Upload package artifact
221+
if: always()
222+
uses: actions/upload-artifact@v4
223+
with:
224+
name: macos-package
225+
path: "*.pkg"
175226
ready_to_merge:
176227
name: macos ready to merge
177228
needs:

docs/maintainers/releasing.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,18 @@ The output of `Start-PSBuild` includes a `powershell.exe` executable which can s
7272
#### Linux / macOS
7373

7474
The `Start-PSPackage` function delegates to `New-UnixPackage`.
75-
It relies on the [Effing Package Management][fpm] project,
76-
which makes building packages for any (non-Windows) platform a breeze.
77-
Similarly, the PowerShell man-page is generated from the Markdown-like file
75+
76+
For **Linux** (Debian-based distributions), it relies on the [Effing Package Management][fpm] project,
77+
which makes building packages a breeze.
78+
79+
For **macOS**, it uses native packaging tools (`pkgbuild` and `productbuild`) from Xcode Command Line Tools,
80+
eliminating the need for Ruby or fpm.
81+
82+
For **Linux** (Red Hat-based distributions), it uses `rpmbuild` directly.
83+
84+
The PowerShell man-page is generated from the Markdown-like file
7885
[`assets/pwsh.1.ronn`][man] using [Ronn][].
79-
The function `Start-PSBootstrap -Package` will install both these tools.
86+
The function `Start-PSBootstrap -Package` will install these tools.
8087

8188
To modify any property of the packages, edit the `New-UnixPackage` function.
8289
Please also refer to the function for details on the package properties
@@ -131,7 +138,7 @@ Without `-Name` specified, the primary `powershell`
131138
package will instead be created.
132139

133140
[fpm]: https://github.com/jordansissel/fpm
134-
[man]: ../../assets/pwsh.1.ronn
141+
[man]: ../../assets/manpage/pwsh.1.ronn
135142
[ronn]: https://github.com/rtomayko/ronn
136143

137144
### Build and Packaging Examples

0 commit comments

Comments
 (0)