From 5e623543658581779e4fe7ded53d6d2296a88fe7 Mon Sep 17 00:00:00 2001 From: developer0hye Date: Tue, 17 Feb 2026 10:23:12 +0900 Subject: [PATCH 01/12] ci: add macOS code signing and notarization Replace ad-hoc signing with Developer ID Application certificate signing and Apple notarization for macOS release builds. This eliminates Gatekeeper warnings when users download the DMG. - Import Developer ID certificate from secrets into a temporary keychain - Sign .app bundle with hardened runtime and Apple timestamp - Sign DMG separately - Submit DMG to Apple notary service and staple the ticket - Gracefully fall back to ad-hoc signing when secrets are unavailable (PR/fork builds) - Clean up temporary keychain after the job completes - Add setup documentation in docs/macos-codesigning.md Co-Authored-By: Claude Opus 4.6 Signed-off-by: developer0hye --- .github/workflows/ci-release.yml | 87 +++++++++++++++++++- docs/macos-codesigning.md | 135 +++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 docs/macos-codesigning.md diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 2d3f5ec..3cf7fa5 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -154,6 +154,47 @@ jobs: copy onnxruntime\lib\onnxruntime.dll deploy\ windeployqt deploy\YoloLabel.exe --release --no-translations + # ── macOS: import signing certificate ──────────────────── + - name: Import signing certificate (macOS) + if: runner.os == 'macOS' + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + run: | + if [ -z "$MACOS_CERTIFICATE" ]; then + echo "No signing certificate found, will use ad-hoc signing" + exit 0 + fi + + # Decode the certificate + echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12 + + # Create a temporary keychain + KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db" + KEYCHAIN_PASSWORD="$(openssl rand -hex 20)" + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + # Import certificate into the keychain + security import certificate.p12 -P "$MACOS_CERTIFICATE_PASSWORD" \ + -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple: \ + -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + # Add temporary keychain to the search list + security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"') + + # Verify signing identity + security find-identity -v -p codesigning "$KEYCHAIN_PATH" + + # Clean up certificate file + rm -f certificate.p12 + + # Export for later steps + echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" + echo "CODESIGN_IDENTITY=Developer ID Application" >> "$GITHUB_ENV" + # ── Package: macOS ──────────────────────────────────────── - name: Package (macOS) if: runner.os == 'macOS' @@ -162,8 +203,50 @@ jobs: cp onnxruntime/lib/libonnxruntime.*.dylib YoloLabel.app/Contents/Frameworks/ install_name_tool -add_rpath @executable_path/../Frameworks YoloLabel.app/Contents/MacOS/YoloLabel || true macdeployqt YoloLabel.app - codesign --force --deep --sign - YoloLabel.app - hdiutil create -volname YoloLabel -srcfolder YoloLabel.app -ov -format UDZO "${{ matrix.artifact_name }}.dmg" + + if [ -n "$CODESIGN_IDENTITY" ]; then + echo "Signing with: $CODESIGN_IDENTITY" + # Sign inside-out: frameworks/dylibs first, then the main bundle + find YoloLabel.app/Contents/Frameworks -type f \( -name '*.dylib' -o -name '*.so' \) \ + -exec codesign --force --sign "$CODESIGN_IDENTITY" --options runtime --timestamp {} \; + find YoloLabel.app/Contents/Frameworks -type d -name '*.framework' \ + -exec codesign --force --sign "$CODESIGN_IDENTITY" --options runtime --timestamp {} \; + codesign --force --sign "$CODESIGN_IDENTITY" \ + --options runtime --timestamp YoloLabel.app + else + echo "No certificate available, using ad-hoc signing" + codesign --force --deep --sign - YoloLabel.app + fi + + hdiutil create -volname YoloLabel -srcfolder YoloLabel.app \ + -ov -format UDZO "${{ matrix.artifact_name }}.dmg" + + if [ -n "$CODESIGN_IDENTITY" ]; then + codesign --force --sign "$CODESIGN_IDENTITY" \ + --timestamp "${{ matrix.artifact_name }}.dmg" + fi + + # ── macOS: notarize DMG ───────────────────────────────── + - name: Notarize DMG (macOS) + if: runner.os == 'macOS' && env.CODESIGN_IDENTITY != '' + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + xcrun notarytool submit "${{ matrix.artifact_name }}.dmg" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_ID_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + + xcrun stapler staple "${{ matrix.artifact_name }}.dmg" + + # ── macOS: cleanup keychain ────────────────────────────── + - name: Cleanup keychain (macOS) + if: always() && runner.os == 'macOS' && env.KEYCHAIN_PATH != '' + run: | + security delete-keychain "$KEYCHAIN_PATH" # ── Package: Linux ──────────────────────────────────────── - name: Package (Linux) diff --git a/docs/macos-codesigning.md b/docs/macos-codesigning.md new file mode 100644 index 0000000..422afc6 --- /dev/null +++ b/docs/macos-codesigning.md @@ -0,0 +1,135 @@ +# macOS Code Signing & Notarization Setup + +This document explains how to configure GitHub Actions secrets so that CI can sign and notarize macOS builds with a Developer ID certificate. Once set up, release DMGs will pass Gatekeeper without any warnings. + +## Prerequisites + +- An [Apple Developer Program](https://developer.apple.com/programs/) membership (US$99/year) +- A **Developer ID Application** certificate issued by Apple +- Xcode or Keychain Access on a Mac (to export the certificate) + +## Required Secrets + +Add the following secrets to your GitHub repository under **Settings > Secrets and variables > Actions**: + +| Secret | Description | +|---|---| +| `MACOS_CERTIFICATE` | Base64-encoded `.p12` file containing the Developer ID Application certificate and private key | +| `MACOS_CERTIFICATE_PASSWORD` | Password used when exporting the `.p12` file | +| `APPLE_ID` | Apple ID email used for notarization (e.g., `you@example.com`) | +| `APPLE_ID_PASSWORD` | App-specific password for the Apple ID (NOT your regular Apple ID password) | +| `APPLE_TEAM_ID` | 10-character Apple Developer Team ID | + +## Step-by-Step Setup + +### 1. Export the Developer ID Certificate as `.p12` + +You need the **Developer ID Application** certificate with its private key. + +**Option A — Using Keychain Access:** +1. Open **Keychain Access** on your Mac +2. In the sidebar, select **login** keychain and the **My Certificates** category +3. Find **Developer ID Application: Your Name (TEAM_ID)** +4. Right-click the certificate > **Export...** +5. Choose **Personal Information Exchange (.p12)** format +6. Set a strong password (this becomes `MACOS_CERTIFICATE_PASSWORD`) +7. Save the file (e.g., `certificate.p12`) + +**Option B — Using the command line:** +```bash +# List available signing identities +security find-identity -v -p codesigning + +# Export (replace the hash with your certificate's SHA-1) +security export -k ~/Library/Keychains/login.keychain-db \ + -t identities -f pkcs12 -o certificate.p12 +``` + +### 2. Base64-Encode the Certificate + +```bash +base64 -i certificate.p12 | pbcopy +``` + +This copies the base64 string to your clipboard. Paste it as the value of the `MACOS_CERTIFICATE` secret on GitHub. + +> **Security note:** Delete the `.p12` file from disk after you have added the secret. + +### 3. Create an App-Specific Password + +Apple requires an **app-specific password** for notarization (your regular Apple ID password will not work). + +1. Go to [appleid.apple.com](https://appleid.apple.com/) +2. Sign in and navigate to **Sign-In and Security > App-Specific Passwords** +3. Click **Generate an app-specific password** +4. Give it a label (e.g., `GitHub Actions Notarization`) +5. Copy the generated password and save it as the `APPLE_ID_PASSWORD` secret + +### 4. Find Your Team ID + +```bash +# If you have Xcode installed: +xcrun simctl list 2>/dev/null | head -1 +# Or check https://developer.apple.com/account → Membership Details +``` + +Your Team ID is a 10-character alphanumeric string (e.g., `A1B2C3D4E5`). + +### 5. Add Secrets to GitHub + +Go to your repository on GitHub: + +1. **Settings** > **Secrets and variables** > **Actions** +2. Click **New repository secret** for each: + - `MACOS_CERTIFICATE` — the base64 string from step 2 + - `MACOS_CERTIFICATE_PASSWORD` — the password from step 1 + - `APPLE_ID` — your Apple ID email + - `APPLE_ID_PASSWORD` — the app-specific password from step 3 + - `APPLE_TEAM_ID` — the Team ID from step 4 + +## How It Works + +The CI workflow (`.github/workflows/ci-release.yml`) handles signing and notarization automatically: + +1. **Certificate import** — Decodes the `.p12` from the secret, creates a temporary keychain, and imports the certificate +2. **Code signing** — Signs the `.app` bundle with `Developer ID Application`, hardened runtime (`--options runtime`), and an Apple timestamp (`--timestamp`). The DMG is also signed separately. +3. **Notarization** — Submits the signed DMG to Apple's notary service via `xcrun notarytool` and waits for approval +4. **Stapling** — Attaches the notarization ticket to the DMG via `xcrun stapler`, so Gatekeeper can verify it offline +5. **Cleanup** — Deletes the temporary keychain + +### PR / Fork Builds (No Secrets) + +When secrets are not available (e.g., pull requests from forks), the workflow gracefully falls back to **ad-hoc signing** (`codesign --sign -`). The build will not break — it simply won't be notarized. This is the same behavior as before this feature was added. + +## Verification + +After a release build completes, download the DMG and verify: + +```bash +# Check code signature +codesign --verify --deep --strict --verbose=2 /Volumes/YoloLabel/YoloLabel.app + +# Check notarization +spctl --assess --type open --context context:primary-signature YoloLabel-macOS.dmg + +# Check stapled ticket +stapler validate YoloLabel-macOS.dmg +``` + +## Troubleshooting + +### "Developer ID Application" identity not found +- Ensure the `.p12` file contains both the certificate **and** the private key +- Verify the certificate is a **Developer ID Application** type (not Developer ID Installer or other types) + +### Notarization fails with "Invalid credentials" +- Confirm `APPLE_ID_PASSWORD` is an **app-specific password**, not your regular Apple ID password +- Ensure the Apple ID is associated with the same team as the Developer ID certificate + +### Notarization fails with "Hardened Runtime not enabled" +- The `--options runtime` flag must be present in the `codesign` command +- Ensure all nested binaries (dylibs in Frameworks/) are also signed — `--deep` handles this + +### Notarization times out +- Apple's notary service usually completes within 5-15 minutes, but can occasionally take longer +- Re-running the workflow will resubmit the DMG From a32cd2fb4218cade87fd7c0e5fd5018cb44e7893 Mon Sep 17 00:00:00 2001 From: developer0hye Date: Tue, 17 Feb 2026 10:43:20 +0900 Subject: [PATCH 02/12] fix: re-sign all nested dylibs including PlugIns for hardened runtime The Qt platform plugin (libqcocoa.dylib) in PlugIns/ was still signed by Qt Company's Team ID, causing library validation failure under hardened runtime. Widen the find scope from Contents/Frameworks to Contents so all nested dylibs are re-signed with our Developer ID. Co-Authored-By: Claude Opus 4.6 Signed-off-by: developer0hye --- .github/workflows/ci-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 3cf7fa5..f55fec5 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -206,8 +206,8 @@ jobs: if [ -n "$CODESIGN_IDENTITY" ]; then echo "Signing with: $CODESIGN_IDENTITY" - # Sign inside-out: frameworks/dylibs first, then the main bundle - find YoloLabel.app/Contents/Frameworks -type f \( -name '*.dylib' -o -name '*.so' \) \ + # Sign inside-out: all nested binaries first, then the main bundle + find YoloLabel.app/Contents -type f \( -name '*.dylib' -o -name '*.so' \) \ -exec codesign --force --sign "$CODESIGN_IDENTITY" --options runtime --timestamp {} \; find YoloLabel.app/Contents/Frameworks -type d -name '*.framework' \ -exec codesign --force --sign "$CODESIGN_IDENTITY" --options runtime --timestamp {} \; From 0960ae81940eb7bbe8ec79cc05352b959b5daaaa Mon Sep 17 00:00:00 2001 From: developer0hye Date: Tue, 17 Feb 2026 12:05:52 +0900 Subject: [PATCH 03/12] fix: add timeout and error logging to notarization step notarytool submit --wait was hanging indefinitely when notarization failed. Add --timeout 10m to the command and timeout-minutes: 15 to the step. On failure, fetch the notarization log to diagnose the issue. Signed-off-by: developer0hye --- .github/workflows/ci-release.yml | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index f55fec5..77d5e1d 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -229,16 +229,37 @@ jobs: # ── macOS: notarize DMG ───────────────────────────────── - name: Notarize DMG (macOS) if: runner.os == 'macOS' && env.CODESIGN_IDENTITY != '' + timeout-minutes: 15 env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | - xcrun notarytool submit "${{ matrix.artifact_name }}.dmg" \ + # Submit and wait with a 10-minute timeout + SUBMIT_OUTPUT=$(xcrun notarytool submit "${{ matrix.artifact_name }}.dmg" \ --apple-id "$APPLE_ID" \ --password "$APPLE_ID_PASSWORD" \ --team-id "$APPLE_TEAM_ID" \ - --wait + --wait --timeout 10m 2>&1) || true + + echo "$SUBMIT_OUTPUT" + + # Extract submission ID for log retrieval on failure + SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1) + + if echo "$SUBMIT_OUTPUT" | grep -q "status: Accepted"; then + echo "Notarization accepted" + else + echo "::error::Notarization did not succeed" + if [ -n "$SUBMISSION_ID" ]; then + echo "Fetching notarization log for $SUBMISSION_ID..." + xcrun notarytool log "$SUBMISSION_ID" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_ID_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" 2>&1 || true + fi + exit 1 + fi xcrun stapler staple "${{ matrix.artifact_name }}.dmg" From 6281dc0c085fb6c3e249bd61f47f37666875bbfd Mon Sep 17 00:00:00 2001 From: developer0hye Date: Tue, 17 Feb 2026 12:19:59 +0900 Subject: [PATCH 04/12] fix: increase notarization timeout and add signature verification Apple notarization timed out at 10m with "In Progress" status, likely due to the deeper code signing (hardened runtime on all nested dylibs). Increase notarytool timeout to 30m and step timeout to 35m to allow Apple's service enough time to complete. Also add codesign --verify before DMG creation to catch signing issues early. Signed-off-by: developer0hye --- .github/workflows/ci-release.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 77d5e1d..7ec9d7a 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -213,6 +213,10 @@ jobs: -exec codesign --force --sign "$CODESIGN_IDENTITY" --options runtime --timestamp {} \; codesign --force --sign "$CODESIGN_IDENTITY" \ --options runtime --timestamp YoloLabel.app + + # Verify the signature before proceeding + echo "Verifying code signature..." + codesign --verify --deep --strict --verbose=2 YoloLabel.app else echo "No certificate available, using ad-hoc signing" codesign --force --deep --sign - YoloLabel.app @@ -229,18 +233,18 @@ jobs: # ── macOS: notarize DMG ───────────────────────────────── - name: Notarize DMG (macOS) if: runner.os == 'macOS' && env.CODESIGN_IDENTITY != '' - timeout-minutes: 15 + timeout-minutes: 35 env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | - # Submit and wait with a 10-minute timeout + # Submit and wait with a 30-minute timeout SUBMIT_OUTPUT=$(xcrun notarytool submit "${{ matrix.artifact_name }}.dmg" \ --apple-id "$APPLE_ID" \ --password "$APPLE_ID_PASSWORD" \ --team-id "$APPLE_TEAM_ID" \ - --wait --timeout 10m 2>&1) || true + --wait --timeout 30m 2>&1) || true echo "$SUBMIT_OUTPUT" From 8613456344a1084809473e18964470e4506c2a0e Mon Sep 17 00:00:00 2001 From: developer0hye Date: Tue, 17 Feb 2026 12:55:43 +0900 Subject: [PATCH 05/12] fix: split notarization into submit + poll for better diagnostics notarytool submit --wait hangs indefinitely with no diagnostic output. Split into explicit submit + poll loop with notarytool info so each poll iteration logs the full status. Also add debug steps to: - list all Mach-O binaries in the app bundle - show notarytool history (previous submission statuses) Signed-off-by: developer0hye --- .github/workflows/ci-release.yml | 70 ++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 7ec9d7a..960566a 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -233,39 +233,77 @@ jobs: # ── macOS: notarize DMG ───────────────────────────────── - name: Notarize DMG (macOS) if: runner.os == 'macOS' && env.CODESIGN_IDENTITY != '' - timeout-minutes: 35 + timeout-minutes: 25 env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | - # Submit and wait with a 30-minute timeout - SUBMIT_OUTPUT=$(xcrun notarytool submit "${{ matrix.artifact_name }}.dmg" \ + # Debug: list all signed binaries and their signature status + echo "=== Signed binaries in the app bundle ===" + find YoloLabel.app -type f -perm +111 -exec sh -c \ + 'file "$1" | grep -q Mach-O && echo "$1"' _ {} \; 2>/dev/null || true + echo "" + echo "=== Checking previous submission statuses ===" + xcrun notarytool history \ --apple-id "$APPLE_ID" \ --password "$APPLE_ID_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" \ - --wait --timeout 30m 2>&1) || true + --team-id "$APPLE_TEAM_ID" 2>&1 | head -30 || true + echo "" + # Submit without --wait so we can poll manually with verbose output + echo "=== Submitting DMG for notarization ===" + SUBMIT_OUTPUT=$(xcrun notarytool submit "${{ matrix.artifact_name }}.dmg" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_ID_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" 2>&1) echo "$SUBMIT_OUTPUT" - # Extract submission ID for log retrieval on failure SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1) - - if echo "$SUBMIT_OUTPUT" | grep -q "status: Accepted"; then - echo "Notarization accepted" - else - echo "::error::Notarization did not succeed" - if [ -n "$SUBMISSION_ID" ]; then - echo "Fetching notarization log for $SUBMISSION_ID..." + if [ -z "$SUBMISSION_ID" ]; then + echo "::error::Failed to submit for notarization" + exit 1 + fi + echo "Submission ID: $SUBMISSION_ID" + + # Poll for status with explicit logging + echo "=== Polling for notarization result ===" + MAX_ATTEMPTS=40 # 40 * 30s = 20 minutes + for i in $(seq 1 $MAX_ATTEMPTS); do + sleep 30 + INFO_OUTPUT=$(xcrun notarytool info "$SUBMISSION_ID" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_ID_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" 2>&1) || true + echo "[$i/$MAX_ATTEMPTS] $(date -u '+%H:%M:%S') $INFO_OUTPUT" + + if echo "$INFO_OUTPUT" | grep -q "status: Accepted"; then + echo "Notarization accepted!" + xcrun stapler staple "${{ matrix.artifact_name }}.dmg" + exit 0 + elif echo "$INFO_OUTPUT" | grep -q "status: Invalid"; then + echo "::error::Notarization rejected (Invalid)" xcrun notarytool log "$SUBMISSION_ID" \ --apple-id "$APPLE_ID" \ --password "$APPLE_ID_PASSWORD" \ --team-id "$APPLE_TEAM_ID" 2>&1 || true + exit 1 + elif echo "$INFO_OUTPUT" | grep -q "status: Rejected"; then + echo "::error::Notarization rejected" + xcrun notarytool log "$SUBMISSION_ID" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_ID_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" 2>&1 || true + exit 1 fi - exit 1 - fi + done - xcrun stapler staple "${{ matrix.artifact_name }}.dmg" + echo "::error::Notarization timed out after $MAX_ATTEMPTS attempts" + xcrun notarytool info "$SUBMISSION_ID" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_ID_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" 2>&1 || true + exit 1 # ── macOS: cleanup keychain ────────────────────────────── - name: Cleanup keychain (macOS) From d465bda087e1ca627b148728b682b5a498444e96 Mon Sep 17 00:00:00 2001 From: developer0hye Date: Tue, 17 Feb 2026 13:27:13 +0900 Subject: [PATCH 06/12] fix: use --deep signing with entitlements instead of inside-out The inside-out signing approach (individually signing each dylib with --options runtime) causes Apple's notary service to hang indefinitely -- all 5 such submissions remain stuck at "In Progress" after 2+ hours. Switch back to --deep signing (which completed notarization in 59s) but add an entitlements.plist with: - allow-unsigned-executable-memory: needed by ONNX Runtime - disable-library-validation: allow loading bundled dylibs This should fix both the notarization hang and the app launch issue. Signed-off-by: developer0hye --- .github/workflows/ci-release.yml | 13 ++++++------- scripts/entitlements.plist | 10 ++++++++++ 2 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 scripts/entitlements.plist diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 960566a..187c71c 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -206,13 +206,12 @@ jobs: if [ -n "$CODESIGN_IDENTITY" ]; then echo "Signing with: $CODESIGN_IDENTITY" - # Sign inside-out: all nested binaries first, then the main bundle - find YoloLabel.app/Contents -type f \( -name '*.dylib' -o -name '*.so' \) \ - -exec codesign --force --sign "$CODESIGN_IDENTITY" --options runtime --timestamp {} \; - find YoloLabel.app/Contents/Frameworks -type d -name '*.framework' \ - -exec codesign --force --sign "$CODESIGN_IDENTITY" --options runtime --timestamp {} \; - codesign --force --sign "$CODESIGN_IDENTITY" \ - --options runtime --timestamp YoloLabel.app + # Sign the entire bundle with --deep and entitlements for hardened runtime. + # Entitlements allow ONNX Runtime's dynamic memory allocation under hardened runtime. + codesign --force --deep --sign "$CODESIGN_IDENTITY" \ + --options runtime --timestamp \ + --entitlements scripts/entitlements.plist \ + YoloLabel.app # Verify the signature before proceeding echo "Verifying code signature..." diff --git a/scripts/entitlements.plist b/scripts/entitlements.plist new file mode 100644 index 0000000..7cd9df0 --- /dev/null +++ b/scripts/entitlements.plist @@ -0,0 +1,10 @@ + + + + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + From 2b246c10f4d12aead179b525c42fb022a7bd016e Mon Sep 17 00:00:00 2001 From: developer0hye Date: Tue, 17 Feb 2026 13:37:37 +0900 Subject: [PATCH 07/12] fix: add sleep before hdiutil to avoid resource busy error hdiutil create fails with "Resource busy" immediately after codesign because macOS hasn't fully released locks on the app bundle yet. Add a brief sleep to let the system release resource locks. Signed-off-by: developer0hye --- .github/workflows/ci-release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 187c71c..5746844 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -221,6 +221,8 @@ jobs: codesign --force --deep --sign - YoloLabel.app fi + # Brief pause to release resource locks after code signing + sleep 2 hdiutil create -volname YoloLabel -srcfolder YoloLabel.app \ -ov -format UDZO "${{ matrix.artifact_name }}.dmg" From f32c95cb9d7f2777b73e13eb7780e44536d47a87 Mon Sep 17 00:00:00 2001 From: developer0hye Date: Tue, 17 Feb 2026 13:51:54 +0900 Subject: [PATCH 08/12] fix: apply hardened runtime only to main bundle, not nested code Apple's notary hangs indefinitely when nested dylibs/frameworks are signed with --options runtime (hardened runtime). All 5+ submissions with hardened-runtime nested code remain "In Progress" after hours, while the original submission without it was accepted in 59 seconds. New approach: sign nested code (dylibs, frameworks, plugins) with Developer ID + timestamp only, then sign the main .app bundle with --options runtime + entitlements. The entitlements grant: - allow-unsigned-executable-memory (ONNX Runtime needs this) - disable-library-validation (allow loading bundled dylibs) Signed-off-by: developer0hye --- .github/workflows/ci-release.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 5746844..ab4307e 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -206,9 +206,14 @@ jobs: if [ -n "$CODESIGN_IDENTITY" ]; then echo "Signing with: $CODESIGN_IDENTITY" - # Sign the entire bundle with --deep and entitlements for hardened runtime. - # Entitlements allow ONNX Runtime's dynamic memory allocation under hardened runtime. - codesign --force --deep --sign "$CODESIGN_IDENTITY" \ + # Sign nested code (frameworks, dylibs, plugins) with Developer ID + # but WITHOUT --options runtime to avoid Apple notarization hang. + find YoloLabel.app/Contents -type f \( -name '*.dylib' -o -name '*.so' \) \ + -exec codesign --force --sign "$CODESIGN_IDENTITY" --timestamp {} \; + find YoloLabel.app/Contents/Frameworks -type d -name '*.framework' \ + -exec codesign --force --sign "$CODESIGN_IDENTITY" --timestamp {} \; + # Sign main bundle with hardened runtime + entitlements + codesign --force --sign "$CODESIGN_IDENTITY" \ --options runtime --timestamp \ --entitlements scripts/entitlements.plist \ YoloLabel.app From caed62ebb1a324b3eb7c226bcc9c9511c7916c14 Mon Sep 17 00:00:00 2001 From: developer0hye Date: Tue, 17 Feb 2026 14:07:53 +0900 Subject: [PATCH 09/12] fix: revert to single --deep codesign pass to match first working build All approaches using individual codesign invocations (find+exec on each dylib/framework) cause Apple's notary to hang permanently. The only build that passed notarization (a674dc2d, 59 seconds) used a single codesign --deep --sign pass. Revert to this exact approach: one codesign --force --deep call with --options runtime and --timestamp on the .app bundle. Remove entitlements for now to exactly match what worked before. Entitlements can be added back once notarization is confirmed working. Signed-off-by: developer0hye --- .github/workflows/ci-release.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index ab4307e..0e1ec66 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -206,17 +206,11 @@ jobs: if [ -n "$CODESIGN_IDENTITY" ]; then echo "Signing with: $CODESIGN_IDENTITY" - # Sign nested code (frameworks, dylibs, plugins) with Developer ID - # but WITHOUT --options runtime to avoid Apple notarization hang. - find YoloLabel.app/Contents -type f \( -name '*.dylib' -o -name '*.so' \) \ - -exec codesign --force --sign "$CODESIGN_IDENTITY" --timestamp {} \; - find YoloLabel.app/Contents/Frameworks -type d -name '*.framework' \ - -exec codesign --force --sign "$CODESIGN_IDENTITY" --timestamp {} \; - # Sign main bundle with hardened runtime + entitlements - codesign --force --sign "$CODESIGN_IDENTITY" \ - --options runtime --timestamp \ - --entitlements scripts/entitlements.plist \ - YoloLabel.app + # Use --deep to sign the entire bundle in a single pass. + # Apple's notary hangs when nested code is individually signed + # with separate codesign invocations (find+exec approach). + codesign --force --deep --sign "$CODESIGN_IDENTITY" \ + --options runtime --timestamp YoloLabel.app # Verify the signature before proceeding echo "Verifying code signature..." From 24d969c1d6d1d0fb918d6c7bf8c86e3104e2b7de Mon Sep 17 00:00:00 2001 From: developer0hye Date: Tue, 17 Feb 2026 14:53:29 +0900 Subject: [PATCH 10/12] chore: clean up macOS signing workflow and add entitlements Remove debugging artifacts (Mach-O listing, submission history, manual polling loop) that were added during Apple notarization service outage investigation. Replace with clean notarytool submit --wait --timeout. Add entitlements.plist to codesign command for hardened runtime compatibility. Update troubleshooting docs with findings from debugging. Co-Authored-By: Claude Opus 4.6 Signed-off-by: developer0hye --- .github/workflows/ci-release.yml | 77 ++++++-------------------------- docs/macos-codesigning.md | 32 ++++++++++++- 2 files changed, 44 insertions(+), 65 deletions(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 0e1ec66..4beecc4 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -206,21 +206,16 @@ jobs: if [ -n "$CODESIGN_IDENTITY" ]; then echo "Signing with: $CODESIGN_IDENTITY" - # Use --deep to sign the entire bundle in a single pass. - # Apple's notary hangs when nested code is individually signed - # with separate codesign invocations (find+exec approach). codesign --force --deep --sign "$CODESIGN_IDENTITY" \ - --options runtime --timestamp YoloLabel.app - - # Verify the signature before proceeding - echo "Verifying code signature..." + --options runtime --timestamp \ + --entitlements scripts/entitlements.plist \ + YoloLabel.app codesign --verify --deep --strict --verbose=2 YoloLabel.app else echo "No certificate available, using ad-hoc signing" codesign --force --deep --sign - YoloLabel.app fi - # Brief pause to release resource locks after code signing sleep 2 hdiutil create -volname YoloLabel -srcfolder YoloLabel.app \ -ov -format UDZO "${{ matrix.artifact_name }}.dmg" @@ -239,71 +234,25 @@ jobs: APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | - # Debug: list all signed binaries and their signature status - echo "=== Signed binaries in the app bundle ===" - find YoloLabel.app -type f -perm +111 -exec sh -c \ - 'file "$1" | grep -q Mach-O && echo "$1"' _ {} \; 2>/dev/null || true - echo "" - echo "=== Checking previous submission statuses ===" - xcrun notarytool history \ - --apple-id "$APPLE_ID" \ - --password "$APPLE_ID_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" 2>&1 | head -30 || true - echo "" - - # Submit without --wait so we can poll manually with verbose output - echo "=== Submitting DMG for notarization ===" SUBMIT_OUTPUT=$(xcrun notarytool submit "${{ matrix.artifact_name }}.dmg" \ --apple-id "$APPLE_ID" \ --password "$APPLE_ID_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" 2>&1) - echo "$SUBMIT_OUTPUT" - - SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1) - if [ -z "$SUBMISSION_ID" ]; then - echo "::error::Failed to submit for notarization" - exit 1 - fi - echo "Submission ID: $SUBMISSION_ID" - - # Poll for status with explicit logging - echo "=== Polling for notarization result ===" - MAX_ATTEMPTS=40 # 40 * 30s = 20 minutes - for i in $(seq 1 $MAX_ATTEMPTS); do - sleep 30 - INFO_OUTPUT=$(xcrun notarytool info "$SUBMISSION_ID" \ - --apple-id "$APPLE_ID" \ - --password "$APPLE_ID_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" 2>&1) || true - echo "[$i/$MAX_ATTEMPTS] $(date -u '+%H:%M:%S') $INFO_OUTPUT" - - if echo "$INFO_OUTPUT" | grep -q "status: Accepted"; then - echo "Notarization accepted!" - xcrun stapler staple "${{ matrix.artifact_name }}.dmg" - exit 0 - elif echo "$INFO_OUTPUT" | grep -q "status: Invalid"; then - echo "::error::Notarization rejected (Invalid)" + --team-id "$APPLE_TEAM_ID" \ + --wait --timeout 20m 2>&1) || { + echo "$SUBMIT_OUTPUT" + SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1) + if [ -n "$SUBMISSION_ID" ]; then + echo "--- Notarization log ---" xcrun notarytool log "$SUBMISSION_ID" \ --apple-id "$APPLE_ID" \ --password "$APPLE_ID_PASSWORD" \ --team-id "$APPLE_TEAM_ID" 2>&1 || true - exit 1 - elif echo "$INFO_OUTPUT" | grep -q "status: Rejected"; then - echo "::error::Notarization rejected" - xcrun notarytool log "$SUBMISSION_ID" \ - --apple-id "$APPLE_ID" \ - --password "$APPLE_ID_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" 2>&1 || true - exit 1 fi - done + exit 1 + } + echo "$SUBMIT_OUTPUT" - echo "::error::Notarization timed out after $MAX_ATTEMPTS attempts" - xcrun notarytool info "$SUBMISSION_ID" \ - --apple-id "$APPLE_ID" \ - --password "$APPLE_ID_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" 2>&1 || true - exit 1 + xcrun stapler staple "${{ matrix.artifact_name }}.dmg" # ── macOS: cleanup keychain ────────────────────────────── - name: Cleanup keychain (macOS) diff --git a/docs/macos-codesigning.md b/docs/macos-codesigning.md index 422afc6..700ef37 100644 --- a/docs/macos-codesigning.md +++ b/docs/macos-codesigning.md @@ -122,13 +122,43 @@ stapler validate YoloLabel-macOS.dmg - Ensure the `.p12` file contains both the certificate **and** the private key - Verify the certificate is a **Developer ID Application** type (not Developer ID Installer or other types) +### "The user name or passphrase you entered is not correct" +- The `MACOS_CERTIFICATE_PASSWORD` secret is empty or does not match the `.p12` export password +- **Secret name matters**: verify the name is exactly `MACOS_CERTIFICATE_PASSWORD` (not `MACOS_CERTIFICATE_PWD` or other variations). Use `gh secret list` to check registered secret names. +- If you forgot the password, re-export the certificate from Keychain Access with a new password and update both `MACOS_CERTIFICATE` and `MACOS_CERTIFICATE_PASSWORD` secrets + ### Notarization fails with "Invalid credentials" - Confirm `APPLE_ID_PASSWORD` is an **app-specific password**, not your regular Apple ID password - Ensure the Apple ID is associated with the same team as the Developer ID certificate +### App crashes with "Could not load the Qt platform plugin" after signing + +This happens when hardened runtime is enabled (`--options runtime`) but not all nested binaries are re-signed with the same Developer ID. + +**Root cause:** `macdeployqt` copies Qt plugins (e.g., `libqcocoa.dylib`) into `Contents/PlugIns/`. These plugins retain Qt Company's original signature. With hardened runtime, macOS enforces **library validation** — all loaded dylibs must be signed by the same Team ID as the main executable. If a plugin is signed by a different team (Qt Company vs. your Developer ID), the app fails to load it at runtime. + +**Solution:** Sign **all** dylibs under `Contents/` (not just `Contents/Frameworks/`): +```bash +# Wrong: only signs Frameworks, misses PlugIns +find YoloLabel.app/Contents/Frameworks -name '*.dylib' -exec codesign ... + +# Correct: signs everything under Contents (Frameworks + PlugIns) +find YoloLabel.app/Contents -name '*.dylib' -exec codesign ... +``` + +**How to diagnose:** Compare signing authorities across binaries: +```bash +# Check main binary +codesign -d --verbose=2 YoloLabel.app/Contents/MacOS/YoloLabel 2>&1 | grep Authority + +# Check a plugin +codesign -d --verbose=2 YoloLabel.app/Contents/PlugIns/platforms/libqcocoa.dylib 2>&1 | grep Authority +``` +If the `Authority` lines show different Team IDs, the plugin needs to be re-signed. + ### Notarization fails with "Hardened Runtime not enabled" - The `--options runtime` flag must be present in the `codesign` command -- Ensure all nested binaries (dylibs in Frameworks/) are also signed — `--deep` handles this +- Ensure all nested binaries (dylibs in Frameworks/ **and** PlugIns/) are also signed with `--options runtime` ### Notarization times out - Apple's notary service usually completes within 5-15 minutes, but can occasionally take longer From 2bef23f3758a90940111f484ddf30d8f24dd2317 Mon Sep 17 00:00:00 2001 From: developer0hye Date: Wed, 18 Feb 2026 00:13:59 +0900 Subject: [PATCH 11/12] fix: use inside-out code signing and improve macOS packaging - Replace deprecated `codesign --deep` with inside-out signing per Apple's current requirements (macOS 13+): sign dylibs, then framework bundles, then plugins, then the main app bundle last. Entitlements are only applied to the main executable. - Run macdeployqt before copying ONNX Runtime dylibs to prevent macdeployqt from interfering with them. - Use `cp -a` with broader glob to copy both versioned dylib and symlink (libonnxruntime.dylib -> libonnxruntime.1.24.1.dylib). - Suppress only "would duplicate" rpath warnings instead of hiding all errors with `|| true`. - Restrict notarization to tag pushes (releases) to avoid unnecessary Apple API calls on every master push. Signed-off-by: developer0hye Co-Authored-By: Claude Opus 4.6 Signed-off-by: developer0hye --- .github/workflows/ci-release.yml | 56 ++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 4beecc4..39636de 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -199,35 +199,77 @@ jobs: - name: Package (macOS) if: runner.os == 'macOS' run: | - mkdir -p YoloLabel.app/Contents/Frameworks - cp onnxruntime/lib/libonnxruntime.*.dylib YoloLabel.app/Contents/Frameworks/ - install_name_tool -add_rpath @executable_path/../Frameworks YoloLabel.app/Contents/MacOS/YoloLabel || true + # 1. Bundle Qt frameworks and plugins (do NOT pass -codesign + # because macdeployqt has a known bug that skips signing + # .framework bundle directories) macdeployqt YoloLabel.app + # 2. Copy ONNX Runtime dylibs into Frameworks (after macdeployqt + # so it does not interfere with them) + cp -a onnxruntime/lib/libonnxruntime*.dylib \ + YoloLabel.app/Contents/Frameworks/ + + # 3. Ensure @executable_path/../Frameworks rpath exists. + # macdeployqt usually adds this, but we add it defensively; + # ignore "already exists" error. + install_name_tool -add_rpath @executable_path/../Frameworks \ + YoloLabel.app/Contents/MacOS/YoloLabel 2>&1 \ + | grep -v "would duplicate" || true + + # 4. Code signing — inside-out per Apple requirements. + # (codesign --deep is deprecated for signing as of macOS 13; + # entitlements must only be applied to the main executable) if [ -n "$CODESIGN_IDENTITY" ]; then echo "Signing with: $CODESIGN_IDENTITY" - codesign --force --deep --sign "$CODESIGN_IDENTITY" \ + + # 4a. Sign all dylibs in Frameworks/ + find YoloLabel.app/Contents/Frameworks -name "*.dylib" -exec \ + codesign --force --sign "$CODESIGN_IDENTITY" \ + --options runtime --timestamp {} \; + + # 4b. Sign .framework bundles (inside-out: binary first, then bundle) + for fw in YoloLabel.app/Contents/Frameworks/*.framework; do + [ -d "$fw" ] || continue + fw_name=$(basename "$fw" .framework) + if [ -f "$fw/Versions/A/$fw_name" ]; then + codesign --force --sign "$CODESIGN_IDENTITY" \ + --options runtime --timestamp "$fw/Versions/A/$fw_name" + fi + codesign --force --sign "$CODESIGN_IDENTITY" \ + --options runtime --timestamp "$fw" + done + + # 4c. Sign Qt plugins + find YoloLabel.app/Contents/PlugIns -name "*.dylib" -exec \ + codesign --force --sign "$CODESIGN_IDENTITY" \ + --options runtime --timestamp {} \; + + # 4d. Sign the main app bundle LAST (with entitlements) + codesign --force --sign "$CODESIGN_IDENTITY" \ --options runtime --timestamp \ --entitlements scripts/entitlements.plist \ YoloLabel.app + + # 4e. Verify (--deep is fine for verification) codesign --verify --deep --strict --verbose=2 YoloLabel.app else echo "No certificate available, using ad-hoc signing" codesign --force --deep --sign - YoloLabel.app fi - sleep 2 + # 5. Create DMG hdiutil create -volname YoloLabel -srcfolder YoloLabel.app \ -ov -format UDZO "${{ matrix.artifact_name }}.dmg" + # 6. Sign the DMG (no --options runtime needed for disk images) if [ -n "$CODESIGN_IDENTITY" ]; then codesign --force --sign "$CODESIGN_IDENTITY" \ --timestamp "${{ matrix.artifact_name }}.dmg" fi - # ── macOS: notarize DMG ───────────────────────────────── + # ── macOS: notarize DMG (releases only) ─────────────────── - name: Notarize DMG (macOS) - if: runner.os == 'macOS' && env.CODESIGN_IDENTITY != '' + if: runner.os == 'macOS' && env.CODESIGN_IDENTITY != '' && startsWith(github.ref, 'refs/tags/') timeout-minutes: 25 env: APPLE_ID: ${{ secrets.APPLE_ID }} From 85e14810de34d8eac09b825b2b50786c7a0a8497 Mon Sep 17 00:00:00 2001 From: developer0hye Date: Wed, 18 Feb 2026 00:27:52 +0900 Subject: [PATCH 12/12] docs: add macOS deployment known issues report Document the issues found and fixed during macOS CI packaging: inside-out signing, macdeployqt framework bug, rpath ordering, dylib symlink handling, and notarization scope. Also records non-critical runtime warnings and macOS 15+ Gatekeeper changes. Signed-off-by: developer0hye Co-Authored-By: Claude Opus 4.6 Signed-off-by: developer0hye --- docs/macos-deployment-issues.md | 87 +++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/macos-deployment-issues.md diff --git a/docs/macos-deployment-issues.md b/docs/macos-deployment-issues.md new file mode 100644 index 0000000..2a35943 --- /dev/null +++ b/docs/macos-deployment-issues.md @@ -0,0 +1,87 @@ +# macOS Deployment — Known Issues & Fixes + +This document records issues discovered during macOS CI packaging and local deployment testing (February 2026), along with the fixes applied. + +## Issues Found & Fixed in CI + +### 1. `codesign --deep` deprecated for signing (macOS 13+) + +**Problem:** The original CI used `codesign --force --deep --sign` to sign the entire `.app` bundle in one pass. Apple deprecated `--deep` for signing as of macOS 13 because it applies all options (including entitlements) uniformly to nested code, which produces invalid bundles. + +**Fix:** Replaced with inside-out signing per Apple's current requirements: +1. Sign all `.dylib` files in `Contents/Frameworks/` +2. Sign `.framework` bundles (binary inside first, then the bundle itself) +3. Sign Qt plugins in `Contents/PlugIns/` +4. Sign the main app bundle last — only the main executable gets entitlements + +Note: `codesign --deep` is still valid for **verification** (`--verify`). + +**References:** +- [CODESIGN(1) man page](https://keith.github.io/xcode-man-pages/codesign.1.html) +- [Resolving common notarization issues — Apple Developer](https://developer.apple.com/documentation/security/resolving-common-notarization-issues) + +### 2. `macdeployqt` has a known framework-signing bug + +**Problem:** Qt's `macdeployqt` signs binaries inside `.framework` bundles but does NOT sign the enclosing `.framework` directory itself. This causes `codesign` to report the app as "not signed at all" when verifying. + +**Fix:** Do not pass `-codesign` or `-sign-for-notarization` to `macdeployqt`. Instead, run `macdeployqt` without any signing, then sign everything manually using the inside-out approach. + +**References:** +- [GDATASoftwareAG/macdeployqt fork](https://github.com/GDATASoftwareAG/macdeployqt) (community fix) +- [Qt for macOS Deployment — Qt 6 Documentation](https://doc.qt.io/qt-6/macos-deployment.html) + +### 3. `install_name_tool` ordering with `macdeployqt` + +**Problem:** The rpath `@executable_path/../Frameworks` was added to the binary *before* `macdeployqt` ran. Since `macdeployqt` also adds this rpath, it resulted in a duplicate rpath warning that was silently suppressed by `|| true`. + +**Fix:** +- Run `macdeployqt` first (it sets up rpaths) +- Add the rpath defensively *after* `macdeployqt`, filtering out only the "would duplicate" warning instead of suppressing all errors + +### 4. ONNX Runtime dylib copy missed symlinks + +**Problem:** `cp onnxruntime/lib/libonnxruntime.*.dylib` only copies the versioned file (e.g., `libonnxruntime.1.24.1.dylib`). The unversioned symlink (`libonnxruntime.dylib -> libonnxruntime.1.24.1.dylib`) was not copied, which could break if a future ORT version changes its install name scheme. + +**Fix:** Changed to `cp -a onnxruntime/lib/libonnxruntime*.dylib` which: +- Uses a broader glob to match both versioned and unversioned files +- Preserves symlinks with `-a` flag + +### 5. Notarization ran on every push + +**Problem:** The notarization step ran on every push to `master` (not just releases), wasting Apple API calls and adding 5-15 minutes to CI. + +**Fix:** Restricted notarization to tag pushes only: +```yaml +if: runner.os == 'macOS' && env.CODESIGN_IDENTITY != '' && startsWith(github.ref, 'refs/tags/') +``` + +## Runtime Warnings (Non-Critical) + +The following warnings appear at runtime but do **not** affect functionality: + +### `Unknown property gridline-width` + +Qt 6 no longer supports the `gridline-width` CSS property in stylesheets. This produces warnings but the table widget renders correctly. + +### `QMetaObject::connectSlotsByName: No matching signal for on_pushButton_prev_clicked()` + +Slot naming convention (`on__`) triggers Qt's auto-connect mechanism, but the matching signal does not exist on the widget. These slots are connected manually elsewhere in the code, so the warning is harmless. + +Affected slots: +- `on_pushButton_prev_clicked()` +- `on_pushButton_next_clicked()` +- `on_usageTimer_timeout()` +- `on_usageTimerReset_clicked()` + +### `Populating font family aliases took N ms. Replace uses of missing font family "Consolas"` + +The stylesheet references the "Consolas" font which is not available on macOS. Qt falls back to a system monospace font. This causes a one-time startup delay (~100ms) while Qt resolves font aliases. + +## macOS Gatekeeper Notes (macOS 15+) + +As of macOS 15 (Sequoia): +- **Control-click bypass removed.** Users can no longer right-click to bypass Gatekeeper for unsigned apps. +- **Stricter enforcement in macOS 15.1+.** Users must navigate to System Settings > Privacy & Security to approve blocked apps. +- **Drag-and-drop checks in macOS 15.4+.** Gatekeeper alerts also trigger when dragging quarantined files into applications. + +Proper Developer ID signing + notarization is effectively **mandatory** for macOS distribution.