Skip to content

fix(build): skip framework main binaries in codesign Step 1#1247

Merged
ErikBjare merged 1 commit intoActivityWatch:masterfrom
TimeToBuildBob:fix/codesign-framework-ambiguity
Apr 8, 2026
Merged

fix(build): skip framework main binaries in codesign Step 1#1247
ErikBjare merged 1 commit intoActivityWatch:masterfrom
TimeToBuildBob:fix/codesign-framework-ambiguity

Conversation

@TimeToBuildBob
Copy link
Copy Markdown
Contributor

Summary

Fixes the Build Tauri master CI failure introduced by #1246.

  • The inside-out signing loop signs ALL Mach-O files in Step 1, but Python.framework/Python is both a Mach-O file and the main binary of a .framework bundle — codesign errors with "bundle format is ambiguous (could be app or framework)"
  • 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)

CI error from run 24137803771:

dist/ActivityWatch.app/Contents/Resources/aw-watcher-window/Python.framework/Python: bundle format is ambiguous (could be app or framework)
make: *** [dist/ActivityWatch.app] Error 1

Test plan

  • Build Tauri CI passes on both macOS-14 and macOS-latest
  • xcnotary precheck succeeds on the signed .app

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 8, 2026

Greptile Summary

This PR fixes the CI failure introduced by #1246 by adding a skip guard in the Step 1 Mach-O signing loop: files whose immediate parent directory is a .framework, .framework/Versions/*, .bundle, or .plugin are now deferred to Step 2, avoiding the "bundle format is ambiguous" codesign error for Python.framework/Python. Step 2 is also extended to cover .bundle and .plugin directories in addition to .framework.

Confidence Score: 5/5

Safe to merge — the fix correctly resolves the CI failure without introducing new issues.

The skip guard precisely targets the ambiguous-bundle-binary problem for .framework/Versions/* paths (the root cause), and the extension of Step 2 to .bundle/.plugin is a sensible hardening. All remaining open findings (entitlements on dylibs, deeper .bundle/.plugin nesting) were already raised in prior review threads and are pre-existing; none are regressions from this PR. No new P0/P1 issues were found.

No files require special attention.

Vulnerabilities

No security concerns identified. The change tightens the codesigning pipeline to produce a correctly sealed macOS app bundle, which improves rather than weakens the code-signing chain.

Important Files Changed

Filename Overview
scripts/package/build_app_tauri.sh Adds parent-directory skip logic in Step 1 to avoid codesigning framework/bundle main binaries as standalone files; extends Step 2 to also sign .bundle and .plugin directories. Logic is correct for the Python.framework case.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[find all files in .app] --> B["batch file-type check via xargs file + grep Mach-O"]
    B --> C{parent_dir matches .framework,\n.framework/Versions/*,\n.bundle, or .plugin?}
    C -- Yes --> D[Skip — defer to Step 2]
    C -- No --> E["sign_binary: codesign --force --options runtime\n--timestamp --entitlements ..."]
    E --> F[Step 1 complete]
    D --> F
    F --> G["find .framework / .bundle / .plugin dirs\n(sorted deepest-first)"]
    G --> H[sign_binary each bundle dir]
    H --> I[Step 2 complete]
    I --> J[sign_binary dist/ActivityWatch.app]
    J --> K[Signing complete]
Loading

Reviews (2): Last reviewed commit: "fix(build): skip framework main binaries..." | Re-trigger Greptile

Comment on lines +128 to +129
if [[ "$parent_dir" == *.framework ]] || [[ "$parent_dir" == *.framework/Versions/* ]] \
|| [[ "$parent_dir" == *.bundle ]] || [[ "$parent_dir" == *.plugin ]]; then
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.

P2 .bundle/.plugin skip patterns don't cover nested binaries

The check [[ "$parent_dir" == *.bundle ]] only matches a file sitting directly inside Foo.bundle/ (e.g., Foo.bundle/Foo). The conventional macOS bundle layout places the main binary one level deeper at Foo.bundle/Contents/MacOS/Foo, whose parent Foo.bundle/Contents/MacOS does not match *.bundle. The same gap applies to *.plugin. This means those main binaries still get signed standalone in Step 1, then re-signed as part of their bundle in Step 2. The --force flag lets Step 2 succeed, but the intermediate Step 1 signature is wasted work. If any .bundle or .plugin encountered in this tree happens to trigger the same codesign "bundle format is ambiguous" error as Python.framework/Python, it would break the build.

Consider also matching the typical Contents/MacOS nesting:

Suggested change
if [[ "$parent_dir" == *.framework ]] || [[ "$parent_dir" == *.framework/Versions/* ]] \
|| [[ "$parent_dir" == *.bundle ]] || [[ "$parent_dir" == *.plugin ]]; then
if [[ "$parent_dir" == *.framework ]] || [[ "$parent_dir" == *.framework/Versions/* ]] \
|| [[ "$parent_dir" == *.bundle ]] || [[ "$parent_dir" == *.bundle/Contents/MacOS ]] \
|| [[ "$parent_dir" == *.plugin ]] || [[ "$parent_dir" == *.plugin/Contents/MacOS ]]; then

Comment on lines +107 to +113
sign_binary() {
echo " Signing: $1"
codesign --force --options runtime --timestamp \
--entitlements "$ENTITLEMENTS" \
--sign "$APPLE_PERSONALID" \
"$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.

P2 Entitlements applied to all Mach-O files including dylibs

sign_binary applies --entitlements "$ENTITLEMENTS" to every signed object — dylibs, .so files, frameworks, and the top-level app alike. Apple's notarization guidance states that entitlements should only be embedded in executables and app/framework bundles, not in plain shared libraries. Embedding them in dylibs is silently accepted by codesign but can cause xcnotary warnings or notarization rejections. The old --deep code had the same issue, so this isn't a regression, but the new per-file loop is a good place to fix it: pass --entitlements only for the .app and optionally .framework/.bundle/.plugin targets, and omit it for bare dylibs/.so files.

…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.
@TimeToBuildBob TimeToBuildBob force-pushed the fix/codesign-framework-ambiguity branch from c3a2738 to 3dfd97b Compare April 8, 2026 20:14
@ErikBjare ErikBjare merged commit e376d6c into ActivityWatch:master Apr 8, 2026
15 checks passed
@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

All macOS CI green — both Build and Build Tauri pass on macOS-14 and macOS-latest.

The "bundle format is ambiguous" error is resolved. Only Windows builds still running (unrelated to codesign).

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

@greptileai review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 8, 2026

Tip:

Greploops — Automatically fix all review issues by running /greploops in Claude Code. It iterates: fix, push, re-review, repeat until 5/5 confidence.

Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal.

TimeToBuildBob added a commit to TimeToBuildBob/activitywatch that referenced this pull request Apr 8, 2026
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.
TimeToBuildBob added a commit to TimeToBuildBob/activitywatch that referenced this pull request Apr 8, 2026
PyInstaller-embedded Python.framework bundles (inside aw-watcher-window,
aw-watcher-input) lack the standard Versions/ directory structure and
Info.plist, so codesign rejects them in Step 2 with:

  bundle format is ambiguous (could be app or framework)

Previously ActivityWatch#1247 correctly skipped the Python.framework/Python binary in
Step 1 (standalone signing of the main framework binary causes ambiguous
errors), but Step 2 still unconditionally attempted bundle-signing the
.framework directory itself, hitting the same error.

Fix: in Step 2, catch "bundle format is ambiguous" errors and fall back
to signing the main binary inside the framework directly (e.g.
Python.framework/Python). All other codesign errors remain fatal.

This completes the signing chain: the .so files inside Python.framework
are signed in Step 1, the Python binary itself is signed in this Step 2
fallback, and standard frameworks/bundles are signed normally.
TimeToBuildBob added a commit to TimeToBuildBob/activitywatch that referenced this pull request Apr 8, 2026
PyInstaller-embedded Python.framework bundles (inside aw-watcher-window,
aw-watcher-input) lack the standard Versions/ directory structure and
Info.plist, so codesign rejects them in Step 2 with:

  bundle format is ambiguous (could be app or framework)

Previously ActivityWatch#1247 correctly skipped the Python.framework/Python binary in
Step 1 (standalone signing of the main framework binary causes ambiguous
errors), but Step 2 still unconditionally attempted bundle-signing the
.framework directory itself, hitting the same error.

Fix: in Step 2, catch "bundle format is ambiguous" errors and fall back
to signing the main binary inside the framework directly (e.g.
Python.framework/Python). All other codesign errors remain fatal.

This completes the signing chain: the .so files inside Python.framework
are signed in Step 1, the Python binary itself is signed in this Step 2
fallback, and standard frameworks/bundles are signed normally.
ErikBjare pushed a commit that referenced this pull request Apr 8, 2026
…ld (#1249)

* fix(build): skip framework main binaries in codesign Step 1 to avoid ambiguity error

The inside-out signing loop from #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 #1246 merge.

* fix(build): handle non-standard Python.framework signing in Tauri build

PyInstaller-embedded Python.framework bundles (inside aw-watcher-window,
aw-watcher-input) lack the standard Versions/ directory structure and
Info.plist, so codesign rejects them in Step 2 with:

  bundle format is ambiguous (could be app or framework)

Previously #1247 correctly skipped the Python.framework/Python binary in
Step 1 (standalone signing of the main framework binary causes ambiguous
errors), but Step 2 still unconditionally attempted bundle-signing the
.framework directory itself, hitting the same error.

Fix: in Step 2, catch "bundle format is ambiguous" errors and fall back
to signing the main binary inside the framework directly (e.g.
Python.framework/Python). All other codesign errors remain fatal.

This completes the signing chain: the .so files inside Python.framework
are signed in Step 1, the Python binary itself is signed in this Step 2
fallback, and standard frameworks/bundles are signed normally.

* fix(build): make missing fw_binary fatal instead of silent skip

If the main binary is not found at the expected path after an ambiguous-bundle
error, emit a clear error and exit 1 rather than silently skipping.

A missing binary here means PyInstaller changed its output structure — a silent
skip would produce a confusing downstream notarization failure instead of a
clear build error.

Addresses Greptile review comment (P2) on #1249.
TimeToBuildBob added a commit to TimeToBuildBob/activitywatch that referenced this pull request Apr 9, 2026
…ld (ActivityWatch#1249)

* fix(build): skip framework main binaries in codesign Step 1 to avoid 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.

* fix(build): handle non-standard Python.framework signing in Tauri build

PyInstaller-embedded Python.framework bundles (inside aw-watcher-window,
aw-watcher-input) lack the standard Versions/ directory structure and
Info.plist, so codesign rejects them in Step 2 with:

  bundle format is ambiguous (could be app or framework)

Previously ActivityWatch#1247 correctly skipped the Python.framework/Python binary in
Step 1 (standalone signing of the main framework binary causes ambiguous
errors), but Step 2 still unconditionally attempted bundle-signing the
.framework directory itself, hitting the same error.

Fix: in Step 2, catch "bundle format is ambiguous" errors and fall back
to signing the main binary inside the framework directly (e.g.
Python.framework/Python). All other codesign errors remain fatal.

This completes the signing chain: the .so files inside Python.framework
are signed in Step 1, the Python binary itself is signed in this Step 2
fallback, and standard frameworks/bundles are signed normally.

* fix(build): make missing fw_binary fatal instead of silent skip

If the main binary is not found at the expected path after an ambiguous-bundle
error, emit a clear error and exit 1 rather than silently skipping.

A missing binary here means PyInstaller changed its output structure — a silent
skip would produce a confusing downstream notarization failure instead of a
clear build error.

Addresses Greptile review comment (P2) on ActivityWatch#1249.
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