fix(build): snapshot pre-signing checksum to correctly detect framework binary duplicates#1257
Conversation
…rk binary duplicates The cmp -s guard in ActivityWatch#1256 runs after signing the canonical binary, comparing signed bytes against unsigned duplicates — they always differ. This causes all three Python.framework copies to be signed separately with independent codesign invocations (different nonces/timestamps), producing inconsistent signature blocks that Apple rejects with 'The signature of the binary is invalid.' Fix: compute shasum of canonical BEFORE signing, then compare each duplicate's checksum against that pre-signing hash. Identical files (PyInstaller duplicate copies) are correctly detected and receive the byte-identical signed binary. Genuinely distinct binaries still fall through to the separate-signing path.
Greptile SummaryThis PR fixes the dedup logic introduced in #1256 by capturing a The fix is minimal, precisely targeted, and well-commented. The only cosmetic gap is the summary log message on line 223, which reports a fixed "N-1 synced" count regardless of how many duplicates actually took the sync path vs. the separate-sign path. Confidence Score: 5/5Safe to merge — the fix is correct, the only remaining finding is a cosmetic log count inaccuracy (P2). No P0 or P1 issues found. The pre-signing checksum approach is logically sound: No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["find Mach-O files inside .framework"] --> B["canonical = fw_bins[0]"]
B --> C["canonical_presign = shasum canonical ← NEW: before signing"]
C --> D["cp canonical → tmp_binary"]
D --> E["codesign tmp_binary"]
E --> F["cp tmp_binary → canonical"]
F --> G{"for each fw_bin in fw_bins[1:]"}
G --> H{"shasum fw_bin == canonical_presign?"}
H -- "Yes (byte-identical duplicate)" --> I["cp canonical → fw_bin\n(sync signed copy)"]
H -- "No (genuinely distinct binary)" --> J["sign fw_bin separately\nvia temp copy"]
I --> G
J --> G
G -- "done" --> K["log: Signed 1 + synced N-1 duplicate(s)"]
|
|
Fixed the P2 log count inaccuracy (16d7938): now tracks |
…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.
…signature divergence (#1258) * fix(build): always sync canonical framework binary to avoid per-copy 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 #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. * fix(build): use safe arithmetic for synced_count to avoid set -e exit ((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.
Root cause of #1256 regression
PR #1256 introduced a
cmp -sguard to detect which Python.framework binary copies are duplicates of the canonical, so they can receive the byte-identical signed binary instead of being signed separately.The bug:
cmp -s canonical fw_binruns after the canonical has been signed (lines 194–195 modifycanonicalin-place). Signing changes the binary bytes, so comparing the now-signed canonical against an unsigned duplicate always returns "differs" — even when the two files were byte-identical before signing.The result: every duplicate falls through to the "signing separately" branch, producing three independent codesign invocations with different nonces/timestamps. Apple's notarization service detects the inconsistent signature blocks and rejects all three paths with "The signature of the binary is invalid."
This explains why the post-#1256 master CI (run 24215193584) fails with the same rejection as before: the dedup logic never activates.
Fix
Capture
shasumof canonical before signing it, then compare each duplicate's checksum against that pre-signing hash:Identical files (PyInstaller duplicate copies) are now correctly detected. Genuinely distinct binaries still fall through to the separate-signing path.
Why #1256's PR branch CI passed but master failed
On the PR branch, the macOS runners may have followed symlinks or produced fat binaries where
Python.framework/PythonvsVersions/3.9/Pythonhappened to be byte-identical after signing (e.g., ifPython.framework/Pythonwas a symlink thatfind -type fexcluded). On master with a fresh full build, all three are distinct inodes, exposing the bug.Tested against