fix(build): always sync canonical framework binary to avoid per-copy signature divergence#1258
Conversation
…signature divergence PyInstaller signs each Python.framework copy independently during the watcher build (via codesign_identity in aw.spec), giving each path a different embedded signature (different timestamp nonce). After cp -r into the reassembled .app, the three paths (Python, Versions/Current/Python, Versions/3.9/Python) carry pre-existing but DIFFERENT signatures. The ActivityWatch#1257 pre-sign SHA guard was intended to detect these as duplicates, but fails precisely because the existing PyInstaller signatures differ — the SHA comparison never matches, all three fall through to the 'sign separately' path, and Apple rejects all three as 'The signature of the binary is invalid'. Fix: remove the SHA guard and always sync the signed canonical to every other path in the framework. These are always the same Python binary; any independent signing produces divergent signatures that Apple rejects.
Greptile SummaryThis PR removes the SHA-comparison guard introduced in #1257 and replaces it with unconditional syncing of the signed canonical Confidence Score: 5/5Safe to merge — the broken SHA guard is removed, the arithmetic set -e hazard is fixed, and temp-file cleanup is handled on all code paths. No P0 or P1 findings remain. The fix is narrowly scoped to the 'bundle format is ambiguous' fallback and the unconditional sync is the only approach that satisfies Apple's requirement for byte-identical signatures across all three Python.framework binary paths. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Sign .framework bundle with codesign] -->|Success| B[Bundle signed ✓]
A -->|Fails: 'bundle format is ambiguous'| C[Fallback: PyInstaller non-standard framework]
C --> D[Find all Mach-O binaries inside .framework]
D --> E{Any binaries found?}
E -->|No| F[ERROR: exit 1]
E -->|Yes| G[Extract existing code-sign Identifier from canonical]
G --> H[cp -p canonical → tmp_binary]
H --> I[codesign --force --timestamp tmp_binary]
I -->|Failure| J[rm tmp_binary; exit 1]
I -->|Success| K[cp tmp_binary → canonical]
K -->|Failure| L[rm tmp_binary; exit 1]
K -->|Success| M[rm tmp_binary]
M --> N[For each remaining path in fw_bins at 1 ...]
N --> O[cp canonical → fw_bin — unconditional sync]
O --> P{More paths?}
P -->|Yes| N
P -->|No| Q[Log: Signed 1 + synced N paths]
Reviews (2): Last reviewed commit: "fix(build): use safe arithmetic for sync..." | Re-trigger Greptile |
((synced_count++)) post-increments: when synced_count=0, the expression evaluates to 0, returning exit status 1 which triggers set -e and exits the script after only the first non-canonical path is synced. The third Python.framework copy (Versions/Current/Python) would be left with its old PyInstaller signature, reproducing the Apple rejection. Use synced_count=$((synced_count + 1)) which always returns exit status 0.
|
Fixed Greptile's P1 finding in d2d31a3: changed |
|
@greptileai review |
|
Ready to merge — Greptile reviewed the fixed commit (d2d31a3) and scored 5/5 with "Safe to merge". All CI checks are passing. Waiting on a maintainer click per your note on ErikBjare/bob#546. |
…llback PyInstaller's codesign_identity stamps each Python.framework copy with a different signature (different timestamp nonce). When codesign tries to sign a non-standard PyInstaller framework as a bundle, it signs each Mach-O binary independently, producing different __LINKEDIT sections. Apple notarization rejects ALL copies as 'The signature of the binary is invalid'. Previous attempts (ActivityWatch#1250-ActivityWatch#1258) tried various dedup strategies (cmp against unsigned ref, SHA guards, unconditional sync) but all failed because they compared binaries that already contained different PyInstaller signatures. Fix: strip ALL existing signatures with codesign --remove-signature before the fallback pass. This makes all copies of the same binary truly byte-identical (only the code page remains). Then sign the canonical (first) binary once and cp -p the result to all duplicate paths. The --remove-signature step is the key insight that previous attempts missed. Only triggers when codesign fails with 'bundle format is ambiguous' on a framework directory. Standard frameworks (with Info.plist and symlinked Versions/) continue to use the normal codesign bundle signing path.
…igning fallback Rebases on top of ActivityWatch#1255-ActivityWatch#1258 and adds two correctness improvements: 1. Strip all existing signatures before comparison, so content hashing identifies true duplicates rather than nonce-only signature differences from PyInstaller's pre-sign codesign_identity step. 2. Group binaries by SHA-256 content hash instead of treating the whole framework as one duplicate set. This correctly handles the (unlikely but possible) case where a framework contains genuinely different Mach-O files — only true duplicates share a signed payload. Both changes preserve master's existing patterns: identifier preservation via temp copy, codesign --force --options runtime, and the ambiguous- framework fallback structure.
…igning fallback (#1259) Rebases on top of #1255-#1258 and adds two correctness improvements: 1. Strip all existing signatures before comparison, so content hashing identifies true duplicates rather than nonce-only signature differences from PyInstaller's pre-sign codesign_identity step. 2. Group binaries by SHA-256 content hash instead of treating the whole framework as one duplicate set. This correctly handles the (unlikely but possible) case where a framework contains genuinely different Mach-O files — only true duplicates share a signed payload. Both changes preserve master's existing patterns: identifier preservation via temp copy, codesign --force --options runtime, and the ambiguous- framework fallback structure.
Root cause
PR #1257 added a pre-signing SHA guard to detect whether
Python.framework/Python,Versions/Current/Python, andVersions/3.9/Pythonare duplicates before syncing from canonical. The guard fails because PyInstaller already signed each copy independently (viacodesign_identityinaw.spec) during the watcher build, giving each path a different embedded signature (different timestamp nonce).After
cp -rinto the reassembled.app, all three paths carry pre-existing but different signatures. The SHA comparison never matches, all three fall through to the "sign separately" path, and Apple rejects all three as"The signature of the binary is invalid".Evidence from post-#1257 master Build Tauri run 24217849800 — all 9 paths still rejected (3 watchers × 3 Python.framework paths), confirming the dedup never fired.
Fix
Remove the SHA guard and always sync the signed canonical to all other paths in the
Python.frameworkfallback. These paths are always the same Python binary; independently signing them is what causes the Apple rejection.This reduces the fallback block from ~30 lines to ~10 lines.
Why this is safe
codesignreports "bundle format is ambiguous" — specifically for non-standard PyInstaller-embeddedPython.frameworkbundlesPython,Versions/Current/Python,Versions/3.9/Pythonare always the same binary (different alias paths for the same Python interpreter)