Skip to content

fix(build): sign all Mach-O binaries inside non-standard framework bundles#1252

Closed
TimeToBuildBob wants to merge 1 commit intoActivityWatch:masterfrom
TimeToBuildBob:fix/sign-all-framework-binaries
Closed

fix(build): sign all Mach-O binaries inside non-standard framework bundles#1252
TimeToBuildBob wants to merge 1 commit intoActivityWatch:masterfrom
TimeToBuildBob:fix/sign-all-framework-binaries

Conversation

@TimeToBuildBob
Copy link
Copy Markdown
Contributor

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:

Python.framework/Python                  — invalid signature, no secure timestamp
Python.framework/Versions/Current/Python — invalid signature, no secure timestamp
Python.framework/Versions/3.9/Python     — invalid signature, no secure timestamp

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 — leaving Versions/Current/Python and Versions/3.9/Python unsigned.

The reason all three are separate files (not symlinks): PyInstaller resolves symlinks when assembling its output, so after build_app_tauri.sh's cp -r copies them into Contents/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":

find "$fw" -type f | xargs file | grep "Mach-O" | cut -d: -f1

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 OK
  • Logic: each framework (e.g. aw-watcher-window/Python.framework) now has all 3 Python binary copies signed with --options runtime --timestamp
  • CI will validate on macOS runners with Apple credentials; PR CI (no APPLE_PERSONALID) passes the signing block unconditionally

This is a targeted fix on top of #1251. Once this lands and master Build Tauri goes green, the scheduled Create dev release run should pass the pre-flight CI check and create v0.13.3b1.

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

greptile-apps bot commented Apr 9, 2026

Greptile Summary

This PR fixes a macOS notarization failure where PyInstaller's symlink-resolved framework copies (Python.framework/Python, Versions/Current/Python, Versions/3.9/Python) were distinct inodes that all required individual signing. The "bundle format is ambiguous" fallback in Step 2 previously signed only one binary; it now iterates over every Mach-O file inside the framework via find … | xargs file | grep "Mach-O" | cut -d: -f1.

Confidence Score: 5/5

Safe 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 signed_count guard replaces the old single-path existence check cleanly, and error handling is tightened. Both open findings (temp-path identifier and xargs null-safety) are pre-existing patterns shared with the Step 1 pipeline and carry no new risk here.

No files require special attention beyond the two P2 notes on scripts/package/build_app_tauri.sh.

Vulnerabilities

No security concerns identified. The signing credentials ($APPLE_PERSONALID) are already present in the environment and not exposed. Temp files are created with mktemp and cleaned up after use.

Important Files Changed

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

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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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)

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

CI is effectively green for all relevant platforms (macOS + Linux pass). The Windows failure (pywin32_ctypes-0.2.2.dist-info not found) is a pre-existing flaky infra issue — the same type of dist-info error appears on the master branch CI as well, unrelated to these macOS signing changes.

Ready for merge when a maintainer has a moment.

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

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.

TimeToBuildBob added a commit to TimeToBuildBob/activitywatch that referenced this pull request Apr 9, 2026
…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.
ErikBjare pushed a commit that referenced this pull request Apr 9, 2026
…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.
TimeToBuildBob added a commit to TimeToBuildBob/activitywatch that referenced this pull request Apr 9, 2026
…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.
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.

1 participant