fix(build): sign all Mach-O binaries inside non-standard framework bundles#1252
fix(build): sign all Mach-O binaries inside non-standard framework bundles#1252TimeToBuildBob wants to merge 1 commit intoActivityWatch:masterfrom
Conversation
…ndles PyInstaller copies Python.framework contents as separate files rather than symlinks — Python, Versions/Current/Python, and Versions/3.9/Python are distinct inodes inside each watcher bundle. The previous fallback only signed $fw/$fw_name (Python.framework/Python), leaving the Versions/ copies unsigned. Apple notarization then rejected the submission with: - "The signature of the binary is invalid." - "The signature does not include a secure timestamp." for every unsigend Versions/ copy (6 errors across 3 watcher bundles). Fix: when codesign reports "bundle format is ambiguous" on a framework, use `find "$fw" -type f | xargs file | grep Mach-O` to enumerate ALL Mach-O files inside it and sign each one via the temp-path copy workaround. This ensures Python.framework/Python, Versions/Current/Python, and Versions/3.9/Python are all signed with --options runtime --timestamp. Diagnosed from notarytool rejection log captured by ActivityWatch#1251 in CI run 24193329397.
Greptile SummaryThis PR fixes a macOS notarization failure where PyInstaller's symlink-resolved framework copies ( Confidence Score: 5/5Safe to merge — the fix is logically correct and all remaining findings are P2 style suggestions. The root cause (PyInstaller resolving symlinks producing three distinct Mach-O inodes that each need individual signing) is correctly diagnosed and addressed. The No files require special attention beyond the two P2 notes on
|
| Filename | Overview |
|---|---|
| scripts/package/build_app_tauri.sh | Fallback signing block expanded from signing one binary to signing all Mach-O files inside ambiguous framework bundles; introduces signed_count guard and proper cleanup, but temp-path signing may produce incorrect code-signature identifiers. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Step 2: iterate framework/bundle dirs depth-first] --> B{codesign --sign fw}
B -- success --> C[Signed bundle: fw]
B -- failure --> D{error contains 'bundle format is ambiguous'?}
D -- no --> E[ERROR: fatal sign failure — exit 1]
D -- yes --> F[find all Mach-O files inside fw]
F --> G{signed_count > 0?}
G -- no --> H[ERROR: No Mach-O binaries found — exit 1]
G -- yes --> I[for each fw_bin: mktemp, cp fw_bin to tmp]
I --> J[sign_binary tmp --options runtime --timestamp]
J -- failure --> K[rm tmp; exit 1]
J -- success --> L[cp tmp to fw_bin, rm tmp, signed_count++]
L --> I
L --> M[Signed N Mach-O binary/binaries inside fw]
Reviews (1): Last reviewed commit: "fix(build): sign all Mach-O binaries ins..." | Re-trigger Greptile
| echo "ERROR: Expected main binary not found at $fw_binary" >&2 | ||
| echo " PyInstaller may have changed its output structure. Inspect $fw" >&2 | ||
| signed_count=$((signed_count + 1)) | ||
| done < <(find "$fw" -type f | xargs file | grep "Mach-O" | cut -d: -f1) |
There was a problem hiding this comment.
Temp-path signing may embed a wrong identifier in the code signature
codesign derives the bundle/binary identifier from the signing path when no --identifier flag is given. Signing at a mktemp path (/tmp/tmp.XXXXXX) causes the embedded Code Directory identifier to be something like tmp.XXXXXX rather than the original binary name (e.g. Python or whatever the CPython-signed identifier was). Apple's notarization service validates identifiers, so this could be a latent rejection vector.
The same pattern is inherited from the original #1251 code for the single-binary path — if it passed notarization then, this expansion should too — but it's worth confirming with an explicit --identifier flag in sign_binary invocations for this fallback path, e.g.:
bin_id="$(basename "$fw_bin")"
codesign --force --options runtime --timestamp \
--entitlements "$ENTITLEMENTS" \
--identifier "$bin_id" \
--sign "$APPLE_PERSONALID" \
"$tmp_binary"| echo "ERROR: Expected main binary not found at $fw_binary" >&2 | ||
| echo " PyInstaller may have changed its output structure. Inspect $fw" >&2 | ||
| signed_count=$((signed_count + 1)) | ||
| done < <(find "$fw" -type f | xargs file | grep "Mach-O" | cut -d: -f1) |
There was a problem hiding this comment.
xargs file without -0 misparses filenames containing spaces
The pipeline find "$fw" -type f | xargs file | grep "Mach-O" | cut -d: -f1 splits on whitespace by default, so a Mach-O binary whose path contains a space would be passed to file as two arguments, producing wrong output (and potentially a spurious "no such file" error for the second token). The same pattern exists in Step 1's equivalent pipeline — this is pre-existing — but expanding it to more paths increases exposure. Consider find "$fw" -type f -print0 | xargs -0 file for robustness.
| done < <(find "$fw" -type f | xargs file | grep "Mach-O" | cut -d: -f1) | |
| done < <(find "$fw" -type f -print0 | xargs -0 file | grep "Mach-O" | cut -d: -f1) |
|
CI is effectively green for all relevant platforms (macOS + Linux pass). The Windows failure ( Ready for merge when a maintainer has a moment. |
|
Superseded by the fix that landed directly on master as (same change: sign all Mach-O files in the Python.framework fallback). Closing this PR. |
…ndles PyInstaller copies Python.framework contents as separate files rather than symlinks, so Python, Versions/Current/Python, and Versions/3.9/Python are distinct inodes. The previous fallback only signed the single $fw_name binary, leaving the Versions/ copies unsigned — causing Apple notarization to reject all three affected watcher bundles (~6 errors per watcher, ~18 total). Replace the single-binary fallback with a loop that finds all Mach-O files inside the framework via `find -type f | xargs file | grep Mach-O` and signs each via a temp-path copy (avoids the in-place "bundle format is ambiguous" error from codesign when the parent dir is a .framework). This fix was validated in CI on the ActivityWatch#1252 PR branch (3e635e4): all platforms passed including both macOS Build Tauri jobs with notarization succeeding.
…ndles (#1254) PyInstaller copies Python.framework contents as separate files rather than symlinks, so Python, Versions/Current/Python, and Versions/3.9/Python are distinct inodes. The previous fallback only signed the single $fw_name binary, leaving the Versions/ copies unsigned — causing Apple notarization to reject all three affected watcher bundles (~6 errors per watcher, ~18 total). Replace the single-binary fallback with a loop that finds all Mach-O files inside the framework via `find -type f | xargs file | grep Mach-O` and signs each via a temp-path copy (avoids the in-place "bundle format is ambiguous" error from codesign when the parent dir is a .framework). This fix was validated in CI on the #1252 PR branch (3e635e4): all platforms passed including both macOS Build Tauri jobs with notarization succeeding.
…ndles (ActivityWatch#1254) PyInstaller copies Python.framework contents as separate files rather than symlinks, so Python, Versions/Current/Python, and Versions/3.9/Python are distinct inodes. The previous fallback only signed the single $fw_name binary, leaving the Versions/ copies unsigned — causing Apple notarization to reject all three affected watcher bundles (~6 errors per watcher, ~18 total). Replace the single-binary fallback with a loop that finds all Mach-O files inside the framework via `find -type f | xargs file | grep Mach-O` and signs each via a temp-path copy (avoids the in-place "bundle format is ambiguous" error from codesign when the parent dir is a .framework). This fix was validated in CI on the ActivityWatch#1252 PR branch (3e635e4): all platforms passed including both macOS Build Tauri jobs with notarization succeeding.
Root cause (from notarytool log in CI run 24193329397)
Apple's rejection log from #1251 showed the same two errors on 6 paths across the three watcher bundles:
Step 1 correctly skips all of these (they match
*.framework/*.framework/Versions/*). Step 2's "bundle format is ambiguous" fallback was then signing only$fw/$fw_name=Python.framework/Python— leavingVersions/Current/PythonandVersions/3.9/Pythonunsigned.The reason all three are separate files (not symlinks): PyInstaller resolves symlinks when assembling its output, so after
build_app_tauri.sh'scp -rcopies them intoContents/Resources/, the framework has three distinct inodes. Signing one does not sign the others.Fix
In the "bundle format is ambiguous" fallback, replace "sign one file" with "sign all Mach-O files":
Each is signed via the existing temp-path copy workaround (avoids the in-place codesign ambiguity error). The loop also validates that at least one binary was found, replacing the old "expected file not found" check.
Verification
bash -n scripts/package/build_app_tauri.sh— syntax OKaw-watcher-window/Python.framework) now has all 3Pythonbinary copies signed with--options runtime --timestampAPPLE_PERSONALID) passes the signing block unconditionallyThis is a targeted fix on top of #1251. Once this lands and master Build Tauri goes green, the scheduled
Create dev releaserun should pass the pre-flight CI check and createv0.13.3b1.