Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
run: swift test
- name: Package integration verification
run: ./scripts/verify-loop.sh
- name: Build Swiftpkgr app
run: xcodebuild -project swiftpkg.xcodeproj -scheme Swiftpkgr -configuration Release CODE_SIGNING_ALLOWED=NO build
- name: Verify release tag version
if: startsWith(github.ref, 'refs/tags/v')
run: |
Expand Down
25 changes: 20 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,29 @@ permissions:
contents: write

jobs:
publish-unsigned-package:
publish-unsigned-package-fallback:
runs-on: macos-15
steps:
- uses: actions/checkout@v5
- name: Build and publish unsigned installer
- name: Build unsigned installer
env:
UNSIGNED: '1'
GH_PUBLISH: '1'
GITHUB_REPOSITORY: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
GH_PUBLISH: '0'
run: ./scripts/release.sh
- name: Publish only when no signed release exists
env:
GH_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "Release already exists; preserving its signed assets."
exit 0
fi
if gh release create "$GITHUB_REF_NAME" dist/*.pkg dist/SHA256SUMS \
--repo "$GITHUB_REPOSITORY" \
--title "swiftpkg ${GITHUB_REF_NAME#v}" \
--verify-tag \
--generate-notes; then
exit 0
fi
gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ Icon
.xcodebuild/
build/
DerivedData/
dist/

# Xcode user and workspace state
xcuserdata/
Expand Down
27 changes: 23 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@

`swiftpkg` is a macOS 13+ command-line tool that creates, imports, and
maintains Apple installer-package projects. It is a Swift implementation of
Munki's `munki-pkg`. The SwiftPM package builds one executable target,
`swiftpkg`; it is not a reusable library product.
Munki's `munki-pkg`. `Swiftpkgr` is its macOS 15+ SwiftUI frontend. Both use
the `SwiftPkgCore` static library; the app supplements rather than replaces
the CLI.

The primary development environment is macOS. The integration suite requires
Apple tools such as `pkgbuild`, `productbuild`, `pkgutil`, `ditto`, and
`lsbom`.

## Architecture

- `swiftpkg/CLI.swift` parses command-line options and resolves them into one
- `swiftpkgCLI/CLI.swift` parses command-line options and resolves them into one
`CLICommand` (`create`, `import`, `synchronize`, or `build`). Preserve the
documented option spelling and current exit behavior.
- `swiftpkg/BuildInfo.swift` owns the configuration boundary. Use
Expand All @@ -28,6 +29,23 @@ Apple tools such as `pkgbuild`, `productbuild`, `pkgutil`, `ditto`, and
- `swiftpkg/Support.swift` contains errors, console output, filesystem helpers,
and `ProcessRunning`. Route subprocess calls through `ProcessRunning` so
behavior is testable with `RecordingRunner`.
- `swiftpkg/PackageSettingsDraft.swift` is the editable configuration boundary;
keep template loading distinct from build-time version substitution.
- `Swiftpkgr/` contains the macOS 15 SwiftUI app. Keep `SwiftpkgrApp.swift` at
the app root and organize the remaining app files by role:
- `Screens/` contains user-facing navigation destinations and other base
views rendered as complete screens.
- `Components/` contains composable UI and command elements used by screens,
the app entry point, or other components.
- `Services/` contains API, panel, and other service-layer integrations.
- `Models/` contains app-specific value and domain types.
- `Extensions/` contains extensions of existing types. Name each file
`<TypeName>+Ext.swift` and keep extensions for different base types in
separate files.
- `State/` contains state retained for the app lifecycle. Observable types,
including types marked with `@Observable`, generally belong here.
Views use the observable `ProjectEditorModel` and call
`PackageOperationService` rather than invoking package tools directly.

Prefer fluent, role-based Swift names and concise documentation comments for
new nontrivial types and entry points. Keep side effects explicit in method
Expand Down Expand Up @@ -66,6 +84,7 @@ Run these from the repository root:
swift test
./scripts/verify-loop.sh
swift build -c release
xcodebuild -project swiftpkg.xcodeproj -scheme Swiftpkgr -configuration Release CODE_SIGNING_ALLOWED=NO build
```

The unit tests use Swift Testing. Keep tests hermetic: use `TemporaryDirectory`
Expand All @@ -83,7 +102,7 @@ before handing off changes.
## CI, branches, and releases

- Pull requests and pushes to `main` run `.github/workflows/ci.yml` on
`macos-15`: `swift test` and `./scripts/verify-loop.sh`.
`macos-15`: `swift test`, `./scripts/verify-loop.sh`, and a Swiftpkgr build.
- Pushing a `v*` tag also validates that the tag exactly matches both `VERSION`
and `swiftpkg/Version.swift`.
- `.github/workflows/release.yml` runs on a version tag and invokes
Expand Down
12 changes: 9 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,30 @@ let package = Package(
name: "swiftpkg",
platforms: [.macOS(.v13)],
products: [
.library(name: "SwiftPkgCore", type: .static, targets: ["SwiftPkgCore"]),
.executable(name: "swiftpkg", targets: ["swiftpkg"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.7.0"),
.package(url: "https://github.com/jpsim/Yams.git", from: "6.2.2")
],
targets: [
.target(
name: "SwiftPkgCore",
dependencies: ["Yams"],
path: "swiftpkg"
),
.executableTarget(
name: "swiftpkg",
dependencies: [
"SwiftPkgCore",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"Yams"
],
path: "swiftpkg"
path: "swiftpkgCLI"
),
.testTarget(
name: "swiftpkgTests",
dependencies: ["swiftpkg", "Yams"],
dependencies: ["SwiftPkgCore", "swiftpkg", "Yams"],
path: "swiftpkgTests"
)
]
Expand Down
122 changes: 68 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,59 @@
[![CI](https://github.com/codecarton/swiftpkg/actions/workflows/ci.yml/badge.svg)](https://github.com/codecarton/swiftpkg/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/codecarton/swiftpkg?display_name=release&sort=semver)](https://github.com/codecarton/swiftpkg/releases)
[![License](https://img.shields.io/github/license/codecarton/swiftpkg)](LICENSE)
[![macOS 13+](https://img.shields.io/badge/macOS-13%2B-000000?logo=apple&logoColor=white)](https://support.apple.com/macos)
[![CLI macOS 13+](https://img.shields.io/badge/CLI-macOS%2013%2B-000000?logo=apple&logoColor=white)](https://support.apple.com/macos)
[![Swiftpkgr macOS 15+](https://img.shields.io/badge/Swiftpkgr-macOS%2015%2B-000000?logo=apple&logoColor=white)](https://support.apple.com/macos)

`swiftpkg` is a native toolkit for building Apple installer packages from
version-control-friendly project directories. Use the command-line tool for
automation today; Swiftpkgr, its macOS desktop companion, brings a visual
workflow in the 0.3.0 release. Both are powered by the same Swift
implementation of [`munki-pkg`](https://github.com/munki/munki-pkg).
`swiftpkg` is a macOS command-line tool for building Apple installer packages
from version-control-friendly project directories. It is a Swift implementation
of [`munki-pkg`](https://github.com/munki/munki-pkg). `Swiftpkgr` is its native
macOS desktop app. Both frontends use the same `SwiftPkgCore` package engine and
open the same portable projects.

## Install for Mac administrators
## Install swiftpkg and Swiftpkgr

Download `swiftpkg-<version>-universal.pkg` and `SHA256SUMS` from the matching
GitHub Release. The installer is Universal 2 (Apple silicon and Intel),
currently unsigned; it supports macOS 13 and later. This is temporary while
Apple signing and notarization are being set up. Verify the published checksum
before deployment and expect macOS to require an administrator override.
GitHub Release. Starting with 0.3.0, the signed and notarized Universal 2
installer includes both the CLI and Swiftpkgr. The combined installer requires
macOS 15 or later; the CLI itself continues to support macOS 13 and later.

Verify the download before installation:

```sh
shasum -a 256 -c SHA256SUMS
pkgutil --check-signature swiftpkg-<version>-universal.pkg
xcrun stapler validate swiftpkg-<version>-universal.pkg
sudo installer -pkg swiftpkg-<version>-universal.pkg -target /
swiftpkg --version
```

The installer places the executable at `/usr/local/bin/swiftpkg`. Deploy that
same installer through Munki, Jamf Pro, or another management system; do not
repackage the executable. Upgrades replace that path. To uninstall, remove
`/usr/local/bin/swiftpkg` and, if desired, the `org.swiftpkg.cli` installer
receipt after confirming it is not needed for inventory.
The installer places the executable at `/usr/local/bin/swiftpkg` and the app at
`/Applications/Swiftpkgr.app`. Deploy that same installer through Munki, Jamf
Pro, or another management system; do not repackage its contents. Subsequent
releases replace the CLI and atomically upgrade the app bundle.

To uninstall, remove `/usr/local/bin/swiftpkg` and
`/Applications/Swiftpkgr.app`, then optionally forget the
`com.codecarton.swiftpkg.installer` receipt after confirming it is not needed
for inventory.

`swiftpkg` requires macOS, Xcode Command Line Tools, and Apple's `pkgbuild`,
`productbuild`, `pkgutil`, `ditto`, and `lsbom` tools. Package signing and
notarization additionally require the appropriate Apple credentials on the
build host.

## Swiftpkgr desktop app

Swiftpkgr provides a focused visual workspace for creating, importing, editing,
building, signing, and notarizing package projects. Open the same
`build-info.plist`, `.json`, `.yaml`, or `.yml` projects in either Swiftpkgr or
the CLI without conversion.

Swiftpkgr can create a new project, convert an existing folder, import a flat or
supported bundle-style installer package, synchronize metadata from `Bom.txt`,
and build the final package. Build progress and output stay visible in the app,
and Finder selects the generated package when the build completes.

## Use

Build a project:
Expand Down Expand Up @@ -72,23 +90,6 @@ Useful options:
--version Show the tool version
```

## Swiftpkgr desktop app — coming in 0.3.0

Coming in the swiftpkg 0.3.0 release, Swiftpkgr is the native macOS 15+
desktop companion to the macOS 13+ `swiftpkg` command-line tool. It opens the
same project directories and uses the same configuration, import, build, and
BOM implementation, so projects can move between visual and automated
workflows without conversion.

Swiftpkgr will let you create, open, or convert package projects; import
existing installer packages; edit package, distribution, signing, and
notarization values; and build packages while viewing operation progress. It
will import and export CLI-compatible plist, JSON, or YAML settings without
losing `${version}` placeholders.

Swiftpkgr will be included in the same installer package as the CLI beginning
with 0.3.0.

## Project layout

```text
Expand Down Expand Up @@ -118,39 +119,52 @@ swift test
```

The Swift Package Manager dependency [Yams](https://github.com/jpsim/Yams)
provides YAML support. The CLI target also builds with:
provides YAML support. Both products also build through Xcode:

```sh
xcodebuild -project swiftpkg.xcodeproj -scheme swiftpkg -configuration Release CODE_SIGNING_ALLOWED=NO build
xcodebuild -project swiftpkg.xcodeproj -scheme swiftpkg -configuration Release build
xcodebuild -project swiftpkg.xcodeproj -scheme Swiftpkgr -configuration Release build
```

## Maintainer releases

`VERSION` and `swiftpkg/Version.swift` are the release-version source of truth
and must match before a `v<version>` tag is created. On a clean, trusted macOS
release machine with the certificates and notary profile installed:
`VERSION`, `swiftpkg/Version.swift`, and the Swiftpkgr Xcode marketing version
must match before a release. On a trusted Mac, export the public Developer ID
identity names and notarytool keychain-profile label:

```sh
APP_SIGN_IDENTITY='Developer ID Application: Example (TEAMID)' \
INSTALLER_SIGN_IDENTITY='Developer ID Installer: Example (TEAMID)' \
NOTARY_PROFILE='swiftpkg-notary' \
./scripts/release.sh
export APP_SIGN_IDENTITY='Developer ID Application: Example (TEAMID)'
export INSTALLER_SIGN_IDENTITY='Developer ID Installer: Example (TEAMID)'
export NOTARY_PROFILE='swiftpkg-notary'
```

The script runs tests, builds a Universal 2 executable, signs it, creates,
notarizes, staples, and validates the installer, then writes artifacts and
`SHA256SUMS` to `dist/`. To publish a GitHub Release after creating and pushing
the matching tag, add `GH_PUBLISH=1 GITHUB_REPOSITORY=owner/repo`; this requires
the GitHub CLI to be authenticated on the release Mac.
Validate the release environment without changing anything:

```sh
./scripts/publish-xcode-release.sh --check
```

Build, sign, notarize, staple, and validate the combined installer locally:

```sh
./scripts/publish-xcode-release.sh --build
```

After the release commit is merged to a clean `main` checkout, publish it:

```sh
./scripts/publish-xcode-release.sh --publish
```

Until Apple credentials are available, pushing a matching `v<version>` tag runs
the GitHub **Release** workflow. It builds an unsigned installer and publishes
it with `SHA256SUMS` to that GitHub Release. The same temporary behavior can be
run locally with `UNSIGNED=1 ./scripts/release.sh`. Unsigned packages must not
be treated as notarized or Gatekeeper-validated.
The publish workflow runs the test and integration suites, exports Universal 2
CLI and app products from Xcode, signs and notarizes them, builds the installer
with the `swiftpkg` in `PATH`, writes the package and `SHA256SUMS` to `dist/`,
pushes `main` and the explicit `v<version>` tag, and creates or updates the
GitHub Release.

GitHub Actions validates pull requests and tags and publishes unsigned tagged
releases; it intentionally has no Apple signing credentials. See [VERIFICATION.md](VERIFICATION.md),
The tag workflow retains an unsigned CI fallback for environments without Apple
credentials, but it publishes only when no signed release exists and never
overwrites signed assets. See [VERIFICATION.md](VERIFICATION.md),
[CONTRIBUTING.md](CONTRIBUTING.md), and [SECURITY.md](SECURITY.md) for project
processes.

Expand Down
49 changes: 49 additions & 0 deletions Swiftpkgr/Components/SwiftpkgrCommands.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import SwiftPkgCore
import SwiftUI

struct SwiftpkgrCommands: Commands {
let model: ProjectEditorModel

var body: some Commands {
CommandGroup(replacing: .newItem) {
Button("New Project…", systemImage: "plus", action: model.createNewProject)
.keyboardShortcut("n")
.disabled(model.isRunning)
Button("Open Project…", systemImage: "folder", action: model.chooseProjectToOpen)
.keyboardShortcut("o")
.disabled(model.isRunning)
Button("Import Package…", systemImage: "shippingbox.and.arrow.backward", action: model.choosePackageToImport)
.disabled(model.isRunning)
}

CommandGroup(after: .saveItem) {
Button("Save Project", systemImage: "square.and.arrow.down", action: model.save)
.keyboardShortcut("s")
.disabled(!model.isProjectOpen || model.isRunning)
Divider()
Button("Import Settings…", systemImage: "square.and.arrow.down.on.square", action: model.importSettings)
.disabled(!model.isProjectOpen || model.isRunning)
Menu("Export Settings…", systemImage: "square.and.arrow.up.on.square") {
ForEach(BuildInfoFormat.allCases) { format in
Button(format.displayName) {
model.requestSettingsExport(as: format)
}
}
}
.disabled(!model.isProjectOpen || model.isRunning)
}

CommandMenu("Package") {
Button("Build", systemImage: "hammer", action: model.requestBuild)
.keyboardShortcut("b")
.disabled(!model.canBuild)
Button("Synchronize from Bom.txt", systemImage: "arrow.triangle.2.circlepath", action: model.synchronizeBOM)
.disabled(!model.isProjectOpen || model.isRunning)
Divider()
Button("Reveal Project", systemImage: "folder", action: model.revealProject)
.disabled(!model.isProjectOpen)
Button("Reveal Built Package", systemImage: "shippingbox", action: model.revealBuiltPackage)
.disabled(model.builtPackageURL == nil)
}
}
}
12 changes: 12 additions & 0 deletions Swiftpkgr/Extensions/BuildInfoFormat+Ext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import SwiftPkgCore

extension BuildInfoFormat {
var displayName: String {
switch self {
case .plist: "Property List"
case .json: "JSON"
case .yaml: "YAML"
case .yml: "YAML (.yml)"
}
}
}
11 changes: 11 additions & 0 deletions Swiftpkgr/Extensions/NotarizationAuthenticationMode+Ext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import SwiftPkgCore

extension NotarizationAuthenticationMode {
var displayName: String {
switch self {
case .none: "None"
case .keychainProfile: "Keychain Profile"
case .appleID: "Apple ID"
}
}
}
Loading