DEV Community

Cover image for 54 of My Apps Carry Intel Code; Two of Them Run It
Mustafa ERBAY
Mustafa ERBAY

Posted on Originally published at mustafaerbay.com.tr

54 of My Apps Carry Intel Code; Two of Them Run It

"Is this app Intel?" has a ready answer in macOS: select the app in Finder, hit
Get Info, read the "Kind" line. It will say "Application (Universal)", or
"Application (Intel)", or "Application (Apple silicon)". A good enough answer
for a user. My question was a little different: how many bytes of Intel code
sit inside that bundle? The Get Info window doesn't say.

So I sat down to count. Across the 64 app bundles under /Applications I walked
601,358 files, 6,149 of which turned out to be Mach-O binaries, and I read the
header of every one of them byte by byte. Here is the tally: 54 app bundles
contain x86_64 code, 6.95 GiB of it in total. This machine is an Apple M4
Pro, so it cannot execute Intel code natively. With two exceptions.

I didn't start out to reclaim disk space, but by the end I had something more
interesting than a cleanup plan: the thing we call "the transition is done" is
not done inside the binaries. And the largest share of it isn't where I can
delete it.

Anatomy of a fat file

A universal binary is one file that holds the complete code of more than one
architecture. Apple's transition guide says so plainly, and it asks developers
to turn not only apps but "all of your compiled code" universal: plug-ins,
custom frameworks, dynamic libraries, command-line tools, daemons. That is why
2,286 of the 2,533 fat files on my disk hold both an Intel and an ARM slice.
(In that count ARM means arm64 and arm64e only; two old Xcode archives that
carry arm plus i386 plus x86_64 fall outside it.)

The format is defined in fat.h inside cctools. The file opens with the magic
number 0xcafebabe, followed by a count of slices, then one record per slice
with its architecture code, its offset in the file and its length. One note in
that header matters: these structures are always written big-endian, so if
you read them in your machine's own byte order you get nonsense.

The same header describes a second format: when a slice is larger than 4 MB
or a slice's offset within the file passes 4 MB, the 64-bit layout with
0xcafebabf takes over. My script recognizes both magic numbers. It found not a
single one of the second kind: all 2,533 fat files on this disk are
0xcafebabe.

Why that is, fat.h says in its own words — the 64-bit format is flagged as "a
work in progress and not yet fully supported in all the Apple Developer Tools".
The condition is written down and not applied. The counter-example is in this
article's own diagram:

Diagram

Those numbers come from Google Chrome's framework file. Of 519,576,720 bytes,
271,164,912 — 52.2 percent — is Intel. The second slice starts at byte
271,187,968, so both the size and the offset are sixty times the threshold, and
the layout is still 32-bit. More than half of Chrome's framework is code that
will never run on this machine.

The inspection command in Apple's documentation is this one:

% lipo -archs /System/Applications/Mail.app/Contents/MacOS/Mail
x86_64 arm64
Enter fullscreen mode Exit fullscreen mode

Running the same command on this machine today gives a slightly different
answer:

% lipo -archs /System/Applications/Mail.app/Contents/MacOS/Mail
x86_64 arm64e
Enter fullscreen mode Exit fullscreen mode

arm64e is the ARM slice with pointer authentication enabled; Apple compiles
its own system binaries that way. The doc prints arm64, the machine says
arm64e. Same mechanism, mismatched label. I can't claim the document was once
right and went stale — no record of mine shows that; all I can say is that today
the example and the machine disagree. Which is a fair warning about every sample
output you copy and paste.

The tally

I added up the slices of all 6,149 Mach-O files by architecture. The slices come
to 23.78 GiB:

Architecture Uncompressed Compressed Total
arm64 12,242.8 MiB 4,750.6 MiB 16,993.4 MiB
x86_64 5,769.6 MiB 1,349.3 MiB 7,118.9 MiB
arm64e 0.1 MiB 175.5 MiB 175.6 MiB
arm64_32 0.0 MiB 62.5 MiB 62.5 MiB
arm 0.0 MiB 2.2 MiB 2.2 MiB
i386 0.0 MiB 0.45 MiB 0.45 MiB

One row is missing from that table: a 0.9 MiB entry whose architecture number my
script did not recognize. It gets its own section below. The arm64_32 row was
in the same state — the script printed cpu33554444 and I named it by hand: the
watchOS architecture, and all 62.5 MiB of it comes from inside Xcode. I own no
watch, but I do own a watch development environment. arm is 32-bit ARM: 2.2 MiB
of an older world, still here.

The Intel share (x86_64 plus i386) is 6.95 GiB, 29.2 percent of all slice bytes.
Here is the denominator too: the whole /Applications folder is 54.29 GiB, so
Intel code is 12.8 percent of my applications. One byte in eight.

Split by main executable, the picture is this: of 64 apps, 24 are ARM-only, 38
carry both ARM and Intel, and 2 are Intel-only. Look at the whole bundle and the
number climbs to 54, because an app with a clean main binary can still ship a fat
helper library inside. Only 10 apps are entirely free of Intel code.

The eight heaviest Intel loads:

App Intel slices
Google Chrome 965.9 MiB
OneDrive 685.0 MiB
Microsoft Outlook 510.7 MiB
Microsoft Teams 499.7 MiB
Xcode 448.9 MiB
Microsoft Word 441.1 MiB
Microsoft PowerPoint 427.9 MiB
Microsoft Excel 392.4 MiB

Chrome's figure is inflated by one more thing: three separate versions of its
framework sit on disk (154.0.8037.98, 155.0.8059.85, 155.0.8059.86), each around
495 MiB. It is reasonable for a browser update not to delete the old version
immediately — you may need to roll back. Still, three versions times half-Intel
gives you pause.

First correction: what the file says is not what the disk holds

At this point it was tempting to write "6.95 GiB wasted" and move on. I'm glad I
ran one more check: a file's logical size and the blocks actually allocated to it
are different numbers. The logical total of those 6,149 Mach-O files is 23.84
GiB; the allocated blocks add up to 19.93 GiB. (The 56 MiB gap against the slice
total is fat headers plus the alignment padding between slices; fat.h notes
that padding may be stored as holes where the filesystem supports them.) So
where did the missing 3.9 GiB go?

Into filesystem compression. 2,333 files have the UF_COMPRESSED flag set, and
their 6.21 GiB of logical content occupies 2.29 GiB of blocks on disk. So the
accurate sentence is this: 5.63 GiB of the Intel slices live in uncompressed
files, byte for byte on disk; the remaining 1.32 GiB sit inside compressed files
and take up less room there. Applying the compression ratio from above to that
1.32 GiB gives roughly 0.5 GiB — a derived figure, not a counted one, and I'm
flagging it as such.

I've fallen into this trap before: asking "how much free space is there" five
different ways on the same disk produced a
13.5 GiB spread.
When someone quotes me a single number about a machine now, my reflex is to ask
what that number is counting.

Second correction: cafebabe doesn't belong to Mach-O alone

The table also had a row labelled cputype 16777239 — 0.93 MiB, apparently
irrelevant. I chased it anyway, because writing off something your tool doesn't
understand means writing off the other places where the tool is wrong.

The files turned out to be .metallib bundles inside Xcode: GPU shader
libraries. And here is the interesting part — lipo refuses to call them
Mach-O, while file does not.

% xxd -l 16 .../CoreImage.metallib
00000000: cafe babe 0000 0002 0100 0017 0000 0008

% lipo -archs .../CoreImage.metallib
lipo: not a mach-o '.../CoreImage.metallib'

% file .../CoreImage.metallib      # shortened
Mach-O universal binary with 2 architectures: [air64_v24:...] [air64:...]
Enter fullscreen mode Exit fullscreen mode

Same file, same magic number, two tools with two answers. The fat container is
not reserved for CPU code; Apple uses it for GPU code too, and air64 slices
live under the same header. My byte-level script counted a file the tool had
rejected. The error is 0.93 MiB, so the table survives — but it taught me not to
see 0xcafebabe and conclude "Mach-O" on the next pass. That same magic number
is also the signature of Java class files; OpenJDK's class file parser
identifies a file by those exact four bytes. I found no .class file in these
bundles, so that wasn't what polluted my table this time. It could have been.

Apple's own share, and a surprise of scale

Everything so far might read like a choice third-party developers made. So I ran
the same count under /System/Applications, across Apple's own 46 apps. 11,221
files, 201 binaries, 0.63 GiB of slices:

  • arm64e: 330.2 MiB (51.1 percent)
  • x86_64: 315.4 MiB (48.9 percent)

45 of the 46 apps carry Intel code. Mail, Notes, Music, Preview, Terminal — all
of them x86_64 arm64e. The heaviest are Books (46.3 MiB of Intel), Freeform
(45.5 MiB) and Music (36.7 MiB). Which also means the 54 in the title is scoped
to /Applications: the number of apps on this machine carrying Intel code is
really 99.

But calling 315 MiB "Apple's share" got the scale badly wrong, because system
apps are thin shells: the code mass lives in the shared cache. So I looked
there:

File set Logical Blocks
dyld_shared_cache_x86_64* (9 files) 5.35 GiB 2.08 GiB
dyld_shared_cache_arm64e* (15 files) 5.42 GiB 1.91 GiB
aot_shared_cache.0..6 5.73 GiB 2.17 GiB

That directory shows up under two paths, but stat reports the same device and
inode for both: one copy, two doors. And the root volume is sealed —
diskutil apfs list says Sealed: Yes, and mount reports /dev/disk3s1s1 on
/ (apfs, sealed, local, read-only, journaled)
.

Add it up and the picture inverts. Intel code I could delete: 6.95 GiB. Intel
code I cannot: 315.4 MiB plus 5.35 GiB, or 5.66 GiB logical, around 2.4 GiB
in blocks. The two are nearly the same weight. I came into this with a thesis
about third-party developers bloating my disk; the count talked me out of it.
This isn't one developer being careless, it's the platform's distribution model.

Can it be removed? An experiment

Curiosity turned practical: what happens to a bundle's signature if I thin it?
Apple's ditto has a documented flag for exactly this, and the man page puts it
in one line: "Thin Universal binaries to the specified architecture."

I ran the experiment on a small notarized app (Shottr, 6,780 KiB):

% ditto --arch arm64 /Applications/Shottr.app ./Shottr-thin.app
% du -sk ./Shottr-thin.app
3852    ./Shottr-thin.app

% lipo -archs ./Shottr-thin.app/Contents/MacOS/Shottr
arm64

% codesign -v ./Shottr-thin.app ; echo $?
0

% spctl -a -vv ./Shottr-thin.app
./Shottr-thin.app: accepted
source=Notarized Developer ID
Enter fullscreen mode Exit fullscreen mode

The bundle went from 6,780 KiB to 3,852 KiB, 43.2 percent smaller. The signature
verified, Gatekeeper accepted it, notarization held. So each architecture slice
carries its own signature, and dropping one does not invalidate the other.

Let me state the limits of that experiment, because it is a narrow one: I never
launched the thinned copy, I ran codesign without --deep --strict, and I
never tried a large bundle with frameworks of its own inside. What it proves is
that one notarized bundle's signature chain survived thinning — not that this
works everywhere.

Beyond that, I'm not putting it forward as a way to reclaim disk space, and I
haven't kept it in place. The first update brings the universal bundle right
back, so the space you won is rented. What an app's own updater does with a
thinned bundle is a separate question mark — a tool that ships delta updates may
not recognize it. And the thinnable part isn't actually 6.95 GiB: 6,673.1 MiB of
those Intel bytes live in fat files, but 446.2 MiB sit in 533 single-architecture
files, where dropping the code means dropping the app. The two biggest of those
are the two Intel-only apps themselves.

In short, on a 460 GiB disk with 125 GiB free, the prize doesn't earn a weekend.
Counting was instructive; cleaning wouldn't have been.

The date that actually matters

I said "with two exceptions" at the top. Those two — an old build of a media
player (VLC 3.0.23) and a serial-port tool — carry Intel code only, so when they
launch they run inside the Rosetta translation environment.

Let me be honest here: I never watched a running process. My evidence is a
property of the binaries — these two bundles have no ARM slice, so they cannot
run without translation. And "two" isn't a property of the machine, only the
count under the defaults: per Apple's documentation a user can force any
universal app to open under Rosetta from the Get Info window, and a developer can
reorder the preference with the LSArchitecturePriority key. The Intel slice in
those 38 universal apps isn't dead; it's asleep.

Rosetta is installed (/Library/Apple/usr/libexec/oah is in place) and pkgutil
dates the package's install time to 1 September 2026 — an OS update can refresh
that stamp too, so I'm not reading it as "the day I installed it by hand".

How the translation actually works is in Apple's platform security
documentation, and reading it made me correct my first draft. There are two
paths. At runtime, the kernel spots an x86_64 Mach object and hands control to a
Rosetta translation stub rather than to the dynamic linker, and pages are
translated inside the process. On the ahead-of-time path, every Mach object goes
through the AOT pipeline while macOS is being built — but what lands in the trust
cache is not translated code, it is a hash: three code directory hashes per
object (arm64, x86_64, and the AOT translation of x86_64). Apple states outright
that "the actual translated products don't ship with the operating system";
translation is deterministic, so the products are reconstituted on demand, signed
with a device-specific key managed by the Secure Enclave and kept in a vault only
the Rosetta service can reach.

The table above has an aot_shared_cache row: 5.73 GiB logical, 2.17 GiB in
blocks, with file dates matching the OS build. From the outside I can't tell
whether it arrived with the installer or was produced during installation; what I
can tell is that it is on my disk right now.

And this is where the calendar arrives. Apple announced on 1 September 2026:

macOS 27: Final release to support Rosetta — Intel-only apps will no longer
run on Mac computers with Apple silicon after this update.

The support page says the same thing in user language: Rosetta is available on
macOS 27 and earlier; from the next major release it remains only for certain
older, unmaintained games that rely on Intel-based frameworks. Note the timing:
macOS 27 Golden Gate shipped on 14 September 2026 and supports Apple silicon Macs
only. So the last release to carry Rosetta is already out; this machine is on
26.6.2, one major release behind. My two Intel-only apps don't have a distant
expiry date — it is one release away.

What strikes me is that the two numbers pull in opposite directions. The code
under calendar pressure amounts to 2 apps. The code taking up disk space
lives inside 54 apps and weighs 6.95 GiB. The second never runs, so Rosetta's
retirement won't touch it; it will simply stay there. The troublesome part is
small, and the heavy part travels quietly.

A natural question follows, and I don't know the answer: when Rosetta goes, do
the x86_64 slices go too? Will macOS 28 drop the x86_64 shared cache from the
sealed volume, will developers stop building universal, or will this 6.95 GiB be
dragged along for years? All Apple has said so far is that the translation ends;
nothing has been promised about what the binaries contain. The carve-out for
games even suggests some x86_64 plumbing stays in place.

If you want to count your own machine

The Get Info window is a fine start, but it says nothing about the helper
binaries inside a bundle. If you plan to go deeper, mind four things:

  1. Don't stop at the main binary. The file under Contents/MacOS/ may be clean; the weight sits in frameworks, plug-ins and helper processes. In my count the gap was between 38 and 54.
  2. Separate logical size from block count. stat hands you both numbers; if you don't know which one you're reporting, a 16 percent error follows you around.
  3. Don't treat every 0xcafebabe file as Mach-O. Compare lipo -archs against file; when one refuses, look at what the other says.
  4. Don't stop at the applications folder. The real mass is in the shared cache and that volume is sealed; a number without its scope points at the wrong culprit.

Finally, ask this: is anything actually running this code? For me the answer was
two. What sits in the other fifty-four bundles isn't load, it's luggage.

What's left

On the user's side the Apple silicon transition started in 2020 and looks long
finished; apps open, they run fast, nobody asks about architecture. Look inside
the binaries, though, and the transition hasn't ended, it has only gone
invisible. Carrying two architectures in one file is a clever compatibility
answer; the bill for that answer is split across every user's disk, and nobody
itemizes it.

The thing that stayed with me isn't the 6.95 GiB. It's how many times I reached
that number from the wrong place: the first count gave what the file says, the
second what the disk holds, the third what the tool got wrong, and the fourth
showed the real mass was never where I was looking. Someone quoting a single
number about their own machine usually doesn't know which question that number
answers. When I counted
the root certificate store
I ended up in the same place: once you start measuring, the work stops being
about finding the number and becomes about finding out what the number counts.

Official Sources

Top comments (0)