diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e6f0c1..a9b515d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,21 +6,21 @@ on: - "v[0-9]*" permissions: - contents: write + contents: read concurrency: group: release-${{ github.ref }} cancel-in-progress: false jobs: - release: + build: name: Build unsigned release runs-on: macos-15 timeout-minutes: 25 steps: - name: Check out source - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Show toolchain run: | @@ -75,6 +75,74 @@ jobs: dist \ "Startle-${release_version}-macOS-unsigned" + - name: Upload release inputs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: release-${{ github.ref_name }} + path: dist + if-no-files-found: error + retention-days: 1 + + publish: + name: Sign and publish release + needs: build + runs-on: macos-15 + timeout-minutes: 10 + environment: release + permissions: + contents: write + + steps: + - name: Download release inputs + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + with: + name: release-${{ github.ref_name }} + path: dist + + - name: Install pinned Sparkle signing tool + env: + SPARKLE_ARCHIVE_SHA256: ce89daf967db1e1893ed3ebd67575ed82d3902563e3191ca92aaec9164fbdef9 + SPARKLE_VERSION: 2.9.4 + run: | + archive="$RUNNER_TEMP/Sparkle-$SPARKLE_VERSION.tar.xz" + tools_dir="$RUNNER_TEMP/sparkle-tools" + + curl \ + --fail \ + --location \ + --proto '=https' \ + --tlsv1.2 \ + --output "$archive" \ + "https://github.com/sparkle-project/Sparkle/releases/download/$SPARKLE_VERSION/Sparkle-$SPARKLE_VERSION.tar.xz" + printf '%s %s\n' "$SPARKLE_ARCHIVE_SHA256" "$archive" | shasum --algorithm 256 --check + mkdir -p "$tools_dir" + tar -xJf "$archive" -C "$tools_dir" ./bin/generate_appcast + chmod 700 "$tools_dir/bin/generate_appcast" + + - name: Generate signed update feed + env: + SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} + run: | + if [ -z "$SPARKLE_PRIVATE_KEY" ]; then + echo "SPARKLE_PRIVATE_KEY is not configured." >&2 + exit 78 + fi + + release_version=${GITHUB_REF_NAME#v} + asset_name="Startle-${release_version}-macOS-unsigned" + update_dir=build/update-feed + mkdir -p "$update_dir" + cp "dist/$asset_name.zip" "$update_dir/" + + printf '%s' "$SPARKLE_PRIVATE_KEY" | \ + "$RUNNER_TEMP/sparkle-tools/bin/generate_appcast" \ + --ed-key-file - \ + --download-url-prefix \ + "https://github.com/${GITHUB_REPOSITORY}/releases/download/${GITHUB_REF_NAME}/" \ + "$update_dir" + + test -f "$update_dir/appcast.xml" + - name: Publish prerelease env: GH_TOKEN: ${{ github.token }} @@ -104,3 +172,18 @@ jobs: --notes "$release_notes" \ --generate-notes fi + + - name: Publish update feed + env: + GH_TOKEN: ${{ github.token }} + run: | + if gh release view appcast >/dev/null 2>&1; then + gh release upload appcast build/update-feed/appcast.xml --clobber + else + gh release create \ + appcast \ + build/update-feed/appcast.xml \ + --prerelease \ + --title "Startle update feed" \ + --notes "Machine-readable update feed used by Startle." + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bfae46..5b4a7e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes will be documented here. The project follows [Semantic Versi ## Unreleased +### Added + +- Secure in-app update checks, downloads, installation, and relaunch through Sparkle. +- Check-for-updates actions in the app menu, menu-bar menu, and About screen. + ## 1.0.4 - 2026-07-30 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f130d38..d0483aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ Contributions are welcome. Safety-sensitive behavior needs especially careful re ## Before opening a change - Search existing issues and pull requests. -- Keep changes focused and preserve the public-framework-only approach. +- Keep changes focused, use public Apple APIs, and preserve the existing Sparkle update path. - Do not add bundled copyrighted video, telemetry, private APIs, or behavior that bypasses the user's active window or safety settings. - For a security vulnerability, follow [SECURITY.md](SECURITY.md) instead of opening a public report. diff --git a/Config/Info.plist b/Config/Info.plist index 0ffe333..471ee5a 100644 --- a/Config/Info.plist +++ b/Config/Info.plist @@ -15,5 +15,9 @@ NSHighResolutionCapable NSHumanReadableCopyrightCopyright © 2026 Startle contributors. NSSupportsAutomaticGraphicsSwitching + SUEnableDownloaderService + SUEnableInstallerLauncherService + SUFeedURLhttps://github.com/Charlie284/Startle/releases/download/appcast/appcast.xml + SUPublicEDKeyzrBtzrc2pGL/vji3HnZBRptI2HLURhTT4tWmO0jMpzk= diff --git a/Config/Startle.entitlements b/Config/Startle.entitlements index f2ef3ae..fbcb170 100644 --- a/Config/Startle.entitlements +++ b/Config/Startle.entitlements @@ -6,5 +6,10 @@ com.apple.security.files.user-selected.read-only + com.apple.security.temporary-exception.mach-lookup.global-name + + $(PRODUCT_BUNDLE_IDENTIFIER)-spks + $(PRODUCT_BUNDLE_IDENTIFIER)-spki + diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..029370c --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "649efe5a428f68fc0d01b19ccc662cdd452405cf9ddfba94f843ce1072731fbf", + "pins" : [ + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle", + "state" : { + "revision" : "b6496a74a087257ef5e6da1c5b29a447a60f5bd7", + "version" : "2.9.4" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index 51b75da..503ce24 100644 --- a/Package.swift +++ b/Package.swift @@ -8,11 +8,17 @@ let package = Package( .library(name: "StartleCore", targets: ["StartleCore"]), .executable(name: "Startle", targets: ["StartleApp"]), ], + dependencies: [ + .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.9.4") + ], targets: [ .target(name: "StartleCore"), .executableTarget( name: "StartleApp", - dependencies: ["StartleCore"], + dependencies: [ + "StartleCore", + .product(name: "Sparkle", package: "Sparkle"), + ], resources: [.process("Resources")] ), .testTarget(name: "StartleCoreTests", dependencies: ["StartleCore"]), diff --git a/README.md b/README.md index bc30075..c37228a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Startle -Startle is a native macOS prank/productivity utility that waits in the menu bar and plays a randomly selected local video at carefully constrained times. It is built entirely with SwiftUI, AppKit, AVFoundation, and public Apple frameworks. +Startle is a native macOS prank/productivity utility that waits in the menu bar and plays a randomly selected local video at carefully constrained times. It is built with SwiftUI, AppKit, AVFoundation, public Apple frameworks, and Sparkle; it does not use private APIs. [![CI](https://github.com/Charlie284/Startle/actions/workflows/ci.yml/badge.svg)](https://github.com/Charlie284/Startle/actions/workflows/ci.yml) @@ -48,6 +48,8 @@ Version tags matching `v*` build a universal macOS app and publish it as a GitHu After verifying the checksum, open the DMG and drag **Startle** to the **Applications** shortcut. macOS will identify the app as coming from an unidentified developer, so use **Control-click → Open** in Applications for the first launch. Do not treat an unsigned preview as equivalent to a Developer ID-signed and notarized release. +Once installed, Startle checks for updates through Sparkle. You can also choose **Check for Updates…** from the app menu, menu-bar menu, or About screen. Update archives are verified with Startle's Ed25519 release key before installation. + ## Architecture - `SettingsStore` persists simple and structured preferences through Codable data in UserDefaults. @@ -59,6 +61,7 @@ After verifying the checksum, open the DMG and drag **Startle** to the **Applica - `ScareWindowController` preloads AVFoundation media, creates borderless high-level AppKit windows, supports every display mode, maintains aspect ratio or crop-to-fill, captures Escape, restores the cursor and previous app, and removes all observers/resources. - `EmergencyShortcutManager` registers **Command–Option–Shift–Escape** as a system hot key using the public Carbon hot-key API. Scheduled scares fail closed until that registration succeeds. - `LaunchAtLoginManager` wraps `SMAppService.mainApp` and respects “Never run at login.” +- `SoftwareUpdater` uses Sparkle's sandboxed installer and downloader services to verify, install, and relaunch updates from the signed release feed. - SwiftUI views provide onboarding, Dashboard, Videos, Schedule, Safety, Appearance, About, drag-and-drop, per-video controls, and a persistent `MenuBarExtra`. ## Scheduling and wake behavior @@ -69,7 +72,7 @@ Only one scheduling task exists at a time. Disabling scares cancels it. Preferen Startle is sandboxed. The file picker grants read-only access only to videos selected by the user; bookmarks preserve that access across launches. Videos are never copied or uploaded. Device-running, Apple screen-capture app, display, battery, output-volume, and window checks use public system APIs. Focus status is unavailable through public macOS APIs, and third-party sharing detection is best-effort. -Startle contains no analytics, advertising SDKs, remote services, or telemetry. Its privacy manifest declares no tracking or collected data. Local file paths and security-scoped bookmark data remain on the Mac. +Startle contains no analytics, advertising SDKs, or telemetry. Its only network use is checking for and downloading signed releases from GitHub through Sparkle. Its privacy manifest declares no tracking or collected data. Local file paths and security-scoped bookmark data remain on the Mac. Automatic safety checks can be incomplete when macOS or another app does not expose the needed state. Read [SAFETY.md](SAFETY.md) before enabling scheduled scares. diff --git a/RELEASE.md b/RELEASE.md index a646b19..e8b36ce 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -8,6 +8,8 @@ Pushing a version tag such as `v1.0.1` runs `.github/workflows/release.yml`. The Unsigned previews are intended for development and evaluation. Gatekeeper will identify them as coming from an unidentified developer, and they do not satisfy the production release process below. +The release workflow builds and tests without signing-key access. A separate job protected by the `release` environment signs the ZIP with Sparkle's Ed25519 key, uploads the versioned assets, and then replaces the fixed `appcast` release asset consumed by installed copies of Startle. The private key must exist only in that environment's `SPARKLE_PRIVATE_KEY` secret and the release operator's Keychain; never commit or print it. + ## 1. Prepare the version 1. Start from a clean default branch with passing CI. @@ -71,6 +73,7 @@ Exercise the final signed artifact on a clean account on macOS 14 and the curren - full-screen, centered, current-display, and all-display modes - cursor and previous-application restoration after completion, failure, and dismissal - launch-at-login enablement and the “Never run at login” override +- automatic and manual update checks, download verification, installation, and relaunch from the previous release - recovery from corrupt settings and video-library files ## 5. Publish diff --git a/Scripts/verify-distribution.sh b/Scripts/verify-distribution.sh index 963141c..341bd87 100755 --- a/Scripts/verify-distribution.sh +++ b/Scripts/verify-distribution.sh @@ -47,6 +47,16 @@ if [ "$(/usr/libexec/PlistBuddy -c 'Print :com.apple.security.files.user-selecte exit 70 fi +if [ "$(/usr/libexec/PlistBuddy -c 'Print :com.apple.security.temporary-exception.mach-lookup.global-name:0' "$entitlements_path")" != "com.startle.app-spks" ]; then + echo "The app signature does not allow communication with Sparkle's status service." >&2 + exit 70 +fi + +if [ "$(/usr/libexec/PlistBuddy -c 'Print :com.apple.security.temporary-exception.mach-lookup.global-name:1' "$entitlements_path")" != "com.startle.app-spki" ]; then + echo "The app signature does not allow communication with Sparkle's installer service." >&2 + exit 70 +fi + echo "Checking Gatekeeper acceptance" spctl --assess --type execute --verbose=2 "$app_path" diff --git a/Scripts/verify-unsigned-build.sh b/Scripts/verify-unsigned-build.sh index 8b8d936..0fb5ec6 100755 --- a/Scripts/verify-unsigned-build.sh +++ b/Scripts/verify-unsigned-build.sh @@ -18,6 +18,8 @@ executable_name=$(plutil -extract CFBundleExecutable raw -o - "$info_plist") executable_path="$app_path/Contents/MacOS/$executable_name" framework_path="$app_path/Contents/Frameworks/StartleCore.framework" framework_executable="$framework_path/Versions/A/StartleCore" +sparkle_framework_path="$app_path/Contents/Frameworks/Sparkle.framework" +sparkle_executable="$sparkle_framework_path/Versions/B/Sparkle" if [ ! -x "$executable_path" ]; then echo "The application executable is missing: $executable_path" >&2 @@ -29,6 +31,21 @@ if [ ! -x "$framework_executable" ]; then exit 66 fi +if [ ! -x "$sparkle_executable" ]; then + echo "The embedded Sparkle framework is missing or incomplete." >&2 + exit 66 +fi + +if [ ! -d "$sparkle_framework_path/Versions/B/XPCServices/Installer.xpc" ]; then + echo "Sparkle's installer service is missing from the sandboxed app." >&2 + exit 66 +fi + +if [ ! -d "$sparkle_framework_path/Versions/B/XPCServices/Downloader.xpc" ]; then + echo "Sparkle's downloader service is missing from the sandboxed app." >&2 + exit 66 +fi + if ! otool -l "$executable_path" | grep -q '@executable_path/../Frameworks'; then echo "The application does not search its embedded Frameworks directory." >&2 exit 70 @@ -39,8 +56,35 @@ if ! otool -L "$executable_path" | grep -q '@rpath/StartleCore.framework/Version exit 70 fi +if ! otool -L "$executable_path" | grep -q '@rpath/Sparkle.framework/Versions/B/Sparkle'; then + echo "The application is not linked to the expected Sparkle framework." >&2 + exit 70 +fi + +if [ "$(plutil -extract SUEnableInstallerLauncherService raw -o - "$info_plist")" != "true" ]; then + echo "Sparkle's installer service is not enabled in Info.plist." >&2 + exit 70 +fi + +if [ "$(plutil -extract SUEnableDownloaderService raw -o - "$info_plist")" != "true" ]; then + echo "Sparkle's downloader service is not enabled in Info.plist." >&2 + exit 70 +fi + +if [ -z "$(plutil -extract SUFeedURL raw -o - "$info_plist")" ]; then + echo "The Sparkle update feed URL is missing from Info.plist." >&2 + exit 70 +fi + +public_key=$(plutil -extract SUPublicEDKey raw -o - "$info_plist") +if [ -z "$public_key" ] || [ "$public_key" = "SPARKLE_PUBLIC_KEY" ]; then + echo "The Sparkle public signing key is missing from Info.plist." >&2 + exit 70 +fi + app_architectures=$(lipo -archs "$executable_path") framework_architectures=$(lipo -archs "$framework_executable") +sparkle_architectures=$(lipo -archs "$sparkle_executable") for architecture in arm64 x86_64; do case " $app_architectures " in *" $architecture "*) ;; @@ -50,6 +94,10 @@ for architecture in arm64 x86_64; do *" $architecture "*) ;; *) echo "StartleCore.framework is missing the $architecture architecture." >&2; exit 70 ;; esac + case " $sparkle_architectures " in + *" $architecture "*) ;; + *) echo "Sparkle.framework is missing the $architecture architecture." >&2; exit 70 ;; + esac done plutil -lint "$info_plist" diff --git a/Sources/StartleApp/AppState.swift b/Sources/StartleApp/AppState.swift index ba8de25..c6b1c3e 100644 --- a/Sources/StartleApp/AppState.swift +++ b/Sources/StartleApp/AppState.swift @@ -12,6 +12,7 @@ final class AppState { let scheduler: ScareScheduler let coordinator: ScareCoordinator let launchAtLogin: LaunchAtLoginManager + let softwareUpdater: SoftwareUpdater private var emergencyShortcut: EmergencyShortcutManager! init() { @@ -24,6 +25,7 @@ final class AppState { scheduler = ScareScheduler(settings: settings, activity: activity) coordinator = ScareCoordinator(settings: settings, library: library) launchAtLogin = LaunchAtLoginManager() + softwareUpdater = SoftwareUpdater() emergencyShortcut = EmergencyShortcutManager { [weak self] in self?.emergencyDisable() } scheduler.onTrigger = { [weak self] in await self?.coordinator.trigger() } library.onEnabledVideosChanged = { [weak self] hasVideos in diff --git a/Sources/StartleApp/AppearanceView.swift b/Sources/StartleApp/AppearanceView.swift index 6dbf7f3..3f08614 100644 --- a/Sources/StartleApp/AppearanceView.swift +++ b/Sources/StartleApp/AppearanceView.swift @@ -53,6 +53,8 @@ extension Color { } struct AboutView: View { + @Environment(AppState.self) private var state + private var version: String { Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Development" @@ -71,6 +73,8 @@ struct AboutView: View { Text( "Startle never uploads your videos. Imported files remain where you put them and are reopened through security-scoped bookmarks." ).multilineTextAlignment(.center).frame(maxWidth: 520) + Button("Check for Updates…") { state.softwareUpdater.checkForUpdates() } + .disabled(!state.softwareUpdater.canCheckForUpdates) }.frame(maxWidth: .infinity).padding(.vertical, 40) } } diff --git a/Sources/StartleApp/SoftwareUpdater.swift b/Sources/StartleApp/SoftwareUpdater.swift new file mode 100644 index 0000000..a08148a --- /dev/null +++ b/Sources/StartleApp/SoftwareUpdater.swift @@ -0,0 +1,33 @@ +import Foundation +import Observation +import Sparkle + +@MainActor @Observable +final class SoftwareUpdater { + @ObservationIgnored private let controller: SPUStandardUpdaterController + @ObservationIgnored private var canCheckForUpdatesObservation: NSKeyValueObservation? + + private(set) var canCheckForUpdates = false + + init() { + let controller = SPUStandardUpdaterController( + startingUpdater: true, + updaterDelegate: nil, + userDriverDelegate: nil + ) + self.controller = controller + canCheckForUpdatesObservation = controller.updater.observe( + \.canCheckForUpdates, + options: [.initial, .new] + ) { [weak self] _, change in + let canCheckForUpdates = change.newValue ?? false + Task { @MainActor [weak self] in + self?.canCheckForUpdates = canCheckForUpdates + } + } + } + + func checkForUpdates() { + controller.checkForUpdates(nil) + } +} diff --git a/Sources/StartleApp/StartleApp.swift b/Sources/StartleApp/StartleApp.swift index 519ab69..6611693 100644 --- a/Sources/StartleApp/StartleApp.swift +++ b/Sources/StartleApp/StartleApp.swift @@ -14,7 +14,13 @@ struct StartleApp: App { .frame(minWidth: 880, minHeight: 540) } .defaultSize(width: 1050, height: 610) - .commands { CommandGroup(replacing: .newItem) {} } + .commands { + CommandGroup(replacing: .newItem) {} + CommandGroup(after: .appInfo) { + Button("Check for Updates…") { state.softwareUpdater.checkForUpdates() } + .disabled(!state.softwareUpdater.canCheckForUpdates) + } + } MenuBarExtra { MenuBarContent() @@ -64,6 +70,8 @@ private struct MenuBarContent: View { openWindow(id: "main") NSApp.activate() } + Button("Check for Updates…") { state.softwareUpdater.checkForUpdates() } + .disabled(!state.softwareUpdater.canCheckForUpdates) Button("Quit Startle") { NSApplication.shared.terminate(nil) } .keyboardShortcut("q") } diff --git a/Startle.xcodeproj/project.pbxproj b/Startle.xcodeproj/project.pbxproj index 636f830..9246a12 100644 --- a/Startle.xcodeproj/project.pbxproj +++ b/Startle.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ B2000000000000000000000A /* SafetyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2000000000000000000000A /* SafetyView.swift */; }; B2000000000000000000000B /* AppearanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2000000000000000000000B /* AppearanceView.swift */; }; B2000000000000000000000C /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2000000000000000000000C /* OnboardingView.swift */; }; + B2000000000000000000000D /* SoftwareUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2000000000000000000000D /* SoftwareUpdater.swift */; }; B30000000000000000000001 /* ScheduleEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30000000000000000000001 /* ScheduleEngineTests.swift */; }; B30000000000000000000002 /* SettingsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30000000000000000000002 /* SettingsStoreTests.swift */; }; B30000000000000000000003 /* VideoLibraryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30000000000000000000003 /* VideoLibraryTests.swift */; }; @@ -37,6 +38,7 @@ B40000000000000000000001 /* StartleCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = P10000000000000000000002 /* StartleCore.framework */; }; B40000000000000000000002 /* StartleCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = P10000000000000000000002 /* StartleCore.framework */; }; B40000000000000000000003 /* StartleCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = P10000000000000000000002 /* StartleCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B50000000000000000000001 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = Q50000000000000000000001 /* Sparkle */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -94,6 +96,7 @@ F2000000000000000000000A /* SafetyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafetyView.swift; sourceTree = ""; }; F2000000000000000000000B /* AppearanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceView.swift; sourceTree = ""; }; F2000000000000000000000C /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; + F2000000000000000000000D /* SoftwareUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareUpdater.swift; sourceTree = ""; }; F30000000000000000000001 /* ScheduleEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleEngineTests.swift; sourceTree = ""; }; F30000000000000000000002 /* SettingsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStoreTests.swift; sourceTree = ""; }; F30000000000000000000003 /* VideoLibraryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoLibraryTests.swift; sourceTree = ""; }; @@ -111,6 +114,7 @@ buildActionMask = 2147483647; files = ( B40000000000000000000001 /* StartleCore.framework in Frameworks */, + B50000000000000000000001 /* Sparkle in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -173,6 +177,7 @@ F2000000000000000000000A /* SafetyView.swift */, F2000000000000000000000B /* AppearanceView.swift */, F2000000000000000000000C /* OnboardingView.swift */, + F2000000000000000000000D /* SoftwareUpdater.swift */, G10000000000000000000007 /* Resources */, ); path = Sources/StartleApp; @@ -236,6 +241,9 @@ D10000000000000000000001 /* PBXTargetDependency */, ); name = Startle; + packageProductDependencies = ( + Q50000000000000000000001 /* Sparkle */, + ); productName = Startle; productReference = P10000000000000000000001 /* Startle.app */; productType = "com.apple.product-type.application"; @@ -303,6 +311,9 @@ Base, ); mainGroup = G10000000000000000000001; + packageReferences = ( + X50000000000000000000001 /* XCRemoteSwiftPackageReference "Sparkle" */, + ); productRefGroup = G10000000000000000000006 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -341,6 +352,7 @@ B2000000000000000000000A /* SafetyView.swift in Sources */, B2000000000000000000000B /* AppearanceView.swift in Sources */, B2000000000000000000000C /* OnboardingView.swift in Sources */, + B2000000000000000000000D /* SoftwareUpdater.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -563,6 +575,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + X50000000000000000000001 /* XCRemoteSwiftPackageReference "Sparkle" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sparkle-project/Sparkle"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.9.4; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + Q50000000000000000000001 /* Sparkle */ = { + isa = XCSwiftPackageProductDependency; + package = X50000000000000000000001 /* XCRemoteSwiftPackageReference "Sparkle" */; + productName = Sparkle; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = A10000000000000000000001 /* Project object */; } diff --git a/Startle.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Startle.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..feb7b4b --- /dev/null +++ b/Startle.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "e721da7f9826abdffcb6185e886155efa2514bd6234475f1afa893e29eb258d6", + "pins" : [ + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle", + "state" : { + "revision" : "b6496a74a087257ef5e6da1c5b29a447a60f5bd7", + "version" : "2.9.4" + } + } + ], + "version" : 3 +}