Files
openless/Package.swift
T
TRIPandbaiqing f38643f17c feat: Sparkle 2 auto-update
* feat: integrate Sparkle 2 for auto-update

Adds the standard pieces for Sparkle-based silent updates:

- Package.swift: add Sparkle 2 SPM dep, wired into OpenLessApp.
- Sources/OpenLessApp/Updater/UpdaterController.swift: thin wrapper
  around SPUStandardUpdaterController. Owned by AppDelegate so the
  updater starts on app launch.
- AppDelegate: instantiate UpdaterController before installing the
  main menu; pass it to ApplicationMenu so the menu item can target
  it.
- ApplicationMenu: add "检查更新…" as the first item in the OpenLess
  menu when an UpdaterController target is present.
- scripts/build-app.sh: copy Sparkle.framework (with Updater.app +
  XPC services + Autoupdate) from the SPM artifacts dir into
  Contents/Frameworks/, inject SUFeedURL / SUPublicEDKey /
  SUEnableAutomaticChecks / SUEnableInstallerLauncherService /
  SUScheduledCheckInterval into Info.plist, and ad-hoc sign the
  embedded helpers explicitly (deep sign alone is unreliable for
  Sparkle's nested XPC bundles).
- scripts/release.sh: one-shot release pipeline.
  ./scripts/release.sh <version> [<build>] [<notes>]
  Bumps version in build-app.sh, builds, zips, signs the zip with the
  Keychain-stored EdDSA private key (auto-downloads sign_update from
  the Sparkle 2.9.1 release on first run), prepends a new <item> to
  appcast.xml, commits + tags + pushes main, and creates the GitHub
  release with the zip uploaded.
- appcast.xml: empty initial feed file at repo root, served via the
  raw.githubusercontent.com URL referenced by SUFeedURL. Items are
  appended automatically by release.sh.

Public EdDSA key embedded in the build:
iT00+eUw/55obn1suEnWqI7za2pc8mHIFIdRbOWXW1Q=

Private key lives only in the local Keychain on the release machine
(account: macOS login user; service: https://sparkle-project.org).
First-time install still requires the user to remove the quarantine
xattr (no Apple Developer ID); subsequent updates flow through
Sparkle's installer helper and bypass quarantine.

* fix(sparkle): add @executable_path/../Frameworks rpath to OpenLessApp

Without this, the SPM-built executable links Sparkle's symbols but
ships with no rpath that points at the embedded framework, so dyld
fails at launch with:

  Library not loaded: @rpath/Sparkle.framework/Versions/B/Sparkle

…and the user sees the macOS Gatekeeper-styled "请与开发者联系,以确保
OpenLess 可以配合此 macOS 版本使用" error. Inject the rpath via
linkerSettings.unsafeFlags so the binary itself knows where to look.

---------

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-04-27 18:58:37 +08:00

93 lines
3.3 KiB
Swift

// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "OpenLess",
platforms: [.macOS("15.0")],
products: [
.executable(name: "OpenLess", targets: ["OpenLessApp"]),
.library(name: "OpenLessCore", targets: ["OpenLessCore"]),
.library(name: "OpenLessHotkey", targets: ["OpenLessHotkey"]),
.library(name: "OpenLessUI", targets: ["OpenLessUI"]),
.library(name: "OpenLessRecorder", targets: ["OpenLessRecorder"]),
.library(name: "OpenLessASR", targets: ["OpenLessASR"]),
.library(name: "OpenLessPolish", targets: ["OpenLessPolish"]),
.library(name: "OpenLessInsertion", targets: ["OpenLessInsertion"]),
.library(name: "OpenLessPersistence", targets: ["OpenLessPersistence"]),
],
dependencies: [
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.9.0"),
],
targets: [
.target(name: "OpenLessCore", path: "Sources/OpenLessCore"),
.target(
name: "OpenLessHotkey",
dependencies: ["OpenLessCore"],
path: "Sources/OpenLessHotkey"
),
.target(
name: "OpenLessUI",
dependencies: ["OpenLessCore"],
path: "Sources/OpenLessUI"
),
.target(
name: "OpenLessRecorder",
dependencies: ["OpenLessCore"],
path: "Sources/OpenLessRecorder"
),
.target(
name: "OpenLessASR",
dependencies: ["OpenLessCore"],
path: "Sources/OpenLessASR"
),
.target(
name: "OpenLessPolish",
dependencies: ["OpenLessCore"],
path: "Sources/OpenLessPolish"
),
.target(
name: "OpenLessInsertion",
dependencies: ["OpenLessCore"],
path: "Sources/OpenLessInsertion"
),
.target(
name: "OpenLessPersistence",
dependencies: ["OpenLessCore"],
path: "Sources/OpenLessPersistence"
),
.executableTarget(
name: "OpenLessApp",
dependencies: [
"OpenLessCore",
"OpenLessHotkey",
"OpenLessUI",
"OpenLessRecorder",
"OpenLessASR",
"OpenLessPolish",
"OpenLessInsertion",
"OpenLessPersistence",
.product(name: "Sparkle", package: "Sparkle"),
],
path: "Sources/OpenLessApp",
// 让 dyld 能在 Contents/Frameworks/ 里找到嵌入的 Sparkle.framework。
// SPM 默认不给 executable 加这个 rpath,需要显式注入。
linkerSettings: [
.unsafeFlags(["-Xlinker", "-rpath", "-Xlinker", "@executable_path/../Frameworks"])
]
),
.testTarget(
name: "OpenLessCoreTests",
dependencies: ["OpenLessCore"],
path: "Tests/OpenLessCoreTests"
),
.testTarget(
name: "OpenLessPolishTests",
dependencies: ["OpenLessPolish"],
path: "Tests/OpenLessPolishTests"
),
],
// 暂留 Swift 5 语言模式,避免一次性吞掉 Swift 6 严格并发的全部改造。
// 升 .v6 时需要重新审视 Recorder / VolcengineStreamingASR 等 @unchecked Sendable 类。
swiftLanguageModes: [.v5]
)