diff --git a/Sources/LockIME/AppState.swift b/Sources/LockIME/AppState.swift index 441f3ca..13e1d26 100644 --- a/Sources/LockIME/AppState.swift +++ b/Sources/LockIME/AppState.swift @@ -465,6 +465,12 @@ final class AppState { alert.alertStyle = .warning alert.messageText = loc("Update failed") alert.informativeText = loc(failure.messageKey) + #if DEBUG + case .disabledInDevelopment: + alert.alertStyle = .informational + alert.messageText = loc("Development build") + alert.informativeText = loc("Automatic updates are disabled in development builds. To test the update flow, use the make update-test-* lab.") + #endif } alert.addButton(withTitle: loc("OK")) NSApp.activate(ignoringOtherApps: true) diff --git a/Sources/LockIME/Localizable.xcstrings b/Sources/LockIME/Localizable.xcstrings index 917d1e9..0e4a6a6 100644 --- a/Sources/LockIME/Localizable.xcstrings +++ b/Sources/LockIME/Localizable.xcstrings @@ -6345,6 +6345,110 @@ } } } + }, + "Development build": { + "localizations": { + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "开发版本" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "開發版本" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "開発ビルド" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Version de développement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Entwicklungs-Build" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilación de desarrollo" + } + }, + "pt": { + "stringUnit": { + "state": "translated", + "value": "Compilação de desenvolvimento" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Сборка для разработки" + } + } + } + }, + "Automatic updates are disabled in development builds. To test the update flow, use the make update-test-* lab.": { + "localizations": { + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "开发构建已禁用自动更新。如需测试更新流程,请使用 make update-test-* 实验链路。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "開發建置已停用自動更新。如需測試更新流程,請使用 make update-test-* 實驗鏈路。" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "開発ビルドでは自動更新が無効になっています。更新フローをテストするには、make update-test-* のラボを使用してください。" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les mises à jour automatiques sont désactivées dans les versions de développement. Pour tester le flux de mise à jour, utilisez le banc d'essai make update-test-*." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Automatische Updates sind in Entwicklungs-Builds deaktiviert. Um den Update-Ablauf zu testen, verwende das make update-test-*-Labor." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las actualizaciones automáticas están deshabilitadas en las compilaciones de desarrollo. Para probar el flujo de actualización, usa el laboratorio make update-test-*." + } + }, + "pt": { + "stringUnit": { + "state": "translated", + "value": "As atualizações automáticas estão desativadas em compilações de desenvolvimento. Para testar o fluxo de atualização, use o laboratório make update-test-*." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Автоматические обновления отключены в сборках для разработки. Чтобы протестировать процесс обновления, используйте лабораторию make update-test-*." + } + } + } } } } diff --git a/Sources/LockIME/Updates/UpdateController.swift b/Sources/LockIME/Updates/UpdateController.swift index 59ef9ac..42d25dc 100644 --- a/Sources/LockIME/Updates/UpdateController.swift +++ b/Sources/LockIME/Updates/UpdateController.swift @@ -7,6 +7,11 @@ import Sparkle enum UpdateCheckOutcome { case upToDate case failed(UpdateFailure) + #if DEBUG + /// A bare `make run` dev build declined to contact the real production feed + /// (see `UpdateController.updatesDisabledForDevelopment`). + case disabledInDevelopment + #endif } /// Owns the Sparkle updater wired to our custom user driver. @@ -40,6 +45,20 @@ final class UpdateController { private(set) var canCheckForUpdates = false + #if DEBUG + /// A bare `make run` dev build must never reach the real production feed or + /// install a stable release over the local build: its version is always + /// `0.0.0-development`, so every check would "find" the newest stable and + /// could replace the build under test. The update lab (`make update-test-*`) + /// is the one exception — it redirects the feed to a loopback server via + /// `LOCKIME_UPDATE_FEED` and deliberately exercises real download/install, + /// so the presence of that env var is exactly what tells the lab apart from + /// a plain run. Release builds never reach this property (compiled out). + private var updatesDisabledForDevelopment: Bool { + (ProcessInfo.processInfo.environment["LOCKIME_UPDATE_FEED"] ?? "").isEmpty + } + #endif + init() { driver = LockIMEUserDriver(model: model) driver.onUpdateAvailable = { [weak self] in @@ -56,6 +75,16 @@ final class UpdateController { /// Build and start the updater. Fails gracefully if `SUPublicEDKey` is /// missing/invalid (updates simply stay unavailable). func start() { + #if DEBUG + if updatesDisabledForDevelopment { + // Never start Sparkle in a plain dev build: no scheduled check can + // fire and the production feed is never contacted. The manual + // "Check for Updates…" button stays enabled and surfaces a + // "disabled in development" notice instead (see `checkForUpdates`). + canCheckForUpdates = true + return + } + #endif let updater = SPUUpdater( hostBundle: .main, applicationBundle: .main, @@ -86,6 +115,12 @@ final class UpdateController { /// check — nothing is shown until the result is known (an available update /// opens the window; "up to date"/errors surface as a native alert). func checkForUpdates() { + #if DEBUG + if updatesDisabledForDevelopment { + onCheckOutcome?(.disabledInDevelopment) + return + } + #endif if pendingUpdateVersion != nil { onPresentUpdateWindow?() } else {