Skip to content

Commit bb9d046

Browse files
authored
Merge pull request #178 from Rhizomatica/macos-dmg-version
macos: version-stamp the .dmg, ship the CLI in it, and sign with rcodesign
2 parents 6dec3b0 + 8321efc commit bb9d046

1 file changed

Lines changed: 140 additions & 9 deletions

File tree

Makefile

Lines changed: 140 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ FYNE_UI_DIR = gui_interface/fyne-ui
9898
FYNE_UI_BIN = mercury-ui.exe
9999
MINGW_GO_CC = x86_64-w64-mingw32-gcc
100100

101-
.PHONY: all install internal_deps utils clean doxygen doxygen-clean windows windows-zip windows-installer-signed windows-installer-stage check-installer-names fyne-ui fyne-ui-macos fyne-ui-macos-dmg macos-universal fyne-ui-macos-universal fyne-ui-macos-universal-dmg fyne-ui-windows windows-installer test integration-test FORCE
101+
.PHONY: all install internal_deps utils clean doxygen doxygen-clean windows windows-zip windows-installer-signed windows-installer-stage check-installer-names fyne-ui fyne-ui-macos fyne-ui-macos-dmg macos-universal fyne-ui-macos-universal fyne-ui-macos-universal-dmg sign-macos-bin macos-notarize-dmg fyne-ui-windows windows-installer test integration-test FORCE
102102

103103
prefix ?= /usr
104104
bindir ?= $(prefix)/bin
@@ -258,7 +258,61 @@ fyne-ui-macos: libmercury_core.a
258258
# Wrap the .app in a compressed, drag-to-install .dmg (Applications symlink).
259259
# Run on macOS after fyne-ui-macos. Unsigned — Gatekeeper will warn on first
260260
# open (right-click → Open), which is expected for an unnotarised build.
261-
MACOS_DMG ?= $(MACOS_APP_NAME).dmg
261+
# Version-stamped, like the Windows artifacts (mercury-$(MERCURY_VERSION)-w64-*.zip
262+
# and Mercury_$(MERCURY_VERSION)_Setup.exe). A bare Mercury.dmg is impossible to
263+
# tell apart from the previous one in a downloads folder or on a release page.
264+
MACOS_DMG ?= $(MACOS_APP_NAME)-$(MERCURY_VERSION).dmg
265+
MACOS_DMG_UNIVERSAL ?= $(MACOS_APP_NAME)-$(MERCURY_VERSION)-universal.dmg
266+
MACOS_CLI_PARK = mercury-cli-universal
267+
268+
# ---- macOS code signing (rcodesign) --------------------------------------
269+
# github.com/indygreg/apple-platform-rs -- a pure-Rust reimplementation that
270+
# signs Mach-O binaries, BUNDLES, .dmg images and .pkg archives, and notarizes
271+
# and staples, with no Mac, no Xcode and no keychain. Used instead of Apple's
272+
# codesign so the signing certificate never has to reach a macOS runner, and
273+
# instead of anchore/quill, which cannot sign bundles or disk images at all
274+
# (anchore/quill#815, #550, both open) -- the two things Gatekeeper judges.
275+
#
276+
# Verified: a Mercury.app signed by rcodesign 0.28.0 on Linux passes Apple's
277+
# own `codesign --verify --deep --strict` on macOS 15.7.7 ("valid on disk",
278+
# "satisfies its Designated Requirement"), with CodeResources written and the
279+
# universal Mach-O sealed.
280+
#
281+
# NOTE this does not remove macOS from the release entirely: hdiutil builds the
282+
# .dmg and is Apple-only. What moves off the Mac is signing and notarization.
283+
#
284+
# MACOS_SIGN_P12 / MACOS_SIGN_P12_PASSWORD Developer ID cert
285+
#
286+
# Opt-in, same shape as win_sign: with no certificate the build still completes
287+
# and says so, keeping developer builds unchanged. 0.28.0 is the version
288+
# pinned by indygreg/apple-code-sign-action.
289+
RCODESIGN ?= rcodesign
290+
291+
# $(call macos_sign,<mach-o | bundle dir | dmg>)
292+
# --code-signature-flags runtime is the hardened runtime, which notarization
293+
# requires; Apple rejects the upload without it.
294+
define macos_sign
295+
@if [ -n "$(MACOS_SIGN_P12)" ]; then \
296+
command -v $(RCODESIGN) >/dev/null 2>&1 || { \
297+
echo "error: MACOS_SIGN_P12 is set but '$(RCODESIGN)' is not installed"; \
298+
echo " https://github.com/indygreg/apple-platform-rs/releases"; \
299+
exit 1; }; \
300+
echo "Signing (rcodesign): $(1)"; \
301+
$(RCODESIGN) sign \
302+
--p12-file "$(MACOS_SIGN_P12)" \
303+
--p12-password "$(MACOS_SIGN_P12_PASSWORD)" \
304+
--code-signature-flags runtime \
305+
$(if $(2),--binary-identifier "$(2)",) \
306+
"$(1)" || exit 1; \
307+
else \
308+
echo "WARNING: MACOS_SIGN_P12 unset — $(1) is unsigned"; \
309+
fi
310+
endef
311+
312+
# Sign an already-built artifact by hand (binary, .app or .dmg):
313+
# make sign-macos-bin BIN=mercury MACOS_SIGN_P12=cert.p12 MACOS_SIGN_P12_PASSWORD=...
314+
sign-macos-bin:
315+
$(call macos_sign,$(BIN))
262316
fyne-ui-macos-dmg: fyne-ui-macos
263317
@echo "Building $(MACOS_DMG)..."
264318
rm -f $(abspath $(MACOS_DMG))
@@ -282,6 +336,7 @@ macos-universal:
282336
@for A in x86_64 arm64; do \
283337
echo "== building mercury slice: $$A =="; \
284338
$(MAKE) clean >/dev/null; \
339+
$(MAKE) internal_deps CC="clang -arch $$A" || exit 1; \
285340
$(MAKE) $(BINARY) CC="clang -arch $$A" || exit 1; \
286341
mv $(BINARY) mercury-$$A || exit 1; \
287342
done
@@ -317,18 +372,88 @@ fyne-ui-macos-universal:
317372
@lipo -archs $(FYNE_UI_DIR)/$(MACOS_APP_NAME).app/Contents/MacOS/* || true
318373

319374
# Universal .app wrapped in a drag-to-install .dmg. The finished .dmg lands at
320-
# the repo top level (e.g. ./Mercury.dmg) — the distribution artifact to upload.
321-
fyne-ui-macos-universal-dmg: fyne-ui-macos-universal
322-
@echo "Building universal $(MACOS_DMG)..."
323-
rm -f $(abspath $(MACOS_DMG))
375+
# the repo top level (e.g. ./Mercury-1.9.11-universal.dmg) — the artifact to
376+
# upload.
377+
#
378+
# The image also carries the headless CLI, which it did not before: the Windows
379+
# zip has always shipped mercury.exe next to the GUI, but the Mac image held
380+
# only Mercury.app, so a Mac operator wanting a TNC/uucp station had nothing to
381+
# install. The GUI is built -tags mercury_embedded (the modem is linked into
382+
# it), so the CLI is a genuinely separate artifact, not a duplicate of it.
383+
#
384+
# It is staged NEXT TO the .app rather than inside Contents/MacOS: a drag-install
385+
# still copies exactly one thing, and stray executables inside a bundle are the
386+
# kind of thing that complicates signing/notarisation later.
387+
#
388+
# Ordering here is deliberate and cannot be expressed as prerequisites: BOTH
389+
# universal targets run `make clean` between their two arch slices, and clean
390+
# removes `mercury` AND Mercury.app — so whichever ran second would delete what
391+
# the first produced. Build the CLI first, park it under a name clean does not
392+
# match, then package the .app.
393+
fyne-ui-macos-universal-dmg:
394+
rm -f $(MACOS_CLI_PARK)
395+
$(MAKE) macos-universal
396+
mv $(BINARY) $(MACOS_CLI_PARK)
397+
$(MAKE) fyne-ui-macos-universal
398+
@echo "Building universal $(MACOS_DMG_UNIVERSAL)..."
399+
rm -f $(abspath $(MACOS_DMG_UNIVERSAL))
324400
rm -rf $(FYNE_UI_DIR)/dmg-stage
325401
mkdir -p $(FYNE_UI_DIR)/dmg-stage
326402
cp -R $(FYNE_UI_DIR)/$(MACOS_APP_NAME).app $(FYNE_UI_DIR)/dmg-stage/
327403
ln -s /Applications $(FYNE_UI_DIR)/dmg-stage/Applications
328-
hdiutil create -volname "$(MACOS_APP_NAME)" -srcfolder $(FYNE_UI_DIR)/dmg-stage \
329-
-ov -format UDZO "$(abspath $(MACOS_DMG))"
404+
mkdir -p "$(FYNE_UI_DIR)/dmg-stage/Command Line"
405+
cp $(abspath $(MACOS_CLI_PARK)) "$(FYNE_UI_DIR)/dmg-stage/Command Line/$(BINARY)"
406+
$(call macos_sign,$(FYNE_UI_DIR)/dmg-stage/Command Line/$(BINARY))
407+
cp mercury.ini.example "$(FYNE_UI_DIR)/dmg-stage/Command Line/"
408+
printf '%s\n' \
409+
'Mercury $(MERCURY_VERSION) - command-line (headless) modem' \
410+
'' \
411+
'Mercury.app next to this folder is the GUI and needs nothing else.' \
412+
'This folder is for running Mercury headless: as a TNC for Winlink/BPQ32,' \
413+
'or under uucp.' \
414+
'' \
415+
'Install:' \
416+
' sudo cp mercury /usr/local/bin/' \
417+
' cp mercury.ini.example ~/.mercury.ini # then edit for your radio' \
418+
' mercury -h # options' \
419+
'' \
420+
'Universal binary (Intel + Apple Silicon). If this build is unsigned, the' \
421+
'first run needs: xattr -d com.apple.quarantine /usr/local/bin/mercury' \
422+
> "$(FYNE_UI_DIR)/dmg-stage/Command Line/README.txt"
423+
@# Seal the bundle before it goes into the image: rcodesign recurses into
424+
@# nested Mach-Os and writes Contents/_CodeSignature/CodeResources.
425+
@# Signing the image afterwards does NOT sign what is inside it.
426+
$(call macos_sign,$(FYNE_UI_DIR)/dmg-stage/$(MACOS_APP_NAME).app)
427+
hdiutil create -volname "$(MACOS_APP_NAME) $(MERCURY_VERSION)" \
428+
-srcfolder $(FYNE_UI_DIR)/dmg-stage \
429+
-ov -format UDZO "$(abspath $(MACOS_DMG_UNIVERSAL))"
430+
$(call macos_sign,$(abspath $(MACOS_DMG_UNIVERSAL)),$(MACOS_APP_ID))
330431
rm -rf $(FYNE_UI_DIR)/dmg-stage
331-
@echo " -> $(abspath $(MACOS_DMG)) (universal)"
432+
rm -f $(abspath $(MACOS_CLI_PARK))
433+
@echo " -> $(abspath $(MACOS_DMG_UNIVERSAL)) (universal, GUI + CLI)"
434+
435+
# ---- Notarization (rcodesign; network, no Mac) ---------------------------
436+
# Separate from signing on purpose: signing is local and offline, this uploads
437+
# to Apple, waits for the verdict and staples the ticket into the image so
438+
# Gatekeeper accepts it offline afterwards.
439+
#
440+
# Credentials are an App Store Connect API key, encoded once into a JSON file:
441+
# rcodesign encode-app-store-connect-api-key -o ~/.mercury-notary.json \
442+
# <issuer-id> <key-id> /path/to/AuthKey_<key-id>.p8
443+
#
444+
# then: make macos-notarize-dmg MACOS_NOTARY_KEY=~/.mercury-notary.json
445+
MACOS_NOTARY_KEY ?=
446+
macos-notarize-dmg:
447+
@[ -n "$(MACOS_NOTARY_KEY)" ] || { \
448+
echo "error: set MACOS_NOTARY_KEY to an encoded App Store Connect key"; \
449+
echo " rcodesign encode-app-store-connect-api-key -o key.json <issuer> <key-id> AuthKey.p8"; \
450+
exit 1; }
451+
@[ -f "$(MACOS_DMG_UNIVERSAL)" ] || { \
452+
echo "error: $(MACOS_DMG_UNIVERSAL) not built yet"; exit 1; }
453+
$(RCODESIGN) notary-submit \
454+
--api-key-file "$(MACOS_NOTARY_KEY)" --staple \
455+
"$(MACOS_DMG_UNIVERSAL)"
456+
@echo " -> $(abspath $(MACOS_DMG_UNIVERSAL)) (notarized + stapled)"
332457

333458
# ---- Authenticode signing (Windows binaries) ----
334459
# Two modes:
@@ -522,6 +647,12 @@ clean:
522647
rm -f $(FYNE_UI_DIR)/engine/mercury_bridge.o $(FYNE_UI_DIR)/engine/mercury_bridge_w64.o
523648
rm -f mercury-ui $(FYNE_UI_DIR)/mercury-ui $(FYNE_UI_DIR)/mercury-fyne-ui
524649
rm -f $(MACOS_DMG) $(FYNE_UI_DIR)/$(MACOS_DMG)
650+
rm -f $(MACOS_DMG_UNIVERSAL) $(FYNE_UI_DIR)/$(MACOS_DMG_UNIVERSAL)
651+
@# NOT $(MACOS_CLI_PARK): the dmg recipe parks the CLI there precisely so it
652+
@# survives the `clean` that fyne-ui-macos-universal runs between its arch
653+
@# slices. Cleaning it here deletes the binary mid-build. The recipe removes
654+
@# it itself on success, and clears it before starting so a leftover from a
655+
@# failed run can never be staged as if it were fresh.
525656
rm -rf $(FYNE_UI_DIR)/$(MACOS_APP_NAME).app $(FYNE_UI_DIR)/dmg-stage
526657
$(MAKE) -C modem clean
527658
$(MAKE) -C datalink_arq clean

0 commit comments

Comments
 (0)