Zum Inhalt

scripts/build-macos-app.sh

#!/usr/bin/env bash
# Builds the native Physarum macOS app (apple/PhysarumApp) and a distributable .dmg.
#
# Output: dist-macos/Physarum.app and dist-macos/Physarum.dmg
# Requires: Xcode (xcodebuild), xcodegen, hdiutil. For a notarised release also a
# "Developer ID Application" identity + a notarytool keychain profile.
#
# Signing (opt-in via env):
#   PHYSARUM_SIGN_IDENTITY   "Developer ID Application: NAME (TEAMID)" or "auto"
#   PHYSARUM_NOTARY_PROFILE  notarytool keychain profile name
#   Both unset -> ad-hoc (local dev). Identity only -> signed, not notarised.
#   Both set -> Developer ID signed + hardened + notarised + stapled (app and dmg).
set -euo pipefail
cd "$(dirname "$0")/.."

APP_NAME="Physarum"
SCHEME="Physarum"
TEAM_ID="U9X7M39R56"
PROJ_DIR="apple/PhysarumApp"
PROJ="$PROJ_DIR/$APP_NAME.xcodeproj"
ENTITLEMENTS="$PROJ_DIR/Physarum.macOS.entitlements"
EXPORT_PLIST="scripts/ExportOptions.plist"
OUT="dist-macos"
ARCHIVE="$OUT/$APP_NAME.xcarchive"
APP="$OUT/$APP_NAME.app"

for tool in xcodebuild xcodegen hdiutil codesign; do
  command -v "$tool" >/dev/null 2>&1 || { echo "error: required tool not found: $tool" >&2; exit 1; }
done
[ -f "$ENTITLEMENTS" ] || { echo "error: missing $ENTITLEMENTS" >&2; exit 1; }
[ -f "$EXPORT_PLIST" ] || { echo "error: missing $EXPORT_PLIST" >&2; exit 1; }
# xcodebuild resolves CODE_SIGN_ENTITLEMENTS relative to the project SRCROOT
# (apple/PhysarumApp), not this script's cwd — pass an absolute path so it can't double up.
ENTITLEMENTS_ABS="$(pwd)/$ENTITLEMENTS"
EXPORT_PLIST_ABS="$(pwd)/$EXPORT_PLIST"

SIGN_IDENTITY="${PHYSARUM_SIGN_IDENTITY:-}"
NOTARY_PROFILE="${PHYSARUM_NOTARY_PROFILE:-}"
SIGN_MODE="none"
if [ -n "$SIGN_IDENTITY" ] || [ -n "$NOTARY_PROFILE" ]; then
  [ -n "$SIGN_IDENTITY" ] || { echo "error: PHYSARUM_NOTARY_PROFILE set but PHYSARUM_SIGN_IDENTITY missing" >&2; exit 1; }
  for t in xcrun ditto spctl; do command -v "$t" >/dev/null 2>&1 || { echo "error: required tool not found: $t" >&2; exit 1; }; done
  if [ "$SIGN_IDENTITY" = auto ]; then
    SIGN_IDENTITY="$(security find-identity -v -p codesigning | sed -n 's/.*"\(Developer ID Application: [^"]*\)".*/\1/p' | head -1)"
    [ -n "$SIGN_IDENTITY" ] || { echo "error: no 'Developer ID Application' identity in keychain (auto)" >&2; exit 1; }
    echo "→ signing identity (auto): $SIGN_IDENTITY"
  fi
  if [ -n "$NOTARY_PROFILE" ]; then SIGN_MODE="notarize"; else SIGN_MODE="sign"; fi
fi

rm -rf "$OUT"; mkdir -p "$OUT"

echo "→ xcodegen generate"
( cd "$PROJ_DIR" && xcodegen generate >/dev/null )

echo "→ archiving (Release, arm64 + x86_64)"
ARCHIVE_ARGS=(-project "$PROJ" -scheme "$SCHEME" -configuration Release
  -destination 'generic/platform=macOS' -archivePath "$ARCHIVE"
  ARCHS="arm64 x86_64" ONLY_ACTIVE_ARCH=NO)
if [ "$SIGN_MODE" = none ]; then
  xcodebuild "${ARCHIVE_ARGS[@]}" \
    CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY="-" CODE_SIGNING_REQUIRED=NO archive
else
  xcodebuild "${ARCHIVE_ARGS[@]}" \
    CODE_SIGN_STYLE=Manual "CODE_SIGN_IDENTITY=$SIGN_IDENTITY" DEVELOPMENT_TEAM="$TEAM_ID" \
    CODE_SIGNING_REQUIRED=YES ENABLE_HARDENED_RUNTIME=YES \
    OTHER_CODE_SIGN_FLAGS="--timestamp" CODE_SIGN_ENTITLEMENTS="$ENTITLEMENTS_ABS" archive
fi

if [ "$SIGN_MODE" = none ]; then
  cp -R "$ARCHIVE/Products/Applications/$APP_NAME.app" "$APP"
else
  echo "→ exporting (Developer ID)"
  xcodebuild -exportArchive -archivePath "$ARCHIVE" \
    -exportOptionsPlist "$EXPORT_PLIST_ABS" -exportPath "$OUT" >/dev/null
fi
[ -d "$APP" ] || { echo "error: $APP not produced" >&2; exit 1; }
echo "  archs: $(lipo -archs "$APP/Contents/MacOS/$APP_NAME" 2>/dev/null || echo '?')"

if [ "$SIGN_MODE" = notarize ]; then
  echo "→ notarising + stapling app (waits for Apple)"
  ditto -c -k --keepParent "$APP" "$OUT/$APP_NAME.app.zip"
  xcrun notarytool submit "$OUT/$APP_NAME.app.zip" -p "$NOTARY_PROFILE" --wait
  xcrun stapler staple "$APP"
  rm -f "$OUT/$APP_NAME.app.zip"
fi

echo "→ creating DMG"
STAGE="$OUT/.dmg-stage"; mkdir -p "$STAGE"
cp -R "$APP" "$STAGE/"
ln -s /Applications "$STAGE/Applications"
[ -f macos/INSTALL.txt ] && cp macos/INSTALL.txt "$STAGE/"
hdiutil create -volname "$APP_NAME" -srcfolder "$STAGE" -ov -format UDZO "$OUT/$APP_NAME.dmg" >/dev/null
rm -rf "$STAGE"

if [ "$SIGN_MODE" != none ]; then
  echo "→ signing DMG"
  codesign --force --timestamp --sign "$SIGN_IDENTITY" "$OUT/$APP_NAME.dmg"
  if [ "$SIGN_MODE" = notarize ]; then
    echo "→ notarising + stapling DMG"
    xcrun notarytool submit "$OUT/$APP_NAME.dmg" -p "$NOTARY_PROFILE" --wait
    xcrun stapler staple "$OUT/$APP_NAME.dmg"
    echo "→ Gatekeeper check (ship only if accepted/valid)"
    xcrun stapler validate "$OUT/$APP_NAME.dmg"
    spctl -a -vvv -t open --context context:primary-signature "$OUT/$APP_NAME.dmg" || true
    spctl -a -vvv -t exec "$APP" || true
  fi
fi

echo; echo "✓ $APP"; echo "✓ $OUT/$APP_NAME.dmg  ($(du -h "$OUT/$APP_NAME.dmg" | cut -f1))"