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
…ambiguity error The inside-out signing loop from ActivityWatch#1246 signs ALL Mach-O files in Step 1, then .framework bundles in Step 2. But Python.framework/Python is both a Mach-O binary AND the main binary of a .framework bundle — codesign errors with "bundle format is ambiguous (could be app or framework)" when it's signed as a standalone file. Fix: Skip files whose parent directory is a .framework, .bundle, or .plugin in Step 1. These are correctly signed as part of their bundle in Step 2. Also extends Step 2 to cover .bundle and .plugin directories (not just .framework) for completeness. Fixes the Build Tauri master CI failure after ActivityWatch#1246 merge.
…ambiguity error The inside-out signing loop from ActivityWatch#1246 signs ALL Mach-O files in Step 1, then .framework bundles in Step 2. But Python.framework/Python is both a Mach-O binary AND the main binary of a .framework bundle — codesign errors with "bundle format is ambiguous (could be app or framework)" when it's signed as a standalone file. Fix: Skip files whose parent directory is a .framework, .bundle, or .plugin in Step 1. These are correctly signed as part of their bundle in Step 2. Also extends Step 2 to cover .bundle and .plugin directories (not just .framework) for completeness. Fixes the Build Tauri master CI failure after ActivityWatch#1246 merge.
…ambiguity error (#1247) The inside-out signing loop from #1246 signs ALL Mach-O files in Step 1, then .framework bundles in Step 2. But Python.framework/Python is both a Mach-O binary AND the main binary of a .framework bundle — codesign errors with "bundle format is ambiguous (could be app or framework)" when it's signed as a standalone file. Fix: Skip files whose parent directory is a .framework, .bundle, or .plugin in Step 1. These are correctly signed as part of their bundle in Step 2. Also extends Step 2 to cover .bundle and .plugin directories (not just .framework) for completeness. Fixes the Build Tauri master CI failure after #1246 merge.
…ld (#1249) * fix(build): skip framework main binaries in codesign Step 1 to avoid ambiguity error The inside-out signing loop from #1246 signs ALL Mach-O files in Step 1, then .framework bundles in Step 2. But Python.framework/Python is both a Mach-O binary AND the main binary of a .framework bundle — codesign errors with "bundle format is ambiguous (could be app or framework)" when it's signed as a standalone file. Fix: Skip files whose parent directory is a .framework, .bundle, or .plugin in Step 1. These are correctly signed as part of their bundle in Step 2. Also extends Step 2 to cover .bundle and .plugin directories (not just .framework) for completeness. Fixes the Build Tauri master CI failure after #1246 merge. * fix(build): handle non-standard Python.framework signing in Tauri build PyInstaller-embedded Python.framework bundles (inside aw-watcher-window, aw-watcher-input) lack the standard Versions/ directory structure and Info.plist, so codesign rejects them in Step 2 with: bundle format is ambiguous (could be app or framework) Previously #1247 correctly skipped the Python.framework/Python binary in Step 1 (standalone signing of the main framework binary causes ambiguous errors), but Step 2 still unconditionally attempted bundle-signing the .framework directory itself, hitting the same error. Fix: in Step 2, catch "bundle format is ambiguous" errors and fall back to signing the main binary inside the framework directly (e.g. Python.framework/Python). All other codesign errors remain fatal. This completes the signing chain: the .so files inside Python.framework are signed in Step 1, the Python binary itself is signed in this Step 2 fallback, and standard frameworks/bundles are signed normally. * fix(build): make missing fw_binary fatal instead of silent skip If the main binary is not found at the expected path after an ambiguous-bundle error, emit a clear error and exit 1 rather than silently skipping. A missing binary here means PyInstaller changed its output structure — a silent skip would produce a confusing downstream notarization failure instead of a clear build error. Addresses Greptile review comment (P2) on #1249.
…ld (ActivityWatch#1249) * fix(build): skip framework main binaries in codesign Step 1 to avoid ambiguity error The inside-out signing loop from ActivityWatch#1246 signs ALL Mach-O files in Step 1, then .framework bundles in Step 2. But Python.framework/Python is both a Mach-O binary AND the main binary of a .framework bundle — codesign errors with "bundle format is ambiguous (could be app or framework)" when it's signed as a standalone file. Fix: Skip files whose parent directory is a .framework, .bundle, or .plugin in Step 1. These are correctly signed as part of their bundle in Step 2. Also extends Step 2 to cover .bundle and .plugin directories (not just .framework) for completeness. Fixes the Build Tauri master CI failure after ActivityWatch#1246 merge. * fix(build): handle non-standard Python.framework signing in Tauri build PyInstaller-embedded Python.framework bundles (inside aw-watcher-window, aw-watcher-input) lack the standard Versions/ directory structure and Info.plist, so codesign rejects them in Step 2 with: bundle format is ambiguous (could be app or framework) Previously ActivityWatch#1247 correctly skipped the Python.framework/Python binary in Step 1 (standalone signing of the main framework binary causes ambiguous errors), but Step 2 still unconditionally attempted bundle-signing the .framework directory itself, hitting the same error. Fix: in Step 2, catch "bundle format is ambiguous" errors and fall back to signing the main binary inside the framework directly (e.g. Python.framework/Python). All other codesign errors remain fatal. This completes the signing chain: the .so files inside Python.framework are signed in Step 1, the Python binary itself is signed in this Step 2 fallback, and standard frameworks/bundles are signed normally. * fix(build): make missing fw_binary fatal instead of silent skip If the main binary is not found at the expected path after an ambiguous-bundle error, emit a clear error and exit 1 rather than silently skipping. A missing binary here means PyInstaller changed its output structure — a silent skip would produce a confusing downstream notarization failure instead of a clear build error. Addresses Greptile review comment (P2) on ActivityWatch#1249.
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.