go-macos-pkg

module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 1, 2026 License: MIT

README

macospkg

CI Release Go Reference Go Version License

A cross-platform, self-contained toolkit for macOS installer packages (.pkg). It reads and writes the xar container, the bill of materials, the cpio payload, PackageInfo and Distribution directly, without pkgbuild, productbuild, productsign, pkgutil, notarytool, stapler or a Mac, so a package can be inspected, built, signed, notarized and stapled on a Linux or Windows CI runner exactly as on macOS.

$ macospkg build ./root Foo.pkg --identifier com.example.foo --version 1.2.0 --scripts ./scripts
built Foo.pkg: com.example.foo 1.2.0, 14 files, 306 KB installed

$ macospkg sign Foo.pkg Foo-signed.pkg --p12 developer-id.p12 --p12-password-stdin < pw.txt
signed Foo.pkg -> Foo-signed.pkg (Developer ID Installer: Example Ltd (ABCDE12345), timestamped)

$ macospkg notarize Foo-signed.pkg --wait --staple
submission id 2efe2717-52ef-43a5-96dc-0797e4ca1041
2efe2717-52ef-43a5-96dc-0797e4ca1041: Accepted
stapled the notarization ticket to Foo-signed.pkg

$ macospkg verify --online Foo-signed.pkg
Status:    valid
Chain:     trusted
Timestamp: 2026-08-29T14:48:07Z
Staple:    notarization ticket present
Notarized: yes (ticket on record with Apple)

Why

Apple ships no way to build, sign, notarize or staple a .pkg outside of a macOS device. pkgbuild, productbuild, productsign, pkgutil, notarytool and stapler are macOS-only, so any pipeline that produces an installer puts macOS availability on its critical path. For many teams that availability is not a given at all: plenty of CI environments offer no macOS runners, and locked-down, on-premise or air-gapped ones cannot bring a Mac in to fix that. Where macOS is available it is a standing cost and a bottleneck instead, a hosted runner that bills several times a Linux one, or a self-hosted machine to provision, patch and hold signing keys on. Either way, for a team whose product is otherwise built on Linux, that single step is the reason a Mac has to be in the picture at all.

The formats underneath are not the real obstacle. A .pkg is a xar archive holding a cpio payload, a bill of materials and XML metadata, signed with CMS over the table of contents; none of it depends on a macOS kernel or framework. What was missing was a complete, faithful reimplementation, because the pieces are under-documented and each one (the BOM layout, the pbzx and LZBITMAP containers, the exact bytes Apple signs) has to be recovered by measurement against the real tools.

macospkg is that reimplementation, in one static Go binary with no cgo and no runtime dependencies. It runs the whole lifecycle, inspect through to staple, on Linux, Windows or macOS, and aims to produce byte-for-byte the packages Apple's own tools do, checked continuously against them (see How it is tested). The payoff is a CI job that ships a signed, notarized installer on a cheaper Linux runner as every other workload, and a library other Go programs can embed instead of shelling out to tools that are available.

Features

macOS Linux Windows
Inspect: info, list, cat, inspect
Unpack: expand (pkgutil parity), extract
Repack: flatten (pkgutil --flatten)
Build component packages and product archives, reproducibly
Sign with a Developer ID Installer certificate, with Apple timestamps
Verify signatures against Apple's roots; team, timestamp, staple
Notarize (App Store Connect API key) and staple
Payloads: read and write gzip cpio, pbzx, pbze, pbzb and --large-payload
Payloads: read pbz4 and pbzz, which macOS cannot install
Hard links and extended attributes (._ sidecars), as pkgbuild carries them xattrs as ._ files; links as copies

See TOOLS_STATUS.md for the exact state of each area.

Payload containers

A Payload is a cpio archive, wrapped in a container its first bytes identify. The readers and writers are ordinary Go, so every platform behaves identically.

Container What it is Support
gzip cpio the default, and the only container every macOS can install read + write
pbzx xz chunks: smaller, but only macOS 12 and later installs it read + write (--compression pbzx, also spelled latest)
pbze, pbzb the same container with LZFSE or LZBITMAP chunks. macOS reads both read + write (--compression lzfse|lzbitmap)
pbz4, pbzz the same container with Apple-framed LZ4 or zlib chunks read only: macOS cannot install either, so build refuses to write one

--large-payload carries files of 8 GiB and over, which no cpio header can describe: the file is split into 1 GiB segments under one path and the entry is named LargeSegmentedPayload. The container is unchanged, so the two flags compose. Only macOS 12 and later reads one, and --compression pbzx likewise sets the package's minimum system version to 12.0 unless you ask for a higher one.

LZBITMAP has no published specification. pkg/lzbitmap is a Go translation of Corellium's MIT-licensed libzbitmap; NOTICE carries the copyright. Both directions are judged against Apple's own aa.

Which containers macOS will actually install, and why pbz4 and pbzz are refused, is measured and recorded in docs/formats/payload.md.

Install

Download a release archive for your platform from the releases page, or build from source:

go build -o macospkg ./cmd/macospkg

Release archives are pure Go binaries with no runtime dependencies. Each release ships a checksums.txt signed keylessly with cosign:

cosign verify-blob \
  --bundle macospkg_<version>_checksums.txt.sigstore.json \
  --certificate-identity-regexp 'https://github.com/deploymenttheory/go-macos-pkg/.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  macospkg_<version>_checksums.txt
shasum -a 256 -c macospkg_<version>_checksums.txt --ignore-missing

Commands

Every command takes the package first. -o json turns any output into JSON, one document or one line per entry. docs/cli.md is the reference: every flag, its default, and when to reach for it.

Command What it does
info PKG Kind, each component's identity, payload, scripts, signature and staple
list PKG The files the package installs, from the bill of materials; --archive lists xar entries instead
cat PKG ENTRY One archive entry to stdout, decoded; --payload PATH for one file out of the payload
inspect PKG VERB One structure as stored: header, toc, bom, signature, cms, rsa, digest, ticket
expand PKG DIR Every entry decoded into a new directory; --full unpacks the payloads too
flatten DIR OUT.pkg The inverse of expand, so one entry can be edited without a rebuild
extract PKG DIR The payload files, as they would land under the install location
build SRC OUT.pkg A component package from a destination root, a bundle, or a build-info.yaml project
product OUT.pkg A product archive from component packages, with a Distribution synthesized or supplied
sign PKG OUT.pkg An RSA and a CMS signature over the table of contents, chain embedded, timestamped
verify PKG Digest, signatures, chain to Apple's roots, team, timestamp, staple, each reported separately
notarize PKG Submit to Apple's notary service; --wait for the verdict, --staple to attach the ticket
staple PKG|APP Fetch the ticket from Apple's database and attach it, to a flat package or a .app bundle; unstaple removes one
receipts What a volume records about the packages installed on it

Three things are worth knowing before reading the reference.

Signing takes no keychain. The identity is a PKCS#12 file (--p12) or a PEM pair (--cert and --key), which is what lets the same command run on Linux and Windows. build and product carry the same flags under a sign- prefix, and the notary credentials under a notary- prefix, so a whole release is one run: build ... --sign-p12 devid.p12 --notarize.

Extended attributes and hard links survive a round trip. Attributes travel as ._ AppleDouble entries, applied to their owners where the host stores them and left as files where it does not, so expanding a package on Linux and building it again reproduces it. See docs/formats/payload.md.

Output is reproducible. Set --source-date-epoch or SOURCE_DATE_EPOCH and identical input gives identical bytes on every platform. See docs/reproducible-output.md.

Global flags

Flag Description
-o, --output text|json|plist output format
-q, --quiet suppress progress messages
--verbose diagnostics on stderr
--source-date-epoch N pin every timestamp for reproducible output
--temp-dir DIR where scratch files go while building (default: beside the output)

These five are the only flags with an environment variable and a config file key: MACOSPKG_OUTPUT, MACOSPKG_QUIET, MACOSPKG_VERBOSE, MACOSPKG_SOURCE_DATE_EPOCH and MACOSPKG_TEMP_DIR. Precedence is flag, then variable, then ~/.config/macospkg/config.yaml. SOURCE_DATE_EPOCH is the exception: the bare variable outranks MACOSPKG_SOURCE_DATE_EPOCH.

Exit codes

Code Meaning
0 success
1 error
2 usage error
3 not a flat package (missing, not a xar, or a xar without PackageInfo/Distribution)
4 credentials missing or rejected (PKCS#12 password, key mismatch, notary API key)
5 unsupported (a payload container macOS cannot install, ownership on Windows, non-RSA key)
6 partial result (some entries skipped)
7 signature or ticket check failed, or no ticket available
8 notarization rejected
9 wait timed out

The contract lives in pkg/exitcode.

In CI

- uses: actions/checkout@v7
- name: Build, sign, notarize and staple
  env:
    APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
    APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
    APPLE_PRIVATE_KEY_PEM: ${{ secrets.APPLE_PRIVATE_KEY_PEM }}
    MACOSPKG_P12_PASSWORD: ${{ secrets.DEVID_P12_PASSWORD }}
    SOURCE_DATE_EPOCH: ${{ github.event.head_commit.timestamp }}
  run: |
    echo "${{ secrets.DEVID_P12_BASE64 }}" | base64 -d > devid.p12
    macospkg build ./root Foo-${VERSION}.pkg --identifier com.example.foo --version "$VERSION" \
      --scripts ./scripts --sign-p12 devid.p12 --notarize
    macospkg verify --require-stapled --online Foo-${VERSION}.pkg

The same job runs on ubuntu-latest, windows-latest and macos-latest. See docs/notarization.md and docs/signing.md.

Using it as a library

import "github.com/deploymenttheory/go-macos-pkg/pkg/flatpkg"

p, err := flatpkg.Open("Foo.pkg")
for _, c := range p.Components {
    fmt.Println(c.Info.Identifier, c.Info.Version)
}

Key packages: pkg/xar (container), pkg/bom (bill of materials), pkg/cpio and pkg/pbzx (payloads), pkg/appledouble (the ._ sidecars that carry extended attributes), pkg/flatpkg (packages, build, expand, extract), pkg/pkgsign (sign, verify), pkg/notary, pkg/staple. The format details are written down in docs/formats/.

How it is tested

The binary never calls an Apple tool. Apple's pkgbuild, pkgutil, lsbom, xar, installer, stapler and spctl are used only by the acceptance suite on macOS, as independent references: the fixtures in testdata/cli were produced by Apple's tools once and are committed, so Linux and Windows test the reader against Apple's bytes; the macOS leg additionally builds the same tree with pkgbuild and with macospkg and compares what lsbom, pkgutil and xar say about each, installs our package with installer, checks our signature with pkgutil --check-signature and openssl cms, and our staple with stapler validate and spctl. A gated job signs and notarizes with a real Developer ID against Apple's services.

Two real-world packages are the references on every platform: Google's Go installer (go1.27.0.darwin-arm64.pkg, stapled, no bundles) and PowerShell (powershell-7.6.1-osx-arm64.pkg, an app bundle with a symlink and a postinstall script). Each has its signature verified against Apple's roots and its ticket against Apple's database, then is expanded with macospkg and rebuilt with macospkg, and the rebuilt package is compared with the original entry for entry: PackageInfo numbers, every bill-of-materials entry (17,356 of them for Go), every payload file's bytes, the Distribution and resources. On macOS, pkgutil, lsbom and xar compare the two as well, and installer installs the rebuilt package.

Development

go build ./...
go test ./pkg/... ./internal/...
go test -v ./acceptance/

Fixtures are regenerated with scripts/gen-fixtures.sh on macOS. See CONTRIBUTING.md.

Licensing

MIT. See LICENSE and NOTICE for the reference implementations that informed the format code; none is vendored.

Acknowledgements

The flat package format is Apple's and undocumented; this tool stands on the people who worked it out before: Rob Braun's xar, Fabian Renn's bomutils, Gregory Szorc's apple-platform-rs, SAS's relic, libarchive, and Greg Neagle's munki-pkg. See NOTICE.

Directories

Path Synopsis
Package acceptance holds the acceptance tests for this repository.
Package acceptance holds the acceptance tests for this repository.
cmd
macospkg command
macospkg - cross-platform CLI for macOS flat installer packages.
macospkg - cross-platform CLI for macOS flat installer packages.
internal
cli
macospkg build SRC -o OUT.pkg: build a component package from a directory.
macospkg build SRC -o OUT.pkg: build a component package from a directory.
tools
Version reporting: the one place the program's version is written down.
Version reporting: the one place the program's version is written down.
pkg
appledouble
Package appledouble encodes and decodes the AppleDouble "._" sidecar files that carry a file's Finder info, resource fork and extended attributes where the file system cannot: in a cpio payload, on a non-Apple volume, in a zip made by Finder.
Package appledouble encodes and decodes the AppleDouble "._" sidecar files that carry a file's Finder info, resource fork and extended attributes where the file system cannot: in a cpio payload, on a non-Apple volume, in a zip made by Finder.
bom
The checksum a bill of materials records for each file.
The checksum a bill of materials records for each file.
cpio
Package cpio reads and writes the cpio archives that carry a flat package's Payload and Scripts.
Package cpio reads and writes the cpio archives that carry a flat package's Payload and Scripts.
exitcode
Package exitcode defines the exit-code contract of the macospkg command.
Package exitcode defines the exit-code contract of the macospkg command.
flatpkg
Building a component package from a directory: what pkgbuild does.
Building a component package from a directory: what pkgbuild does.
lzbitmap
Package lzbitmap decodes and encodes Apple's LZBITMAP compression, the codec behind the pbzb payload container.
Package lzbitmap decodes and encodes Apple's LZBITMAP compression, the codec behind the pbzb payload container.
notary
Package notary submits packages to Apple's notary service, waits for the verdict and fetches the log (the parts of notarytool a CI job needs) on any platform.
Package notary submits packages to Apple's notary service, waits for the verdict and fetches the log (the parts of notarytool a CI job needs) on any platform.
pbzx
Apple's LZ4 framing, as libcompression and the kernel (xnu osfmk/vm/lz4.h) write it.
Apple's LZ4 framing, as libcompression and the kernel (xnu osfmk/vm/lz4.h) write it.
pkgsign
BER to DER: Apple's Security framework writes the CMS signature with BER indefinite lengths and constructed strings, which encoding/asn1 refuses.
BER to DER: Apple's Security framework writes the CMS signature with BER indefinite lengths and constructed strings, which encoding/asn1 refuses.
receipts
Reading the receipt database: the record macOS keeps of what each installed package put on a volume.
Reading the receipt database: the record macOS keeps of what each installed package put on a volume.
staple
Stapling application bundles.
Stapling application bundles.
xar
Package xar reads and writes the eXtensible ARchive container that a macOS flat package is built from.
Package xar reads and writes the eXtensible ARchive container that a macOS flat package is built from.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL