207 lines
6.7 KiB
Bash
Executable File
207 lines
6.7 KiB
Bash
Executable File
#!/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 <<USAGE
|
|
Usage: $(basename "$0") [options]
|
|
|
|
Builds a Developer ID archive, exports the signed app, creates a drag-to-Applications
|
|
DMG, notarizes/staples it, verifies Gatekeeper, and writes SHA-256 checksums.
|
|
Nothing is uploaded to your website or update feed.
|
|
|
|
Options:
|
|
--notary-profile NAME Keychain profile created by notarytool (default: $NOTARY_PROFILE)
|
|
--output DIR Release output root (default: build/releases)
|
|
--skip-preflight Skip Scripts/release-preflight.sh
|
|
--skip-notarization Build/sign/package only; do not submit to Apple
|
|
-h, --help Show this help
|
|
|
|
Environment:
|
|
DEVELOPMENT_TEAM Optional Apple Developer Team ID. Recommended for CI.
|
|
NOTARY_PROFILE Alternative to --notary-profile.
|
|
RELEASE_ROOT Alternative to --output.
|
|
|
|
First-time notarization setup:
|
|
xcrun notarytool store-credentials "$NOTARY_PROFILE"
|
|
USAGE
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--notary-profile) [[ $# -ge 2 ]] || { echo "Missing value for $1" >&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" <<PLIST
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>method</key>
|
|
<string>developer-id</string>
|
|
<key>signingStyle</key>
|
|
<string>automatic</string>
|
|
<key>stripSwiftSymbols</key>
|
|
<true/>
|
|
</dict>
|
|
</plist>
|
|
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" <<TXT
|
|
$DISPLAY_NAME $VERSION
|
|
|
|
Install: drag $DISPLAY_NAME to Applications.
|
|
Then eject this disk image and launch $DISPLAY_NAME from Applications.
|
|
TXT
|
|
hdiutil create \
|
|
-volname "$DISPLAY_NAME $VERSION" \
|
|
-srcfolder "$DMG_STAGING" \
|
|
-ov \
|
|
-format UDZO \
|
|
-imagekey zlib-level=9 \
|
|
"$DMG" >/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" <<INFO
|
|
Product: $DISPLAY_NAME
|
|
Bundle ID: $BUNDLE_ID
|
|
Version: $VERSION
|
|
Build: $BUILD
|
|
Artifact: $(basename "$DMG")
|
|
Bytes: $DMG_SIZE
|
|
SHA-256: $DMG_SHA256
|
|
Notarized: $([[ "$SKIP_NOTARIZATION" -eq 0 ]] && echo yes || echo no)
|
|
Notary profile: $NOTARY_PROFILE
|
|
Generated (UTC): $(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
|
INFO
|
|
|
|
rm -f "$EXPORT_OPTIONS"
|
|
|
|
echo
|
|
echo "Release artifact created successfully:"
|
|
echo " $DMG"
|
|
echo " $CHECKSUM"
|
|
echo " $METADATA"
|
|
echo
|
|
echo "Publishing is intentionally NOT automated. Test the DMG on a clean Mac before upload."
|