the fattest binary

history

Apple is a strange, strange, beast. Rather than forcing an unprecedented amount of backwards compatibility like Microsoft does, they insist on hopping ISAs constantly - first the classic m68k Macintoshes, then the beloved PowerPC era, followed by the controversial Intel era that in retrospect never felt like anything more than a stopgap for their current ecosystem of ARM64 Apple Silicon. To make matters worse, Apple also had to undergo the 32-bit to 64-bit transition twice, first for PowerPC and then again for Intel1. Unmitigated, it would have been a disaster for a company whose whole identity is built around the veneer of user experience.

As it turns out, Jobs and his team at NeXTSTEP had foreseen this during their exile from Apple - they designed their executable format, Mach-O, to support "fat binaries" - one file that contains multiple versions of the same program, each for a different ISA.2 Alongside tools like Rosetta, these "universal binaries" as they're now called have been a decades-long cornerstone of Apple's core philosophy that its customers should never know anything about the machine they own.

So, just how universal can we get?

the experiment

I'll be using my modern MacBook to get the arm64 binaries, NeXTSTEP to get m68k binaries, and MacOS 10.5 to get the ppc, ppc64, x86, and amd64 binaries. Getting files between MacOS 10.5 and 15 is easy with SMB, but NeXTSTEP is a different problem entirely. Luckily, we can always just... trick our emulator into believing a FAT16-formatted file is a normal hard drive.3 So, let's allocate and format a 256MB "disk", and write our shitty test program:

#if defined(__x86_64__)	const char* arch_name = "darwin-amd64";#elif defined(__i386__)	const char* arch_name = "(darwin/nextstep)-i386";#elif defined(__aarch64__)	const char* arch_name = "darwin-aarch64";#elif defined(__powerpc__)	const char* arch_name = "darwin-ppc";#elif defined(__ppc64__)	const char* arch_name = "darwin-ppc64";#elif defined(__m68k__)	const char* arch_name = "NeXTSTEP-m68K";#else	#error "Unrecognized architecture type."#endif#include int main() {	printf("Hello from %s\n", arch_name);	return 0;}

After the absurdly long install time for XCode 3 and the NeXTSTEP developer tools, getting all the individual binaries is trivial - now it's time to combine them! Entertainingly, the dubiously-named lipo tool used to create fat binaries has remained nearly identical since it was first released, and still maintains that veneer of 80s CLI simplicity.4 So, without further ado, let's go run it!

Terminal output. The 'lipo' command says the binary has 6 architectures, but can only identify m68k and i386. Running the binary outputs 'Hello from NeXTSTEP-m68k!'
NeXTSTEP on m68k via Previous, entertainingly not recognizing any architectures but m68k and i386.
Terminal output. The 'lipo -info' command lists the architectures as m68k, i386, x86_64, ppc, and ppc64, and one unknown. Running the binary outputs 'Hello from darwin-ppc64!', but running it via 'arch -ppc' gives 'hello from darwin-ppc!'
MacOS 10.5 on a PowerPC G5.
Terminal output. The 'lipo -info' command lists the architectures as m68k, i386, x86_64, ppc, ppc64, and arm64 unknown. Running the binary outputs 'Hello from darwin-aarch64!', but running it via 'arch -x86_64' gives 'hello from darwin-amd64!'
Macbook Pro M3, MacOS 15 Sequoia.

With one file, we can target 4 decades worth of computers with ease. The process is fairly automatable, too, if one were to invest the time into proper cross-compilers. Yet, I somehow think we can do better!

even more architectures?

running man arch on both NeXTSTEP and macOS 15 shows support for some interesting architectures - the former claims support for HPPA (the direct predecessor to itanium), i8605, m88k, m98k (an early PowerPC codename), MIPS, SPARC, and VAX, while the latter makes a distinction between "arm64e" and "arm64" as well as between "x86_64" and "x86_64h".

With regards to NeXTSTEP, only x86, m68k, SPARC, and HPPA versions ever saw actual releases and are therefore the only architectures supported by the developer CDs. Since I don't have a SPARC or HPPA machine, I won't be able to test the binary on those systems, but there's nothing stopping us from compiling for them!

That x86_64h and arm64e are listed as their own architectures is mostly a series of technical workarounds - from what I can tell, the former exists because Rosetta can't translate AVX2 instructions presumably due to Apple Silicon only having 128-bit vector units, and the latter exists to distinguish arm64 executables compiled with certain security features such as pointer authentication. These are entirely useless "architectures" to add, but let's do it anyway for the hell of it:

Terminal output. The 'lipo' command says the binary has 10 architectures, now including hppa, sparc, arm64e, and x86_64h
10 Architectures. Incredible.

This is, somehow, more broken than before - despite the system executables being compiled with arm64e, macOS will refuse to load it from my giant binary presumably due to missing some special authentication. Despite this, it still has preference for arm64e, so adding it will result in the kernel attempting to kill the program immediately. Sweet! Furthermore, as predicted, Rosetta refuses to work with the x86_64h version at all.

This is, as far as I can tell, as far as we can go with "legitimate" toolchains. One can probably trick lipo into adding billions and billions of architectures by manually editing the CPU type to arbitrary integers, but it's rather pointless if no actual CPUs can run it.

verdict

So how feasible is this to do with a real codebase? If you're willing to use old APIs and write everything in C89, the sky's the limit! For the rest of us who actually care about any language feature created in the past 40 years, getting even a slightly more modern compiler to work on NeXTSTEP is going to be a pain as its GCC ports are a fork of pre-EGCS GCC. However, adding MacOS PowerPC support in a fork of GCC 16 is relatively feasible, and would allow 4-fat binaries containing ppc, i386, amd64, and arm64 - surprisingly practical if you know how to write portable code!

Compiling actually modern C++26 software into one file that can run on over 25 years of Apple systems would be a very satisfying achievement. Maybe I'll add it to the list of things I would absolutely love to do yet will never get around to? Who knows, maybe I'll even add NeXTSTEP!

footnotes

  1. ^ As a result of this, 64-bit PowerPC never fully materialized to users - while MacOS 10.4 introduced 64-bit support for CLI apps, 64-bit versions of system libraries like AppKit didn't arrive until several years later in MacOS 10.5, the last release to support PowerPC in any form. It's unclear how many 64-bit PowerPC apps were ever shipped, if any.
  2. ^ Fun fact - they even patented it! See US patents 5432937A and 5604905A.
  3. ^ Yes, FAT*16* - NeXTSTEP is too old for FAT32, and is rather uncooperative with emulated networking in my experience. While NeXTSTEP may have had a pivotal role in the creation of the modern internet, it seems it can't exactly partake in it.
  4. ^ lipo was, in many ways, a far more essential tool in the NeXTSTEP years than during Apple's isa transitions - from what I can tell, installing support for another architecture via the developer CD was done using it, and every system library object file was then a fat object. Ostensibly, a true multi-ISA install could have been constructed where every binary and library was fat, although I'm not sure if anyone's ever done it. Modern Apple is far more inconsistent with its uses, since all their devices share an ISA and kernel but export different libraries and APIs.
  5. ^ While it never received plans for a NeXTSTEP port, an i860 running a modified Mach kernel was the basis for the NeXTdimension graphics accelerator, hence the toolchain support.