macos: version-stamp the .dmg, ship the CLI in it, and sign with rcodesign - #178
Merged
Conversation
…esign Three gaps in the macOS packaging, all visible next to the Windows side. The image was always named Mercury.dmg, with nothing to distinguish 1.9.10 from 1.9.11 in a downloads folder or on a release page -- while Windows has shipped mercury-$(MERCURY_VERSION)-w64-<hash>.zip and Mercury_$(MERCURY_VERSION)_Setup.exe all along. MERCURY_VERSION was already parsed from common/mercury_version.h for exactly that purpose; the .dmg simply never used it. Now Mercury-1.9.11-universal.dmg. The image also held only Mercury.app. There is a universal CLI target, but nothing copied its output in, so a Mac operator wanting a headless station -- a TNC for Winlink/BPQ32, or uucp -- had nothing to install. The GUI is built -tags mercury_embedded with the modem linked in, so the CLI is a genuinely separate artifact. It goes in a "Command Line" folder beside the .app with mercury.ini.example and a README, so a drag-install still copies exactly one thing and nothing extra lands inside the bundle to complicate signing. And there was no signing. Added opt-in signing via rcodesign (github.com/indygreg/apple-platform-rs): the CLI, then the .app bundle, then the image -- in that order, because signing a disk image does NOT sign what is inside it. Notarization is a separate target (macos-notarize-dmg) since it needs the network and Apple's verdict. Missing credentials warn instead of failing, so developer builds are unchanged. rcodesign rather than Apple's codesign or anchore/quill. Quill was tried first and rejected: it signs Mach-O binaries only, and cannot sign bundles or disk images (anchore/quill#815, #550 both open, no CodeResources support in its tree) -- which is everything Gatekeeper actually judges. rcodesign covers Mach-O, bundles, .dmg and .pkg, plus notarize and staple, in pure Rust with no Mac, no Xcode and no keychain, so the signing certificate never has to reach a macOS runner. Note this does not remove macOS from the release entirely: hdiutil builds the image and is Apple-only. What moves off the Mac is signing and notarization. Two pre-existing bugs surfaced while testing this: - macos-universal never invoked internal_deps, so after its own `make clean` the link failed on a missing modem/freedv/libfreedvdata.a. That target could not work on a clean tree; nothing had called it from the dmg path before, so it went unnoticed. - the two universal targets both run `make clean`, which removes `mercury` AND Mercury.app, so they cannot be ordered as prerequisites -- whichever ran second deleted the other's output. The CLI is now built first and parked under a name clean does not match (and deliberately NOT added to clean, which would defeat it), cleared at the start of the recipe so a leftover from a failed run can never be staged as if fresh. Tested on macOS 15.7.7 (x86_64): - image mounts and contains Mercury.app, Applications, and Command Line/{mercury,mercury.ini.example,README.txt} - both binaries report `lipo -archs` = x86_64 arm64 - the CLI runs: "Mercury Version 1.9.11 (git 6dec3b0)" - a Mercury.app signed by rcodesign 0.28.0 ON LINUX passes Apple's own `codesign --verify --deep --strict`: "valid on disk", "satisfies its Designated Requirement", with CodeResources written and the universal Mach-O sealed - the .dmg signed on Linux likewise verifies and still mounts Signed with a self-signed test certificate, so spctl still rejects it -- that is Gatekeeper judging the credential, not the signature. A real Developer ID plus notarization is the remaining gate, and is a credentials matter rather than a code one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Three gaps in the macOS packaging, each visible by comparison with the Windows side.
1. The image had no version in its name
Always
Mercury.dmg— indistinguishable from the previous one in a downloads folder or on a release page. Windows has shippedmercury-$(MERCURY_VERSION)-w64-<hash>.zipandMercury_$(MERCURY_VERSION)_Setup.exeall along, andMERCURY_VERSIONwas already parsed fromcommon/mercury_version.hfor exactly this. The.dmgjust never used it.Now
Mercury-1.9.11-universal.dmg.2. The image had no CLI
It held only
Mercury.app. There is a universal CLI target, but nothing copied its output in — so a Mac operator wanting a headless station (TNC for Winlink/BPQ32, or uucp) had nothing to install. The Windows zip has always carriedmercury.exenext to the GUI.The GUI is built
-tags mercury_embeddedwith the modem linked in, so the CLI is a genuinely separate artifact rather than a duplicate. It goes in aCommand Line/folder beside the app, withmercury.ini.exampleand a README — a drag-install still copies exactly one thing, and nothing extra lands inside the bundle to complicate signing.3. There was no signing at all
Opt-in signing via rcodesign: CLI, then the
.app, then the image — in that order, because signing a disk image does not sign what is inside it. Notarization is a separate target (macos-notarize-dmg) since it needs the network and Apple's verdict. Missing credentials warn rather than fail, so developer builds are unchanged.Why rcodesign and not codesign or quill. anchore/quill was tried first and rejected: it signs Mach-O binaries only and cannot sign bundles or disk images (anchore/quill#815, #550 both open; no CodeResources support in its tree) — which is everything Gatekeeper judges. rcodesign covers Mach-O, bundles,
.dmgand.pkg, plus notarize and staple, in pure Rust with no Mac, no Xcode and no keychain, so the signing certificate never has to reach a macOS runner.This does not remove macOS from the release:
hdiutilbuilds the image and is Apple-only, and the universal binaries need the macOS SDK. What moves off the Mac is signing and notarization.Two pre-existing bugs surfaced by testing this
macos-universalnever invokedinternal_deps, so after its ownmake cleanthe link failed on a missingmodem/freedv/libfreedvdata.a. That target could not work on a clean tree — nothing had called it from the dmg path before, so it went unnoticed.make clean, which removesmercuryandMercury.app, so they cannot be ordered as prerequisites — whichever ran second deleted the other's output. The CLI is now built first and parked under a namecleandoes not match (and deliberately not added toclean, which would defeat it), cleared at the start of the recipe so a leftover from a failed run can never be staged as if fresh.Testing
On macOS 15.7.7 (x86_64 VM):
Mercury.app,Applications,Command Line/{mercury,mercury.ini.example,README.txt}lipo -archs→x86_64 arm64Mercury Version 1.9.11 (git 6dec3b01)Mercury.appsigned by rcodesign 0.28.0 on Linux passes Apple's owncodesign --verify --deep --strict: "valid on disk", "satisfies its Designated Requirement", withCodeResourceswritten and the universal Mach-O sealed.dmgsigned on Linux likewise verifies and still mountsSigned with a self-signed test certificate, so
spctlstill rejects — that is Gatekeeper judging the credential, not the signature. A real Developer ID plus notarization is the remaining gate, and is a credentials matter rather than a code one.🤖 Generated with Claude Code