Tagged as macos, dv2mv, packaging
Written on 2026-06-22 22:10:00
I packaged music chop — a
PyInstaller-frozen Python GUI that bundles ffmpeg, ffprobe, and rubberband
— into a macOS .app, and went to sign and notarize it so Gatekeeper would just stop with the warnings. Here's the whole chain:
security find-identity -v -p codesigning shows nothing.allow-jit +
allow-unsigned-executable-memory for numba, and disable-library-validation
for the bundled third-party dylibs).xcrun notarytool submit --wait, then staple the ticket.codesign signs every nested binary individually — dozens of them. Click Allow
and it grants one file, then prompts for the next. Even Always Allow can keep
re-asking. Grant Apple's tools persistent access to the key once and be done:
security set-key-partition-list -S apple-tool:,apple: -s ~/Library/Keychains/login.keychain-db
notarytool came back status: Invalid. The useful move here is to pull the
per-file log — it tells you exactly what failed:
xcrun notarytool log <submission-id> --keychain-profile <your-profile>
Three binaries, same three errors each: not signed with a valid Developer ID,
no secure timestamp, hardened runtime not enabled — for
Contents/Frameworks/bin/{ffmpeg,ffprobe,rubberband}. Everything else signed
fine.
Why? My signing loop matched by extension:
find "$APP/Contents" \( -name "*.dylib" -o -name "*.so" \) ...
But ffmpeg and friends are Mach-O executables with no extension. The glob
skipped them, so they shipped with their original (non-Developer-ID) signatures.
The fix is to match by file type, not name:
find "$APP/Contents" -type f -print0 | while IFS= read -r -d '' f; do
if file -b "$f" | grep -q "Mach-O"; then
codesign --force --timestamp --options runtime -s "$DEVELOPER_ID" "$f"
fi
done
When only the signing was wrong, the bundle itself is fine. Skip the slow
PyInstaller run: re-sign all the nested Mach-O with the loop above, re-sign the
.app bundle with your entitlements, rebuild the .dmg around it, and
resubmit. And run the wait in the background — if your local
notarytool --wait gets killed, Apple keeps processing server-side; just
xcrun notarytool wait <id> (or info) to pick the verdict back up.
The whole recipe — preconditions, identity auto-detection (no hardcoded Team ID,
which isn't secret anyway — it's stamped into every app you ship), and the
failure handling — is a self-contained skill in the repo at
.claude/skills/sign-macos/SKILL.md.
Steal it.