Skip to content

Add macOS universal binary (arm64 + x64) support#5

Closed
kdroidFilter wants to merge 10 commits intomainfrom
feature/universal-binary
Closed

Add macOS universal binary (arm64 + x64) support#5
kdroidFilter wants to merge 10 commits intomainfrom
feature/universal-binary

Conversation

@kdroidFilter
Copy link
Copy Markdown
Owner

Summary

  • Adds universalBinary { enabled = true; x64JdkPath = "..." } DSL under macOS {} to build fat macOS apps running natively on both Apple Silicon and Intel
  • Uses a subprocess strategy: launches a separate Gradle build under Rosetta with an x64 JDK, then merges both distributables with lipo
  • Registers mergeUniversalBinary, packageUniversalDmg/Pkg, and notarizeUniversalDmg/Pkg tasks
  • Installer filenames get a _universal suffix instead of _arm64/_x64
  • Compatible with AOT cache generation (both architectures get their own cache)

Files changed

File Change
UniversalBinarySettings.kt New — DSL class (enabled, x64JdkPath)
AbstractCreateDistributableX64Task.kt New — Subprocess task launching x64 Gradle build
AbstractMergeUniversalBinaryTask.kt New — Merges arm64+x64 with lipo, clears xattr, re-signs
PlatformSettings.kt Add universalBinary block to JvmMacOSPlatformSettings
JvmApplicationContext.kt Suffix appTmpDir with -x64 in subprocess to isolate intermediates
configureJvmApplication.kt Output dir redirect (-x64), configureUniversalBinaryTasks()
AbstractJPackageTask.kt Add universalBinaryFlag for _universal filename suffix
README.md Full documentation of the feature

Test plan

  • Plugin compiles with JDK 21
  • mergeUniversalBinary produces a universal .app (verified with lipo -info)
  • packageUniversalDmg produces _universal.dmg with correct content
  • codesign --verify --deep --strict passes on the universal app
  • Test on an Intel Mac to confirm the app launches natively

Build universal fat binaries by launching a separate Gradle process
under Rosetta with an x64 JDK, then merging both distributables
with lipo. Includes universal DMG/PKG packaging and notarization.
Allow producing x64-only installers from an Apple Silicon Mac
without merging into a universal binary. The tasks run the full
packaging pipeline in a subprocess with the x64 JDK.
The universal binary and x64-only packaging tasks are only useful on
Apple Silicon (arm64) Macs. Guard their registration with a
currentArch == Arm64 check so they are not created on x64 runners.
Setting x64JdkPath on the macOS block is sufficient to enable
universal binary and x64-only tasks. No need for a separate
UniversalBinarySettings class or an enabled flag.
When the environment variable is set, universal binary and x64-only
tasks are enabled automatically without any build script configuration.
The JDK runtime's legal/ directory contains relative symlinks. The
previous copyRecursively() call followed symlinks and failed when the
target didn't resolve after being copied. Use java.nio.file.Files.walk
with NOFOLLOW_LINKS to preserve symlinks as-is during both the base
copy and the x64 merge walk.
xattr can fail on symlinks in the JDK runtime legal/ directory.
Since clearing extended attributes is a best-effort cleanup step,
log a warning instead of failing the build.
The .jpackage.xml file is jpackage internal metadata that encodes the
jpackage version. When the host JDK (e.g. JBR-25) and the x64 JDK
(e.g. Liberica 25.0.2) differ, jpackage rejects the file as
"generated by another jpackage version". Skip it during both the
base copy and the x64 merge so jpackage treats the universal app as
a fresh app-image.
jpackage requires .jpackage.xml to be present in the app-image.
Keep the arm64 version (generated by the host jpackage) during the
base copy, and only skip it during the x64 merge to avoid version
conflicts between different JDK distributions.
JBR-25 does not create .jpackage.xml in createDistributable, but
requires it when using --app-image for packaging. The x64 build
(Liberica) does create one but with a different version string.

After the merge, if .jpackage.xml is missing from the universal app,
copy it from the x64 build and patch the version attribute to match
the host jpackage (via jpackage --version). This ensures the
universal app is accepted by the host jpackage for DMG/PKG creation.
@kdroidFilter kdroidFilter deleted the feature/universal-binary branch February 17, 2026 09:23
kdroidFilter added a commit that referenced this pull request Apr 19, 2026
X11 ICCCM compliance (#4/#5):
- SetSelectionOwner now uses real server timestamp via PropertyNotify probe,
  not XCB_CURRENT_TIME (violates ICCCM §2.1). Added get_server_timestamp_locked()
  which fires a zero-byte ChangeProperty to trigger timestamp event.
- TIMESTAMP replies now return g_own_ts (real value) instead of truncated 0.
- Verified: xclip -o -t TIMESTAMP returns non-zero after our clipboard write.

INCR cleanup (#3):
- On INCR read timeout, delete property to unblock sender waiting for
  PropertyNotify=Delete (ICCCM compliant termination).

Process lifecycle (#7):
- Wayland: runCaptureBytes, runSilently, writeBytes now escalate to
  destroyForcibly() if SIGTERM doesn't terminate after 500ms grace.

AccessBehavior mapping (#12):
- Kotlin: explicit when() mapping (0→AlwaysAllow, 1→AskEveryTime, 2→AlwaysDeny)
  instead of ordinal/entries.getOrNull (fragile with future macOS versions).
- ObjC: validate input 0..2 on set; return -1 if get() returns out-of-range.

Documentation & robustness (#13, #1):
- Clipboard.watch() doc: clarify poll interval is always honored; source of
  counter differs by backend (Mach IPC / XFixes / wl-paste).
- Re-check isActive after slow availableFormats() to avoid emitting to
  cancelled flow.

Added X11TimestampSmokeTest to verify real timestamps are used.
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