#!/bin/bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" PROJECT="$ROOT/Rsync Studio.xcodeproj" SCHEME="Rsync Studio" CONFIGURATION="Release" PRODUCT_NAME="Rsync Studio" DISPLAY_NAME="Rsync Studio" BUNDLE_ID="net.absdev.RsyncStudio" NOTARY_PROFILE="${NOTARY_PROFILE:-Rsync Studio Notary}" RELEASE_ROOT="${RELEASE_ROOT:-$ROOT/build/releases}" SKIP_PREFLIGHT=0 SKIP_NOTARIZATION=0 usage() { cat <&2; exit 2; }; NOTARY_PROFILE="$2"; shift 2 ;; --output) [[ $# -ge 2 ]] || { echo "Missing value for $1" >&2; exit 2; }; RELEASE_ROOT="$2"; shift 2 ;; --skip-preflight) SKIP_PREFLIGHT=1; shift ;; --skip-notarization) SKIP_NOTARIZATION=1; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; esac done [[ "$(uname -s)" == "Darwin" ]] || { echo "ERROR: release.sh must run on macOS." >&2; exit 2; } for tool in xcodebuild xcrun hdiutil codesign spctl plutil shasum ditto; do command -v "$tool" >/dev/null || { echo "ERROR: Missing required tool: $tool" >&2; exit 2; } done PBX="$PROJECT/project.pbxproj" VERSION="$(awk -F' = ' '/MARKETING_VERSION = / {gsub(/;/,"",$2); print $2; exit}' "$PBX")" BUILD="$(awk -F' = ' '/CURRENT_PROJECT_VERSION = / {gsub(/;/,"",$2); print $2; exit}' "$PBX")" [[ -n "$VERSION" && -n "$BUILD" ]] || { echo "ERROR: Could not determine version/build from Xcode project." >&2; exit 1; } RELEASE_DIR="$RELEASE_ROOT/$VERSION-$BUILD" ARCHIVE="$RELEASE_DIR/$PRODUCT_NAME.xcarchive" EXPORT_DIR="$RELEASE_DIR/export" DMG_STAGING="$RELEASE_DIR/dmg-root" DMG="$RELEASE_DIR/$PRODUCT_NAME-$VERSION.dmg" CHECKSUM="$DMG.sha256" METADATA="$RELEASE_DIR/release-info.txt" EXPORT_OPTIONS="$RELEASE_DIR/ExportOptions.plist" if [[ -e "$RELEASE_DIR" ]]; then echo "ERROR: Release directory already exists: $RELEASE_DIR" >&2 echo "Refusing to overwrite an existing version/build artifact." >&2 exit 1 fi mkdir -p "$RELEASE_DIR" cleanup() { rm -rf "$DMG_STAGING" } trap cleanup EXIT if [[ "$SKIP_PREFLIGHT" -eq 0 ]]; then "$SCRIPT_DIR/release-preflight.sh" fi cat > "$EXPORT_OPTIONS" < method developer-id signingStyle automatic stripSwiftSymbols PLIST plutil -lint "$EXPORT_OPTIONS" >/dev/null XCODE_SIGN_ARGS=() if [[ -n "${DEVELOPMENT_TEAM:-}" ]]; then XCODE_SIGN_ARGS+=("DEVELOPMENT_TEAM=$DEVELOPMENT_TEAM") fi echo "== Archive $DISPLAY_NAME $VERSION ($BUILD) ==" xcodebuild \ -project "$PROJECT" \ -scheme "$SCHEME" \ -configuration "$CONFIGURATION" \ -destination 'generic/platform=macOS' \ -archivePath "$ARCHIVE" \ clean archive \ "${XCODE_SIGN_ARGS[@]}" echo "== Export Developer ID app ==" xcodebuild \ -exportArchive \ -archivePath "$ARCHIVE" \ -exportPath "$EXPORT_DIR" \ -exportOptionsPlist "$EXPORT_OPTIONS" \ -allowProvisioningUpdates APP="$EXPORT_DIR/$PRODUCT_NAME.app" [[ -d "$APP" ]] || { echo "ERROR: Exported app not found: $APP" >&2; exit 1; } ACTUAL_BUNDLE_ID="$(plutil -extract CFBundleIdentifier raw -o - "$APP/Contents/Info.plist" 2>/dev/null || true)" [[ "$ACTUAL_BUNDLE_ID" == "$BUNDLE_ID" ]] || { echo "ERROR: Exported bundle id is '$ACTUAL_BUNDLE_ID', expected '$BUNDLE_ID'." >&2; exit 1; } "$SCRIPT_DIR/verify-signed-app.sh" "$APP" if [[ "$SKIP_NOTARIZATION" -eq 0 ]]; then echo "== Validate notarization credentials ==" xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" >/dev/null echo "== Notarize exported app ==" APP_ZIP="$RELEASE_DIR/$PRODUCT_NAME-$VERSION-notary.zip" ditto -c -k --keepParent "$APP" "$APP_ZIP" xcrun notarytool submit "$APP_ZIP" --keychain-profile "$NOTARY_PROFILE" --wait xcrun stapler staple "$APP" xcrun stapler validate "$APP" rm -f "$APP_ZIP" "$SCRIPT_DIR/verify-signed-app.sh" "$APP" fi echo "== Create drag-to-Applications DMG ==" mkdir -p "$DMG_STAGING" ditto "$APP" "$DMG_STAGING/$PRODUCT_NAME.app" ln -s /Applications "$DMG_STAGING/Applications" cat > "$DMG_STAGING/README.txt" </dev/null hdiutil verify "$DMG" >/dev/null if [[ "$SKIP_NOTARIZATION" -eq 0 ]]; then echo "== Notarize final DMG ==" xcrun notarytool submit "$DMG" --keychain-profile "$NOTARY_PROFILE" --wait xcrun stapler staple "$DMG" xcrun stapler validate "$DMG" echo "== Final Gatekeeper verification ==" "$SCRIPT_DIR/verify-signed-app.sh" "$APP" spctl --assess --type open --context context:primary-signature --verbose=4 "$DMG" else echo "WARNING: Notarization was skipped. This output is NOT ready for public distribution." >&2 fi printf '%s %s\n' "$(shasum -a 256 "$DMG" | awk '{print $1}')" "$(basename "$DMG")" > "$CHECKSUM" DMG_SIZE="$(stat -f '%z' "$DMG")" DMG_SHA256="$(awk '{print $1}' "$CHECKSUM")" cat > "$METADATA" <