Skip to content

fix(build): handle ambiguous bundle format in codesign Step 2#1248

Closed
TimeToBuildBob wants to merge 3 commits intoActivityWatch:masterfrom
TimeToBuildBob:fix/codesign-step2-ambiguity
Closed

fix(build): handle ambiguous bundle format in codesign Step 2#1248
TimeToBuildBob wants to merge 3 commits intoActivityWatch:masterfrom
TimeToBuildBob:fix/codesign-step2-ambiguity

Conversation

@TimeToBuildBob
Copy link
Copy Markdown
Contributor

Summary

  • Make bundle-level codesign failures non-fatal in Step 2 of the inside-out signing process
  • Fixes Python.framework: bundle format is ambiguous (could be app or framework) error that fails master CI

Context

PR #1247 fixed the same ambiguity error in Step 1 (per-binary signing) by skipping framework main binaries. However, Step 2 (bundle-level signing) still tries to sign Python.framework as a bundle, which fails because PyInstaller creates a non-standard framework structure.

Since all individual Mach-O files inside the framework are already signed in Step 1, and the top-level .app is signed in Step 3, the framework-level seal is not strictly required.

Master CI failure: Build Tauri run 24157066534 — both macOS jobs fail at Package dmg step.

Test plan

  • CI macOS Build Tauri jobs pass (the actual test — codesign only runs on master, not PRs)
  • Non-ambiguous .framework/.bundle/.plugin bundles still signed normally
  • Error message provides clear explanation when a bundle is skipped

…ambiguity error

The inside-out signing loop from ActivityWatch#1246 signs ALL Mach-O files in Step 1,
then .framework bundles in Step 2. But Python.framework/Python is both a
Mach-O binary AND the main binary of a .framework bundle — codesign errors
with "bundle format is ambiguous (could be app or framework)" when it's
signed as a standalone file.

Fix: Skip files whose parent directory is a .framework, .bundle, or .plugin
in Step 1. These are correctly signed as part of their bundle in Step 2.

Also extends Step 2 to cover .bundle and .plugin directories (not just
.framework) for completeness.

Fixes the Build Tauri master CI failure after ActivityWatch#1246 merge.
Python.framework (created by PyInstaller) has an ambiguous bundle
structure that causes codesign to error with "bundle format is
ambiguous (could be app or framework)" when signed as a bundle.

The previous fix (ActivityWatch#1247) only skipped the framework's main binary
in Step 1 (per-binary signing). But Step 2 (bundle-level signing)
also encounters this error when trying to sign the framework bundle
itself.

Fix: make bundle signing non-fatal in Step 2. Since all individual
Mach-O files inside the framework are already signed in Step 1,
and the top-level .app is signed in Step 3, the framework-level
seal is not strictly required.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 8, 2026

Greptile Summary

This PR makes Step 2 of the inside-out macOS codesign process non-fatal so that Python.framework's ambiguous bundle structure no longer aborts CI. The fix is correct in spirit but overly broad: if ! sign_binary \"$fw\" suppresses all Step 2 failures, not just the specific bundle format is ambiguous error, meaning a genuine signing failure for any other .framework, .bundle, or .plugin will be silently swallowed and produce an app with unsigned nested bundles — which will fail notarization.

  • P1 – Step 2 swallows all codesign failures: The catch should be narrowed to only the known ambiguity error string; every other failure should still abort the build.

Confidence Score: 4/5

Safe to merge for the targeted CI fix, but the overly broad error suppression in Step 2 should be addressed to avoid masking real signing failures in future.

One P1 finding: all Step 2 codesign failures are silently swallowed, not just the known Python.framework ambiguity. This can produce an app with unsigned nested bundles that fails notarization without a clear error. A P2 log-clarity issue also exists but does not affect correctness.

scripts/package/build_app_tauri.sh — Step 2 error handling (lines 149–155)

Vulnerabilities

No security concerns identified. The change only affects the error-handling path of a macOS codesign script; no secrets, user input, or auth boundaries are touched.

Important Files Changed

Filename Overview
scripts/package/build_app_tauri.sh Step 2 now tolerates all codesign failures non-fatally, not just the intended Python.framework ambiguity — legitimate bundle signing errors will be silently ignored, risking unsigned nested bundles that cause notarization failures.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Start signing\nAPPLE_PERSONALID set] --> B

    B["Step 1: Sign all Mach-O leaf files\n(dylibs, .so, executables)\nSort by path depth descending"]
    B --> C{Parent dir is\n.framework/.bundle/.plugin?}
    C -- yes --> D[Skip — signed\nas bundle in Step 2]
    C -- no --> E[codesign --force\n--options runtime\n--timestamp]

    E --> F

    F["Step 2: Sign bundle dirs\n(.framework, .bundle, .plugin)\nDeepest first"]
    F --> G{sign_binary\nsucceeds?}
    G -- yes --> H[Bundle signed ✓]
    G -- no --> I["⚠ WARNING printed\nscript continues\n(ALL failures, not just ambiguity)"]

    H --> J
    I --> J

    J["Step 3: Sign top-level\ndist/ActivityWatch.app"]
    J --> K[Signing complete ✓]

    style I fill:#ffcccc,stroke:#cc0000
Loading

Comments Outside Diff (1)

  1. scripts/package/build_app_tauri.sh, line 107-113 (link)

    P2 sign_binary prints "Signing:" before a potential skip

    When Step 2 calls sign_binary "$fw" and it fails, the console/CI log shows Signing: Python.framework immediately followed by the codesign error and then the WARNING. This looks like signing was attempted and partially succeeded, which can be misleading during log triage. Consider moving the echo outside sign_binary or into the caller so you can choose the right message based on outcome.

Reviews (1): Last reviewed commit: "fix(build): handle ambiguous bundle form..." | Re-trigger Greptile

Comment thread scripts/package/build_app_tauri.sh Outdated
Comment on lines +150 to +151
if ! sign_binary "$fw"; then
echo " WARNING: Skipping $fw (bundle format may be ambiguous, contents already signed in Step 1)"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 All Step 2 failures silently swallowed, not just ambiguity errors

if ! sign_binary "$fw" catches every non-zero exit from codesign, not only the bundle format is ambiguous case. If a legitimately structured .bundle or .plugin fails to sign for an unrelated reason (wrong permissions, a corrupted binary, a keychain timeout), the script continues with a quietly unsigned nested bundle. The resulting .app will then fail notarization with a cryptic integrity error that is much harder to trace back to this step.

A more targeted fix captures codesign's stderr and only suppresses the known-ambiguous error, re-raising anything else:

while IFS= read -r fw; do
    cs_err=$(codesign --force --options runtime --timestamp \
        --entitlements "$ENTITLEMENTS" --sign "$APPLE_PERSONALID" \
        "$fw" 2>&1) || {
        if echo "$cs_err" >&2 && echo "$cs_err" | grep -q "bundle format is ambiguous"; then
            echo "  WARNING: Skipping $fw (bundle format is ambiguous, contents already signed in Step 1)"
        else
            echo "  ERROR: codesign failed for $fw" >&2
            echo "$cs_err" >&2
            exit 1
        fi
    }
done < <(...)

This keeps the fix surgical — only the known PyInstaller ambiguity is tolerated; every other signing failure still aborts the build.

… only

The previous fix suppressed all Step 2 codesign failures, which could silently
produce an app with unsigned nested bundles (failing notarization without a
clear error). Now only the known Python.framework ambiguity error is swallowed;
any other signing failure aborts the build immediately with an error message.

Addresses Greptile P1 review finding on PR ActivityWatch#1248.
@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

Superseded by #1249, which handles the case more correctly: when Step 2 finds an ambiguous-format bundle (e.g. PyInstaller's Python.framework), it falls back to explicitly signing the main binary inside, rather than just warning and skipping. This is necessary because the main binary (e.g. Python.framework/Python) is also skipped in Step 1 — so the skip-in-both-steps approach in this PR could leave it unsigned.

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