Skip to content

Add macOS Apple Silicon native build support#536

Open
MarcDufresne wants to merge 3 commits into
black-sliver:masterfrom
MarcDufresne:fix/macos-dist-codesign
Open

Add macOS Apple Silicon native build support#536
MarcDufresne wants to merge 3 commits into
black-sliver:masterfrom
MarcDufresne:fix/macos-dist-codesign

Conversation

@MarcDufresne

@MarcDufresne MarcDufresne commented Jun 21, 2026

Copy link
Copy Markdown

Hi! I noticed that PopTracker didn't have a native Apple Silicon build, which will be required starting from the next macOS release (Rosetta will be removed). Considering this, I wanted to add proper support to avoid any future issues.

I was able to build PopTracker, run it on my M1 Pro MacBook Pro, using a Majora's Mask pack, and connect to an AP server. As far as I could see, everything seemed to work perfectly fine. Build atrifacts available here: https://github.com/MarcDufresne/PopTracker/actions/runs/27890842995

I had to update the build process a bit, like proper signature and packaging, and some small tweaks to naming since macOS now has 2 architectures to build.

Cheers!

The macOS DIST bundle could not be launched on another Mac: after
download/AirDrop (which quarantines the copy) Gatekeeper reported it as
"damaged and can't be opened". Two independent defects caused this:

1. bundle_macosx_app.sh rewrites each Mach-O's dependent library paths
   with install_name_tool *after* the linker has signed them, which
   invalidates the signature, and never re-seals the bundle. On Apple
   Silicon a valid signature is mandatory to execute. Re-sign the
   dylibs, the executable, and then the whole bundle (ad-hoc) as the
   final step of bundling.

2. The Makefile packaged the .app with Info-ZIP `zip`, which drops the
   extended attributes that hold the code signatures of the nested
   resources under Contents/MacOS. Extraction then yielded an
   unsigned/"damaged" bundle. Package with `ditto` instead, which
   preserves signing metadata and is always available on macOS.

The signature is ad-hoc (not Developer ID / notarized), so a quarantined
copy still needs a one-time approval on first launch (right-click > Open,
or `xattr -dr com.apple.quarantine <app>`), but it is no longer reported
as damaged and runs normally thereafter.

Also gitignore the generated macosx/libs/ third-party build tree.
The macOS build/release jobs were pinned to macos-15-intel with a
hardcoded ARCH=x86_64, so only an Intel artifact was ever produced.
Apple Silicon Macs had no native build.

Turn both macOS jobs into a 2-arch matrix (macos-15-intel + macos-15),
mirroring the existing Ubuntu x86_64/arm64 matrix, and derive ARCH from
`uname -m` instead of hardcoding it. Also fix the release job's attest
subject-path, which hardcoded build/darwin-x86_64, to use the per-arch
build dir.

To keep the two macOS artifacts from colliding (the zip was named
poptracker_<ver>_macos.zip with no arch), include the arch in the name:
poptracker_<ver>_macos_<arch>.zip — consistent with the Linux tarballs,
which already embed the arch.

Standard macos-15 runners are free for public repositories, so this adds
no CI cost.
@MarcDufresne MarcDufresne changed the title Add macOS Apple Silicone native build support Add macOS Apple Silicon native build support Jun 21, 2026
@black-sliver

Copy link
Copy Markdown
Owner

Hello,

I think having separate Intel and Apple Silicon builds is not the best solution for the end-user, so fat binaries and a single download would be preferred.

We briefly talked about this in Discord already. Afaict, Rosetta will cease to exist around Sep 2027 with macOS 28, so we have more than a year to "fix" this (if you have other info, please link it), but the "big" problem is that there will be a year of overlap where Intel macs are still supported with security updates on macOS 26 and macOS 28 can't run Intel apps anymore. I would like to have fat binaries from ~Sep 2027 at the latest until ~Sep 2028 at the earliest, when we can drop Intel support ourselves1.

You can merge separate Intel and Apple Silicon binaries into one fat binary using the lipo command. I (currently) don't have access to a macos device, so if you would like to tackle this, that would be greatly appreciated.

Re ditto: We should probably specify --zlibCompressionLevel 9, since a good chunk of the data in the zip is highly compressible.

Footnotes

  1. actually we kinda still support macOS 10.14 in the source for retro reasons, but I think we can drop this at any point. Users should run the retro games and PopTracker on 2 separate machines. The reason I have not done so yet is because I want to compare boost::filesystem to std::filesystem with emscripten before dropping boost::filesystem and std::filesystem is the only thing missing from 10.14, so it just works at the moment.

The macOS CI built two separate downloads (arm64 and x86_64); this
combines them into one universal2 app that runs on any Mac.

A single fat compile isn't practical here, since the bundled deps
(SDL2, OpenSSL, freetype, ...) are built from source and don't emit
universal binaries in one pass. So each arch still builds natively,
then a merge job lipo-combines the two .app bundles:

- build-macos / release-macos now upload their .app (tar'd to keep
  symlinks and perms) instead of a per-arch zip.
- New *-universal jobs lipo-merge every Mach-O (macosx/make_universal.sh),
  re-sign ad-hoc (lipo breaks the signature), verify, then ditto-package
  a single poptracker_<version>_macos_universal.zip.

Also pass --zlibCompressionLevel 9 to ditto, since the bundle data
compresses well. A local `make CONF=DIST` still produces a per-arch zip.
@MarcDufresne

Copy link
Copy Markdown
Author

Hi! Good call on the universal build, I am so used to seeing x86_64 and arm64 builds as a dev that I forgot universal builds existed. I kept the two build separate since the underlying dependencies are built from source and can't go universal in one go. I added a merge job that combines both archs with lipo and ships a single universal zip. I also included the compression level flag.

Let me know if that works for you, thanks!

PS: As far as I know Rosetta will indeed be dropped starting on macOS 28, so next Fall.

@black-sliver

Copy link
Copy Markdown
Owner

PS: As far as I know Rosetta will indeed be dropped starting on macOS 28, so next Fall.

Ah, it seems like with 28 it'll be limited to specific apps and games. Somehow I missed that when researching earlier.

Let me know if that works for you, thanks!

At first glance, the change looks good. I am gonna ask in Discord if we have someone to test on Intel and M1 (or newer). Builds should be those: https://github.com/MarcDufresne/PopTracker/actions/runs/27913528305

@black-sliver

black-sliver commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Wait, isn't macos 28 coming in before 2028? So we do still have a year?

@MarcDufresne

Copy link
Copy Markdown
Author

They release the versions like one year "ahead". 26 released at the end 2025. So we'll get 28 at the end of 2027. I wasn't clear with my "next Fall" comment. We do still have a year and a half pretty much, yes.

@black-sliver black-sliver left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was tested now on 3 separate machines (1 intel, 1 m4 and another apple silicon) over on Discord and I looked over the code and almost everything looks good.

It's missing the attestation of the universal build for manually triggered build binaries - those builds are used for feature preview and bugfix testing.

I left a suggestion below. I hope I did not typo anything.

# ditto preserves the signature's extended attributes (a plain zip strips them)
ditto -c -k --keepParent --sequesterRsrc --zlibCompressionLevel 9 poptracker.app "dist/poptracker_${VS}_macos_universal.zip"
ls -l dist
- name: Store universal ZIP

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- name: Store universal ZIP
- name: Attest Build and AppBundle
if: ${{ github.event_name == 'workflow_dispatch' }}
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-path: |
poptracker.app/Contents/MacOS/poptracker
dist/*
- name: Store universal ZIP

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