mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
feat(apple): add direct-download macOS channel (notarized DMG + Sparkle + Homebrew)
Add a second macOS shipping channel alongside the App Store build: - New VniDropDirect target (Release-Direct config) sharing VniDrop's sources via an AppBase target template; links Sparkle behind the DIRECT_DISTRIBUTION flag so the App Store binary never bundles a self-updater. arm64-only (core is arm64). - Sparkle updater (SparkleUpdater.swift) + "Check for Updates" menu, compiled only under DIRECT_DISTRIBUTION; Info.plist SUFeedURL points at the GitHub Release /latest/download/appcast.xml, non-sandboxed entitlements for Developer ID. - build-dmg.sh (archive → Developer ID export → DMG → sign → notarize → staple), generate-appcast.sh, and ExportOptions-DeveloperID.plist. - apple-release.yml: on tag v*.*.*, build/notarize the DMG, publish the GitHub Release with appcast, and push the Homebrew cask to sudosylabs/homebrew-vnidrop. apple.yml gains a PR compile-check of the direct target. - CFBundleVersion is stamped at build time as a UTC YYMMDD.HHMM timestamp for both channels, replacing the hand-maintained build number. - Docs (RELEASE-MACOS.md, README), cask template + tap README, localized updates_check string, Makefile targets, gitignore for dist/ artifacts.
This commit is contained in:
@@ -8,6 +8,10 @@ private let mainWindowId = "main"
|
||||
@main
|
||||
struct VniDropApp: App {
|
||||
@StateObject private var externalInvitations = ExternalInvitationController()
|
||||
#if DIRECT_DISTRIBUTION && os(macOS)
|
||||
// Sparkle auto-updater, present only in the direct-download (.dmg) build.
|
||||
@StateObject private var updater = SparkleUpdaterController()
|
||||
#endif
|
||||
|
||||
var body: some Scene {
|
||||
#if os(macOS)
|
||||
@@ -18,6 +22,11 @@ struct VniDropApp: App {
|
||||
.ignoresSafeArea()
|
||||
.onOpenURL(perform: openInvitation)
|
||||
}
|
||||
#if DIRECT_DISTRIBUTION
|
||||
.commands {
|
||||
UpdatesCommands(controller: updater)
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
WindowGroup(id: mainWindowId) {
|
||||
RootView(dependencies: makeAppDependencies(externalInvitations: externalInvitations))
|
||||
|
||||
49
apple/VniDrop/Platform/SparkleUpdater.swift
Normal file
49
apple/VniDrop/Platform/SparkleUpdater.swift
Normal file
@@ -0,0 +1,49 @@
|
||||
#if DIRECT_DISTRIBUTION && os(macOS)
|
||||
import Combine
|
||||
import Sparkle
|
||||
import SwiftUI
|
||||
|
||||
/// Owns the Sparkle updater for the direct-download (.dmg) build.
|
||||
///
|
||||
/// Compiled only under `DIRECT_DISTRIBUTION`, so the App Store / TestFlight target
|
||||
/// (which must not ship a self-updater) never compiles or links Sparkle. The feed
|
||||
/// URL and public EdDSA key are read from Info.plist (`SUFeedURL`, `SUPublicEDKey`).
|
||||
@MainActor
|
||||
final class SparkleUpdaterController: ObservableObject {
|
||||
private let updaterController: SPUStandardUpdaterController
|
||||
/// Mirrors `SPUUpdater.canCheckForUpdates` so the menu item can disable itself
|
||||
/// while a check is already in flight.
|
||||
@Published private(set) var canCheckForUpdates = false
|
||||
|
||||
init() {
|
||||
// `startingUpdater: true` begins the automatic background check schedule.
|
||||
updaterController = SPUStandardUpdaterController(
|
||||
startingUpdater: true,
|
||||
updaterDelegate: nil,
|
||||
userDriverDelegate: nil
|
||||
)
|
||||
updaterController.updater
|
||||
.publisher(for: \.canCheckForUpdates)
|
||||
.assign(to: &$canCheckForUpdates)
|
||||
}
|
||||
|
||||
func checkForUpdates() {
|
||||
updaterController.checkForUpdates(nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a "Check for Updates…" item to the application menu (right after the
|
||||
/// standard "About VniDrop" item), matching the macOS convention.
|
||||
struct UpdatesCommands: Commands {
|
||||
@ObservedObject var controller: SparkleUpdaterController
|
||||
|
||||
var body: some Commands {
|
||||
CommandGroup(after: .appInfo) {
|
||||
Button(String(localized: L10n.Updates.check)) {
|
||||
controller.checkForUpdates()
|
||||
}
|
||||
.disabled(!controller.canCheckForUpdates)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -57,6 +57,20 @@
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<!-- Sparkle auto-update (direct-download .dmg build only). These keys are inert
|
||||
in the App Store build, which never loads Sparkle (DIRECT_DISTRIBUTION off).
|
||||
The feed is appcast.xml attached as an asset to each GitHub Release; the
|
||||
/releases/latest/download/ path always redirects to the newest (non-
|
||||
prerelease) release's copy, and its <enclosure> points at that same release's
|
||||
.dmg — no GitHub Pages or repo commits needed. SUPublicEDKey must be the EdDSA
|
||||
public key printed by Sparkle's `generate_keys` — replace the placeholder
|
||||
before shipping (see apple/RELEASE-MACOS.md). -->
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://github.com/sudosylabs/vnidrop/releases/latest/download/appcast.xml</string>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>/vcOgyrhPi3e58yL8M7hZvDCOgsAKyBsQu/7ChAUk1M=</string>
|
||||
<key>SUEnableAutomaticChecks</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>NFCReaderUsageDescription</key>
|
||||
|
||||
15
apple/VniDrop/Resources/VniDropDirect.entitlements
Normal file
15
apple/VniDrop/Resources/VniDropDirect.entitlements
Normal file
@@ -0,0 +1,15 @@
|
||||
<?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">
|
||||
<!-- Entitlements for the direct-download (Developer ID + notarized) macOS build.
|
||||
|
||||
Deliberately NOT sandboxed: a sandboxed app signed for Developer ID requires a
|
||||
provisioning profile, whereas direct-distribution apps run outside the App
|
||||
Store sandbox by convention. Gatekeeper trust here comes from the hardened
|
||||
runtime (ENABLE_HARDENED_RUNTIME) plus notarization, not the sandbox. The App
|
||||
Store target (VniDrop) keeps VniDrop.entitlements with the sandbox enabled.
|
||||
|
||||
Networking and user file access need no entitlements once unsandboxed. -->
|
||||
<dict>
|
||||
</dict>
|
||||
</plist>
|
||||
Reference in New Issue
Block a user