Skip to content

Native Apple Silicon macOS build: app bundle, DMG installer and CI - #766

Draft
hobleyd wants to merge 4 commits into
OoliteProject:masterfrom
hobleyd:macos-arm64-port
Draft

hobleyd wants to merge 4 commits into
OoliteProject:masterfrom
hobleyd:macos-arm64-port

Conversation

@hobleyd

@hobleyd hobleyd commented Oct 19, 2026

Copy link
Copy Markdown

Overlaps with #761, #762 and #763 (darwin support by @Depal1), which take the same overall approach. I'm opening this as a draft so maintainers can compare the two, take the parts they prefer from each, or combine them. The differences are summarised at the end.

Summary

This makes Oolite build and run natively on Apple Silicon Macs, using Apple Foundation with the existing Meson + SDL3 front end instead of GNUstep. Each build type produces a self-contained oolite.app and a drag-to-install DMG. CI builds all three build types on macOS, runs the snapshot smoke test, and attaches the DMGs to the prerelease.

Build steps on a Mac (also in the README):

ShellScripts/macOS/install_packages.sh      # Homebrew packages
ShellScripts/macOS/build_mozilla_js.sh      # arm64 SpiderMonkey, built from source into build/mozilla_js
./mk.sh build dev && ./mk.sh test dev
./mk.sh pkg-dmg deployment                  # or: ./mk.sh install deployment  (copies to /Applications)

Changes

Build system

  • src/meson/darwin/meson.build and a darwin.ini native file (Apple clang and ld64, since Apple's toolchain has no lld). mk.sh picks the native file automatically on macOS, and re-runs itself under Homebrew bash 4+.
  • The Info plist is embedded in the binary, so [[NSBundle mainBundle] infoDictionary] works on Apple Foundation; the GNUstep build uses NSBundle+Override for this.
  • get_version.sh, get_build_date_fn.sh and post_build.sh work with macOS tools: the meson parent-process check, GNU date/cp from coreutils, and the padded output of BSD wc.

SpiderMonkey

  • ShellScripts/macOS/build_mozilla_js.sh builds the same mozillajs-linux source (pinned commit) for arm64 without the old configure step, which needs Python 2. It builds the interpreter only, because the tracing, method and YARR JITs don't support arm64.
  • Three small source patches, in mozilla_js_arm64.patch: 64-bit detection for arm64, finite → isfinite, and a debug-build-only declaration that was guarded by JS_METHODJIT.
  • In CI the library is cached, keyed on the script and the patch.

Platform macros

  • On Apple Foundation, OOLITE_MAC_OS_X and OOLITE_SDL are both 1. A new OOLITE_MAC_COCOA_UI (OOLITE_MAC_OS_X && !OOLITE_SDL) guards the leftovers from the removed Cocoa front end, so those sites no longer pull in AppKit.
  • OOLITE_MAC_OS_X now only selects Foundation and OS-level behaviour, such as log, cache and add-on paths and plist handling.

OpenGL (macOS only offers a legacy 2.1 context alongside fixed-function GL)

  • The 3.3 version check accepts 2.1 when ARB_framebuffer_object, APPLE_vertex_array_object, ARB_texture_float, ARB_color_buffer_float and ARB_draw_buffers are present.
  • glGen/Bind/DeleteVertexArrays map to the *APPLE functions and glClampColor maps to glClampColorARB. The MSAA colour buffer is a multisample renderbuffer, since multisample textures aren't available.
  • The post-processing programs created with a #version 330 prefix are translated to GLSL 1.20 (plus EXT_gpu_shader4) when they load (TranslateGLSL330To120 in OOShaderProgram.m), so the shader files stay shared across platforms. To make that possible I rewrote two switch statements as if/else and removed one interface block. Both changes are valid GLSL 3.30, so other platforms are unaffected.

macOS app bundle and installer

  • post_build.sh creates Contents/MacOS, Contents/Resources (the game data), Contents/AddOns and an Info.plist generated from Resources/Info-gnustep.plist with the Cocoa-only keys removed.
  • Non-system dylibs are copied into Contents/Frameworks with @rpath load paths, so the app runs without Homebrew. The bundle is ad-hoc signed.
  • ./mk.sh pkg-dmg <type> (installers/macos/create_dmg_fn.sh) builds a compressed DMG with the app and an /Applications link. ./mk.sh install copies the bundle to /Applications/Oolite.app.

CI

  • build-all.yaml gains a build-macos job (deployment, test and dev on macos-latest). The release job attaches the DMGs, its expected file count goes from 8 to 11, and it declares contents: write, which forks need because their default token is read-only.
  • test_builds.yaml gains a macos job (build, smoke test and DMG). On macOS the smoke test uses the real video driver, because SDL's offscreen driver has no OpenGL there.

Fixes in shared code

These were found while porting. They're needed on macOS and should also help elsewhere:

  • Saved window size on high-DPI displays: the size was saved in pixels but restored as points, so the window doubled every launch on a 2× display. It's now saved in points.
  • Mouse position on high-DPI displays: SDL mouse coordinates are in points but were compared with the pixel-sized view, so clicks landed in the wrong place. They're now scaled by SDL_GetWindowPixelDensity, which is 1.0 on standard displays.
  • addShipToShipyard crash: with no personality given it inserted nil into a dictionary, which throws on GNUstep and Apple Foundation alike.
  • Snapshot readback: a GNUstep-only stringByReplacingString:withString: in the HDR snapshot path is replaced with stringByReplacingOccurrencesOfString:withString:.

macOS-only behaviour, guarded so other platforms are unchanged:

  • No SDL_GetWindowSurface() on the GL window. On macOS it replaces the GL drawable and the window goes black. VSync uses SDL_GL_SetSwapInterval, and snapshots read the front buffer.
  • Debug console: read only while hasBytesAvailable, because Apple's socket streams block in -read: and freeze the game loop. Connect with a plain non-blocking socket, because NSHost and CFNetwork's host lookup stalled for about 18 seconds on GitHub's macOS runners.
  • When run from Contents/MacOS, main() changes directory to Contents, so the existing Resources/ and AddOns/ lookups work unchanged.

Testing

  • On an Apple Silicon Mac (Apple M5), with the dev, test and deployment build types:
    • the title screen renders through the post-processing path;
    • flight works after starting a new commander;
    • keyboard and mouse input work on a Retina display;
    • a commander can be saved, and loaded after a restart;
    • ./mk.sh test dev and ./mk.sh test test pass;
    • the DMG mounts with a correctly signed app, which launches from /Applications.
  • CI on my fork: all jobs pass in build-all (all 11 release files, including the three DMGs) and test_builds. The Ubuntu, Fedora, Arch and Windows smoke tests pass, which covers the shared-code changes. On the macOS runner the smoke test captures a frame from Apple's software renderer.
  • clang-format 22 is clean for the changed files, shellcheck is clean for the new scripts and actionlint is clean for the new jobs.

Known limitations

Comparison with #761–#763

Both PRs share the core approach: Foundation + SDL3, a macro guarding the Cocoa-era code, legacy GL 2.1, and an interpreter-only arm64 SpiderMonkey. As I read them, the main differences are:

#761–#763 This PR
Post-processing shaders Hand-written GLSL 1.20 copies inside each shader file (about +1,000 lines) One set of shaders, translated when they load (about +150 lines)
SpiderMonkey Downloads a prebuilt library from OoliteProject/mozillajs-macos, which doesn't exist yet Built from the existing mozillajs-linux source in-tree, cached in CI
Packaging App bundle, plus Developer ID signing and notarisation scripts App bundle and DMG for each build type, attached to releases
CI dev build only, .app uploaded as an artifact All three build types, smoke test, DMGs in the release
GL fallbacks Yes (RGBA8, 24-bit depth, single render target) No
High-DPI window size and mouse Not changed Fixed (see above)
Docs Documentation/BUILDING-macOS.md README section

The two would conflict if both were merged. I'm happy to rebase this onto #761 instead, or to move pieces such as the DMG packaging, the from-source SpiderMonkey build, the CI smoke test and the high-DPI fixes into follow-up PRs, whichever the maintainers prefer.

David Hobley added 4 commits October 19, 2026 17:44
The macOS build uses Apple Foundation and the existing SDL3 front end
instead of GNUstep:

- Meson: a darwin branch (src/meson/darwin), a darwin.ini native file
  that mk.sh selects on macOS, and the Info plist embedded in the binary.
- SpiderMonkey: ShellScripts/macOS/build_mozilla_js.sh builds an
  interpreter-only arm64 library from source with three small patches.
- OOFoundation.h: Apple Foundation with OOLITE_SDL; OOLITE_MAC_COCOA_UI
  marks code for the removed Cocoa front end.
- OpenGL: macOS only offers a legacy 2.1 context, so the version gate
  accepts it with the required extensions, VAOs map to the APPLE
  extension, MSAA uses a renderbuffer, and the #version 330
  post-processing shaders are translated to GLSL 1.20 when loaded.
- Packaging: post_build.sh creates a self-contained, ad-hoc signed
  oolite.app (Homebrew dylibs in Contents/Frameworks), install copies it
  to /Applications, and ./mk.sh pkg-dmg builds a drag-to-install DMG.
- CI: macOS jobs in both workflows; the release attaches the DMGs.

Fixes in shared code found along the way:

- Save the window size in points and scale mouse coordinates by the
  pixel density (window grew each launch, clicks were misplaced on
  high-DPI displays).
- Don't create an SDL window surface on the GL window on macOS.
- Read the debug console socket only while data is available on macOS
  (Apple streams block, freezing the game loop).
- addShipToShipyard inserted nil when no personality was given.
- Shader switch statements and an interface block replaced with
  equivalent GLSL that also works in 1.20.
NSHost does blocking DNS lookups, and on GitHub's macOS runners the game
hung while connecting to the snapshot test's console. Use
CFStreamCreatePairWithSocketToHost with the address instead. The test
launcher now samples the process on macOS if it fails to connect.
CFNetwork's host-based stream connection stalled for around 18 seconds
on GitHub's macOS runners, close to the snapshot test's timeout.
Connect a socket directly (non-blocking, 3 second timeout) and wrap it
in streams.
@mcarans

mcarans commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

@hobleyd Thanks for this PR which makes it easier to see what is changed than the other ones. Have you run the game locally loading a few OXPs to see how well it works on Mac? I tested the appimage briefly on Linux and it seems fine so nothing appears to be broken.

Rather than trying to get an old version of OpenGL working (which might lead to a suboptimal Mac experience?), I think it would be preferable to switch to SDL_GPU for all platforms: #579 (or failing that Vulkan which also has a Metal backend).

Given the age of the JS engine, it would be great to investigate QuickJS-NG as a JS engine replacement for all platforms: see #580. If you're prepared to use your AI, you might be able to build on your working Mac setup and check if this library (or any other lightweight cross platform JavaScript library) can easily be used a substitute.

I had planned to look into those things first, but your PR is in a pretty good state so it's probably better to start with it first.

@phkb What do you think?

@hobleyd

hobleyd commented Oct 20, 2026 •

Copy link
Copy Markdown
Author

I have run it briefly locally, not with loading OXPs yet - will test that next. I can also look at both of those issues.... I'll raise some more PRs...

This branch has not been deployed

No deployments
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.

2 participants