general: rename
This commit is contained in:
760
self_distribute
Executable file
760
self_distribute
Executable file
@@ -0,0 +1,760 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Load environment variables from .env file
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
# Function to ask for version bump confirmation
|
||||
ask_version_bump() {
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════"
|
||||
echo " Gaze Version Bump & Build Tool "
|
||||
echo "═══════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
# Check if we're in a git repository
|
||||
if [ ! -d .git ] && [ ! -f .git ]; then
|
||||
echo "Error: Not in a git repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for uncommitted changes
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
echo "Warning: You have uncommitted changes"
|
||||
git status --short
|
||||
echo ""
|
||||
read -p "Continue anyway? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Aborted"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Get current version from git tag or fallback to project file
|
||||
CURRENT_VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//')
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
echo "Warning: No existing git tags found"
|
||||
echo "Attempting to read version from Xcode project..."
|
||||
PROJECT_FILE="Gaze.xcodeproj/project.pbxproj"
|
||||
CURRENT_VERSION=$(grep -A 1 "MARKETING_VERSION" "$PROJECT_FILE" | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1)
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
echo "Error: Could not extract version from git tags or project file"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using version from project file as fallback"
|
||||
fi
|
||||
|
||||
echo "Current version: v${CURRENT_VERSION}"
|
||||
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
|
||||
|
||||
MAJOR=$(echo "$MAJOR" | sed 's/[^0-9].*//')
|
||||
MINOR=$(echo "$MINOR" | sed 's/[^0-9].*//')
|
||||
PATCH=$(echo "$PATCH" | sed 's/[^0-9].*//')
|
||||
|
||||
echo ""
|
||||
echo "Select version bump type:"
|
||||
echo " 1) Major (breaking changes) ${MAJOR}.${MINOR}.${PATCH} → $((MAJOR+1)).0.0"
|
||||
echo " 2) Minor (new features) ${MAJOR}.${MINOR}.${PATCH} → ${MAJOR}.$((MINOR+1)).0"
|
||||
echo " 3) Patch (bug fixes) ${MAJOR}.${MINOR}.${PATCH} → ${MAJOR}.${MINOR}.$((PATCH+1))"
|
||||
echo " 4) Custom version"
|
||||
echo " 5) Keep current version"
|
||||
echo ""
|
||||
read -p "Enter choice (1-5): " -n 1 -r CHOICE
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
case $CHOICE in
|
||||
1)
|
||||
NEW_MAJOR=$((MAJOR+1))
|
||||
NEW_MINOR=0
|
||||
NEW_PATCH=0
|
||||
BUMP_TYPE="major"
|
||||
;;
|
||||
2)
|
||||
NEW_MAJOR=$MAJOR
|
||||
NEW_MINOR=$((MINOR+1))
|
||||
NEW_PATCH=0
|
||||
BUMP_TYPE="minor"
|
||||
;;
|
||||
3)
|
||||
NEW_MAJOR=$MAJOR
|
||||
NEW_MINOR=$MINOR
|
||||
NEW_PATCH=$((PATCH+1))
|
||||
BUMP_TYPE="patch"
|
||||
;;
|
||||
4)
|
||||
read -p "Enter custom version (e.g., 1.0.0-beta): " CUSTOM_VERSION
|
||||
NEW_VERSION="$CUSTOM_VERSION"
|
||||
BUMP_TYPE="custom"
|
||||
;;
|
||||
5)
|
||||
echo "Keeping current version v${CURRENT_VERSION}"
|
||||
NEW_VERSION="$CURRENT_VERSION"
|
||||
BUMP_TYPE="keep"
|
||||
;;
|
||||
*)
|
||||
echo "Invalid choice"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Construct new version (unless custom)
|
||||
if [ "$BUMP_TYPE" != "custom" ] && [ "$BUMP_TYPE" != "keep" ]; then
|
||||
NEW_VERSION="${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
|
||||
fi
|
||||
|
||||
echo "New version: v${NEW_VERSION}"
|
||||
|
||||
# Get current build number for display
|
||||
DISPLAY_CURRENT_BUILD=$(grep -A 1 "CURRENT_PROJECT_VERSION" Gaze.xcodeproj/project.pbxproj | grep -o '[0-9]\+' | head -1)
|
||||
if [ -z "$DISPLAY_CURRENT_BUILD" ]; then
|
||||
DISPLAY_CURRENT_BUILD=0
|
||||
fi
|
||||
DISPLAY_NEW_BUILD=$((DISPLAY_CURRENT_BUILD + 1))
|
||||
|
||||
# Ask for confirmation to proceed with version bumping
|
||||
echo ""
|
||||
if [ "$BUMP_TYPE" = "keep" ]; then
|
||||
echo "This will:"
|
||||
echo " 1. Keep MARKETING_VERSION = ${NEW_VERSION}"
|
||||
echo " 2. Update project.pbxproj → CURRENT_PROJECT_VERSION = ${DISPLAY_NEW_BUILD} (currently ${DISPLAY_CURRENT_BUILD})"
|
||||
echo " 3. Create git commit (no new tag)"
|
||||
else
|
||||
echo "This will:"
|
||||
echo " 1. Update project.pbxproj → MARKETING_VERSION = ${NEW_VERSION}"
|
||||
echo " 2. Update project.pbxproj → CURRENT_PROJECT_VERSION = ${DISPLAY_NEW_BUILD} (currently ${DISPLAY_CURRENT_BUILD})"
|
||||
echo " 3. Create git tag v${NEW_VERSION}"
|
||||
fi
|
||||
echo ""
|
||||
read -p "Proceed with version bump? (y/n) " -n 1 -r
|
||||
echo ""
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Aborted"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Update project files
|
||||
echo "[1/3] Updating project version..."
|
||||
|
||||
# Update MARKETING_VERSION (unless keeping)
|
||||
if [ "$BUMP_TYPE" != "keep" ]; then
|
||||
sed -i.bak "s/MARKETING_VERSION = [0-9.]*;/MARKETING_VERSION = ${NEW_VERSION};/" Gaze.xcodeproj/project.pbxproj
|
||||
rm -f Gaze.xcodeproj/project.pbxproj.bak
|
||||
fi
|
||||
|
||||
# Update CURRENT_PROJECT_VERSION (build number) - increment by 1
|
||||
CURRENT_BUILD=$(grep -A 1 "CURRENT_PROJECT_VERSION" Gaze.xcodeproj/project.pbxproj | grep -o '[0-9]\+' | head -1)
|
||||
if [ -z "$CURRENT_BUILD" ]; then
|
||||
CURRENT_BUILD=0
|
||||
fi
|
||||
BUILD_NUMBER=$((CURRENT_BUILD + 1))
|
||||
sed -i.bak "s/CURRENT_PROJECT_VERSION = [0-9]*;/CURRENT_PROJECT_VERSION = ${BUILD_NUMBER};/" Gaze.xcodeproj/project.pbxproj
|
||||
rm -f Gaze.xcodeproj/project.pbxproj.bak
|
||||
|
||||
echo "✓ Project version updated"
|
||||
|
||||
# Stage changes and commit
|
||||
echo "[2/3] Committing changes..."
|
||||
git add Gaze.xcodeproj/project.pbxproj
|
||||
if [ "$BUMP_TYPE" = "keep" ]; then
|
||||
git commit -m "Build bump to v${NEW_VERSION} (${BUILD_NUMBER})"
|
||||
else
|
||||
git commit -m "Version bump to v${NEW_VERSION}"
|
||||
fi
|
||||
|
||||
# Create tag (skip if keeping version)
|
||||
if [ "$BUMP_TYPE" != "keep" ]; then
|
||||
echo "[3/3] Creating tag..."
|
||||
git tag -a "v${NEW_VERSION}" -m "Release version ${NEW_VERSION}"
|
||||
echo "✓ Version bumped to v${NEW_VERSION}"
|
||||
else
|
||||
echo "[3/3] Skipping tag creation (keeping version)"
|
||||
echo "✓ Build number bumped to ${BUILD_NUMBER}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Ask whether to bump version before continuing
|
||||
read -p "Do you want to bump the version before building? (y/n) " -n 1 -r
|
||||
echo ""
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
ask_version_bump
|
||||
fi
|
||||
|
||||
# Validate required code signing and notarization credentials
|
||||
echo "🔐 Validating credentials..."
|
||||
MISSING_CREDS=()
|
||||
|
||||
if [ -z "$DEVELOPER_ID_APPLICATION" ]; then
|
||||
MISSING_CREDS+=("DEVELOPER_ID_APPLICATION")
|
||||
fi
|
||||
|
||||
if [ -z "$NOTARY_KEYCHAIN_PROFILE" ]; then
|
||||
MISSING_CREDS+=("NOTARY_KEYCHAIN_PROFILE")
|
||||
fi
|
||||
|
||||
if [ -z "$APPLE_TEAM_ID" ]; then
|
||||
MISSING_CREDS+=("APPLE_TEAM_ID")
|
||||
fi
|
||||
|
||||
if [ ${#MISSING_CREDS[@]} -gt 0 ]; then
|
||||
echo "❌ ERROR: Missing required credentials in .env file:"
|
||||
for cred in "${MISSING_CREDS[@]}"; do
|
||||
echo " - $cred"
|
||||
done
|
||||
echo ""
|
||||
echo "Required environment variables:"
|
||||
echo " DEVELOPER_ID_APPLICATION='Developer ID Application: Your Name (TEAM_ID)'"
|
||||
echo " APPLE_TEAM_ID='XXXXXXXXXX'"
|
||||
echo " NOTARY_KEYCHAIN_PROFILE='notary-profile'"
|
||||
echo ""
|
||||
echo "Setup instructions:"
|
||||
echo " 1. Find your Developer ID certificate:"
|
||||
echo " security find-identity -v -p codesigning"
|
||||
echo ""
|
||||
echo " 2. Store notarization credentials in keychain (one-time setup):"
|
||||
echo " xcrun notarytool store-credentials \"notary-profile\" \\"
|
||||
echo " --apple-id \"your@email.com\" \\"
|
||||
echo " --team-id \"TEAM_ID\""
|
||||
echo ""
|
||||
echo " You'll be prompted for an app-specific password."
|
||||
echo " Generate one at: https://appleid.apple.com/account/manage"
|
||||
echo ""
|
||||
echo " 3. Set NOTARY_KEYCHAIN_PROFILE='notary-profile' in .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify keychain profile exists
|
||||
echo "🔍 Verifying keychain profile..."
|
||||
if ! xcrun notarytool history --keychain-profile "$NOTARY_KEYCHAIN_PROFILE" &>/dev/null; then
|
||||
echo "❌ ERROR: Keychain profile '$NOTARY_KEYCHAIN_PROFILE' not found or invalid"
|
||||
echo ""
|
||||
echo "Create the profile with:"
|
||||
echo " xcrun notarytool store-credentials \"$NOTARY_KEYCHAIN_PROFILE\" \\"
|
||||
echo " --apple-id \"your@email.com\" \\"
|
||||
echo " --team-id \"$APPLE_TEAM_ID\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All credentials validated"
|
||||
|
||||
# Extract version from Xcode project (Release configuration)
|
||||
PROJECT_FILE="Gaze.xcodeproj/project.pbxproj"
|
||||
VERSION=$(grep -A 1 "MARKETING_VERSION" "$PROJECT_FILE" | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1)
|
||||
BUILD_NUMBER=$(grep -A 1 "CURRENT_PROJECT_VERSION" "$PROJECT_FILE" | grep -o '[0-9]\+' | head -1)
|
||||
|
||||
# Fallback to manual values if extraction fails
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "⚠️ Could not extract MARKETING_VERSION from project, using fallback"
|
||||
VERSION="0.2.0"
|
||||
fi
|
||||
|
||||
if [ -z "$BUILD_NUMBER" ]; then
|
||||
echo "⚠️ Could not extract CURRENT_PROJECT_VERSION from project, using fallback"
|
||||
BUILD_NUMBER="1"
|
||||
fi
|
||||
|
||||
echo "📦 Building Gaze v${VERSION} (build ${BUILD_NUMBER}) for distribution"
|
||||
|
||||
RELEASES_DIR="./releases"
|
||||
ARCHIVE_PATH="./build/Gaze.xcarchive"
|
||||
EXPORT_PATH="./build/export"
|
||||
APPCAST_OUTPUT="${RELEASES_DIR}/appcast.xml"
|
||||
FEED_URL="https://freno.me/api/Gaze/appcast.xml"
|
||||
DOWNLOAD_URL_PREFIX="https://freno.me/api/downloads/"
|
||||
DMG_NAME="Gaze-${VERSION}.dmg"
|
||||
|
||||
# Validate required code signing and notarization credentials
|
||||
echo "🔐 Validating credentials..."
|
||||
MISSING_CREDS=()
|
||||
|
||||
if [ -z "$DEVELOPER_ID_APPLICATION" ]; then
|
||||
MISSING_CREDS+=("DEVELOPER_ID_APPLICATION")
|
||||
fi
|
||||
|
||||
if [ -z "$NOTARY_KEYCHAIN_PROFILE" ]; then
|
||||
MISSING_CREDS+=("NOTARY_KEYCHAIN_PROFILE")
|
||||
fi
|
||||
|
||||
if [ -z "$APPLE_TEAM_ID" ]; then
|
||||
MISSING_CREDS+=("APPLE_TEAM_ID")
|
||||
fi
|
||||
|
||||
if [ ${#MISSING_CREDS[@]} -gt 0 ]; then
|
||||
echo "❌ ERROR: Missing required credentials in .env file:"
|
||||
for cred in "${MISSING_CREDS[@]}"; do
|
||||
echo " - $cred"
|
||||
done
|
||||
echo ""
|
||||
echo "Required environment variables:"
|
||||
echo " DEVELOPER_ID_APPLICATION='Developer ID Application: Your Name (TEAM_ID)'"
|
||||
echo " APPLE_TEAM_ID='XXXXXXXXXX'"
|
||||
echo " NOTARY_KEYCHAIN_PROFILE='notary-profile'"
|
||||
echo ""
|
||||
echo "Setup instructions:"
|
||||
echo " 1. Find your Developer ID certificate:"
|
||||
echo " security find-identity -v -p codesigning"
|
||||
echo ""
|
||||
echo " 2. Store notarization credentials in keychain (one-time setup):"
|
||||
echo " xcrun notarytool store-credentials \"notary-profile\" \\"
|
||||
echo " --apple-id \"your@email.com\" \\"
|
||||
echo " --team-id \"TEAM_ID\""
|
||||
echo ""
|
||||
echo " You'll be prompted for an app-specific password."
|
||||
echo " Generate one at: https://appleid.apple.com/account/manage"
|
||||
echo ""
|
||||
echo " 3. Set NOTARY_KEYCHAIN_PROFILE='notary-profile' in .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify keychain profile exists
|
||||
echo "🔍 Verifying keychain profile..."
|
||||
if ! xcrun notarytool history --keychain-profile "$NOTARY_KEYCHAIN_PROFILE" &>/dev/null; then
|
||||
echo "❌ ERROR: Keychain profile '$NOTARY_KEYCHAIN_PROFILE' not found or invalid"
|
||||
echo ""
|
||||
echo "Create the profile with:"
|
||||
echo " xcrun notarytool store-credentials \"$NOTARY_KEYCHAIN_PROFILE\" \\"
|
||||
echo " --apple-id \"your@email.com\" \\"
|
||||
echo " --team-id \"$APPLE_TEAM_ID\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All credentials validated"
|
||||
|
||||
# Extract version from Xcode project (Release configuration)
|
||||
PROJECT_FILE="Gaze.xcodeproj/project.pbxproj"
|
||||
VERSION=$(grep -A 1 "MARKETING_VERSION" "$PROJECT_FILE" | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1)
|
||||
BUILD_NUMBER=$(grep -A 1 "CURRENT_PROJECT_VERSION" "$PROJECT_FILE" | grep -o '[0-9]\+' | head -1)
|
||||
|
||||
# Fallback to manual values if extraction fails
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "⚠️ Could not extract MARKETING_VERSION from project, using fallback"
|
||||
VERSION="0.2.0"
|
||||
fi
|
||||
|
||||
if [ -z "$BUILD_NUMBER" ]; then
|
||||
echo "⚠️ Could not extract CURRENT_PROJECT_VERSION from project, using fallback"
|
||||
BUILD_NUMBER="1"
|
||||
fi
|
||||
|
||||
echo "📦 Building Gaze v${VERSION} (build ${BUILD_NUMBER}) for distribution"
|
||||
|
||||
RELEASES_DIR="./releases"
|
||||
ARCHIVE_PATH="./build/Gaze.xcarchive"
|
||||
EXPORT_PATH="./build/export"
|
||||
APPCAST_OUTPUT="${RELEASES_DIR}/appcast.xml"
|
||||
FEED_URL="https://freno.me/api/Gaze/appcast.xml"
|
||||
DOWNLOAD_URL_PREFIX="https://freno.me/api/downloads/"
|
||||
DMG_NAME="Gaze-${VERSION}.dmg"
|
||||
|
||||
# Find Sparkle generate_appcast tool
|
||||
SPARKLE_BIN=$(find ~/Library/Developer/Xcode/DerivedData/Gaze-* -path "*/artifacts/sparkle/Sparkle/bin" -type d 2>/dev/null | head -1)
|
||||
if [ -z "$SPARKLE_BIN" ]; then
|
||||
echo "⚠️ Warning: Sparkle bin directory not found"
|
||||
echo "Appcast generation will be skipped"
|
||||
SPARKLE_BIN=""
|
||||
fi
|
||||
|
||||
# Create build and releases directories
|
||||
mkdir -p "$RELEASES_DIR"
|
||||
mkdir -p "$(dirname "$ARCHIVE_PATH")"
|
||||
|
||||
# Clean previous builds
|
||||
echo ""
|
||||
echo "🧹 Cleaning previous builds..."
|
||||
rm -rf "$ARCHIVE_PATH"
|
||||
rm -rf "$EXPORT_PATH"
|
||||
rm -f "$DMG_NAME"
|
||||
|
||||
# Clean Xcode build cache to ensure version changes are picked up
|
||||
echo "🧹 Cleaning Xcode build cache..."
|
||||
xcodebuild clean -project Gaze.xcodeproj -scheme Gaze -configuration Release
|
||||
|
||||
# Step 1: Archive the application
|
||||
echo ""
|
||||
echo "📦 Creating archive..."
|
||||
xcodebuild archive \
|
||||
-project Gaze.xcodeproj \
|
||||
-scheme Gaze \
|
||||
-configuration Release \
|
||||
-archivePath "$ARCHIVE_PATH" \
|
||||
CODE_SIGN_IDENTITY="$DEVELOPER_ID_APPLICATION" \
|
||||
CODE_SIGN_STYLE=Manual \
|
||||
DEVELOPMENT_TEAM="$APPLE_TEAM_ID"
|
||||
|
||||
if [ ! -d "$ARCHIVE_PATH" ]; then
|
||||
echo "❌ ERROR: Archive creation failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Archive created successfully"
|
||||
|
||||
# Step 2: Create exportOptions.plist
|
||||
echo ""
|
||||
echo "📝 Creating export options..."
|
||||
cat > /tmp/exportOptions.plist <<EOF
|
||||
<?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>manual</string>
|
||||
<key>teamID</key>
|
||||
<string>$APPLE_TEAM_ID</string>
|
||||
<key>signingCertificate</key>
|
||||
<string>Developer ID Application</string>
|
||||
<key>stripSwiftSymbols</key>
|
||||
<true/>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# Step 3: Export the archive
|
||||
echo ""
|
||||
echo "📤 Exporting signed application..."
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$ARCHIVE_PATH" \
|
||||
-exportPath "$EXPORT_PATH" \
|
||||
-exportOptionsPlist /tmp/exportOptions.plist
|
||||
|
||||
if [ ! -d "$EXPORT_PATH/Gaze.app" ]; then
|
||||
echo "❌ ERROR: Export failed - Gaze.app not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Application exported and signed"
|
||||
|
||||
# Step 4: Verify code signature
|
||||
echo ""
|
||||
echo "🔍 Verifying code signature..."
|
||||
codesign --verify --deep --strict --verbose=2 "$EXPORT_PATH/Gaze.app"
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Code signature valid"
|
||||
else
|
||||
echo "❌ ERROR: Code signature verification failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show signature details
|
||||
echo ""
|
||||
echo "📋 Signature details:"
|
||||
codesign -dv --verbose=4 "$EXPORT_PATH/Gaze.app" 2>&1 | grep -E "Authority|TeamIdentifier|Identifier"
|
||||
|
||||
# Step 5: Create ZIP archive for notarization
|
||||
echo ""
|
||||
echo "📦 Creating ZIP archive for notarization..."
|
||||
APP_ZIP="/tmp/Gaze-notarize.zip"
|
||||
rm -f "$APP_ZIP"
|
||||
ditto -c -k --keepParent "$EXPORT_PATH/Gaze.app" "$APP_ZIP"
|
||||
|
||||
if [ ! -f "$APP_ZIP" ]; then
|
||||
echo "❌ ERROR: Failed to create ZIP archive"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ ZIP archive created"
|
||||
|
||||
# Step 6: Notarize the application
|
||||
echo ""
|
||||
echo "🔐 Submitting application for notarization..."
|
||||
NOTARIZE_OUTPUT=$(xcrun notarytool submit "$APP_ZIP" \
|
||||
--keychain-profile "$NOTARY_KEYCHAIN_PROFILE" \
|
||||
--wait \
|
||||
--timeout 30m 2>&1)
|
||||
|
||||
echo "$NOTARIZE_OUTPUT"
|
||||
|
||||
if echo "$NOTARIZE_OUTPUT" | grep -q "status: Accepted"; then
|
||||
echo "✅ Application notarization accepted"
|
||||
|
||||
# Extract submission ID for logging
|
||||
SUBMISSION_ID=$(echo "$NOTARIZE_OUTPUT" | grep "id:" | head -1 | awk '{print $2}')
|
||||
echo " Submission ID: $SUBMISSION_ID"
|
||||
else
|
||||
echo "❌ ERROR: Application notarization failed"
|
||||
echo ""
|
||||
echo "Notarization output:"
|
||||
echo "$NOTARIZE_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up temporary ZIP
|
||||
rm -f "$APP_ZIP"
|
||||
|
||||
# Step 7: Staple notarization ticket to app
|
||||
echo ""
|
||||
echo "📎 Stapling notarization ticket to application..."
|
||||
xcrun stapler staple "$EXPORT_PATH/Gaze.app"
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Notarization ticket stapled to application"
|
||||
else
|
||||
echo "❌ ERROR: Failed to staple notarization ticket"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 8: Verify with Gatekeeper
|
||||
echo ""
|
||||
echo "🔍 Verifying Gatekeeper acceptance..."
|
||||
set +e # Temporarily disable exit on error
|
||||
spctl --assess --type execute --verbose=4 "$EXPORT_PATH/Gaze.app" 2>&1
|
||||
SPCTL_RESULT=$?
|
||||
set -e # Re-enable exit on error
|
||||
if [ $SPCTL_RESULT -eq 0 ]; then
|
||||
echo "✅ Application passes Gatekeeper verification"
|
||||
else
|
||||
echo "⚠️ Warning: Gatekeeper assessment returned non-zero status"
|
||||
echo " This may be expected for some configurations"
|
||||
fi
|
||||
|
||||
# Step 9: Create DMG from notarized app
|
||||
echo ""
|
||||
echo "💿 Creating DMG..."
|
||||
create-dmg \
|
||||
--volname "Gaze Installer" \
|
||||
--eula "./LICENSE" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 600 400 \
|
||||
--icon-size 100 \
|
||||
--background "./dmg_background.png" \
|
||||
--icon "Gaze.app" 160 200 \
|
||||
--app-drop-link 440 200 \
|
||||
"$DMG_NAME" \
|
||||
"$EXPORT_PATH/Gaze.app"
|
||||
|
||||
if [ ! -f "$DMG_NAME" ]; then
|
||||
echo "❌ ERROR: DMG creation failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ DMG created successfully"
|
||||
|
||||
# Step 10: Sign the DMG
|
||||
echo ""
|
||||
echo "🔏 Signing DMG..."
|
||||
codesign --sign "$DEVELOPER_ID_APPLICATION" \
|
||||
--timestamp \
|
||||
--options runtime \
|
||||
--force \
|
||||
"$DMG_NAME"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ DMG signed successfully"
|
||||
else
|
||||
echo "❌ ERROR: DMG signing failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify DMG signature
|
||||
codesign --verify --deep --strict --verbose=2 "$DMG_NAME"
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ DMG signature valid"
|
||||
else
|
||||
echo "❌ ERROR: DMG signature verification failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 11: Notarize the DMG
|
||||
echo ""
|
||||
echo "🔐 Submitting DMG for notarization..."
|
||||
DMG_NOTARIZE_OUTPUT=$(xcrun notarytool submit "$DMG_NAME" \
|
||||
--keychain-profile "$NOTARY_KEYCHAIN_PROFILE" \
|
||||
--wait \
|
||||
--timeout 30m 2>&1)
|
||||
|
||||
echo "$DMG_NOTARIZE_OUTPUT"
|
||||
|
||||
if echo "$DMG_NOTARIZE_OUTPUT" | grep -q "status: Accepted"; then
|
||||
echo "✅ DMG notarization accepted"
|
||||
|
||||
# Extract submission ID for logging
|
||||
DMG_SUBMISSION_ID=$(echo "$DMG_NOTARIZE_OUTPUT" | grep "id:" | head -1 | awk '{print $2}')
|
||||
echo " Submission ID: $DMG_SUBMISSION_ID"
|
||||
else
|
||||
echo "❌ ERROR: DMG notarization failed"
|
||||
echo ""
|
||||
echo "Notarization output:"
|
||||
echo "$DMG_NOTARIZE_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 12: Staple notarization ticket to DMG
|
||||
echo ""
|
||||
echo "📎 Stapling notarization ticket to DMG..."
|
||||
xcrun stapler staple "$DMG_NAME"
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Notarization ticket stapled to DMG"
|
||||
else
|
||||
echo "❌ ERROR: Failed to staple notarization ticket to DMG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 13: Final verification
|
||||
echo ""
|
||||
echo "🔍 Final DMG verification..."
|
||||
set +e # Temporarily disable exit on error
|
||||
spctl --assess --type open --context context:primary-signature --verbose=4 "$DMG_NAME" 2>&1
|
||||
DMG_SPCTL_RESULT=$?
|
||||
set -e # Re-enable exit on error
|
||||
if [ $DMG_SPCTL_RESULT -eq 0 ]; then
|
||||
echo "✅ DMG passes all Gatekeeper checks"
|
||||
else
|
||||
echo "⚠️ Warning: Gatekeeper assessment returned non-zero status"
|
||||
echo " This may be expected for disk images"
|
||||
fi
|
||||
|
||||
# Copy DMG to releases directory
|
||||
echo "Moving DMG to releases directory..."
|
||||
mv "$DMG_NAME" "$RELEASES_DIR/"
|
||||
|
||||
# Generate appcast if Sparkle tools are available
|
||||
if [ -n "$SPARKLE_BIN" ] && [ -d "$SPARKLE_BIN" ]; then
|
||||
echo ""
|
||||
echo "Generating appcast..."
|
||||
|
||||
# Check for private key (Keychain or file)
|
||||
PRIVATE_KEY_FILE="$HOME/sparkle_private_key_backup.pem"
|
||||
KEY_OPTION=""
|
||||
|
||||
if [ -f "$PRIVATE_KEY_FILE" ]; then
|
||||
echo "Using private key from: $PRIVATE_KEY_FILE"
|
||||
KEY_OPTION="--ed-key-file $PRIVATE_KEY_FILE"
|
||||
else
|
||||
echo "Using private key from Keychain (account: ed25519)"
|
||||
KEY_OPTION="--account ed25519"
|
||||
fi
|
||||
|
||||
# Generate appcast with download URL prefix and key
|
||||
"$SPARKLE_BIN/generate_appcast" \
|
||||
--download-url-prefix "$DOWNLOAD_URL_PREFIX" \
|
||||
$KEY_OPTION \
|
||||
"$RELEASES_DIR"
|
||||
|
||||
# Verify appcast was generated
|
||||
if [ -f "$APPCAST_OUTPUT" ]; then
|
||||
echo "✅ Appcast generated successfully"
|
||||
echo "📋 Appcast location: $APPCAST_OUTPUT"
|
||||
|
||||
# Check for signature - if missing, add it manually
|
||||
if grep -q "edSignature" "$APPCAST_OUTPUT"; then
|
||||
echo "✅ EdDSA signature verified in appcast"
|
||||
else
|
||||
echo "⚠️ No signature found, generating manually with sign_update..."
|
||||
|
||||
# Get signature for the DMG
|
||||
SIGNATURE_OUTPUT=$("$SPARKLE_BIN/sign_update" "$RELEASES_DIR/$DMG_NAME" 2>&1)
|
||||
|
||||
if echo "$SIGNATURE_OUTPUT" | grep -q "edSignature"; then
|
||||
# Extract the signature
|
||||
ED_SIGNATURE=$(echo "$SIGNATURE_OUTPUT" | grep -o 'sparkle:edSignature="[^"]*"' | sed 's/sparkle:edSignature="\([^"]*\)"/\1/')
|
||||
FILE_LENGTH=$(echo "$SIGNATURE_OUTPUT" | grep -o 'length="[^"]*"' | sed 's/length="\([^"]*\)"/\1/')
|
||||
|
||||
echo "✅ Generated signature: ${ED_SIGNATURE:0:20}..."
|
||||
|
||||
# Add signature to appcast XML
|
||||
# Find the enclosure line and add sparkle:edSignature attribute
|
||||
sed -i '' "s|<enclosure url=\"${DOWNLOAD_URL_PREFIX}$DMG_NAME\" length=\"[0-9]*\"|<enclosure url=\"${DOWNLOAD_URL_PREFIX}$DMG_NAME\" sparkle:edSignature=\"$ED_SIGNATURE\" length=\"$FILE_LENGTH\"|g" "$APPCAST_OUTPUT"
|
||||
|
||||
# Verify signature was added
|
||||
if grep -q "edSignature" "$APPCAST_OUTPUT"; then
|
||||
echo "✅ EdDSA signature added to appcast"
|
||||
else
|
||||
echo "❌ ERROR: Failed to add signature to appcast!"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ ERROR: Failed to generate signature with sign_update"
|
||||
echo "Output: $SIGNATURE_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "❌ Failed to generate appcast"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo ""
|
||||
echo "⚠️ Skipping appcast generation (Sparkle tools not found)"
|
||||
echo "To generate appcast manually, run:"
|
||||
echo " ./generate_appcast --ed-key-file ~/sparkle_private_key_backup.pem --download-url-prefix '$DOWNLOAD_URL_PREFIX' '$RELEASES_DIR'"
|
||||
fi
|
||||
|
||||
# Upload to AWS S3 if environment variables are set
|
||||
if [ -n "$AWS_ACCESS_KEY_ID" ] && [ -n "$AWS_SECRET_ACCESS_KEY" ] && [ -n "$AWS_BUCKET_NAME" ] && [ -n "$AWS_REGION" ]; then
|
||||
echo ""
|
||||
echo "Uploading to S3 bucket: $AWS_BUCKET_NAME..."
|
||||
|
||||
# Export AWS credentials for aws-cli
|
||||
export AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID"
|
||||
export AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY"
|
||||
export AWS_DEFAULT_REGION="$AWS_REGION"
|
||||
|
||||
# Upload all DMG files in releases directory
|
||||
echo "Uploading DMG files..."
|
||||
for dmg_file in "$RELEASES_DIR"/*.dmg; do
|
||||
if [ -f "$dmg_file" ]; then
|
||||
dmg_basename=$(basename "$dmg_file")
|
||||
echo " Uploading $dmg_basename..."
|
||||
aws s3 cp "$dmg_file" "s3://$AWS_BUCKET_NAME/downloads/$dmg_basename" --region "$AWS_REGION"
|
||||
fi
|
||||
done
|
||||
|
||||
# Upload all delta files in releases directory
|
||||
echo "Uploading delta files..."
|
||||
for delta_file in "$RELEASES_DIR"/*.delta; do
|
||||
if [ -f "$delta_file" ]; then
|
||||
delta_basename=$(basename "$delta_file")
|
||||
echo " Uploading $delta_basename..."
|
||||
aws s3 cp "$delta_file" "s3://$AWS_BUCKET_NAME/downloads/$delta_basename" --region "$AWS_REGION"
|
||||
fi
|
||||
done
|
||||
|
||||
# Upload appcast if it exists
|
||||
if [ -f "$APPCAST_OUTPUT" ]; then
|
||||
echo "Uploading appcast..."
|
||||
aws s3 cp "$APPCAST_OUTPUT" "s3://$AWS_BUCKET_NAME/api/Gaze/appcast.xml" --region "$AWS_REGION"
|
||||
echo "✅ Appcast uploaded to S3"
|
||||
fi
|
||||
|
||||
echo "✅ Upload complete!"
|
||||
echo " DMG files: s3://$AWS_BUCKET_NAME/downloads/*.dmg"
|
||||
echo " Delta files: s3://$AWS_BUCKET_NAME/downloads/*.delta"
|
||||
echo " Appcast: s3://$AWS_BUCKET_NAME/api/Gaze/appcast.xml"
|
||||
else
|
||||
echo ""
|
||||
echo "⚠️ Skipping S3 upload - AWS credentials not found in .env"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "✅ Release build complete and notarized!"
|
||||
echo ""
|
||||
echo "Release artifacts:"
|
||||
echo " 📦 DMG: $RELEASES_DIR/$DMG_NAME"
|
||||
if [ -f "$APPCAST_OUTPUT" ]; then
|
||||
echo " 📋 Appcast: $APPCAST_OUTPUT"
|
||||
fi
|
||||
echo " 🏗️ Archive: $ARCHIVE_PATH"
|
||||
echo " 📁 Export: $EXPORT_PATH/Gaze.app"
|
||||
echo ""
|
||||
echo "Verification:"
|
||||
echo " ✅ Application code signed with Developer ID"
|
||||
echo " ✅ Application notarized by Apple"
|
||||
echo " ✅ DMG signed with Developer ID"
|
||||
echo " ✅ DMG notarized by Apple"
|
||||
echo " ✅ Gatekeeper approved"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Verify appcast is accessible and valid"
|
||||
echo " 2. Test installation on clean macOS system"
|
||||
echo " 3. Test update from previous version"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
Reference in New Issue
Block a user