Skip to content
macOSDeep Dive Published Updated 8 min readViews unavailable

macOS Installer Packages: Build, Sign, Notarize, and Verify .pkg Releases

Create reproducible macOS installer packages with pkgbuild and productbuild, correct installer identities, safe scripts, notarization, and receipt checks.

An Installer package is appropriate when a macOS product needs multiple components, a specific install location, or an installer workflow. It is a signed distribution container with its own identity and lifecycle. Signing the application inside a package does not sign the package, and signing the package does not replace signing the app, frameworks, plug-ins, or helper executables it contains. Treat these as separate artifacts with separate verification steps.

For direct distribution, Apple distinguishes a Developer ID Application identity for code from a Developer ID Installer identity for the package. Mac App Store distribution uses a different installer identity. Choosing the wrong identity can produce an artifact that appears locally buildable but is rejected by the distribution channel. Verify signing identities before packaging and use a release pipeline that records the exact build tools and inputs.

Decide whether a package is necessary

A single self-contained app may be simpler to distribute as a signed and notarized archive or disk image. Use a package when installation semantics matter: placing multiple components, installing a launch item, migrating supported state, or running a narrowly scoped installer action. Do not create a package merely to make an app appear more professional. The installer adds system-level complexity and a responsibility to uninstall cleanly.

Define the payload manifest before invoking tooling. Specify every file, install path, owner and group where relevant, and whether a component is essential or relocatable. Avoid writing to user data directories unless the product has a clear reason and a migration strategy. If a package installs a privileged helper, document its ownership, launch mechanism, update relationship, IPC authorization, and removal behavior. A postinstall script should not be used to silently establish persistence that the app itself would need to disclose.

Use a clean staging directory generated from the build output. Do not package from a developer’s working tree, where untracked files, secrets, debug builds, or stale resources can leak. Record the version and bundle identifiers of every embedded app and helper, then compare them with the release manifest before creating the package.

Choose component or payload packaging

For a product consisting of one app bundle, productbuild --component can create a basic package that installs the app at a chosen destination. For a more complex payload, use pkgbuild to construct a component package from a carefully staged root and then use a distribution definition with productbuild when multiple components or installer choices are needed. Read the current command manuals for exact flags supported by the Xcode command-line tools on the build host.

A minimal component command has this shape:

productbuild \
  --sign "Developer ID Installer: Example Company (TEAMID)" \
  --component "build/Example.app" "/Applications" \
  "artifacts/Example-1.2.3.pkg"

For a staged payload, a component package can be built from an explicit root:

pkgbuild \
  --root "staging/root" \
  --identifier "com.example.product.payload" \
  --version "1.2.3" \
  --install-location "/" \
  "artifacts/ProductPayload.pkg"

These examples use placeholders and should be adapted to the product’s actual install model. Avoid using / as a destination without understanding every staged path. Do not put a home directory path into a package that will be installed for arbitrary users. Inspect the resulting payload and scripts before signing; the source staging directory is not proof of what the package contains.

Keep installer scripts predictable and safe

Installer scripts run with elevated installer context. Treat their input as untrusted and their side effects as privileged. Prefer declarative payload contents over shell scripts. If a script is required, make it idempotent, bounded, and defensive: quote paths, validate arguments, avoid network downloads, and do not assume the currently logged-in user is the account that owns the target home directory.

Scripts must not disable SIP, Gatekeeper, FileVault, firewall policy, or management controls. Do not collect credentials, write secret values into logs, or silently modify another application’s bundle. If the install requires user approval or a first-run configuration, let the app present the supported workflow. On failure, exit nonzero with a useful, redacted diagnostic and leave the machine in a recoverable state.

Test upgrades over the previous supported version, reinstall of the current version, interrupted installation, insufficient disk space, multiple users, and uninstall. Define how the package handles old receipts and removed components. A receipt can show that a package was installed, but it does not prove the payload remains intact or the app is currently functional.

Sign nested code and the package in the right order

Sign every executable and nested code object using the distribution signing identity appropriate to that code. Verify nested signatures before building the outer package. Then sign the Installer package with the installer identity. If the product is distributed inside nested containers, create and sign from the innermost artifact outward: code first, then package, then outer disk image if used. This preserves integrity at each layer.

Use security find-identity -v to confirm the expected certificate is available to the release account. Do not filter installer identities as if they were application code-signing identities. Protect signing credentials with the CI platform’s secure key storage and restrict who can invoke release signing. A package signature is a supply-chain control; it is not a substitute for reproducible build provenance or code review.

Verify the signed package with the appropriate package inspection tools. pkgutil --check-signature can show the package signer, and spctl --assess --type install can evaluate the installer policy. Verify the app’s own signature separately with codesign --verify --deep --strict where appropriate, and inspect its designated requirements and entitlements. Do not treat one successful command as proof that all nested contents are expected.

Notarize the artifact users actually download

Directly distributed products need the appropriate notarization workflow. Submit and staple the artifact that users receive. For nested distribution containers, Apple advises signing each supported inner container and notarizing the outermost one. A ticket stapled to an inner app does not automatically mean the downloaded outer package or disk image has been stapled as intended.

Keep the notarization submission ID and result with the release record. Validate the final artifact on a clean test Mac, ideally with network access disabled after stapling to confirm the ticket is available locally where applicable. Gatekeeper assessment, package signature verification, notarization status, and successful installation answer different questions. Test all relevant checks on the exact file that will be published, after the last byte-changing operation.

Do not edit or rebuild the package after signing or notarization. If a script, resource, package metadata, or nested app changes, rebuild and repeat the full signing and notarization sequence. The release pipeline should compute a cryptographic hash after finalization and publish that hash beside the release artifact so downstream systems can verify they received the same file.

Inspect payload and receipts after installation

On a disposable test Mac or VM, inspect package metadata and payload before running installation. Confirm the package identifier, version, install location, component identifiers, scripts, and file list. Then install the exact signed artifact, review Installer logs, and verify expected files and permissions. Use pkgutil --pkgs and pkgutil --pkg-info to inspect receipts, but remember that receipts are inventory records and not live health checks.

Verify the installed app launches, helpers register as expected, updates preserve user data, and removal leaves no orphaned background process. If the app uses SMAppService or another supported service-management API, confirm the user’s registration state separately from package receipt presence. Test MDM or enterprise deployment if the package is expected to work in managed environments; an installer that works interactively can behave differently under policy or without a GUI login.

For troubleshooting, preserve the package hash, signing identity, notarization submission, macOS build, Installer log, and redacted command output. Never ask a user to run an unsigned package or bypass Gatekeeper as a generic fix. If the signature is invalid, rebuild from trusted inputs and investigate why integrity changed. If installation fails after validation, inspect the payload and authorization context before changing system security settings.

Make release artifacts reproducible and auditable

Build in a clean, controlled environment with pinned source revision and documented Xcode command-line tools. Generate package version and identifiers from one release manifest. Keep the file list deterministic and verify it in CI. Use a separate signing step after payload validation, and an explicit approval gate before notarization and publication. Preserve logs that demonstrate each stage succeeded.

Create a test matrix for fresh install, supported upgrade paths, reinstall, uninstall, offline validation, low disk space, multiple user accounts, FileVault-enabled devices, and managed deployment. Confirm that every script has a bounded runtime and predictable exit status. Validate the release after upload by downloading it from the published location and comparing its hash with the finalized artifact.

Installer packages are powerful because they can place software into protected locations and coordinate a multi-component install. That same power requires a disciplined boundary: minimal payload, transparent actions, separate signing identities, verified nested code, notarization of the delivered artifact, and a tested removal path. When those controls are automated and recorded, a .pkg becomes a repeatable release unit instead of an opaque privileged script bundle.

Related:

Sources:

Comments