From c37fdec0dc80988337eae37dc8ce9e1e91f4fb57 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Thu, 9 Jul 2026 22:47:26 +0200 Subject: [PATCH] Add mac-release script for notarized Developer ID builds --- bin/mac-release | 141 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100755 bin/mac-release diff --git a/bin/mac-release b/bin/mac-release new file mode 100755 index 00000000..30de32ab --- /dev/null +++ b/bin/mac-release @@ -0,0 +1,141 @@ +#!/bin/bash +# Build, sign (Developer ID), notarize and package the macOS app for +# distribution outside the App Store — produces a .zip and .dmg you can +# send to other Macs. +# +# Usage: bin/mac-release [--skip-build] [--skip-notarize] +# --skip-build reuse the existing archive from a previous run +# --skip-notarize stop after signing (app won't pass Gatekeeper on other Macs) +# +# Requirements: +# - "Developer ID Application" certificate in the login keychain +# - "match Direct stream.yattee.app macos" provisioning profile installed +# (run `fastlane match developer_id --platform macos --readonly` if missing) +# - fastlane/.env with DEVELOPER_KEY_ID / DEVELOPER_KEY_ISSUER_ID / +# DEVELOPER_KEY_CONTENT (base64-encoded .p8) for notarization +set -euo pipefail + +cd "$(dirname "$0")/.." + +APP_NAME=Yattee +BUNDLE_ID=stream.yattee.app +TEAM_ID=78Z5H3M6RJ +PROFILE="match Direct ${BUNDLE_ID} macos" + +SKIP_BUILD=0 +SKIP_NOTARIZE=0 +for arg in "$@"; do + case "$arg" in + --skip-build) SKIP_BUILD=1 ;; + --skip-notarize) SKIP_NOTARIZE=1 ;; + *) echo "Unknown option: $arg"; exit 1 ;; + esac +done + +# Marketing version is a build setting (no Info.plist entry), so agvtool +# can't read it — take it from the pbxproj. +VERSION=$(sed -n 's/.*MARKETING_VERSION = \([^;]*\);.*/\1/p' "${APP_NAME}.xcodeproj/project.pbxproj" | head -1) +BUILD=$(xcrun agvtool what-version -terse) +[ -n "$VERSION" ] && [ -n "$BUILD" ] || { echo "Could not determine version/build"; exit 1; } + +OUT_DIR="fastlane/builds/${VERSION}-${BUILD}/macOS" +ARCHIVE="${OUT_DIR}/${APP_NAME}.xcarchive" +APP="${OUT_DIR}/${APP_NAME}.app" +ZIP="${OUT_DIR}/${APP_NAME}-${VERSION}-macOS.zip" +DMG="${OUT_DIR}/${APP_NAME}-${VERSION}-macOS.dmg" +LOG="${OUT_DIR}/xcodebuild.log" +mkdir -p "$OUT_DIR" + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +echo "==> ${APP_NAME} ${VERSION} (${BUILD}) → ${OUT_DIR}" + +if [ "$SKIP_BUILD" = 1 ] && [ -d "$ARCHIVE" ]; then + echo "==> Skipping build, reusing ${ARCHIVE}" +else + echo "==> Archiving (Release-DeveloperID)… log: ${LOG}" + xcodebuild archive \ + -project "${APP_NAME}.xcodeproj" \ + -scheme "$APP_NAME" \ + -configuration Release-DeveloperID \ + -destination 'generic/platform=macOS' \ + -archivePath "$ARCHIVE" \ + CODE_SIGN_STYLE=Manual \ + CODE_SIGN_IDENTITY="Developer ID Application" \ + DEVELOPMENT_TEAM="$TEAM_ID" \ + PROVISIONING_PROFILE_SPECIFIER="$PROFILE" \ + > "$LOG" 2>&1 || { tail -50 "$LOG"; echo "Archive failed — full log: ${LOG}"; exit 1; } +fi + +echo "==> Exporting with Developer ID…" +cat > "$TMP/export.plist" < + + + + methoddeveloper-id + signingStylemanual + teamID${TEAM_ID} + provisioningProfiles + + ${BUNDLE_ID}${PROFILE} + + + +PLIST +rm -rf "$APP" +xcodebuild -exportArchive \ + -archivePath "$ARCHIVE" \ + -exportPath "$OUT_DIR" \ + -exportOptionsPlist "$TMP/export.plist" \ + >> "$LOG" 2>&1 || { tail -50 "$LOG"; echo "Export failed — full log: ${LOG}"; exit 1; } + +if [ "$SKIP_NOTARIZE" = 0 ]; then + # Notarization credentials come from the same ASC API key fastlane uses. + set -a; source fastlane/.env; set +a + : "${DEVELOPER_KEY_ID:?missing in fastlane/.env}" + : "${DEVELOPER_KEY_ISSUER_ID:?missing in fastlane/.env}" + : "${DEVELOPER_KEY_CONTENT:?missing in fastlane/.env}" + KEY_FILE="$TMP/AuthKey_${DEVELOPER_KEY_ID}.p8" + echo "$DEVELOPER_KEY_CONTENT" | base64 -d > "$KEY_FILE" + + echo "==> Submitting for notarization (this can take a few minutes)…" + ditto -c -k --keepParent "$APP" "$TMP/notarize.zip" + NOTARY_OUT=$(xcrun notarytool submit "$TMP/notarize.zip" \ + --key "$KEY_FILE" \ + --key-id "$DEVELOPER_KEY_ID" \ + --issuer "$DEVELOPER_KEY_ISSUER_ID" \ + --wait --timeout 30m 2>&1 | tee /dev/stderr) || true + + if ! grep -q "status: Accepted" <<< "$NOTARY_OUT"; then + SUBMISSION_ID=$(grep -m1 -o 'id: [0-9a-f-]*' <<< "$NOTARY_OUT" | head -1 | cut -d' ' -f2) + echo "Notarization failed." + if [ -n "$SUBMISSION_ID" ]; then + echo "==> Fetching notarization log for ${SUBMISSION_ID}…" + xcrun notarytool log "$SUBMISSION_ID" \ + --key "$KEY_FILE" --key-id "$DEVELOPER_KEY_ID" --issuer "$DEVELOPER_KEY_ISSUER_ID" + fi + exit 1 + fi + + echo "==> Stapling notarization ticket…" + xcrun stapler staple "$APP" +fi + +echo "==> Packaging…" +rm -f "$ZIP" "$DMG" +ditto -c -k --keepParent "$APP" "$ZIP" +hdiutil create -volname "${APP_NAME} ${VERSION}" -srcfolder "$APP" -ov -format UDZO "$DMG" > /dev/null + +echo "==> Verifying…" +codesign --verify --deep --strict "$APP" +if [ "$SKIP_NOTARIZE" = 0 ]; then + xcrun stapler validate "$APP" + spctl --assess --type exec -vv "$APP" 2>&1 | tail -2 +fi + +echo +echo "Done:" +echo " ${ZIP}" +echo " ${DMG}"