@@ -502,49 +502,117 @@ jobs:
502502 echo "=== dist/ contents ($(find dist -type f | wc -l) files) ==="
503503 find dist -type f | sort
504504
505- # ---- Package: macOS (macdeployqt) ----
506- # Note: upload-artifact strips execute bits (zip limitation).
507- # We tar the dist/ contents so permissions are preserved on download.
508- # macdeployqt (copies ~300MB of Qt frameworks) only runs on tag builds;
509- # push/workflow_dispatch builds skip it to save ~3-5 minutes per job.
505+ # ---- Package: macOS ----
506+ # Order is critical (see issue #139): Python.framework + resources must
507+ # be placed INSIDE the .app BEFORE macdeployqt / codesign run. If we
508+ # copy them as siblings of the .app (old behavior) the bundle can't find
509+ # them at runtime; if we embed after signing, dyld on macOS 14+ kills
510+ # the process with SIGKILL (Code Signature Invalid).
511+ #
512+ # On tag / workflow_dispatch: full stage -> macdeployqt -> deep ad-hoc
513+ # codesign -> verify. On push builds: stage + skip macdeployqt/codesign
514+ # (saves ~3-5 min; the resulting bundle won't run on stock macOS but is
515+ # fine as a CI artifact for inspection).
516+ #
517+ # upload-artifact strips execute bits (zip limitation) so we tar dist/.
510518 - name : Package (macOS)
511519 if : runner.os == 'macOS'
512520 working-directory : fincept-qt
513521 run : |
522+ set -euo pipefail
514523 mkdir -p dist
515524 MACDEPLOYQT="${QT_ROOT_DIR}/bin/macdeployqt"
516- APP="build/${{ matrix.exe }}.app"
517525 IS_TAG="${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}"
518- if [ -d "$APP" ]; then
519- # .app bundle exists — run macdeployqt only on tag builds
520- if [ "$IS_TAG" = "true" ]; then
521- "$MACDEPLOYQT" "$APP" 2>/dev/null || true
522- fi
523- cp -r "$APP" dist/
526+
527+ # ── Locate or synthesize .app bundle ──────────────────────────────
528+ if [ -d "build/${{ matrix.exe }}.app" ]; then
529+ APP_BUNDLE="build/${{ matrix.exe }}.app"
524530 else
525- # Plain binary — create a minimal .app bundle manually
526- BUNDLE="dist/${{ matrix.exe }}.app"
527- mkdir -p "$BUNDLE/Contents/MacOS"
528- cp "build/${{ matrix.exe }}" "$BUNDLE/Contents/MacOS/${{ matrix.exe }}"
529- chmod +x "$BUNDLE/Contents/MacOS/${{ matrix.exe }}"
530- cat > "$BUNDLE/Contents/Info.plist" <<'PLIST'
531+ APP_BUNDLE="build/${{ matrix.exe }}.app"
532+ mkdir -p "${APP_BUNDLE}/Contents/MacOS"
533+ cp "build/${{ matrix.exe }}" "${APP_BUNDLE}/Contents/MacOS/${{ matrix.exe }}"
534+ chmod +x "${APP_BUNDLE}/Contents/MacOS/${{ matrix.exe }}"
535+ cat > "${APP_BUNDLE}/Contents/Info.plist" <<'PLIST'
531536 <?xml version="1.0" encoding="UTF-8"?>
532537 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
533538 <plist version="1.0"><dict>
534539 <key>CFBundleExecutable</key><string>FinceptTerminal</string>
535- <key>CFBundleIdentifier</key><string>in .fincept.terminal</string>
540+ <key>CFBundleIdentifier</key><string>com .fincept.terminal</string>
536541 <key>CFBundleName</key><string>FinceptTerminal</string>
537542 <key>CFBundlePackageType</key><string>APPL</string>
538- <key>CFBundleShortVersionString</key><string>4.0.1</string>
543+ <key>CFBundleShortVersionString</key><string>4.0.2</string>
544+ <key>CFBundleVersion</key><string>4.0.2</string>
545+ <key>LSMinimumSystemVersion</key><string>13.0</string>
539546 <key>NSHighResolutionCapable</key><true/>
540547 </dict></plist>
541548 PLIST
542- if [ "$IS_TAG" = "true" ]; then
543- "$MACDEPLOYQT" "$BUNDLE" 2>/dev/null || true
549+ fi
550+
551+ # ── Stage resources INSIDE the bundle (not as siblings) ───────────
552+ mkdir -p "${APP_BUNDLE}/Contents/Resources"
553+ cp -r resources "${APP_BUNDLE}/Contents/Resources/" 2>/dev/null || true
554+ cp -r scripts "${APP_BUNDLE}/Contents/Resources/scripts" 2>/dev/null || true
555+
556+ # ── Embed Python.framework (tag builds only — skip on push) ───────
557+ if [ "$IS_TAG" = "true" ]; then
558+ PY_FRAMEWORK=""
559+ for candidate in \
560+ "/Library/Frameworks/Python.framework" \
561+ "$(brew --prefix python@3.12 2>/dev/null)/Frameworks/Python.framework" \
562+ "$(brew --prefix python3 2>/dev/null)/Frameworks/Python.framework"; do
563+ [ -d "${candidate}/Versions" ] && { PY_FRAMEWORK="${candidate}"; break; }
564+ done
565+ BUNDLE_FRAMEWORKS="${APP_BUNDLE}/Contents/Frameworks"
566+ mkdir -p "${BUNDLE_FRAMEWORKS}"
567+ if [ -n "${PY_FRAMEWORK}" ]; then
568+ echo "Embedding Python.framework from: ${PY_FRAMEWORK}"
569+ rsync -a --copy-unsafe-links "${PY_FRAMEWORK}/" "${BUNDLE_FRAMEWORKS}/Python.framework/" 2>/dev/null || \
570+ cp -R "${PY_FRAMEWORK}" "${BUNDLE_FRAMEWORKS}/" 2>/dev/null || true
571+ find "${BUNDLE_FRAMEWORKS}/Python.framework" -name "PrivateHeaders" -type l \
572+ ! -exec test -e {} \; -delete 2>/dev/null || true
573+ PY_VER="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
574+ ln -sf "../Frameworks/Python.framework/Versions/${PY_VER}/bin/python${PY_VER}" \
575+ "${APP_BUNDLE}/Contents/MacOS/python3" 2>/dev/null || true
576+ else
577+ echo "::warning::Python.framework not found — bundle will rely on system python"
544578 fi
545579 fi
546- cp -r resources dist/resources 2>/dev/null || true
547- cp -r scripts dist/scripts 2>/dev/null || true
580+
581+ # ── Run macdeployqt on the fully staged bundle (tag builds only) ──
582+ if [ "$IS_TAG" = "true" ] && [ -x "$MACDEPLOYQT" ]; then
583+ echo "Running macdeployqt on ${APP_BUNDLE}"
584+ "$MACDEPLOYQT" "${APP_BUNDLE}" -verbose=1 || \
585+ echo "::warning::macdeployqt exited non-zero — continuing to codesign"
586+ fi
587+
588+ # ── Deep ad-hoc codesign (tag builds only) — fix for issue #139 ───
589+ # Strip every signature leaf-to-root, then re-sign whole bundle with
590+ # the ad-hoc identity ("-"). Do NOT pass --preserve-metadata: there
591+ # are no prior entitlements to preserve on a fresh bundle and the
592+ # flag errors out on resource-fork detritus left by macdeployqt.
593+ if [ "$IS_TAG" = "true" ]; then
594+ echo "Stripping existing signatures..."
595+ find "${APP_BUNDLE}" -type f \( -name "*.dylib" -o -name "*.so" -o -perm +111 \) 2>/dev/null | \
596+ while read -r f; do
597+ if file "$f" 2>/dev/null | grep -q "Mach-O"; then
598+ codesign --remove-signature "$f" 2>/dev/null || true
599+ fi
600+ done
601+ codesign --remove-signature "${APP_BUNDLE}" 2>/dev/null || true
602+
603+ echo "Re-signing bundle ad-hoc (deep)..."
604+ codesign --force --deep --sign - --timestamp=none "${APP_BUNDLE}"
605+
606+ echo "Verifying signature..."
607+ codesign --verify --deep --strict --verbose=2 "${APP_BUNDLE}" || {
608+ echo "::error::Bundle verification failed"
609+ codesign --display --deep --verbose=4 "${APP_BUNDLE}" || true
610+ exit 1
611+ }
612+ fi
613+
614+ # ── Copy signed bundle to dist/ ──────────────────────────────────
615+ cp -R "${APP_BUNDLE}" dist/
548616 # Tar to preserve execute permissions (upload-artifact uses zip which strips them)
549617 tar -czf dist.tar.gz -C dist .
550618
0 commit comments