Skip to content

fix(build): restore Python.framework symlinks before signing#1260

Merged
ErikBjare merged 2 commits intomasterfrom
fix/framework-symlinks-notarization
Apr 13, 2026
Merged

fix(build): restore Python.framework symlinks before signing#1260
ErikBjare merged 2 commits intomasterfrom
fix/framework-symlinks-notarization

Conversation

@ErikBjare
Copy link
Copy Markdown
Member

Summary

Verified locally that restoring symlinks fixes the signing:

  • Without symlinks: codesign → "bundle format is ambiguous" (current CI behavior)
  • With symlinks: codesign → signs and verifies successfully

What changed

After cp -r copies watcher components into the .app, a new step finds all Python.framework directories and restores the canonical layout:

Python.framework/
  Python -> Versions/Current/Python          (was a copy)
  Resources -> Versions/Current/Resources    (was a copy)
  Versions/
    Current -> 3.9                           (was a directory)
    3.9/
      Python      (the actual binary - unchanged)
      Resources/  (unchanged)

The existing fallback code (strip-sign-sync) is left in place as a safety net but should no longer trigger for Python.framework.

Test plan

  • CI Build Tauri macOS jobs should pass (both macos-14 and macos-latest)
  • Notarization should succeed (no more "The signature of the binary is invalid" rejections)
  • After merge, Create dev release workflow should produce v0.13.3b1

Fixes #1216
Related: ErikBjare/bob#546

PyInstaller copies Python.framework using real files/directories
instead of preserving the standard macOS symlink layout. This causes
codesign to reject the framework with "bundle format is ambiguous",
triggering a fallback path that signs individual binaries without
creating a proper framework bundle signature. Apple's notarization
then rejects all Python.framework binaries with "The signature of
the binary is invalid."

Fix: after copying watchers into the .app bundle, restore the
canonical macOS framework layout using symlinks:
  - Versions/Current -> <version>
  - Python -> Versions/Current/Python
  - Resources -> Versions/Current/Resources

This lets codesign sign the framework as a proper bundle (verified
locally), producing valid bundle signatures that Apple should accept.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 13, 2026

Greptile Summary

This PR inserts a pre-signing step in build_app_tauri.sh that replaces the regular files/directories PyInstaller copies in Python.framework with the correct macOS symlink structure (Versions/Current → <version>, and root-level Python/Resources/Headers pointing into Versions/Current/), resolving the codesign "bundle format is ambiguous" rejection that has been breaking notarization. The existing strip-sign-sync fallback is retained as a safety net. Two minor robustness nits: ls | grep | head for version discovery could emit SIGPIPE under certain conditions and is better replaced with a glob loop, and the -name "*.framework" | grep -i python filter can be simplified to -iname "Python.framework" directly in find.

Confidence Score: 5/5

Safe to merge — the fix is logically correct and all remaining comments are P2 style suggestions.

The symlink restoration logic correctly handles the described PyInstaller layout (Versions/3.9 as the real dir, Current as the copy to replace). Guards against already-correct symlinks are present. The two P2 comments are robustness suggestions that don't affect correctness in the expected build environment.

No files require special attention.

Important Files Changed

Filename Overview
scripts/package/build_app_tauri.sh Adds a pre-signing step to restore canonical macOS framework symlinks (Versions/Current, Python, Resources, Headers) that PyInstaller replaces with plain directories/files. Logic is correct for the common single-version case; minor robustness issues with ls-based version discovery and overly broad grep filter.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[cp -r components into .app] --> B["find Python.framework dirs"]
    B --> C{Any found?}
    C -- No --> G[Set executable permissions]
    C -- Yes --> D["Discover version_dir\n(ls Versions/ | grep -v Current)"]
    D --> E{version_dir empty?}
    E -- Yes --> F[Warning: skip this framework]
    F --> D2[Next framework]
    E -- No --> H{"Versions/Current is\na real dir?"}
    H -- Yes --> I["rm -rf Versions/Current\nln -s version_dir Versions/Current"]
    H -- Already a symlink --> J
    I --> J["For Python, Resources, Headers:\nif real file/dir → replace with symlink"]
    J --> D2
    D2 --> C
    G --> K[Sign .app inside-out\nStep 1: Mach-O leaves\nStep 2: .framework bundles\nStep 3: top-level .app]
Loading

Reviews (1): Last reviewed commit: "fix(build): restore Python.framework sym..." | Re-trigger Greptile

Comment thread scripts/package/build_app_tauri.sh Outdated
while IFS= read -r fw; do
echo " Fixing: $fw"
# Find the actual version directory (e.g., "3.9"), skipping "Current"
version_dir=$(ls "$fw/Versions/" 2>/dev/null | grep -v Current | head -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 ls | grep | head is fragile for version discovery

Parsing ls output in shell scripts is unreliable — locale settings can affect ordering, and grep exits non-zero via SIGPIPE when head -1 terminates the pipeline early. A glob-based approach is safer and sidesteps all of these:

Suggested change
version_dir=$(ls "$fw/Versions/" 2>/dev/null | grep -v Current | head -1)
version_dir=""
for d in "$fw/Versions"/*/; do
bname="$(basename "$d")"
if [ "$bname" != "Current" ] && [ -d "$d" ]; then
version_dir="$bname"
break
fi
done

This also implicitly verifies that the matched entry is an actual directory, guarding against unexpected files in Versions/.

Comment thread scripts/package/build_app_tauri.sh Outdated
Comment on lines +79 to +80
done < <(find "dist/${APP_NAME}.app" -type d -name "*.framework" \
| grep -i python)
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 grep -i python may over-match framework paths

grep -i python applied to the full path can match any framework whose path contains "python" (e.g., a hypothetical SomePythonTool.framework). Using -iname directly in find is more precise and removes the extra pipe:

Suggested change
done < <(find "dist/${APP_NAME}.app" -type d -name "*.framework" \
| grep -i python)
done < <(find "dist/${APP_NAME}.app" -type d -iname "Python.framework")

…fy find

- Replace ls|grep|head with glob loop for version dir discovery (avoids
  SIGPIPE and locale issues)
- Use find -iname "Python.framework" instead of -name "*.framework"|grep
@ErikBjare ErikBjare merged commit e60719b into master Apr 13, 2026
14 of 15 checks passed
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.

Automated nightly/dev releases

1 participant