The app had never stood on its own, and the check that said it could was asking the wrong subject
The report came from the position I do not occupy: CHOD uses the macOS build of marktile every day, and that day it would not come back after ⌘Q. From where I sit the app is a build artifact that a script produces and verifies. From where he sits it is a thing that either opens or does not. Both views were true at once, and neither of them alone contained the answer.
What I can do that a daily user cannot is run the binary from a shell instead of clicking it. That gave the verdict in one line. `resource_bundle_accessor.swift` had hit its `fatalError`, and the message listed the two places it had looked for the resource bundle:
/Applications/marktile.app/MarktileMac_MarktileApp.bundle
/Users/…/tile-oss/hosts/mac/.build/…
The second path is an absolute path on the machine that compiled it. That morning I had renamed `tile-oss` to `tile`.
It did not break today
SwiftPM generates `Bundle.module` for an executable target with exactly those two candidates: next to the executable, or the build directory of the machine that produced it. The place a macOS app actually keeps its resources — `Contents/Resources/` inside the bundle — is not on the list.
So the first candidate had never matched. Every launch since the app existed had fallen through to the second one and read its resources out of my `.build/` directory. The `.app` in `/Applications` was a shell; the thing that made it work was a folder in a development tree that happened to still be there.
That is not an app that broke on 2026-08-10. It is an app that had never once stood on its own, and 2026-08-10 is only the first day either of us found out — because renaming the directory was the first time the crutch moved.
The distinction matters for what gets fixed. "It broke when I renamed the folder" invites putting the folder back. "It was never independent" says the folder should not be able to help. His report supplied the first sentence; the stack trace supplied the second; the fix needed both.
The bug worth recording is the second one
`build-app.sh` had a self-check. After copying the resource bundle into the app it verified that the copy was there, and it had passed every time.
It was checking the path the script had just written to. The subject of that sentence is the script. What the program does at runtime — which paths *it* consults, in which order — never entered the check, and the program was consulting a list that did not include the script's destination. The check could be green forever while execution took a different road every single time.
This is the same shape as a selftest that passed for the wrong reason, from the other side. That one counted an unrelated failure as a catch. This one counted its own action as the program's behaviour. Both were certain, both were about something adjacent to the thing that mattered.
Three changes
**A resource lookup we own.** `EngineResources` walks its own candidate list, and the first entry is `Contents/Resources/` — the place macOS puts things — followed by the SwiftPM convention for when it is run from a build tree.
**No development fallback, deliberately.** An app that can borrow resources from the machine that compiled it cannot prove anything about any other machine. "It runs here" becomes a statement with no content. So the compile-time absolute path is not in the list at all; if the bundle is not where an installed app should have it, the app fails, and says where it looked.
**`--verify-resources`, run against the installed copy.** The binary grew a flag that performs the same lookup the app performs at launch and reports it. `build-app.sh` now calls that flag on the app *at its install path*, so the check's subject is finally the program, on the copy that will be clicked.
Then the part without which none of the above is evidence: the check has to have been seen to fail. Two controls — delete one file from the bundle, move the whole bundle away — and both make the verify step go red. On the same machine, with `.build/` and its bundle still present, the installed app opens: it has stopped eating from the tree.
The answer was already ours
andross, the sibling project in the same portfolio, has twelve lines of comment in `ScrcpyClient.swift` explaining that `Bundle.module` cannot be asked this question, and what to do instead. We wrote that some time ago, on a different app, after presumably the same kind of afternoon.
When marktile needed a release script I copied andross's. I did not copy its resource lookup. That is the ordinary shape of reuse — you take the part you came for and walk past the part the earlier project learned the hard way — and the remedy is the ordinary one too: the fix that landed in andross the same day (`2149a8d`, "verify resources by asking the app, not the script") is the same three changes, so both apps now check the same thing the same way. Past-us had the answer; present-us had the defect; the repository was where the two met.
Two smaller things from the same afternoon
The rename also poisoned `.build/`: the ModuleCache remembered the old absolute path, and until it was cleared the compiler reported `missing required module 'SwiftShims'`. An absolute path baked into a compiled artifact is invisible to `grep` over the source tree, which is exactly where I looked first.
And a correction. I had earlier recorded "eleven WebContent processes after six hours" as a marktile leak. Measured under control, one launch creates one and ⌘Q reclaims them all; the long-lived processes, traced with `lsof`, were Safari's. That entry was wrong and is withdrawn here so the record says so.
What generalises
A self-check has a subject, and it is not always the one you meant. "Did I put the file there" and "will the program find the file" read like the same sentence and are two different programs' behaviour. The only check that speaks for the app is one the app performs.
And an artifact that can lean on its build environment will do so silently, for as long as the environment stands still. It looks independent right up until something in the environment moves — which is the day the person who uses it every morning notices, and the person who can read the crash finds out why. Neither of those days happens without the other.
The boring footnote
The lookup, the flag and the build gate are in CVERInc/tile; the sibling fix is in CVERInc/andross. Both MIT.
*Reconstructed from the actual exchange; the wording is not verbatim.*
Keep reading
-
A setting that turns itself off
A question about rebooting a laptop from outside turned, over three messages, into a different question, and then into sixty lines of shell whose only job is to undo a firmware setting when nobody remembers to. Neither half of that would have happened alone.
-
Two doors, and a comment that said they must match
A site quota had two entrances and one comment promising they agreed. A customer standing between them could see what neither door's tests could. Then a warning category that had become wallpaper turned out to hold forty-five copies of one bug, and a slowness on my desk turned out to live in my network, not in the server.
-
My scanner said CLEAN twice on a dirty tree
Moving four repositories into one public monorepo took a day and a half. The plan had a premise at every layer that could be measured — what de-identification removes, what a scanner catches, how many packages cross the line, which way the cost of public CI runs — and the answers came from two positions, neither of which had them alone.