# macOS

## What it is

macOS is the operating system Apple ships on every Mac: the desktop and the
Finder, the bundled apps, and underneath them a Unix system with a terminal, a
shell and the familiar command-line tools. Apple builds it for its own hardware
and sells the two together, so in practice choosing macOS means choosing a Mac.

The core is Darwin, whose source Apple publishes. Its kernel, XNU, is a hybrid
that joins the Mach kernel from Carnegie Mellon University with code taken from
FreeBSD and with IOKit, a C++ framework for drivers; the name stands for “X is
Not Unix”. Much of the userland comes from FreeBSD too, and the manual pages for
`sed` and `date` on a Mac describe FreeBSD's versions of those tools. Since
macOS 10.15 Catalina the default shell has been zsh.

The hardware changed under it recently. In late 2020 Apple began moving the Mac
from Intel processors to its own chips, Apple silicon, and every Mac line from
the MacBook Air to the Mac Pro now has Apple silicon models. Those chips are
arm64, which matters the moment software built on a Mac has to run on an x86-64
server.

On laptops and desktops macOS is weighed against Windows and Linux.

## Why it matters

For a company that writes software, macOS plays two parts that are easy to blur:
it is the machine many developers work on, and it is a platform customers run
software on.

As a workstation, a Mac differs from the Linux servers it deploys to in three
places. Its command-line tools come from FreeBSD rather than GNU, so the same
flag can mean different things in a script. Its processor is arm64 where the
server may be x86-64. And it has no Linux kernel, while containers share the
kernel of their host, so container tools on a Mac start a Linux virtual machine
first; Apple's own `container` tool, which needs Apple silicon and macOS 26,
gives each container a lightweight virtual machine of its own. None of this
rules a Mac out; it means testing on the system you deploy to rather than
trusting a green run on a laptop.

As a platform, macOS raises the question of how to build for it. A
cross-platform shell, one codebase with an embedded browser engine shipped for
Windows, Linux and the Mac, keeps several systems in step cheaply, but on the
Mac it carries a browser runtime a native application has no use for and draws
its own controls instead of the system's. A native application in Swift and
SwiftUI uses the system's controls and materials, and follows the user's
settings for appearance, accent colour, contrast, text size and reduced motion
without extra code. The price is a separate client and people who know the
platform. Native pays off when Mac users are the customers.

Shipping to Macs also passes through a gate. Software distributed outside the
Mac App Store is signed with a Developer ID certificate and sent to Apple's
notary service, an automated scan for malicious content and code-signing
problems that is separate from App Review. Since macOS 10.15, all software built
after 1 June 2019 and distributed with Developer ID must be notarised. Apple
returns a ticket to staple to the software and publishes the same ticket online,
where Gatekeeper looks for it when the user first opens the app. The certificate
comes with the Apple Developer Program, at $99 a year. Plan the membership and
the signing step into the first release.

## What it looks like in practice

The desktop experience of one messaging product on macOS was a cross-platform
shell that neither looked nor behaved like a Mac application. Its well-tested
Rust core exposes a C interface, and Swift can call C, so
[a native client in Swift and SwiftUI](/portfolio/built-a-native-macos-messaging-client-in-swift-6-154/)
was built directly on that interface. The core is linked in as a static archive
from a pinned upstream revision, which keeps the build reproducible, and a thin
wrapper owns every pointer that crosses between the languages. The client starts
as a Mac application, uses the platform's own materials and controls, and
carries no embedded browser. The page is frank about the gap: there is no
notarised distribution and no update channel, so it is a working application
rather than a product other people run. The notary step above is what stands
between the two.

The platform's look can be enforced rather than imitated.
[A design system of 48 named tokens](/portfolio/built-a-48-token-design-system-160/)
in six groups builds colour and material on the platform's semantic colours and
glass material instead of fixed values, so the interface follows the system
appearance, the accent colour and the contrast settings with no code watching
for them, and its text styles scale with the user's size preference. A linter
rejects a raw number where a token belongs, a hardcoded font size, and an
animation that ignores the reduce-motion preference. The page names its limit
too: tokens make an interface consistent, which is different from making it
good.

Compressed memory and swap let a macOS app grow far past the memory of the
machine it runs on, and
[macOS application work](/portfolio/stopped-an-application-filling-memory-at-41mb-a-second-155/)
measured how far. Growing by roughly 41 MB a second, one app reached 111 GB of
compressed pages against 36 GB of physical memory, and by the time the kill
arrived the rest of the desktop had stopped responding. Three causes multiplied
each other: an unbounded event stream, every subscriber receiving every event,
and logging with no budget. Bounded buffers, subscription by event type and a
rate budget on logging fixed all three, and a regression test holds the memory
ceiling. The session that wrote 610,996 log lines now writes 1,411, a trade
made knowingly: when something goes wrong fast, its log is deliberately
incomplete.

## Where it goes wrong

**Activity Monitor shows almost no memory to spare on a Mac that runs perfectly
well.** macOS caches recently used files in otherwise unused memory and, as
memory fills, compresses inactive apps to make room for active ones, so Memory
Used close to Physical Memory and a large Compressed figure are normal. Apple's
own guidance is that free memory does not necessarily make a Mac faster. Read
the Memory Pressure graph instead: green means the RAM is used efficiently,
yellow that the Mac might eventually need more, red that it needs more. Swap is
the plainest signal:

```sh
sysctl vm.swapusage
vm_stat | grep -E '^Swap(ins|outs)'
```

The `vm_stat` counters run from boot, so compare two readings a minute apart;
swapouts that keep climbing mean the work has outgrown the RAM.

**A script that works on the Mac stops with an odd error on the Linux server, or
the other way round.** The FreeBSD-derived `sed` on a Mac treats the word after
`-i` as a backup suffix, so the GNU habit `sed -i 's/a/b/' file` takes the
expression as the suffix and the file name as the script. The Mac habit
`sed -i '' …` makes GNU `sed` read the expression as a file name. A suffix
attached to the flag works on both:

```sh
sed -i.bak 's/^port=80$/port=8080/' app.conf && rm app.conf.bak
```

The lasting fix is to run scripts in CI on the system they will meet in
production.

**A build shared outside the team is blocked by Gatekeeper when the recipient
opens it.** The app was signed for development, or signed but never notarised.
Sign it with a Developer ID certificate, with the hardened runtime enabled and a
secure timestamp, send it to the notary service, and staple the ticket so
Gatekeeper finds it even offline. A ZIP archive can be notarised but cannot hold
a ticket, so staple the app inside it:

```sh
ditto -c -k --keepParent App.app App.zip
xcrun notarytool submit App.zip --keychain-profile notary --wait
xcrun stapler staple App.app
```

**A container image runs on the developer's Mac and dies on the server with
`exec format error`.** Docker builds for the platform of the BuildKit daemon by
default, and on an Apple silicon Mac that daemon runs in an arm64 virtual
machine. A container shares the host's kernel, so without emulation an x86-64
host runs x86-64 code only. Check the server with `uname -m`, then build for it
with `docker build --platform linux/amd64`, or build in CI on a machine with the
server's CPU type.

## Further reading

- [Mac User Guide](https://support.apple.com/guide/mac-help/welcome/mac)
  — Apple's own guide to macOS, kept for each current release.
- [Check if your Mac needs more RAM](https://support.apple.com/guide/activity-monitor/check-if-your-mac-needs-more-ram-actmntr34865/mac)
  — the Memory Pressure graph and what its three colours mean.
- [Notarizing macOS software before distribution](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution)
  — Developer ID, the hardened runtime and how Gatekeeper uses the ticket.
- [Multi-platform builds](https://docs.docker.com/build/building/multi-platform/)
  — why arm64 and amd64 hosts need different images, and how one build makes
  both.
- [container](https://github.com/apple/container)
  — Apple's tool for running Linux containers on a Mac, each in a lightweight
  virtual machine.
- [XNU](https://github.com/apple-oss-distributions/xnu)
  — the macOS kernel's source, with a short account of how Mach, FreeBSD and
  IOKit fit together.

<https://engineer.company/glossary/macos/>
