Reproducible native installer builds (Apple Silicon + Windows) via GitHub Actions#534
Merged
Conversation
Build macOS (.dmg) and Windows (.msi) installers in CI on a matrix of
macos-latest + windows-latest, using the same Temurin JDK on both.
jpackage bundles a runtime matching the runner architecture, so macOS
now ships a native arm64 app (no Rosetta) instead of the previous
Intel-only build that depended on the now-hardcoded JDK 14 path.
Mirrors the figment project's trigger model: push to master produces
nightly workflow artifacts; v* tags attach signed installers to a
GitHub Release.
- build.xml: resolve jpackage from ${java.home} (overridable via
-Djpackage=...) instead of a hardcoded Intel JDK 14 path
- Notarizer: replace retired `altool` notarization with `notarytool`,
reading APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID from env
- PackageSigner: take the signing identity from MACOS_SIGN_IDENTITY and
fail the build on a non-zero codesign exit
- docs/RELEASING.md: document required secrets and the release flow
macOS signing/notarization steps are skipped when secrets are absent
(forks/PRs), falling back to an unsigned app zip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two pre-existing bugs that surfaced building locally on arm64:
- dist-mac passed --resource-dir dist/resources, but the resources
target writes to ${dist.res} (dist/unpacked/resources). jpackage
exited 1. Use ${dist.res} to match dist-win.
- The jpackage <exec> and PackageSigner/Notarizer <java> tasks had no
failonerror, so a non-zero exit still reported BUILD SUCCESSFUL -- in
CI that means a green check with no installer. Add failonerror="true".
Verified: `ant dist-mac` on an arm64 JDK now produces a native arm64
app (launcher, bundled JVM, and JNA darwin-aarch64 slice all arm64).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bundled platform/mac/bin/ffmpeg was an x86_64-only static build (evermeet 4.2.2), so video export shelled out through Rosetta even after the app itself became native arm64 — and would break entirely on macOS 28 when Rosetta is removed. Swap in a static arm64 build (ffmpeg 8.1, osxexperts.net — same maintainer as the previous binary). Verified self-contained (links only system frameworks) and includes the codecs NodeBox invokes: libx264 (h264/mp4) and libvpx (webm). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…branch The TODO marker on the push branch list must be reverted before merge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Set the imported keychain as default and dump 'security find-identity' so signing-identity / keychain-visibility issues are diagnosable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
notarytool submit --wait can exit 0 with status=Invalid; the old code then tried to staple a non-existent ticket and failed opaquely. Parse the JSON status and, on any non-Accepted result, dump 'notarytool log' (which names the offending file/issue) before failing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The notary service rejected the bundled JRE dylibs (libjvm, libjli, libzip, ...) for missing a secure timestamp / valid Developer ID signature. Cause: scanRecursive signs each nested binary correctly inside-out, but the final --deep sign of the .app re-signed all that nested code without per-binary timestamps. Apple deprecates --deep for exactly this; the inside-out pass already covers nested code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jpackage --type dmg --app-image re-signs the app-image with an ad-hoc signature while wrapping it, clobbering the Developer ID signatures PackageSigner applies to the bundled JRE dylibs -- so the notarized dmg contained adhoc sigs and Apple rejected every runtime dylib for 'not signed with valid Developer ID' + 'no secure timestamp'. Build the dmg with hdiutil (which copies the signed app verbatim) and sign the dmg itself. Verified locally: all 44 Mach-O binaries inside the resulting dmg carry a Developer ID signature with a secure timestamp. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The notary service inspects Mach-O code inside jars and rejected the unsigned native libs bundled in the fat jar (JNA's libjnidispatch for both arch slices, jffi, jansi) -- 12 hard errors. Sign each in place (extract, codesign with Developer ID + timestamp + runtime, write back) before the app bundle is sealed. Verified locally that all four carry a Developer ID signature with a secure timestamp inside the jar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Notary rejected jython-standalone 2.7.2's bundled libjffi-1.2.jnilib for using the 10.7 SDK (older than 10.9). Jython 2.7.4 ships the same native libs rebuilt against current SDKs (jffi now 12.0, jansi 10.11). Minimal single-dependency bump; unit tests pass and jffi still loads at runtime. Add platform/mac/verify-signing.sh: checks every Mach-O on disk AND inside nodebox.jar for Developer ID signature + secure timestamp + >=10.9 SDK, plus Gatekeeper/staple when given the dmg. The workflow's verify step now calls it, so all three failure modes we hit are caught (offline for the first three) instead of via a blind notary round-trip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reverts release.yml to triggering on master + tags only. The full sign/notarize/staple/verify pipeline was confirmed green on the PR branch (run 27510350014: notarization Accepted, VERIFICATION PASSED). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Bump actions/checkout and actions/setup-java to v5 (native Node 24). - Set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 to cover actions without a Node-24 release yet (upload-artifact, action-gh-release), clearing the Node 20 deprecation warnings ahead of GitHub's forced migration. - Only run 'brew install ant' when ant isn't already on PATH (it is preinstalled on macos runners), removing the 'already installed' warning. Co-Authored-By: Claude Opus 4.8 (1M context) <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.
Why
The shipped macOS build of NodeBox is Intel-only and runs through Rosetta. Apple removes general Rosetta translation in macOS 28 (~fall 2027), so that build will stop launching on Apple Silicon. Root cause:
build.xmlhardcodedjpackageto an Intel JDK 14 path, andjpackagebundles a runtime matching its own architecture.Everything else was already arm64-ready —
jna5.18.1 ships adarwin-aarch64slice. So the core change is a packaging fix, not a port. This PR turns that into a reproducible, signed, notarized CI pipeline for both macOS and Windows.What
Reproducible installer builds in GitHub Actions, mirroring the figment trigger model:
master→.dmg+.msias workflow artifacts (nightly)v*tag → installers attached to a GitHub Releasemacos-latest+windows-latest, same Temurin JDK on both → native arm64 macOS app, x64 Windows.msiVerified green on the branch (run 27510350014): macOS signed + notarized + stapled, both artifacts uploaded.
Packaging & build
build.xml— resolvejpackagefrom${java.home}(override with-Djpackage=…) instead of the hardcoded Intel JDK 14; fixdist-mac --resource-dir; addfailonerrorso a failedjpackage/sign/notarize no longer reports green.platform/mac/bin/ffmpeg— replaced the Intel-only static binary (evermeet 4.2.2) with a native arm64 static build (osxexperts 8.1); video export no longer needs Rosetta. Verified self-contained with the libx264/libvpx encoders NodeBox uses.macOS signing & notarization (the bulk of the work)
This took four distinct fixes — each Apple-notarization failure mode was only revealed after fixing the previous one:
--deepstripped per-binary secure timestamps →PackageSignersigns nested code inside-out individually, no--deep.jpackage --type dmgre-signs the app ad-hoc, clobbering our Developer ID signatures → build the dmg withhdiutiland sign the dmg itself.nodebox.jarwere unsigned (the notary service inspects jars) →PackageSignernow signs the Mach-O libs (JNA'slibjnidispatch×2, jffi, jansi) in place before the bundle is sealed.libjffiwas built with the 10.7 SDK (notary requires ≥10.9) → bumped Jython 2.7.2 → 2.7.4 (minimal single-dependency bump; its natives are rebuilt against modern SDKs).ant testpasses.Notarizer.java— replaced retiredaltoolwithnotarytool; on any non-Acceptedresult it now dumps the notary log (names the offending file/issue) instead of failing opaquely.PackageSigner.java— signing identity fromMACOS_SIGN_IDENTITY; fails the build on non-zerocodesign.platform/mac/verify-signing.sh— verification gate the workflow runs: checks every Mach-O on disk and insidenodebox.jarfor Developer ID signature + secure timestamp + ≥10.9 SDK, plus Gatekeeper acceptance and a stapled ticket. Catches all four failure modes above (offline for the first three).Required secrets (macOS only — Windows ships unsigned for now)
MACOS_CERTIFICATE,MACOS_CERTIFICATE_PWD,KEYCHAIN_PASSWORD,MACOS_SIGN_IDENTITY,APPLE_ID,APPLE_APP_SPECIFIC_PASSWORD,APPLE_TEAM_ID. Seedocs/RELEASING.md. If absent (e.g. forks), the build still runs and produces an unsigned app zip.Releasing
Bump
nodebox.version, then tag and push:git tag v3.0.53 && git push origin v3.0.53The workflow signs, notarizes, and attaches the installers to a GitHub Release.
Follow-ups (not in this PR)
ffmpegare still x86_64 (fine for current targets).checkout@v4/setup-java@v4run on Node 20 — worth bumping action majors soon.🤖 Generated with Claude Code