Build a macOS installer .pkg from the staged install payload - #16
Merged
chrisuthe merged 2 commits intoAug 19, 2026
Merged
Conversation
chrisuthe
marked this pull request as ready for review
August 19, 2026 16:29
The payload item 10 already stages is the input rather than something built again, so the installer ships exactly the files the install() rules do and cannot drift from the tarball published beside it. scripts/build_macos_pkg.sh wraps it: pkgbuild for the component, rooted at the payload's usr/local so BUILD-INFO.txt is excluded by where the root points rather than by a filter, then productbuild for the product archive. That layer earns itself twice over -- a component package has no architecture gate, so an arm64-only installer would report success on an Intel Mac and leave a binary answering "Bad CPU type in executable", and it has nowhere to say the installer is unsigned at the moment somebody is deciding whether to run it. The architectures are read off the binary with lipo, so a universal build widens the declaration by itself. It goes in build.yml, the reusable build ci.yml and release.yml both call, next to the Package step whose staged tree it consumes. Not in ci.yml: that file takes no part in a tag, and an installer only a branch push produced would be exactly the seam build.yml's no-inputs comment refuses. So release.yml attaches it, which is the third obligation the matrix comment there already says a publishing leg owes -- an entry in the asset set the release diffs before it publishes, plus its line in SHA256SUMS, because a release asset a downloader cannot verify is worse than one that is not there. Signing and notarization are deliberately absent, not stubbed: they are gated on an enrolment that has not happened, and a public repo gets no secrets on a fork PR. The .pkg is what makes them possible later -- xcrun stapler refuses a bare Mach-O -- and it is shipped honestly meanwhile, in the welcome pane, in README.md, in the release notes and in BUILD-INFO.txt, none of which presents it as the Gatekeeper fix. The build installs it at / on the macOS leg and checks what came out: the receipt's file list, version and location, then --version and the whole smoke suite against /usr/local/bin/sendspin-cli. An installer nobody installed is not shipped, and this was the untested half of the payload. Being in build.yml, that happens on a tag as well as a push -- the one build that reaches a release is not the one build that never gets installed.
Three corrections, each from something observed rather than reasoned about. A CI run proves the install: the receipt reads `location: usr/local` and the version it was given, and the full smoke suite passes against the installed binary. It also shows the receipt listing no AppleDouble members at all -- the .pkg's own BOM carries them, the receipt's does not, because installer folds them back into attributes. The filter stays, since it is what keeps the assertion right in both directions, but the comment now says which BOM has them instead of claiming both do. `cd` on a relative path resolved through a non-empty CDPATH prints where it landed on stdout, and stdout there is the welcome pane -- so a developer with CDPATH exported would have got a stray absolute path as its first line. The unfiltered echo covers the filter, not a wrong identifier: that has already failed at the --pkg-info above and never reaches it.
chrisuthe
force-pushed
the
chrisuthe/task/macos-installer-pkg-from-the-install-payload
branch
from
August 19, 2026 16:53
ceea15f to
105e822
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third slice of roadmap item 10, and the one it named as the untested half of the
payload: the
.pkg. The payload the first slice already stages is the inputrather than something built again, so the installer ships exactly the files the
install()rules do and cannot drift from the tarball published beside it.What is here
scripts/build_macos_pkg.shwraps a stagedDESTDIRtree. A script rather thanlines in
ci.ymlfor two reasons: an installer has to be buildable andinstallable by a developer with no runner, and
scripts/*.shis already gatedby the shellcheck job, so it arrives linted with no workflow change.
pkgbuild --root <payload>/usr/local --install-location /usr/local, not aroot one level up paired with
/usr. The staged tree carriesBUILD-INFO.txtbeside
usr/for the tarball's readers, and the wider root would install thatfile at
/— excluded structurally, by where the root points, rather than bya filter a later tidy-up can drop.
--ownership recommendedis named althoughit is the default, because it is what makes the installed files
root:wheel.productbuildover the component, for two things a component packagecannot do. It has no architecture gate, so an arm64-only installer reports
success on an Intel Mac and leaves a binary answering
Bad CPU type in executable;<options hostArchitectures>is the declarative gate, and thevalue is read off the binary with
liporather than passed in, so a universalbuild widens the installer by itself. And distribution panes are a
product-archive feature, so a component package has nowhere to say the
installer is unsigned at the moment somebody is deciding whether to run it.
/usr/local, not/opt— not a free choice. Theprefix is baked in at configure time and the install rules already chose it.
Signing is absent, not stubbed
Out of scope per the brief, and gated on an enrolment that has not happened. No
placeholder
--signbranch, no commented-outcodesign, nothing assumingsecrets — a public repo gets none on a fork PR. The
.pkgis what makes signingpossible later:
xcrun staplerstaples to a.app,.dmgor.pkgandrefuses a bare Mach-O, and that constraint is written at the build site.
It is shipped honestly meanwhile. The welcome pane and
README.mdboth say itis unsigned, and
README.mdstates plainly that the.pkgis not theGatekeeper fix, then separates the three paths that genuinely differ: an
Actions artifact arrives as a zip, so the browser quarantines the zip and the
.pkginherits the flag only from Finder's Archive Utility and not fromunzip;installer -pkgmakes no Gatekeeper assessment at all; a locally builtone is never quarantined. What the
.pkgdoes fix is narrow and named as such —installerdoes not quarantine what it writes, so the installed binary needs noxattr -d. That is convenience, not identity.Verification
CI installs it at
/on the macOS leg and checks what came out: the receipt'sfile list,
version:andlocation:, then--versionand the whole smokesuite against
/usr/local/bin/sendspin-clirather than a build-tree binary. Aninstaller nobody installed is not shipped.
Green on all five jobs.
installerreports success, the receipt readslocation: usr/local, and the smoke suite passes against the installed binary.Locally: a real universal binary (
clang -arch arm64 -arch x86_64) widens thedeclaration to
hostArchitectures="x86_64,arm64"by itself, and the emptyversion and missing payload guards both refuse with the message that names the
fix.
shellcheckandactionlintclean.Roadmap item 10 records what shipped, and keeps two entries owed with owners
named: Developer ID signing and notarization, and a minimum-macOS gate the
architecture gate's asymmetry exposes.