Skip to content

Commit a41abfb

Browse files
fix(build): replace codesign --deep with inside-out per-binary signing for notarization (#1246)
* fix(build): replace codesign --deep with inside-out per-binary signing codesign --deep fails to reliably sign all nested Mach-O binaries in PyInstaller-built watcher bundles (aw-watcher-window/input/afk each embed hundreds of .dylib/.so files and Python.framework with symlinks). Apple's notarization log showed 502 rejections: - 248 missing secure timestamps - 239 binaries not signed with valid Developer ID certificate - 9 invalid signatures (Python.framework symlink issue) Fix: sign every Mach-O binary individually using `file | grep Mach-O`, working inside-out (leaves → .framework bundles → top-level .app), with --timestamp on every codesign call as required by notarization. Also add --timestamp to the DMG codesign step in both build.yml and build-tauri.yml, which was also missing. Reported in ErikBjare/bob#546 via xcrun notarytool log analysis. * fix(build): address Greptile P2 suggestions — xargs batching and bundle type coverage - Batch Mach-O file detection with `xargs file` (O(1) subprocess calls vs O(n)) for large PyInstaller bundles with hundreds of dylib/so files - Extend bundle signing step to cover .bundle and .plugin directories in addition to .framework, preventing missing CodeResources catalog seals that can trigger notarytool bundle-integrity warnings
1 parent 6211ad5 commit a41abfb

3 files changed

Lines changed: 47 additions & 7 deletions

File tree

.github/workflows/build-tauri.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ jobs:
157157
make dist/ActivityWatch.dmg
158158
159159
if [ -n "$APPLE_EMAIL" ]; then
160-
codesign --verbose -s ${APPLE_PERSONALID} dist/ActivityWatch.dmg
160+
codesign --force --verbose --timestamp -s ${APPLE_PERSONALID} dist/ActivityWatch.dmg
161161
162162
brew install akeru-inc/tap/xcnotary
163163
xcnotary precheck dist/ActivityWatch.app

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ jobs:
168168
169169
# codesign and notarize
170170
if [ -n "$APPLE_EMAIL" ]; then
171-
codesign --verbose -s ${APPLE_PERSONALID} dist/ActivityWatch.dmg
171+
codesign --force --verbose --timestamp -s ${APPLE_PERSONALID} dist/ActivityWatch.dmg
172172
173173
# Run prechecks
174174
brew install akeru-inc/tap/xcnotary

scripts/package/build_app_tauri.sh

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,51 @@ echo "Creating PkgInfo..."
9595
echo "APPL????" > "dist/${APP_NAME}.app/Contents/PkgInfo"
9696

9797
if [ -n "$APPLE_PERSONALID" ]; then
98-
echo "Signing app with identity: $APPLE_PERSONALID"
99-
# Hardened runtime is required for notarization prechecks on macOS.
100-
# The Tauri bundle still embeds PyInstaller-built helpers from dist/activitywatch/,
101-
# so keep the existing entitlements that those binaries need under hardened runtime.
102-
codesign --deep --force --options runtime --entitlements scripts/package/entitlements.plist --sign "$APPLE_PERSONALID" "dist/${APP_NAME}.app"
98+
echo "Signing app with identity: $APPLE_PERSONALID (inside-out, per-binary)"
99+
# codesign --deep is unreliable for bundles with PyInstaller helpers:
100+
# it doesn't reach all nested dylibs/so files and mishandles Python.framework
101+
# symlinks, leaving hundreds of binaries unsigned or invalidly signed.
102+
# The correct approach is inside-out: sign all Mach-O leaves first,
103+
# then .framework bundles, then the top-level .app last.
104+
# --timestamp is required for notarization (Apple rejects submissions without it).
105+
ENTITLEMENTS="scripts/package/entitlements.plist"
106+
107+
sign_binary() {
108+
echo " Signing: $1"
109+
codesign --force --options runtime --timestamp \
110+
--entitlements "$ENTITLEMENTS" \
111+
--sign "$APPLE_PERSONALID" \
112+
"$1"
113+
}
114+
115+
# Step 1: Sign all Mach-O binary files (dylibs, .so files, standalone executables).
116+
# Use `xargs file` to batch all type queries in O(1) subprocess calls instead of
117+
# one `file` invocation per binary (PyInstaller bundles can contain hundreds of files).
118+
# Sort by path length descending so deeper binaries are signed before shallower containers.
119+
echo " Signing Mach-O binary files..."
120+
while IFS= read -r f; do
121+
sign_binary "$f"
122+
done < <(find "dist/${APP_NAME}.app" -type f \
123+
| xargs file \
124+
| grep "Mach-O" \
125+
| cut -d: -f1 \
126+
| awk '{ print length, $0 }' | sort -rn | cut -d' ' -f2-)
127+
128+
# Step 2: Sign bundle directories (.framework, .bundle, .plugin) after their contents.
129+
# Deepest bundles first (sort by path length descending) to maintain inside-out order.
130+
# .bundle/.plugin coverage prevents missing CodeResources catalog seals that can
131+
# trigger notarytool bundle-integrity warnings.
132+
echo " Signing bundle directories (.framework, .bundle, .plugin)..."
133+
while IFS= read -r fw; do
134+
sign_binary "$fw"
135+
done < <(find "dist/${APP_NAME}.app" -type d \
136+
\( -name "*.framework" -o -name "*.bundle" -o -name "*.plugin" \) \
137+
| awk '{ print length, $0 }' | sort -rn | cut -d' ' -f2-)
138+
139+
# Step 3: Sign the top-level .app bundle last.
140+
echo " Signing top-level .app bundle..."
141+
sign_binary "dist/${APP_NAME}.app"
142+
103143
echo "App signing complete."
104144
else
105145
echo "APPLE_PERSONALID not set. Skipping code signing."

0 commit comments

Comments
 (0)