ClassiCube icon indicating copy to clipboard operation
ClassiCube copied to clipboard

Hacky implementation of Universal Builds for macOS

Open iblowmymind opened this issue 4 months ago • 9 comments

Very WIP PR for implementing a Universal Executable build. I don't know my way around this makefile that well so this will create an application directory for each architecture before removing it and merging them with lipo and creating another directory AGAIN. (I couldn't figure out how to make it stop doing that, sorry) I'm open to suggestions and improvements! I've achieved this by defining two executable-only build types (darwin_i and darwin_a, for Intel and Apple silicon respectively) and a darwin_u build type that will glue the two executable files using the lipo utility provided in the macOS SDK, creating a single Universal binary, and create the application directories for it. I've also made the renamed directories for darwin_i and darwin_a to be build/macos_i and build/macos_a, but I don't think clean can pick those up at the moment and I haven't gotten that to work either. Again, open to suggestions.

iblowmymind avatar Aug 20 '25 07:08 iblowmymind

TODO:

  • [ ] Add GitHub action for universal (and also a seperate one for just Apple silicon, maybe?) builds
  • [ ] Fix make clean not removing macos_i and macos_a directories
  • [ ] Tidy up the build process by removing unnecessary bundle creations

iblowmymind avatar Aug 20 '25 08:08 iblowmymind

The 64-bit macOS github workflow does generate a separate arm64 executable. I haven't tried combining them into a universal build, because if I recall, there are additional restrictions on running a native arm64 executable compared to running an x86_64 executable under Rosetta. I don't have an apple silicon device for testing though.

The 32-bit macOS github workflow does generate a univeral i386 and powerpc build.

If I recall, you can possibly have clang compile for multiple architectures at once by e.g. doing clang -arch x86_64 -arch arm64

UnknownShadow200 avatar Aug 20 '25 08:08 UnknownShadow200

if I recall, there are additional restrictions on running a native arm64 executable compared to running an x86_64 executable under Rosetta

How so? Everything seems to be working fine for me, I could go into the game on Singleplayer, save/load etc. If there's anything specific you want me to test, please let me know.

iblowmymind avatar Aug 20 '25 08:08 iblowmymind

If I recall, you can possibly have clang compile for multiple architectures at once by e.g. doing clang -arch x86_64 -arch arm64

Doesn't seem to be doing it for me. The official Apple documentation for creating universal macOS executables documents it the way I've done it here too; build seperately for arm64 and x86_64 and use lipo to merge the binaries together into one Universal executable.

iblowmymind avatar Aug 20 '25 08:08 iblowmymind

How so? Everything seems to be working fine for me, I could go into the game on Singleplayer, save/load etc. If there's anything specific you want me to test, please let me know.

While x86_64 binaries do not require code signing, arm64 binaries do, although adhoc code signing is acceptable. What I didn't realise though, was that apparently by default, arm64 binaries are automatically adhoc code signed. (At least from what comments in https://news.ycombinator.com/item?id=26996578 suggest)

If the mac-arm64 binaries from say https://github.com/ClassiCube/ClassiCube/actions/runs/17093050303 work though, then that's a good sign. My big reluctance when it comes to using a universal binary though is that it will break all current plugins on the macOS build.

Doesn't seem to be doing it for me. The official Apple documentation for creating universal macOS executables documents it the way I've done it here too; build seperately for arm64 and x86_64 and use lipo to merge the binaries together into one Universal executable.

Something like clang -arch arm64 -arch x86_64 src/*.c src/*.m third_party/bearssl/src*.c -framework OpenGL -framework Cocoa -framework Security -framework IOKit -o CC-Universal might work. I haven't been able to try on my own mac though, as the latest macOS SDK it can use does not support arm64, and the i386 arch fails when linking due to the required 32 bit libraries having been removed from the SDK.

UnknownShadow200 avatar Aug 20 '25 11:08 UnknownShadow200

What I didn't realise though, was that apparently by default, arm64 binaries are automatically adhoc code signed.

They indeed are. They're signed with a type of ad-hoc signature called "linker signed" (referenced here, under "Linker (Ad-hoc)") that macOS automatically signs to fulfill that requirement. This is different than a codesign ad-hoc signature, however in practice it makes no difference. This seems to work on my end (assuming that the com.apple.quarantine flag isn't present on the executable, in which case the user has to remove it so we should put some sort of message regarding that in our distribution method)

My big reluctance when it comes to using a universal binary though is that it will break all current plugins on the macOS build.

Plugins should be able to compiled as universal dylibs no problem as well. If plugins that aren't also upgraded to build as universal binaries are needed, the user can simply switch to running ClassiCube through Rosetta. I've tested this and confirmed it works with the SchematicExport plugin:

Before enabling "Open using Rosetta" from the Info menu (running the game bundle as arm64, the dylib fails to load due to the architecture mismatch but otherwise the game runs fine): image

The "Open using Rosetta" tick in question (available when you bring up the information menu for the ClassiCube bundle, unsure if it works with the standalone executable): image

After enabling "Open using Rosetta" from the Info menu and running the game (plugin loads and works!): image

iblowmymind avatar Aug 20 '25 18:08 iblowmymind

I haven't been able to try on my own mac though, as the latest macOS SDK it can use does not support arm64, and the i386 arch fails when linking due to the required 32 bit libraries having been removed from the SDK.

Install OpenCore Legacy Patcher to get latest macOS running.

Eggmanplant avatar Aug 21 '25 06:08 Eggmanplant

Plugins should be able to compiled as universal dylibs no problem as well. If plugins that aren't also upgraded to build as universal binaries are needed, the user can simply switch to running ClassiCube through Rosetta. I've tested this and confirmed it works with the SchematicExport plugin:

Before enabling "Open using Rosetta" from the Info menu (running the game bundle as arm64, the dylib fails to load due to the architecture mismatch but otherwise the game runs fine): The "Open using Rosetta" tick in question (available when you bring up the information menu for the ClassiCube bundle, unsure if it works with the standalone executable):

When it comes to plugins though

  1. None of the ClassiCube plugins macOS files are compiled as universal dylibs (I need to spend time setting up proper CI for them)
  2. I don't have control over how third-party plugins are compiled

While in theory it's simple enough to switch to a universal build with users being able to 'open using rosetta' if necessary, in practice what the average user would experience is that they update ClassiCube and then suddenly all their plugins fail to load with an unclear error message. Which does make me a bit reluctant there. (Maybe there's some possibility of having it so that default download is a universal build, while updating in intel 64 mode updates to an intel 64 bit only binary - at least that way it wouldn't suddenly disrupt existing users)

Install OpenCore Legacy Patcher to get latest macOS running.

I've only got one mac device, and I would prefer not to perform anything drastic with it.

UnknownShadow200 avatar Aug 21 '25 13:08 UnknownShadow200

what the average user would experience is that they update ClassiCube and then suddenly all their plugins fail to load with an unclear error message.

Then perhaps we could add a check when a dlopen fails, getting the current game architecture and the architecture of the plugin we're trying to load (in this case, Apple silicon and Intel respectively) and print out a special error message telling the user to open the game using Rosetta (maybe even provide a link to an Apple support article like this to guide users on how to do that) or contact the developer of the plugin for a newer version.

It's also probably a good idea, though, to update existing users to Intel-only binaries and maybe display some sort of banner in the launcher telling Apple silicon Mac owners that a native Apple Silicon build is available if they choose to upgrade to that. (Or do a check if Intel-only plugins exist in the plugins directory and if not upgrade to a Universal build??? All of these options require at least some time and effort, though, so entirely up to you, I'm just brainstorming!)

iblowmymind avatar Aug 21 '25 19:08 iblowmymind