fix(build): replace codesign --deep with inside-out per-binary signing for notarization#1246
Conversation
codesign --deep fails to reliably sign all nested Mach-O binaries in PyInstaller-built watcher bundles (aw-watcher-window/input/afk each embed hundreds of .dylib/.so files and Python.framework with symlinks). Apple's notarization log showed 502 rejections: - 248 missing secure timestamps - 239 binaries not signed with valid Developer ID certificate - 9 invalid signatures (Python.framework symlink issue) Fix: sign every Mach-O binary individually using `file | grep Mach-O`, working inside-out (leaves → .framework bundles → top-level .app), with --timestamp on every codesign call as required by notarization. Also add --timestamp to the DMG codesign step in both build.yml and build-tauri.yml, which was also missing. Reported in ErikBjare/bob#546 via xcrun notarytool log analysis.
Greptile SummaryThis PR fixes macOS notarization failures by replacing the unreliable Confidence Score: 5/5Safe to merge — the fix correctly implements inside-out signing and all remaining findings are P2 style suggestions. All three files make targeted, correct changes. The inside-out signing approach is the Apple-recommended solution for PyInstaller bundles, and the --timestamp additions directly resolve the notarytool rejections. The two open comments are P2 suggestions (performance and optional bundle-type coverage) that don't affect correctness or notarization success. No files require special attention; the shell logic in build_app_tauri.sh is well-commented and syntactically validated.
|
| Filename | Overview |
|---|---|
| scripts/package/build_app_tauri.sh | Replaces codesign --deep with a correct inside-out per-binary signing loop; adds --timestamp to all signing calls. Two minor P2 gaps: per-file file invocation is slow for large PyInstaller bundles, and .bundle/.plugin directories are not covered in the bundle-sealing step. |
| .github/workflows/build-tauri.yml | Adds --force and --timestamp to DMG codesign step, both required for notarization; straightforward and correct. |
| .github/workflows/build.yml | Same --force --timestamp addition to DMG codesign as build-tauri.yml; correct. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[build_app_tauri.sh] --> B{APPLE_PERSONALID set?}
B -- No --> C[Skip signing]
B -- Yes --> D["Step 1: find all regular files\nsort by path length desc"]
D --> E{"file cmd:\nMach-O?"}
E -- Yes --> F["codesign --force --options runtime\n--timestamp --entitlements\n--sign IDENTITY file"]
E -- No --> G[Skip]
F --> H["Step 2: find *.framework dirs\nsort by path length desc"]
G --> H
H --> I["codesign --force --options runtime\n--timestamp --entitlements\n--sign IDENTITY framework/"]
I --> J["Step 3: sign top-level .app bundle"]
J --> K["codesign --force --options runtime\n--timestamp --entitlements\n--sign IDENTITY ActivityWatch.app"]
K --> L[App signing complete]
M["CI: make dist/ActivityWatch.dmg"] --> N["codesign --force --verbose\n--timestamp -s IDENTITY\ndist/ActivityWatch.dmg"]
N --> O["xcnotary precheck .app + .dmg"]
O --> P["make dist/notarize"]
Reviews (1): Last reviewed commit: "fix(build): replace codesign --deep with..." | Re-trigger Greptile
| while IFS= read -r f; do | ||
| if file "$f" | grep -q "Mach-O"; then | ||
| sign_binary "$f" | ||
| fi | ||
| done < <(find "dist/${APP_NAME}.app" -type f \ | ||
| | awk '{ print length, $0 }' | sort -rn | cut -d' ' -f2-) |
There was a problem hiding this comment.
file called per-file inside loop — slow for large bundles
Spawning a separate file subprocess for each of the hundreds of .dylib/.so files in a PyInstaller bundle will be slow. xargs file (or a single file <dir> -r) can batch all lookups into far fewer subprocess calls:
echo " Signing Mach-O binary files..."
while IFS= read -r f; do
sign_binary "$f"
done < <(find "dist/${APP_NAME}.app" -type f \
| xargs file \
| grep "Mach-O" \
| cut -d: -f1 \
| awk '{ print length, $0 }' | sort -rn | cut -d' ' -f2-)This reduces process spawning from O(n) to roughly O(1) and avoids the inner if file … grep per iteration.
There was a problem hiding this comment.
Fixed in 63939fd — switched to xargs file batching as suggested.
| while IFS= read -r fw; do | ||
| sign_binary "$fw" | ||
| done < <(find "dist/${APP_NAME}.app" -type d -name "*.framework" \ | ||
| | awk '{ print length, $0 }' | sort -rn | cut -d' ' -f2-) |
There was a problem hiding this comment.
.bundle and .plugin directories not handled as bundles
Step 2 only signs .framework directories as bundles. Some PyInstaller distributions and Python packages ship .bundle or .plugin sub-bundles (e.g., Python C-extension bundles on some configurations). Those bundles' contents will be signed as individual Mach-O files in Step 1, but the bundle-level seal (the CodeResources catalog) will be absent, which can trigger notarytool bundle-integrity warnings.
Consider extending the pattern to cover the other common bundle types:
while IFS= read -r fw; do
sign_binary "$fw"
done < <(find "dist/${APP_NAME}.app" -type d \
\( -name "*.framework" -o -name "*.bundle" -o -name "*.plugin" \) \
| awk '{ print length, $0 }' | sort -rn | cut -d' ' -f2-)There was a problem hiding this comment.
Fixed in 63939fd — extended Step 2 to cover .bundle and .plugin in addition to .framework.
…le type coverage - Batch Mach-O file detection with `xargs file` (O(1) subprocess calls vs O(n)) for large PyInstaller bundles with hundreds of dylib/so files - Extend bundle signing step to cover .bundle and .plugin directories in addition to .framework, preventing missing CodeResources catalog seals that can trigger notarytool bundle-integrity warnings
Problem
Notarization has been failing with 502 rejections (from
xcrun notarytool logon submission IDsda217db4-e2aa-43a6-a694-f7263a1aff66andf2dd1e3f-ca82-48eb-b781-8ad0f9e11245):Affected components (inside
Contents/Resources/):aw-watcher-window,aw-watcher-input,aw-watcher-afk— each of which is a PyInstaller bundle containing hundreds of.dylib/.sofiles andPython.framework.Root cause:
codesign --deepinbuild_app_tauri.shdoes not reliably reach all nested binaries in PyInstaller bundles (Apple explicitly warns against relying on--deepfor notarization). Additionally,--timestampwas missing on the.appcodesign and both workflow DMG codesign calls.Fix
scripts/package/build_app_tauri.shReplace the single
codesign --deepcall with inside-out per-binary signing:file | grep Mach-O, sorted by path depth (deepest first)--force --options runtime --timestamp --entitlements.frameworkbundles after their contents (deepest first).applast.github/workflows/build-tauri.yml+.github/workflows/build.ymlAdd
--force --timestampto the DMGcodesignstep (was missing--timestamprequired for notarization).Testing
Shell syntax validated (
bash -n). Full validation requires a macOS runner with valid Apple Developer credentials — this should be visible in CI on this PR.Tracked in ErikBjare/bob#546.