Repository navigation
Add an in-app updater with Sparkle 2 - #260
Conversation
Link Sparkle 2.10.1 into the production AskKeyApp executable only. AskKeyAppKit defines the updater interface, the enable policy (official Release install only) and the relaunch gate, which postpones Sparkle's relaunch while an approval is pending or unused, a Broker call is running, an upload is open or an authentication prompt is showing. Settings > General and the status item menu show update controls only when the updater exists. build-app.sh embeds Sparkle.framework without XPC services in every mode, fixes the rpaths, signs nested code inside out without --deep, and writes Sparkle keys only in Release, failing closed without scripts/sparkle-public-key.txt. ADR 0011 records the decisions, including the request headers an update check sends.
|
Review (planner, with an independent Codex GPT-6.1-Sol review): changes requested.
Everything else covered by the review is fine as is: integrity settings, Release-only gating, the postpone selector match, XPC removal, inside-out signing, the rpath changes, the handler wrapping and the tracker's locking. Please make no other changes in this round. |
- Release sets SUSignedFeedFailureExpirationInterval to 0, so Sparkle never accepts an unsigned feed after its 20-day failure window. - Support Sparkle's gentle reminders for scheduled updates: Sparkle still shows its alert without taking focus, and when notifications are already authorized or provisional one local notification names the version. Clicking it brings the alert forward through a new softwareUpdate notification route without opening the management window; it is removed once the user attends to the update or the session ends. - Shorten the Simplified Chinese relaunch subtitle. - Mark the Chinese test literals for the hygiene check.
|
Review round 1 addressed in 092590c.
One change beyond the list: the hygiene Local results at 092590c:
|
|
Review round 2 (planner, with Codex GPT-6.1-Sol): accepted, pending CI.
No further changes requested. The PR can be merged once |
AskKeyApp now links @rpath/Sparkle.framework. The packaged authentication tests copy the executable into a temporary Ask Key.app, where dyld could no longer find the framework, so the child exited before reading stdin and the second stdin write raised SIGPIPE in xctest on CI. Copy Sparkle.framework from the same build products next to the copied executable, where its @loader_path rpath resolves, and fail clearly when the build products lack it.
Closes #257
Summary
exact: "2.10.1", revisionab7d712) as a dependency of the productionAskKeyAppexecutable only.AskKeyAppKitowns theSoftwareUpdaterprotocol, the observableSoftwareUpdateControls, and the pureSoftwareUpdateAvailability(enable) andSoftwareUpdateRelaunchPolicy(postpone) types.SparkleSoftwareUpdaterinSources/AskKeyApp/wrapsSPUStandardUpdaterControllerandSPUUpdaterDelegate. The production entry passes it as a factory throughAppRuntimeConfiguration, and AppKit calls the factory only when the policy enables it.SoftwareUpdateRelaunchGate, which waits while any of these is present: a pending approval, an approved request the agent has not used yet, a running Broker call (runs with their deliveries, text, file and organization writes), an open upload, or a system authentication prompt. It rechecks every second and invokes Sparkle's handler exactly once when the app is idle. Timed allowances do not postpone the relaunch.scripts/build-app.shembedsSparkle.frameworkin every mode and removesXPCServices. It makes sure the executable's rpath includes@executable_path/../Frameworks, signs nested code inside out without--deep, and writes the Sparkle keys only in Release mode. Release fails closed unlessscripts/sparkle-public-key.txtholds one base64-encoded 32-byte key.features.md(row now Keep),architecture.md, both READMEs,SECURITY.md,CHANGELOG.md(Unreleased) and AGENTS.md → Product identifiers (feed URL) are updated.Diff stat
git diff --stat origin/main: 33 files changed, 1356 insertions(+), 22 deletions(-) (21 modified, 12 new).Package.swift,Package.resolved,Sources/AskKeyApp/AskKeyAppEntry.swift; AppKitAppDelegate.swift(+1),AppDelegate+BrokerLifecycle.swift(+9/−4),AppDelegate+Approval.swift(+1),AppDelegate+StatusItem.swift(+8/−2),AppRuntimeConfiguration.swift(+6/−1),VaultViewModel.swift(+2),FrozenSettingsPage.swift(+33),Localizable.xcstrings(+32);scripts/build-app.sh(+66/−2); docs.Sources/AskKeyApp/SparkleSoftwareUpdater.swift;Sources/AskKeyAppKit/SoftwareUpdate/{SoftwareUpdateAvailability,SoftwareUpdateRelaunchPolicy,SoftwareUpdater,AgentActivityTracker}.swift;Sources/AskKeyAppKit/App/AppDelegate+SoftwareUpdate.swift; fourTests/AskKeyAppTests/SoftwareUpdate*Tests.swift;Tests/Automation/test_build_app_sparkle.py;docs/adr/0011-in-app-updates.md.Sources/AskKeyBroker/,Sources/AskKeyHelper/,Sources/AskKeyVault/, entitlements, workflows,site/,site-src/,package-release.shornotarize-release.py.scripts/sparkle-public-key.txtis not added (Create the Sparkle update-signing key #258).Tests
swift test --filter 'SoftwareUpdate': 25 passed, 0 failed, 0 skipped, in 4 new suites:SoftwareUpdateAvailabilityTests(8),SoftwareUpdateRelaunchPolicyTests(6),SoftwareUpdateActivityTrackerTests(7) andSoftwareUpdateControlsTests(4). None of these suites exist onmain.httpor wrongly typedSUFeedURL; each testing flag set totrueand tofalse; nil, dev, e2e and foreign bundle IDs;developmentAccepted,relocatedOrRenamedandhelperMismatch; realOfficialInstallTopology.decideresults; and a source scan confirming that onlySources/AskKeyApp/SparkleSoftwareUpdater.swiftimports Sparkle.python3 -m unittest discover -s Tests/Automation -v: 244 passed, 0 failed, 0 skipped. That is 233 onmainplus 11 new tests intest_build_app_sparkle.py:--deepsigning.build-app.sh Debugbundle: noSU*keys, Sparkle embedded with no.xpc,codesign --verify --strict --deeppasses,@executable_path/../Frameworksis present, and no rpath points into the checkout.build-app.sh Debugoutput. In CI'sbuild-and-testjob no Debug bundle exists at the Automation step, so those 3 tests skip there (see Deviations).swift testand the desktop flows run in CI. I did not run them locally.Checks
swift buildpython3 scripts/check_hygiene.pypython3 scripts/check_module_deps.pybash scripts/check_release_symbols.shPASS: release app binary contains no forbidden E2E symbols or strings(after the fix in Deviations 3)git grep -n 'import Sparkle' -- Sources | grep -v '^Sources/AskKeyApp/'bash scripts/build-app.sh DebugPlistBuddy -c 'Print :SUFeedURL' …/Info.plistEntry, ":SUFeedURL", Does Not Existtest -d …/Contents/Frameworks/Sparkle.frameworkfind .build/AskKeyApp.app -name '*.xpc' | wc -lcodesign --verify --strict --deep .build/AskKeyApp.appotool -l … | grep -A2 LC_RPATH | grep -c '@executable_path/../Frameworks'Rpaths of the bundled Debug executable:
/usr/lib/swift,@loader_path,@executable_path/../liband@executable_path/../Frameworks. Xcode's absolute…/Build/Products/Debug/PackageFrameworksrpath is removed.Decision 8: what a check sends, taken from the pinned Sparkle 2.10.1 source (
.build/checkouts/Sparkle, tag2.10.1,ab7d71291238df6e0492143b120c9b696a35f603), not from launching the app:GET https://askkey.sudohg.com/appcast.xml, with no query string.SUEnableSystemProfilingisNO,SUSendProfileInfois unset and the delegate adds nofeedParametersForUpdater, soparameterizedFeedURLreturns the base URL (Sparkle/SPUUpdater.m:1249-1277). The check uses it atSPUUpdater.m:912andSPUUpdater.m:921.User-Agent: Sparkle's default is<name>/<CFBundleShortVersionString> Sparkle/2.10.1(Sparkle/SPUUserAgent+Private.m:22; set atSPUUpdater.m:115). The name comes from the localizedSUBundleName,CFBundleDisplayNameorCFBundleName(Sparkle/SUHost.m:121-133; version atSUHost.m:165-172), and non-ASCII characters are dropped (SPUUserAgent+Private.m:23). That would turn 请旨 into??, so the adapter setsuserAgentStringtoAsk Key/<CFBundleShortVersionString> Sparkle/<Sparkle CFBundleShortVersionString>. It is applied to the request atSparkle/SPUDownloadDriver.m:131-133.Accept: application/rss+xml,*/*;q=0.1, on the appcast request only (Sparkle/SUAppcastDriver.m:74).httpHeadersis unset and is merged atSPUDownloadDriver.m:137-142. The appcast bypasses the local cache (SPUDownloadDriver.m:129).Host,Accept-Languagefrom the preferred languages,Accept-Encoding) and the default cookie storage, because Sparkle usesdefaultSessionConfiguration(Downloader/SPUDownloader.m:84).Accept(Sparkle/SPUCoreBasedUpdateDriver.m:130). Asparkle:releaseNotesLink, if Publish a signed appcast with each release #259 uses one, is fetched with the same headers.Deviations and questions
AgentActivityTrackerin AppKit counts calls: each Broker handler closure inAppDelegate+BrokerLifecycle.swiftgets onebeginCall/defer endCallline, andfileWriteis wrapped. Approved-but-unused requests are tracked by remembering tickets frompendingRequests()(one line in the pending-count observer inAppDelegate+Approval.swift, active only when the updater exists) and readingstatus(requestID:capability:). Both use existing public APIs only. Supporting one-liners:AppDelegate.swift(tracker property),VaultViewModel.swift(softwareUpdatesproperty for Settings), andAppRuntimeConfiguration.swift(themakeSoftwareUpdaterfactory, which the E2E app does not pass).OfficialInstallDecision.accepted, not justallowsOfficialRuntime(which also allowsdevelopmentAccepted), andSUFeedURLmust equal the product feed URL exactly. Testing flags are detected as "any Info.plist key with theAskKeyprefix", whatever its value, rather than by listing the three names. OnlyLocal(AskKeyLocalTesting), Debug (AskKeyRequiresDebugRunDirectory) and E2E (AskKeyE2ETesting) write such keys. Naming the E2E flag in production code putAskKeyE2Einto the release binary and failedcheck_release_symbols.sh. The tests still cover all three names, plus a hypothetical later one.??.…/.derivedData/…/Build/Products/<config>/PackageFrameworks). It would ship a local path and let dyld find a build-tree Sparkle before the embedded one, sobuild-app.shdeletes rpaths into the checkout or derived data.Contents/Helpers/AskKey_AskKeyHelper.bundleis now signed explicitly.Contents/Helpersis a nested-code location that--deepused to cover.Updates(更新) as its title.build-and-testjob runs the Automation suite without building a Debug bundle, and.github/workflows/is out of Scope, soDebugBundleTests(3) skip there. They and the Acceptance commands ran locally against real output.basic-ui-flowsrunsbuild-app.sh E2E, which uses the same embedding and inside-out signing and ends withcodesign --verify --strict --deep.ASKKEY_*variables in one test.BuildAppReleaseKeyTestsruns a stubbed copy ofbuild-app.shin a temporary directory, with syntheticASKKEY_RELEASES_ENABLED,ASKKEY_CODESIGN_IDENTITYandASKKEY_APPLE_TEAM_IDin the subprocess environment so it reaches the key check. Stubs stop it before any build or signing.docs/README.md's ADR index is not updated; it also lacks 0009 and 0010.SPUStandardUserDriverDelegate.supportsGentleScheduledUpdateReminders) are not implemented. Sparkle may log a warning for thisLSUIElementapp when scheduled checks find an update.