general: improving build process, start repeated code extraction
This commit is contained in:
28
.env.example
Normal file
28
.env.example
Normal file
@@ -0,0 +1,28 @@
|
||||
# Notarization & Code Signing (Required for build_dmg)
|
||||
# =====================================================
|
||||
|
||||
# Developer ID Application certificate identity
|
||||
# Format: "Developer ID Application: Your Name (TEAM_ID)"
|
||||
# Find in Keychain Access or run: security find-identity -v -p codesigning
|
||||
DEVELOPER_ID_APPLICATION="Developer ID Application: Your Name (XXXXXXXXXX)"
|
||||
|
||||
# Apple Team ID
|
||||
# Find at: https://developer.apple.com/account#MembershipDetailsCard
|
||||
APPLE_TEAM_ID="XXXXXXXXXX"
|
||||
|
||||
# Notarization keychain profile name
|
||||
# This references credentials stored securely in your macOS Keychain
|
||||
# One-time setup (run this command once):
|
||||
# xcrun notarytool store-credentials "notary-profile" \
|
||||
# --apple-id "your@email.com" \
|
||||
# --team-id "XXXXXXXXXX"
|
||||
# You'll be prompted for an app-specific password from:
|
||||
# https://appleid.apple.com/account/manage
|
||||
NOTARY_KEYCHAIN_PROFILE="notary-profile"
|
||||
|
||||
# AWS S3 Upload (Optional)
|
||||
# =========================
|
||||
AWS_ACCESS_KEY_ID="your-access-key"
|
||||
AWS_SECRET_ACCESS_KEY="your-secret-key"
|
||||
AWS_BUCKET_NAME="your-bucket-name"
|
||||
AWS_REGION="us-east-1"
|
||||
26
Gaze/Extensions/StringExtensions.swift
Normal file
26
Gaze/Extensions/StringExtensions.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
extension String {
|
||||
var titleCase: String {
|
||||
// The compiler didn't like this chained - `Too long to type-check`
|
||||
|
||||
guard !self.isEmpty else {
|
||||
return ""
|
||||
}
|
||||
|
||||
let words = self.split(separator: " ")
|
||||
.map { word in
|
||||
guard !word.isEmpty else { return String(word) }
|
||||
return String(word.prefix(1)).uppercased() + String(word.dropFirst())
|
||||
}
|
||||
.joined(separator: " ")
|
||||
|
||||
let result = words.split(separator: "-")
|
||||
.map { word in
|
||||
guard !word.isEmpty else { return String(word) }
|
||||
return String(word.prefix(1)).uppercased() + String(word.dropFirst())
|
||||
}
|
||||
.joined(separator: "-")
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
0
Gaze/Views/Components/PreviewTrigger.swift
Normal file
0
Gaze/Views/Components/PreviewTrigger.swift
Normal file
113
Gaze/Views/Components/SliderSection.swift
Normal file
113
Gaze/Views/Components/SliderSection.swift
Normal file
@@ -0,0 +1,113 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SliderSection: View {
|
||||
@Binding var intervalMinutes: Int
|
||||
@Binding var countdownSeconds: Int
|
||||
@Binding var enabled: Bool
|
||||
|
||||
let intervalRange: ClosedRange<Int>
|
||||
let countdownRange: ClosedRange<Int>?
|
||||
let type: String
|
||||
let previewFunc: () -> Void
|
||||
let reminderText: String
|
||||
|
||||
init(
|
||||
intervalMinutes: Binding<Int>,
|
||||
countdownSeconds: Binding<Int>,
|
||||
intervalRange: ClosedRange<Int>,
|
||||
countdownRange: ClosedRange<Int>? = nil,
|
||||
enabled: Binding<Bool>,
|
||||
type: String,
|
||||
reminderText: String,
|
||||
previewFunc: @escaping () -> Void
|
||||
) {
|
||||
self._intervalMinutes = intervalMinutes
|
||||
self._countdownSeconds = countdownSeconds
|
||||
self.intervalRange = intervalRange
|
||||
self.countdownRange = countdownRange
|
||||
self._enabled = enabled
|
||||
self.type = type
|
||||
self.reminderText = reminderText
|
||||
self.previewFunc = previewFunc
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Toggle("Enable \(type.titleCase) Reminders", isOn: $enabled)
|
||||
.font(.headline)
|
||||
|
||||
if enabled {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Remind me every:")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
HStack {
|
||||
Slider(
|
||||
value: Binding(
|
||||
get: { Double(intervalMinutes) },
|
||||
set: { intervalMinutes = Int($0) }
|
||||
),
|
||||
in:
|
||||
Double(intervalRange.lowerBound)...Double(intervalRange.upperBound),
|
||||
step: 5.0)
|
||||
Text("\(intervalMinutes) min")
|
||||
.frame(width: 60, alignment: .trailing)
|
||||
.monospacedDigit()
|
||||
}
|
||||
|
||||
if let range = countdownRange {
|
||||
Text("Look away for:")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
HStack {
|
||||
Slider(
|
||||
value: Binding(
|
||||
get: { Double(countdownSeconds) },
|
||||
set: { countdownSeconds = Int($0) }
|
||||
), in: Double(range.lowerBound)...Double(range.upperBound),
|
||||
step: 5.0)
|
||||
Text("\(countdownSeconds) sec")
|
||||
.frame(width: 60, alignment: .trailing)
|
||||
.monospacedDigit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.padding()
|
||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||
|
||||
if enabled {
|
||||
Text(
|
||||
reminderText
|
||||
)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
} else {
|
||||
Text(
|
||||
"\(type) reminders are currently disabled."
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
previewFunc()
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "eye")
|
||||
.foregroundColor(.white)
|
||||
Text("Preview Reminder")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 10)
|
||||
.contentShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffectIfAvailable(
|
||||
GlassStyle.regular.tint(.accentColor).interactive(), in: .rect(cornerRadius: 10)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,7 @@ struct MenuBarContentView: View {
|
||||
|
||||
// Show all timers using unified identifier system
|
||||
ForEach(getSortedTimerIdentifiers(timerEngine: timerEngine), id: \.self) { identifier in
|
||||
if let state = timerEngine.timerStates[identifier] {
|
||||
if timerEngine.timerStates[identifier] != nil {
|
||||
TimerStatusRowWithIndividualControls(
|
||||
identifier: identifier,
|
||||
timerEngine: timerEngine,
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
// Created by Mike Freno on 1/7/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
@@ -58,83 +58,21 @@ struct LookAwaySetupView: View {
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding()
|
||||
.glassEffectIfAvailable(GlassStyle.regular.tint(.accentColor), in: .rect(cornerRadius: 8))
|
||||
.glassEffectIfAvailable(
|
||||
GlassStyle.regular.tint(.accentColor), in: .rect(cornerRadius: 8))
|
||||
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Toggle("Enable Look Away Reminders", isOn: $enabled)
|
||||
.font(.headline)
|
||||
SliderSection(
|
||||
intervalMinutes: $intervalMinutes,
|
||||
countdownSeconds: $countdownSeconds,
|
||||
intervalRange: 5...90,
|
||||
countdownRange: 10...30,
|
||||
enabled: $enabled,
|
||||
type: "Look away",
|
||||
reminderText:
|
||||
"You will be reminded every \(intervalMinutes) minutes to look in the distance for \(countdownSeconds) seconds",
|
||||
previewFunc: showPreviewWindow
|
||||
)
|
||||
|
||||
if enabled {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Remind me every:")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
HStack {
|
||||
Slider(
|
||||
value: Binding(
|
||||
get: { Double(intervalMinutes) },
|
||||
set: { intervalMinutes = Int($0) }
|
||||
), in: 5...90, step: 5)
|
||||
|
||||
Text("\(intervalMinutes) min")
|
||||
.frame(width: 60, alignment: .trailing)
|
||||
.monospacedDigit()
|
||||
}
|
||||
|
||||
Text("Look away for:")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
HStack {
|
||||
Slider(
|
||||
value: Binding(
|
||||
get: { Double(countdownSeconds) },
|
||||
set: { countdownSeconds = Int($0) }
|
||||
), in: 10...30, step: 5)
|
||||
|
||||
Text("\(countdownSeconds) sec")
|
||||
.frame(width: 60, alignment: .trailing)
|
||||
.monospacedDigit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||
|
||||
if enabled {
|
||||
Text(
|
||||
"You will be reminded every \(intervalMinutes) minutes to look in the distance for \(countdownSeconds) seconds"
|
||||
)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
} else {
|
||||
Text(
|
||||
"Look away reminders are currently disabled."
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
// Preview button
|
||||
Button(action: {
|
||||
showPreviewWindow()
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "eye")
|
||||
.foregroundColor(.white)
|
||||
Text("Preview Reminder")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 10)
|
||||
.contentShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffectIfAvailable(GlassStyle.regular.tint(.accentColor).interactive(), in: .rect(cornerRadius: 10))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -143,34 +81,35 @@ struct LookAwaySetupView: View {
|
||||
.padding()
|
||||
.background(.clear)
|
||||
}
|
||||
|
||||
|
||||
private func showPreviewWindow() {
|
||||
guard let screen = NSScreen.main else { return }
|
||||
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: screen.frame,
|
||||
styleMask: [.borderless, .fullSizeContentView],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
|
||||
|
||||
window.level = .floating
|
||||
window.isOpaque = false
|
||||
window.backgroundColor = .clear
|
||||
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
||||
window.acceptsMouseMovedEvents = true
|
||||
|
||||
let contentView = LookAwayReminderView(countdownSeconds: countdownSeconds) { [weak window] in
|
||||
|
||||
let contentView = LookAwayReminderView(countdownSeconds: countdownSeconds) {
|
||||
[weak window] in
|
||||
window?.close()
|
||||
}
|
||||
|
||||
|
||||
window.contentView = NSHostingView(rootView: contentView)
|
||||
window.makeFirstResponder(window.contentView)
|
||||
|
||||
|
||||
let windowController = NSWindowController(window: window)
|
||||
windowController.showWindow(nil)
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
|
||||
|
||||
previewWindowController = windowController
|
||||
}
|
||||
}
|
||||
|
||||
319
build_dmg
319
build_dmg
@@ -6,6 +6,63 @@ if [ -f .env ]; then
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
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)
|
||||
@@ -22,9 +79,11 @@ if [ -z "$BUILD_NUMBER" ]; then
|
||||
BUILD_NUMBER="1"
|
||||
fi
|
||||
|
||||
echo "📦 Building Gaze DMG for v${VERSION} (build ${BUILD_NUMBER})"
|
||||
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/"
|
||||
@@ -38,13 +97,169 @@ if [ -z "$SPARKLE_BIN" ]; then
|
||||
SPARKLE_BIN=""
|
||||
fi
|
||||
|
||||
# Create releases directory
|
||||
# Create build and releases directories
|
||||
mkdir -p "$RELEASES_DIR"
|
||||
mkdir -p "$(dirname "$ARCHIVE_PATH")"
|
||||
|
||||
# Remove old DMG if exists
|
||||
# Clean previous builds
|
||||
echo ""
|
||||
echo "🧹 Cleaning previous builds..."
|
||||
rm -rf "$ARCHIVE_PATH"
|
||||
rm -rf "$EXPORT_PATH"
|
||||
rm -f "$DMG_NAME"
|
||||
|
||||
echo "Creating DMG..."
|
||||
# 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" \
|
||||
| xcpretty || 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 \
|
||||
| xcpretty || 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..."
|
||||
spctl --assess --type execute --verbose=4 "$EXPORT_PATH/Gaze.app" 2>&1
|
||||
if [ $? -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" \
|
||||
@@ -55,7 +270,85 @@ create-dmg \
|
||||
--icon "Gaze.app" 160 200 \
|
||||
--app-drop-link 440 200 \
|
||||
"$DMG_NAME" \
|
||||
"./Gaze.app"
|
||||
"$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..."
|
||||
spctl --assess --type open --context context:primary-signature --verbose=4 "$DMG_NAME" 2>&1
|
||||
if [ $? -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..."
|
||||
@@ -181,15 +474,27 @@ fi
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Release artifacts created:"
|
||||
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. Upload DMG to: ${DOWNLOAD_URL_PREFIX}$DMG_NAME"
|
||||
echo " 2. Upload appcast to: $FEED_URL"
|
||||
echo " 3. Verify appcast is accessible and valid"
|
||||
echo " 4. Test update from previous version"
|
||||
echo " 4. Test installation on clean macOS system"
|
||||
echo " 5. Test update from previous version"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
Reference in New Issue
Block a user