Skip to content

macos: version-stamp the .dmg, ship the CLI in it, and sign with rcodesign - #178

Merged
rafael2k merged 1 commit into
mercuryv2from
macos-dmg-version
Aug 14, 2026
Merged

macos: version-stamp the .dmg, ship the CLI in it, and sign with rcodesign#178
rafael2k merged 1 commit into
mercuryv2from
macos-dmg-version

Conversation

@rafael2k

Copy link
Copy Markdown
Contributor

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 shipped mercury-$(MERCURY_VERSION)-w64-<hash>.zip and Mercury_$(MERCURY_VERSION)_Setup.exe all along, and MERCURY_VERSION was already parsed from common/mercury_version.h for exactly this. The .dmg just 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 carried mercury.exe next to the GUI.

The GUI is built -tags mercury_embedded with the modem linked in, so the CLI is a genuinely separate artifact rather than a duplicate. It goes in a Command Line/ folder beside the app, with mercury.ini.example and 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, .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.

This does not remove macOS from the release: hdiutil builds 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-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.
  • Both universal targets 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.

Testing

On macOS 15.7.7 (x86_64 VM):

  • image mounts, contains Mercury.app, Applications, Command Line/{mercury,mercury.ini.example,README.txt}
  • both binaries: lipo -archsx86_64 arm64
  • CLI runs: Mercury Version 1.9.11 (git 6dec3b01)
  • 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 — 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

…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>
@rafael2k
rafael2k merged commit bb9d046 into mercuryv2 Aug 14, 2026
8 checks passed
@pedromessetti
pedromessetti deleted the macos-dmg-version branch August 14, 2026 12:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant