Skip to content

Building and Signing

Rod Christiansen edited this page Sep 3, 2026 · 1 revision

Building and Signing

How to build BootstrapMate from source, what each make target produces, and how signing and notarization are configured. Read this if you are producing your own installer package, or if you need to understand why the package published by this repository is unsigned.

Prerequisites

  • Swift 6.0 toolchain (Package.swift declares swift-tools-version:6.0).
  • Xcode Command Line Tools, for codesign, pkgbuild, productsign, xcrun actool, xcrun notarytool and xcrun stapler.
  • Apple Developer ID Application and Developer ID Installer certificates in a keychain, if you intend to sign.

The repository states two different minimum macOS versions and they do not agree. Package.swift declares platforms: [.macOS(.v15)], while packaging/resources/Info.plist.template and the actool invocation set a deployment target of 13.0, and README.md says "macOS 13.0 or later". The SPM manifest is the one that binds the build: Swift Package Manager will not build the package on a toolchain targeting anything below macOS 15. The 13.0 deployment target only affects the compiled icon assets and the LSMinimumSystemVersion written into the app bundle.

The GitHub Actions runners build on macos-26 and select the newest installed Xcode_26*.app, so that is the combination the project is known to build under.

Make targets

make with no target runs all, which is build. Every target below is defined in Makefile.

Target What it does and produces
help Prints the target list and the required variables.
check-signing-config Fails the build unless SIGNING_IDENTITY_APP and SIGNING_IDENTITY_PKG are set, and either NOTARIZATION_PROFILE or NOTARIZATION_APPLE_ID is set, and either NOTARIZATION_TEAM_ID or NOTARIZATION_PROFILE is set.
swift-build swift build -c release --arch arm64 --arch x86_64. Produces the three universal binaries in .build/apple/Products/Release.
copy-binary Codesigns each binary with --options runtime --timestamp under the identifiers com.github.bootstrapmate.managedbootstrapinstall, com.github.bootstrapmate.gui and com.github.bootstrapmate.helper, copies them into the package root's Contents/MacOS as managedbootstrapinstall, BootstrapMateGUI and BootstrapMateHelper at mode 755, and creates usr/local/bootstrapmate/.placeholder.
compile-icon xcrun actool over resources/BootstrapMate.icon, producing Assets.car and BootstrapMate.icns in build/actool-out.
create-app-bundle Substitutes {{MARKETING_VERSION}} and {{BUILD_NUMBER}} into Info.plist.template to produce the bundle's Contents/Info.plist, and copies the two icon artifacts into Contents/Resources.
create-launchdaemon Copies com.github.bootstrapmate.plist into the package root's /Library/LaunchDaemons, com.github.bootstrapmate.helper.plist into the app bundle's Contents/Library/LaunchDaemons, and packaging/scripts/postinstall into build/scripts with the executable bit set.
sign-app codesign --force --options runtime --timestamp --deep over the assembled app bundle.
build-pkg Runs pkgbuild --analyze, sets BundleIsRelocatable and BundleIsVersionChecked to false with PlistBuddy, then pkgbuild with identifier com.github.bootstrapmate, the version, and the scripts directory. Produces build/BootstrapMate-<version>.pkg.
sign-pkg productsign --sign "$(SIGNING_IDENTITY_PKG)" --timestamp, replacing the package in place.
notarize-pkg xcrun notarytool submit --wait, using the keychain profile if set and the Apple ID and password otherwise, then xcrun stapler staple.
verify pkgutil --check-signature, xcrun stapler validate, and spctl --assess --type install. Each failure aborts.
build / all The whole chain: check-signing-config swift-build copy-binary create-app-bundle create-launchdaemon sign-app build-pkg sign-pkg notarize-pkg verify.
clean Removes build, makes .build writable, removes .build.

The targets are chained by prerequisite, so make build-pkg also runs sign-app, which needs SIGNING_IDENTITY_APP. README.md describes make build-pkg as building an unsigned package; it does not. There is no make target that produces an unsigned package.

Signing and notarization configuration

The Makefile does -include .env and export, so any variable set in a .env file at the repository root becomes an environment variable for the build. .env is listed in .gitignore. You can also pass the same variables on the make command line.

SIGNING_IDENTITY_APP

The Developer ID Application certificate used for codesign on the three binaries and on the app bundle. Use the full identity string as printed by:

security find-identity -v -p codesigning

SIGNING_IDENTITY_PKG

The Developer ID Installer certificate used by productsign. It comes from the same security find-identity listing.

NOTARIZATION_PROFILE

The name of a notarytool keychain profile. Create one, which stores the credentials in your keychain rather than in a file:

xcrun notarytool store-credentials

When this is set, notarize-pkg submits with --keychain-profile and neither the Apple ID nor the team ID is needed.

NOTARIZATION_APPLE_ID and NOTARIZATION_PASSWORD

The alternative to a keychain profile: the Apple ID of the account attached to your Developer Program membership, and an app-specific password generated for it at appleid.apple.com. Both must be set for this path to work.

NOTARIZATION_TEAM_ID

Your ten-character Apple Developer Team ID, required when notarizing with an Apple ID rather than a keychain profile. It appears in your Apple Developer account and in the parenthesised suffix of the certificate names printed by security find-identity.

VERSION

Optional. Overrides the timestamp version described below.

Two files in the tree look like credential templates and are not quite that. examples/setup-credentials.sh exports the same variables plus BUNDLE_ID, and mentions a SIGNING_KEYCHAIN variable and a ./build.sh script; none of those three exist in this repository, so treat it as a shape reference for the four real variables only. resources/setup-notarization.sh pulls credentials from a specific organisation's secret store and is not usable elsewhere as written. README.md tells you to copy examples/.env.example; that file is not present in the repository, so write your .env by hand.

Never commit a .env, a certificate, an app-specific password or a Team ID to the repository.

Unsigned development builds

Nothing about signing is needed to compile, test or run the code:

swift build
swift test
swift build -c release --arch arm64 --arch x86_64

The release build produces bootstrapmate, BootstrapMateApp and BootstrapMateHelper in .build/apple/Products/Release. The CLI can be run straight from there against a test manifest; see Command Line Reference for the flags and Getting Started for a dry run.

If you want an unsigned installer package rather than bare binaries, run the release workflow manually from the Actions tab — see below — or download the package asset from a release and sign it yourself.

Where build output lands

Path Contents
.build/apple/Products/Release The compiled universal binaries.
build/pkg-root The staged package payload, including Applications/Utilities/BootstrapMate.app, Library/LaunchDaemons and usr/local/bootstrapmate.
build/scripts The postinstall script handed to pkgbuild --scripts.
build/actool-out Assets.car, BootstrapMate.icns and the partial Info.plist from actool.
build/component.plist The analyzed component plist, with relocation and version checking disabled.
build/BootstrapMate-<version>.pkg The finished package.

Both build/ and .build/ are ignored by git and removed by make clean.

Versioning

Versions are YYYY.MM.DD.HHMM. If VERSION is not set, the Makefile evaluates date '+%Y.%m.%d.%H%M' once at parse time. The Makefile then splits it: everything before the last dot becomes MARKETING_VERSION, and the final component becomes BUILD_NUMBER.

A worked example. Building with VERSION=2026.06.10.1430 gives:

Value Result
MARKETING_VERSION 2026.06.10, written to CFBundleShortVersionString
BUILD_NUMBER 1430, written to CFBundleVersion
pkgbuild --version 2026.06.10.1430
Package file build/BootstrapMate-2026.06.10.1430.pkg
Release tag v2026.06.10.1430

The split exists so that macOS shows "2026.06.10 (1430)" in the About box rather than repeating the full string. Because the marketing version has three components and older packages had four, BundleIsVersionChecked is disabled so the installer does not refuse the upgrade on a version comparison.

One caveat that matters when you are verifying which build is on a Mac: the version reported by --version, by the log header and in the run summary is computed from the clock at the moment the process starts, not from the build. It is always "now" and tells you nothing. The authoritative version of an installed copy is the app bundle's:

defaults read /Applications/Utilities/BootstrapMate.app/Contents/Info CFBundleShortVersionString

What CI and the release workflow do

.github/workflows/ci.yml runs on pushes and pull requests to main. It resolves dependencies, builds debug, runs swift test, builds the universal release binaries, and then in a second job assembles a package root and runs pkgbuild unsigned as a smoke test. It publishes no artifacts, and the package it builds is discarded.

.github/workflows/release.yml runs on workflow_dispatch and on any pushed tag matching v*. It takes the version from the tag with the leading v removed, builds all three binaries universal, compiles the icon, assembles the same app bundle and LaunchDaemon layout the Makefile produces, and runs pkgbuild with relocation and version checking disabled. It uploads a workflow artifact named BootstrapMate-<version>-pkg, and on a tag it creates a GitHub release carrying the package.

The workflow deliberately performs no codesign, no productsign, no notarization and no stapling. This public repository holds no Developer ID, so the released package is unsigned and un-notarized; the release notes say so. Signing, notarizing and stapling happen in a separate, private downstream pipeline that pulls the release asset. The Makefile is the local signed-build tool and is not used by CI at all.

If you deploy from these releases, you must sign the package yourself before it will install under a Gatekeeper policy that requires it. See Deployment.

See also

Clone this wiki locally