Skip to content

Reproducible native installer builds (Apple Silicon + Windows) via GitHub Actions#534

Merged
fdb merged 13 commits into
masterfrom
ci/reproducible-native-builds
Jun 14, 2026
Merged

Reproducible native installer builds (Apple Silicon + Windows) via GitHub Actions#534
fdb merged 13 commits into
masterfrom
ci/reproducible-native-builds

Conversation

@fdb

@fdb fdb commented Jun 14, 2026

Copy link
Copy Markdown
Member

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.xml hardcoded jpackage to an Intel JDK 14 path, and jpackage bundles a runtime matching its own architecture.

Everything else was already arm64-ready — jna 5.18.1 ships a darwin-aarch64 slice. 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:

  • push to master.dmg + .msi as workflow artifacts (nightly)
  • v* tag → installers attached to a GitHub Release
  • matrix macos-latest + windows-latest, same Temurin JDK on both → native arm64 macOS app, x64 Windows .msi

Verified green on the branch (run 27510350014): macOS signed + notarized + stapled, both artifacts uploaded.

Packaging & build

  • build.xml — resolve jpackage from ${java.home} (override with -Djpackage=…) instead of the hardcoded Intel JDK 14; fix dist-mac --resource-dir; add failonerror so a failed jpackage/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:

  1. --deep stripped per-binary secure timestampsPackageSigner signs nested code inside-out individually, no --deep.
  2. jpackage --type dmg re-signs the app ad-hoc, clobbering our Developer ID signatures → build the dmg with hdiutil and sign the dmg itself.
  3. Native libs inside nodebox.jar were unsigned (the notary service inspects jars) → PackageSigner now signs the Mach-O libs (JNA's libjnidispatch ×2, jffi, jansi) in place before the bundle is sealed.
  4. Jython 2.7.2's bundled libjffi was 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 test passes.
  • Notarizer.java — replaced retired altool with notarytool; on any non-Accepted result it now dumps the notary log (names the offending file/issue) instead of failing opaquely.
  • PackageSigner.java — signing identity from MACOS_SIGN_IDENTITY; fails the build on non-zero codesign.
  • platform/mac/verify-signing.sh — verification gate the workflow runs: checks every Mach-O on disk and inside nodebox.jar for 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. See docs/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.53

The workflow signs, notarizes, and attaches the installers to a GitHub Release.

Follow-ups (not in this PR)

  • Windows/Linux ffmpeg are still x86_64 (fine for current targets).
  • GitHub is forcing Actions onto Node 24 (~June 16 2026); checkout@v4/setup-java@v4 run on Node 20 — worth bumping action majors soon.

🤖 Generated with Claude Code

fdb and others added 13 commits June 14, 2026 15:48
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>
@fdb fdb merged commit c9f646b into master Jun 14, 2026
2 checks passed
@fdb fdb deleted the ci/reproducible-native-builds branch June 14, 2026 20:52
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