#!/bin/bash # Sign every native binary inside resources/bin and resources/python/runtime # with the Developer ID Application certificate so the .app passes # notarization. Runs as part of the macOS bundle step, after binaries # are downloaded/copied but before verify_bundled_binaries runs them # (signing also clears the quarantine attribute that GitHub release # downloads carry by default). # # Skips silently when APPLE_SIGNING_IDENTITY is unset — local builds # without a Developer ID cert still produce a (Gatekeeper-rejected) # unsigned .app, same as before this script existed. # # Required env (set by CI): # APPLE_SIGNING_IDENTITY Full identity string, e.g. # "Developer ID Application: Name (TEAMID)" # Must already be present in the active keychain. set -euo pipefail if [[ -z "${APPLE_SIGNING_IDENTITY:-}" ]]; then echo "[sign-macos] APPLE_SIGNING_IDENTITY not set — skipping (unsigned build)" exit 0 fi if [[ "$OSTYPE" != "darwin"* ]]; then echo "[sign-macos] not on macOS — skipping" >&2 exit 0 fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" ENTITLEMENTS="$PROJECT_DIR/resources/entitlements.mac.plist" if [[ ! -f "$ENTITLEMENTS" ]]; then echo "[sign-macos] entitlements file missing: $ENTITLEMENTS" >&2 exit 1 fi sign_one() { local target="$1" [[ -e "$target" ]] || return 0 # Skip symlinks — they get followed during bundling and signing the # link target separately is enough. [[ -L "$target" ]] && return 0 echo " $target" codesign --force --options runtime --timestamp \ --sign "$APPLE_SIGNING_IDENTITY" \ --entitlements "$ENTITLEMENTS" \ "$target" } echo "[sign-macos] signing bundled binaries with: $APPLE_SIGNING_IDENTITY" # 1. Top-level executables and bundled dylibs in resources/bin. # dylibbundler copies fluidsynth's deps into this directory and # rewrites their install names to @executable_path/, so each one # has to be signed individually before the app bundle is built. BIN_DIR="$PROJECT_DIR/resources/bin" if [[ -d "$BIN_DIR" ]]; then while IFS= read -r -d '' f; do sign_one "$f" done < <(find "$BIN_DIR" -maxdepth 1 -type f \( -perm -u+x -o -name '*.dylib' \) -print0) fi # 2. Embedded CPython runtime — interpreter + libpython dylib + every # compiled extension. Notarization rejects the bundle if any of # these is unsigned. PY_RUNTIME="$PROJECT_DIR/resources/python/runtime" if [[ -d "$PY_RUNTIME" ]]; then # Interpreter binaries (python3, python3.x, etc.) while IFS= read -r -d '' f; do sign_one "$f" done < <(find "$PY_RUNTIME/bin" -maxdepth 1 -type f -perm -u+x -print0 2>/dev/null || true) # libpython dylib(s) and any other shared libs while IFS= read -r -d '' f; do sign_one "$f" done < <(find "$PY_RUNTIME/lib" -maxdepth 2 -type f -name '*.dylib' -print0 2>/dev/null || true) # Compiled extension modules — both stdlib lib-dynload and any # site-packages C extensions installed via pip. while IFS= read -r -d '' f; do sign_one "$f" done < <(find "$PY_RUNTIME/lib" -type f \( -name '*.so' -o -name '*.dylib' \) -print0 2>/dev/null || true) fi echo "[sign-macos] done" # Spot-check a couple of binaries so a botched sign call fails the # build here rather than mid-notarization 2 minutes later. for probe in "$BIN_DIR/fluidsynth" "$BIN_DIR/ffmpeg" "$BIN_DIR/vgmstream-cli"; do if [[ -f "$probe" ]]; then codesign --verify --strict "$probe" || { echo "[sign-macos] verification failed: $probe" >&2 exit 1 } fi done