Skip to content

fix(build): snapshot pre-signing checksum to correctly detect framework binary duplicates#1257

Merged
ErikBjare merged 2 commits intoActivityWatch:masterfrom
TimeToBuildBob:fix/framework-signing-presign-checksums
Apr 9, 2026
Merged

fix(build): snapshot pre-signing checksum to correctly detect framework binary duplicates#1257
ErikBjare merged 2 commits intoActivityWatch:masterfrom
TimeToBuildBob:fix/framework-signing-presign-checksums

Conversation

@TimeToBuildBob
Copy link
Copy Markdown
Contributor

Root cause of #1256 regression

PR #1256 introduced a cmp -s guard 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_bin runs after the canonical has been signed (lines 194–195 modify canonical in-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 shasum of canonical before signing it, then compare each duplicate's checksum against that pre-signing hash:

canonical_presign=$(shasum "$canonical" | cut -d' ' -f1)
# ... sign canonical ...
for fw_bin in "${fw_bins[@]:1}"; do
    if [ "$(shasum "$fw_bin" | cut -d' ' -f1)" = "$canonical_presign" ]; then
        cp "$canonical" "$fw_bin"   # identical pre-signing → sync signed copy
    else
        ...sign separately...       # genuinely distinct binary
    fi
done

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/Python vs Versions/3.9/Python happened to be byte-identical after signing (e.g., if Python.framework/Python was a symlink that find -type f excluded). On master with a fresh full build, all three are distinct inodes, exposing the bug.

Tested against

…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-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 9, 2026

Greptile Summary

This PR fixes the dedup logic introduced in #1256 by capturing a shasum of the canonical framework binary before signing it (line 189), then using that pre-signing hash to identify byte-identical duplicates. The previous cmp -s guard ran against the already-signed canonical, so it always returned "differs" and fell through to three independent codesign calls — causing Apple's notarization to reject all three paths due to inconsistent signature blocks.

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/5

Safe 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: shasum is captured before codesign mutates the file, and each duplicate is compared against that immutable snapshot. The single P2 comment is about an informational log message count, not signing behaviour.

No files require special attention.

Important Files Changed

Filename Overview
scripts/package/build_app_tauri.sh Adds a pre-signing SHA1 snapshot of the canonical framework binary at line 189, replacing the post-signing cmp -s guard that incorrectly always returned "differs". The logic is correct and well-commented.

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)"]
Loading

Comments Outside Diff (1)

  1. scripts/package/build_app_tauri.sh, line 223 (link)

    P2 Inaccurate "synced" count in summary log

    The count ${#fw_bins[@]} - 1 assumes every duplicate was synced, but when a duplicate fails the checksum guard and is signed separately, the printed number will be too high. In a failure-diagnosis scenario this makes the log misleading.

    Then increment synced_count inside the if branch and separately_count inside the else branch, and use them in the summary:

    echo "  Signed 1 + synced ${synced_count} duplicate(s) + signed ${separately_count} distinct binary/ies inside $fw"
    

Reviews (1): Last reviewed commit: "fix(build): snapshot pre-signing checksu..." | Re-trigger Greptile

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

Fixed the P2 log count inaccuracy (16d7938): now tracks synced_count and separately_count inside the loop and uses both in the summary line, so the count is accurate even when some duplicates are signed separately.

@ErikBjare ErikBjare merged commit 38a87df into ActivityWatch:master Apr 9, 2026
16 checks passed
TimeToBuildBob added a commit to TimeToBuildBob/activitywatch that referenced this pull request Apr 10, 2026
…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.
ErikBjare pushed a commit that referenced this pull request Apr 10, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants