Fine, Apple. You Win

Earlier today I wrote a long piece about the deploy problem nobody talks about — the invisible wall of quarantine flags, Gatekeeper ceremony, and App Translocation that stands between an independent developer’s signed, notarised .zip and the user who actually wants to run it. It was, in fairness, a bit of a rant. It ended on what I thought was a dignified note of principled resignation: ”it works, and ‘it works’ is sometimes the best you can do when the platform isn’t on your side.”

This is the follow-up. I have news. I have climbed down off my high horse. I have, as they say, seen the light.

Every Jorvik app now ships a .pkg installer.

Wait, didn’t you just say .dmg would help?

I did. But it doesn’t. I know half the people reading this were about to ask ”why not a .dmg?”, so let’s get it out of the way.

Browsers stamp com.apple.quarantine on every file they download, full stop. The container doesn’t matter. A quarantined .dmg, mounted in Finder, produces a quarantined .app when you drag the icon to /Applications. First launch triggers App Translocation exactly as it would for a .zip. The only thing .dmg offers over .zip is a slightly nicer drag-to-Applications animation. Which, I will concede, is aesthetically pleasing. But it’s decorative. It solves nothing.

.pkg is different. When you double-click a .pkg, macOS doesn’t hand it to the user to extract. It hands it to Installer.app — a privileged system process. Installer runs the show: it verifies the signature, reads the payload, and places the bundle in /Applications on your behalf. The installed .app doesn’t inherit the .pkg‘s quarantine flag, because the Installer isn’t a user — it’s macOS itself. No quarantine means no Gatekeeper dialog, no Translocation, no first-launch weirdness. The app runs from /Applications like it was always meant to.

This is — and I cannot stress this enough — exactly the experience Apple wants independent developers to deliver. A signed, notarised package, handled by a privileged installer, placed in the user’s Applications folder by the operating system itself. It is the well-trodden path. It is the path I spent three months studiously avoiding because I found it vaguely insulting that I had to jump through additional hoops just to match the experience of apps downloaded from the Mac App Store.

Well, reader. I jumped.

Why now

Two things shifted.

The first was user feedback. Not angry feedback — Jorvik users are, by and large, patient and technical — but a steady trickle of “I had to right-click to open it?” emails. Every single one of those is a friction point. Every friction point is someone who might not bother next time. And I build these apps because I want people to use them, not because I want to prove a rhetorical point about platform sovereignty.

The second was realising that dual-shipping is essentially free. The .pkg is an addition, not a replacement. Every Jorvik release now carries both a .zip (primary, for anyone who prefers to drag-install, and critically for the in-app auto-updater which only knows how to consume .zip) and a .pkg (secondary, for first-time installers who just want the thing to work). Existing users don’t notice anything different. New users get the friction-free path. Nobody loses.

The Release Manager pipeline already knew how to build .pkg installers — ASCII Saver has shipped as one since its first release, because its helper agent needs system-level placement. All I had to do was teach the single-.app case: one pkgbuild invocation, one notarytool submit, one stapler staple, and then attach the result to the GitHub release alongside the .zip.

It took an afternoon to update the entire collection of apps.

The part where I look slightly foolish

So here’s the bit I want to share, because it’s both instructive and — in retrospect — quite funny.

SpaceMan was the first Jorvik app to receive a “don’t open two About windows at once” fix, back in its Phase 2 development. The fix lives in SpaceMan’s own embedded copy of JorvikKit, my shared-components library. It has been sitting there, in SpaceMan’s source tree, since the initial build. When I released SpaceMan v1.0.0, the released binary should have had that fix baked in.

It didn’t.

A user (me, about ten minutes after installing) clicked the About menu item twice. Two windows appeared, stacked. “Huh,” I thought. I checked the source. The source had the fix. I checked the git log. The fix had been committed weeks before the release. I checked the .app binary itself. The fix was nowhere to be found.

The culprit, it turned out, was one line in the Release Manager’s build pipeline. When building a swiftc-based app, it was pulling JorvikKit files from sourcePath.deletingLastPathComponent + "/JorvikKit" — which resolves to the sibling directory of the app’s source, not the embedded copy inside the app. So instead of compiling SpaceMan’s own patched JorvikKit, it was compiling the canonical reference copy that sits alongside in my Jorvik Software/ folder — a copy which, at the exact moment of SpaceMan v1.0.0’s release, had not yet received the fix.

The app shipped against the wrong version of its own shared library. Silently. Successfully. Signed, notarised, stapled, and completely, subtly wrong.

This is the kind of bug you only find when someone reports a symptom that shouldn’t exist given what the source obviously does. It took me a good ten minutes of disbelief before I thought to diff the binary strings against the tagged source. Fixed now — one character change, deletingLastPathComponent removed — and SpaceMan re-released as v1.0.1 with its own fix actually present this time. You can read the gory detail in the commit message.

Anyway. I mention this because it’s a nice demonstration of why dogfooding matters and why releases are not the time to assume the build pipeline knows what it’s doing. Also because it’s funny. Also because this post is getting a bit earnest and we needed a breather.

The philosophical bit

Here’s the thing about capitulation. It’s not always bad.

My previous post was correct on the facts. The playing field is not level. Apple’s distribution channel delivers a frictionless install experience that notarisation-via-.zip-download cannot match. Independent developers who refuse to use the App Store are taxed, in user-experience terms, for that refusal. All of that is still true.

But the .pkg route turns out to be an available middle ground. It doesn’t require me to give Apple 30% of nothing, submit to their review process, or accept their restrictions on what my app can do. It just requires me to package my app the way the operating system expects to receive it. Installer.app will happily run signed, notarised .pkg files from any developer with a Developer ID — no App Store needed, no gatekeeping beyond the notarisation check I was already doing. The only friction is that I had to learn one more command-line tool (pkgbuild) and add one more stage to the pipeline.

So I concede the principle in part: Apple’s trust-by-distribution-channel model isn’t entirely unreasonable when the distribution channel is just “the system installer.” The asymmetry I complained about was real, but most of the remaining gap closes the moment you meet macOS halfway. I should have done this three months ago. I was being stubborn.

Fine, Apple. You win this round.

What this means for you

If you’ve been using Jorvik apps and occasionally grumbling about the right-click-Open dance: that dance is over. The product pages have been updated. Every release from today onwards will have an Installer button prominently displayed next to the traditional Download (.zip) link. Click the Installer. Run the .pkg. The app will appear in your Applications folder without ceremony. No quarantine warnings. No Translocation shenanigans. It just works.

The .zip is still there for anyone who prefers it — scripted installs, people who genuinely enjoy drag-to-Applications, the in-app auto-updater. Nothing you had before has gone away.

It’s a small change. It should have been there from the start. I was wrong about this particular hill, and I’m happy to walk down it.

Anyway. Back to building things.