...
Back

Porting to Hardware You Don't Own

Adding a platform when you have a simulator and no device. What that actually verifies, what it can't, and how to be honest about the difference in your release notes.

Porting to Hardware You Don't Own

Porting to Hardware You Don't Own 📺

Adding support for a new platform in an established ecosystem is a strange kind of project. The APIs are familiar. The language is the same. The build system nominally already knows how. And yet the work is neither trivial nor, when you finish, actually finished — because a simulator is not a device, and you cannot buy your way past that with effort.

Here is what we learned porting a networking client to platforms we could build for but not fully test on.


The native core is the whole problem

If your app is pure application code, a new platform slice is mostly a build-settings exercise. If it contains a native core — a library compiled from another language, vendored as a binary framework — the port is gated entirely on whether that core can be built for the new platform's architecture.

Which means the real question is not "does my app support this platform" but "does my toolchain emit a slice for it." Three things we hit, in the order they blocked us:

The cross-compiler may not know the platform exists. The binding generator we use had targets for the mainstream platforms and nothing for the newer one. The fix was to teach it — a patch adding the target triple, the SDK name, and the platform's minimum version. Unglamorous, mechanical, and entirely blocking until done.

Simulator SDKs have their own names. Every platform has a device SDK and a simulator SDK, and the simulator name is rarely the obvious one. Getting it wrong yields an error about an unknown SDK rather than an unsupported platform, which sends you looking in the wrong place.

Binary frameworks bundle metadata that silently overwrites. When you assemble a multi-platform framework, per-slice metadata files can collide, and the result is a bundle that builds and links while describing itself incorrectly. Nothing errors. You find out later, in a place unrelated to the cause.

The lesson we would carry: audit which slices actually exist in the artifact, rather than trusting that a successful build produced them. Listing the architectures in the binary takes one command and has caught a missing slice for us more than once.


What a simulator honestly verifies

It is worth being precise, because the temptation is to treat "runs in simulator" as "works."

A simulator reliably verifies: your code compiles for the architecture, the app launches, the view hierarchy lays out, navigation works, state management behaves, and your business logic runs. For a large fraction of apps that is genuinely most of the risk.

A simulator does not verify anything that touches the hardware or the platform's real trust boundaries: the network stack under a real radio, entitlement enforcement, background execution and suspension, thermal and memory pressure, real input devices, and — critically for us — anything gated on a capability the simulator stubs out rather than implements.

That last category is where the nastiest surprises live, because the stub succeeds. Code that would be rejected on a device returns success in a simulator, so the failure is not merely unverified — it is actively masked.


Shipping honestly

You can absolutely build and even release a platform port you have not tested on real hardware. What you cannot do is describe it the same way as a platform you have.

Three practices that make this survivable:

  1. Say which platforms are device-verified. Internally, in the release checklist, and where relevant to users. "Builds for X" and "tested on X" are different claims and conflating them is how a platform ships broken for a year because everyone assumed someone else had a device.
  2. Keep a list of what only a device can answer. Written at port time, when you know exactly which code paths the simulator stubbed. Six months later nobody remembers, and the list becomes the test plan the moment hardware appears.
  3. Make the platform slice cheap to keep alive. The port rots if it only builds when someone remembers. Put every slice in CI so a change that breaks the untested platform fails immediately rather than silently — build coverage is not test coverage, but it is the thing that keeps the port from decaying into fiction.

Whether to do it at all

The honest calculus: a platform port you cannot test is worth doing when the incremental cost is low and the slice keeps building in CI, because the alternative — waiting until you have hardware — usually means never starting, and the compiler and toolchain problems are the long pole anyway. Solving those early means that when a device does arrive, you are debugging behavior rather than build systems.

It is not worth doing if you intend to advertise it as supported. That is not a technical judgment. It is just the difference between a port and a promise.