Build1 publisherNot yet confirmed elsewhere3 min readPublished
PyInstaller exits zero, then the real work starts: notarization traps that report success
One engineer's log of shipping a Python app to macOS and Windows beta users. Three checkpoints in the signing pipeline hand back a pass on builds that users cannot open.
The Engineer · Build desk
What happened
- An engineer spent months building a Python desktop app, then weeks getting it to install and run on other people's macOS and Windows machines.
- Qt and Python framework binaries carry no file extension, so name-based signing loops skipped them and Apple rejected the notarization 15 minutes later.
- The notarization tool's clean exit code meant only that the upload finished, so a build Apple marked Invalid was stapled and reported as shipped.
- Beta testers launching from Downloads or a DMG hit macOS path randomization, which put the app's updater into an endless download-and-relaunch loop.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint A passing verify step cannot be used as release evidence on macOS, because it answers a question about integrity that nobody asked instead of the question about the signer that gates distribution.
- cost Teams that leave signing checks to Apple's queue pay for every mistake in 15-minute round trips rather than 20-second ones, and the bill lands on whoever is holding the release.
- exposure Defects that only appear from quarantined download paths are invisible to the people who wrote them, so the discovery route runs through a beta user's broken updater.
- decision Anyone scheduling a desktop release has to decide where weeks of unbudgeted distribution work sits, given that the roadmap item ends when the build succeeds.
The shape shared by most of what broke here is a tool that reports success on an artifact a user cannot run. `codesign --verify` passes ad-hoc signed binaries, because verification checks integrity rather than who signed [9]. `xcrun notarytool submit --wait` returns a clean exit code for a completed upload, not an accepted build, so Apple can answer `status: Invalid` while the script marches on, tries to staple a ticket that was never issued, collects "Record not found" and error 65, and declares the release shipped [11]. Add the script's own success report and that is three places in one pipeline where a green result sits on top of a build that will not install [16].
The identity trap runs the other way. `security find-identity -v -p codesigning` says "0 valid identities found" with the certificate and its private key both sitting in the keychain, because Apple's Developer ID G2 CA intermediate is absent and the trust chain cannot be built; the error text does not mention any of that [5]. The check that actually settles it is `codesign -d -vvv` showing three authority lines, from the Developer ID Application leaf up through the Developer ID Certification Authority to Apple Root CA [6].
What the author ends up building is a distrust of the tools rather than a better one: pick binaries by `file -b` type instead of filename, since Qt and Python framework binaries carry no extension at all and every `find -name "*.dylib"` loop online skips them [7]; use `find -exec` to avoid the argument-length ceiling that a long `Developer ID Application: Name (TEAMID)` string pushes `xargs` into [8]; then grep the authority back out of every Mach-O as a guard [10]. The payoff is arithmetic. A broken build fails on your own machine in 20 seconds instead of 15 minutes into Apple's queue, a loop 45 times shorter on exactly the error class most likely to repeat [15].
The translocation bug is the one no local test reaches. A user who launches from Downloads or a mounted DMG gets a randomized read-only path, so the updater downloaded, installed, relaunched the old version and downloaded again without end, and it never reproduced on the developer's machine because developers do not run their own app out of quarantine [13].
One caution on scope. The account puts Windows antivirus heuristics and browser download blocking next to notarization as the things that consumed weeks [1], but the material here documents the Apple half at error-message granularity and leaves the Windows half asserted. It is one engineer's log of one app, roughly 33k lines of PySide6, MediaPipe and ONNX [3]. The $99 a year Apple Developer fee is the only sum of money in the whole gauntlet; every other cost in it is engineering time [14].
What to watch
- Whether the Windows half of this gauntlet, antivirus heuristics and browser download blocking, shows the same pattern of tools reporting success on artifacts users cannot run.
- Whether Apple ever makes notarytool return a nonzero exit code on a status of Invalid, which would remove the need to grep its text output.
- Whether shipping through a signed installer instead of a zip or DMG removes the translocation update loop or only relocates it.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence58
- Adoption24
- Hype gap−5
- Incentives30
- Confidence46
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
Building the app took months; getting it to install and run on other people's machines took weeks of fighting Apple's notarization, Windows antivirus heuristics and browser download blocking, and almost none of it was documented in one place.
- [2]
The paperwork requires an Apple Developer account at $99/year, a Developer ID Application certificate, an app-specific password, and storing credentials once with xcrun notarytool store-credentials.
- [3]
The author built a desktop app in Python using PySide6, MediaPipe and ONNX, about 33k lines, and shipped it to beta users on macOS and Windows.
- [4]
Without Developer ID signing plus notarization, users see "App is damaged and can't be opened. You should move it to the Trash.", a message the author says kills the app for non-technical users.
- [5]
security find-identity -v -p codesigning reported "0 valid identities found" with the certificate and private key in the keychain, because Apple's Developer ID G2 CA intermediate certificate was missing so the trust chain could not be built, and nothing in the error indicated this.
- [6]
The decisive test is that codesign -d -vvv on the app shows three authorities: Developer ID Application, Developer ID Certification Authority, and Apple Root CA; fewer means a broken chain.
- [7]
Qt (PySide6/PyQt) and Python framework main binaries have no file extension, so signing loops built on find -name "*.dylib" or "*.so" miss them entirely; they stay ad-hoc signed, look fine locally, and Apple rejects notarization about 15 minutes later with unsigned-binary errors buried in the log.
- [8]
Piping the file list through xargs -I{} fails with "command line cannot be assembled, too long" because the signing identity string (Developer ID Application: Your Name (TEAMID)) is long, and a || true anywhere in the pipe silently swallows the failure.
- [9]
codesign --verify reports ad-hoc signed binaries as valid because verification checks integrity, not who signed, so a local verify pass proves nothing.
- [10]
The author's fix is to select binaries by real file type (case on file -b output for Mach-O) using find -exec, then add a guard that checks the authority of every Mach-O after signing so a broken build fails on the developer's machine in 20 seconds rather than on Apple's servers in 15 minutes.
- [11]
The exit code of xcrun notarytool submit --wait only means the upload completed; Apple can answer status: Invalid while the script continues, which led the author's script to staple a nonexistent ticket ("Record not found", error 65) and report success on a rejected build.
- [12]
The fix is to parse the submit output and only staple when it contains "status: Accepted", otherwise fetch xcrun notarytool log for the real errors and exit 1.
- [13]
Found with the first real tester: if a user runs the app from the Downloads folder or a mounted DMG, macOS runs it from a randomized read-only location (App Translocation / Gatekeeper path randomization), and the author's updater downloaded, installed, relaunched the old version and downloaded again in an infinite loop that never occurs on the developer's machine because developers never run their own app from quarantine.
- [14]
The only monetary figure in the account is the $99/year Apple Developer fee; every other cost described is engineering time measured in weeks.
- [15]
Moving the signing failure from Apple's servers to the local machine shortens the feedback loop by a factor of 45.
- [16]
Three checkpoints in the documented pipeline return a passing result on a build that cannot be shipped: codesign --verify on ad-hoc binaries, the notarytool submit exit code, and the script's own success report.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toShipping a Python desktop app to real users: everything that breaks after PyInstaller succeeds
1 article · August 21, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.