diff --git a/.gitignore b/.gitignore index 0e8fec9..b67a018 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +*.orig +*.rej +*.bak # Build artifacts .build/ .swiftpm/ diff --git a/README.md b/README.md index d19ce3b..eddae37 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,15 @@

--- +--- +> **📢 Fork Notes** +> +> I'm not a developer — just a regular user on a **2017 Intel MacBook Air running macOS 12.7.6**. The original version didn't work on my machine, so I fixed it step by step with the help of Codex (an AI coding assistant). Now it works perfectly on my setup. +> +> Huge thanks to [@Attiv](https://github.com/Attiv) for open-sourcing this! If the original version doesn't work for you either, give this fork a try. I can't help with other technical issues though — I'm just a user who got lucky with AI. +> +> **Tested on:** macOS 12.7.6 · Intel MacBook Air (2017) · External display via Thunderbolt +> ClamOpen is a tiny menu-bar app that **truly turns off your MacBook's built-in display** (backlight off, the compositor stops rendering to it) while an external monitor is connected — **with the lid open**. The result is identical to clamshell mode, except you keep the webcam, Touch ID, the keyboard, and better airflow. diff --git a/Sources/ClamOpen/AppDelegate.swift b/Sources/ClamOpen/AppDelegate.swift index afb4844..9f4398c 100644 --- a/Sources/ClamOpen/AppDelegate.swift +++ b/Sources/ClamOpen/AppDelegate.swift @@ -1,4 +1,5 @@ import AppKit +import CoreGraphics final class AppDelegate: NSObject, NSApplicationDelegate { @@ -16,6 +17,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { private var watchdog: Timer? + private var enableLock = false /// CoreGraphics 显示重配置回调(拔插显示器时即时触发,比 NSNotification 更底层、更早)。 /// 闭包不捕获 self,AppDelegate 通过 userInfo 指针传入。 private let reconfigCallback: CGDisplayReconfigurationCallBack = { _, flags, userInfo in @@ -44,6 +46,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate { CGDisplayRegisterReconfigurationCallback( reconfigCallback, Unmanaged.passUnretained(self).toOpaque()) + // 监听分布式通知:恢复内置屏.app 发送的通知,触发恢复内置屏 + DistributedNotificationCenter.default().addObserver( + self, selector: #selector(handleRestoreNotification), + name: NSNotification.Name("com.clamopen.restore-builtin"), + object: nil) + startWatchdog() rebuildMenu() updateIcon() @@ -60,6 +68,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate { if intentDisabled { controller.enableBuiltin() } } + + @objc private func handleRestoreNotification() { + print("[ClamOpen] Received restore notification") + if intentDisabled { + enable() + } + } // MARK: - 动作 @objc private func disable() { @@ -76,8 +91,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } @objc private func enable() { - controller.enableBuiltin() + enableLock = true intentDisabled = false + controller.enableBuiltin() + enableLock = false refresh() } diff --git a/Sources/ClamOpen/DisplayController.swift b/Sources/ClamOpen/DisplayController.swift index c624ca7..03ad6f8 100644 --- a/Sources/ClamOpen/DisplayController.swift +++ b/Sources/ClamOpen/DisplayController.swift @@ -5,25 +5,59 @@ import Foundation /// /// 通过 CoreGraphics 私有符号 `CGSConfigureDisplayEnabled` 真正关闭内置面板 /// (停止渲染 + 关闭背光),效果等同合盖(clamshell),但盖子保持打开。 +/// +/// 注意:`CGDisplayConfigurationFlags` 是私有 typedef UInt32,SDK 不暴露。 +/// 所有涉及配置标志的地方都用 UInt32 代替: +/// 0 = .forSession(默认,不触发完整重配置) +/// 1 = .forceConfiguration(强制 WindowServer 重配显示管线) + final class DisplayController { + // MARK: - 私有 API 签名 + /// CGError CGSConfigureDisplayEnabled(CGDisplayConfigRef, CGDirectDisplayID, bool) typealias ConfigureDisplayEnabledFn = @convention(c) (CGDisplayConfigRef?, CGDirectDisplayID, Bool) -> CGError - private let configureEnabled: ConfigureDisplayEnabledFn? + /// CGError CGSGetDisplayList(UInt32, CGDirectDisplayID*, UInt32*) — 能拿到被禁用的显示器 + typealias GetDisplayListFn = + @convention(c) (UInt32, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CGError + + /// CGError CGCompleteDisplayConfiguration(CGDisplayConfigRef, UInt32) + /// UInt32 == CGDisplayConfigurationFlags (0=forSession, 1=forceConfiguration) + typealias CompleteDisplayConfigurationFn = + @convention(c) (CGDisplayConfigRef?, UInt32) -> CGError + + /// CGError CGDisplayRestoreDisplayConfiguration(CGDirectDisplayID, UInt32) + typealias RestoreDisplayConfigurationFn = + @convention(c) (CGDirectDisplayID, UInt32) -> CGError + + private var configureEnabled: ConfigureDisplayEnabledFn? = nil + private var getDisplayList: GetDisplayListFn? = nil + private var completeDisplayConfiguration: CompleteDisplayConfigurationFn? = nil + private var restoreDisplayConfiguration: RestoreDisplayConfigurationFn? = nil init() { - // RTLD_DEFAULT (== -2):符号随 CoreGraphics 已载入本进程,直接取即可 - let rtldDefault = UnsafeMutableRawPointer(bitPattern: -2) - if let sym = dlsym(rtldDefault, "CGSConfigureDisplayEnabled") { - configureEnabled = unsafeBitCast(sym, to: ConfigureDisplayEnabledFn.self) - } else { - configureEnabled = nil + let rtldDefault: UnsafeMutableRawPointer? = UnsafeMutableRawPointer(bitPattern: -2) + + if let s = rtldDefault.flatMap({ dlsym($0, "CGSConfigureDisplayEnabled") }) { + self.configureEnabled = unsafeBitCast(s, to: ConfigureDisplayEnabledFn.self) + } + + if let s = rtldDefault.flatMap({ dlsym($0, "CGSGetDisplayList") }) { + self.getDisplayList = unsafeBitCast(s, to: GetDisplayListFn.self) + } + + if let s = rtldDefault.flatMap({ dlsym($0, "CGCompleteDisplayConfiguration") }) { + self.completeDisplayConfiguration = unsafeBitCast(s, to: CompleteDisplayConfigurationFn.self) + } + + if let s = rtldDefault.flatMap({ dlsym($0, "CGDisplayRestoreDisplayConfiguration") }) { + self.restoreDisplayConfiguration = unsafeBitCast(s, to: RestoreDisplayConfigurationFn.self) } } - /// 私有 API 是否可用(理论上所有现代 macOS 都可用) + /// 私有 API 是否可用 var isAPIAvailable: Bool { configureEnabled != nil } // MARK: - 查询 @@ -37,10 +71,28 @@ final class DisplayController { return Array(ids.prefix(Int(count))) } + /// 获取所有显示器(包括被禁用的),用于恢复场景。 + func allDisplays() -> [CGDirectDisplayID] { + guard let getDisplayList else { return onlineDisplays() } + var count: UInt32 = 0 + if getDisplayList(0, nil, &count) == .success, count > 0 { + var ids = [CGDirectDisplayID](repeating: 0, count: Int(count)) + if getDisplayList(count, &ids, &count) == .success { + return Array(ids.prefix(Int(count))) + } + } + return onlineDisplays() + } + func builtinDisplay() -> CGDirectDisplayID? { onlineDisplays().first { CGDisplayIsBuiltin($0) != 0 } } + /// 从 allDisplays 中查找内置屏(包括被禁用的) + func findBuiltinInAll() -> CGDirectDisplayID? { + allDisplays().first { CGDisplayIsBuiltin($0) != 0 } + } + /// 在线的外接显示器(非内置) func externalDisplays() -> [CGDirectDisplayID] { onlineDisplays().filter { CGDisplayIsBuiltin($0) == 0 } @@ -60,7 +112,7 @@ final class DisplayController { case ok case apiMissing case noBuiltin - case noExternal // 安全拦截:没有外接显示器,拒绝关闭内置(否则全黑无法操作) + case noExternal case beginFailed(Int32) case configureFailed(Int32) case completeFailed(Int32) @@ -82,39 +134,184 @@ final class DisplayController { // MARK: - 操作 - /// 关闭内置屏。**仅当存在在线外接显示器时**才会执行,否则返回 `.noExternal`。 + /// 关闭内置屏。 + /// 两阶段提交:先镜像到外接屏(forceConfiguration),等待生效后再禁用内置屏。 + /// 使用 forceConfiguration 确保 WindowServer 完整迁移窗口,防止画中画。 + /// 仅当存在在线外接显示器时才会执行,否则返回 .noExternal。 @discardableResult func disableBuiltin() -> Result { guard let fn = configureEnabled else { return .apiMissing } - guard let builtin = builtinDisplay() else { return .noBuiltin } - guard hasExternalDisplay() else { return .noExternal } - return apply(fn, display: builtin, enabled: false) + guard let completeFn = completeDisplayConfiguration else { return .apiMissing } + + var lastError: Result = .ok + + for _ in 0..<3 { + guard let builtin = builtinDisplay() ?? findBuiltinInAll() else { return .noBuiltin } + guard let external = externalDisplays().first else { return .noExternal } + + // 阶段 1:镜像到外接屏(forceConfiguration) + var mirrorConfig: CGDisplayConfigRef? + let begin1 = CGBeginDisplayConfiguration(&mirrorConfig) + if begin1 != .success { + lastError = .beginFailed(begin1.rawValue) + Thread.sleep(forTimeInterval: 0.5) + continue + } + let mirrorOK = CGConfigureDisplayMirrorOfDisplay(mirrorConfig, builtin, external) == .success + if !mirrorOK { + print("[ClamOpen] Mirror failed before disable, continuing") + } + let mirrorComplete = completeFn(mirrorConfig, 1) // forceConfiguration + if mirrorComplete != .success { + print("[ClamOpen] Mirror commit failed: CGError \(mirrorComplete.rawValue)") + } + mirrorConfig = nil + + // 等待镜像完全生效(macOS 12.x 上需要更长时间) + Thread.sleep(forTimeInterval: 0.5) + + // 阶段 2:禁用内置屏(forceConfiguration) + var config: CGDisplayConfigRef? + let begin2 = CGBeginDisplayConfiguration(&config) + if begin2 != .success { + lastError = .beginFailed(begin2.rawValue) + Thread.sleep(forTimeInterval: 0.5) + continue + } + + let e = fn(config, builtin, false) + if e != .success { + CGCancelDisplayConfiguration(config) + lastError = .configureFailed(e.rawValue) + print("[ClamOpen] disableBuiltin attempt failed: CGError \(e.rawValue)") + Thread.sleep(forTimeInterval: 0.5) + continue + } + + let complete = completeFn(config, 1) // forceConfiguration + if complete == .success { + print("[ClamOpen] disableBuiltin succeeded") + return .ok + } + lastError = .completeFailed(complete.rawValue) + print("[ClamOpen] disableBuiltin complete failed: CGError \(complete.rawValue)") + + Thread.sleep(forTimeInterval: 0.5) + } + + print("[ClamOpen] disableBuiltin all retries exhausted: \(lastError.message)") + return lastError } - /// 恢复内置屏 + + /// 恢复内置屏。带重试机制,最多重试 5 次,每次间隔 0.8 秒。 + /// 始终使用 forceConfiguration(1)强制 WindowServer 重配显示管线。 + /// 通过 CGSGetDisplayList 查找已被禁用的内置屏(onlineDisplays 找不到它)。 @discardableResult func enableBuiltin() -> Result { guard let fn = configureEnabled else { return .apiMissing } - guard let builtin = builtinDisplay() else { return .noBuiltin } - return apply(fn, display: builtin, enabled: true) + guard let completeFn = completeDisplayConfiguration else { return .apiMissing } + + let maxRetries = 5 + + for attempt in 0.. Result { + /// 单次启用操作,始终使用 forceConfiguration。 + private func applyEnable(_ fn: ConfigureDisplayEnabledFn, + completeFn: CompleteDisplayConfigurationFn, + display: CGDirectDisplayID) -> Result { var config: CGDisplayConfigRef? let begin = CGBeginDisplayConfiguration(&config) if begin != .success { return .beginFailed(begin.rawValue) } - let e = fn(config, display, enabled) + // 先取消镜像(如果有),再启用 + CGConfigureDisplayMirrorOfDisplay(config, display, kCGNullDirectDisplay) + + let e = fn(config, display, true) if e != .success { CGCancelDisplayConfiguration(config) return .configureFailed(e.rawValue) } - // .forSession:当前登录会话内持久(App 退出后仍生效),注销/重启自动恢复 —— 最安全 - let complete = CGCompleteDisplayConfiguration(config, .forSession) + let complete = completeFn(config, 1) // forceConfiguration if complete != .success { return .completeFailed(complete.rawValue) } return .ok } + + /// 恢复所有显示器(包括被禁用的),用于唤醒后的全面恢复。 + /// 返回成功恢复的数量。 + @discardableResult + func enableAllDisplays() -> Int { + guard let fn = configureEnabled else { return 0 } + guard let completeFn = completeDisplayConfiguration else { return 0 } + + let displays = allDisplays() + print("[ClamOpen] enableAllDisplays: found \(displays.count) displays") + var restored = 0 + + for d in displays { + var displayRestored = false + for _ in 0..<3 { + var config: CGDisplayConfigRef? + CGBeginDisplayConfiguration(&config) + CGConfigureDisplayMirrorOfDisplay(config, d, kCGNullDirectDisplay) + let e = fn(config, d, true) + if e == .success { + let c = completeFn(config, 1) + if c == .success { + displayRestored = true + break + } + } + if config != nil { CGCancelDisplayConfiguration(config) } + Thread.sleep(forTimeInterval: 0.5) + } + + // CGSConfigure 失败时,尝试 CGDisplayRestoreDisplayConfiguration 兜底 + if !displayRestored, let rc = restoreDisplayConfiguration { + if rc(d, 0) == .success { + displayRestored = true + print("[ClamOpen] enableAllDisplays: display \(d) restored via CGDisplayRestoreDisplayConfiguration") + } + } + + if displayRestored { + restored += 1 + } else { + print("[ClamOpen] enableAllDisplays: display \(d) failed to restore") + } + } + + print("[ClamOpen] enableAllDisplays: restored \(restored)/\(displays.count) displays") + return restored + } } diff --git a/Sources/ClamRestore/main.swift b/Sources/ClamRestore/main.swift index cb6302e..fc13a14 100644 --- a/Sources/ClamRestore/main.swift +++ b/Sources/ClamRestore/main.swift @@ -2,29 +2,56 @@ import CoreGraphics import Foundation // ClamRestore —— 独立急救工具:启用所有显示器(重点恢复内置屏)。 -// 不依赖主程序 ClamOpen,可在主程序崩溃 / 退出 / 全黑时单独运行。 -// 设计为“粗暴可靠”:枚举所有能找到的显示器(含被禁用的),逐个执行 enable, -// 对已启用的显示器执行 enable 无副作用。 +// 设计为"粗暴可靠":枚举所有能找到的显示器(含被禁用的),逐个执行 enable。 +// +// 关键:CGDisplayConfigurationFlags 是私有类型(typedef UInt32),SDK 不暴露。 +// 我们通过 dlsym 调用 CGCompleteDisplayConfiguration,手动传入 flag 值: +// 0 = .forSession(默认) +// 1 = .forceConfiguration(强制重配置) + +// MARK: - 函数类型 typealias typealias SetEnabledFn = @convention(c) (CGDisplayConfigRef?, CGDirectDisplayID, Bool) -> CGError typealias GetListFn = @convention(c) (UInt32, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CGError +typealias CompleteFn = @convention(c) (CGDisplayConfigRef?, UInt32) -> CGError +typealias RestoreFn = @convention(c) (CGDirectDisplayID, UInt32) -> CGError let rtld = UnsafeMutableRawPointer(bitPattern: -2) +// CGSConfigureDisplayEnabled guard let sEnabled = dlsym(rtld, "CGSConfigureDisplayEnabled") else { - FileHandle.standardError.write("ERROR: CGSConfigureDisplayEnabled 不可用\n".data(using: .utf8)!) + let msg = "ERROR: CGSConfigureDisplayEnabled 不可用\n" + FileHandle.standardError.write(msg.data(using: .utf8)!) exit(2) } let setEnabled = unsafeBitCast(sEnabled, to: SetEnabledFn.self) -// CGSGetDisplayList 能列出包括“被禁用”的显示器;拿不到就退回 online list -let getCGSList = dlsym(rtld, "CGSGetDisplayList").map { unsafeBitCast($0, to: GetListFn.self) } + +// CGCompleteDisplayConfiguration +guard let sComplete = dlsym(rtld, "CGCompleteDisplayConfiguration") else { + let msg = "ERROR: CGCompleteDisplayConfiguration 不可用\n" + FileHandle.standardError.write(msg.data(using: .utf8)!) + exit(3) +} +let completeConfig = unsafeBitCast(sComplete, to: CompleteFn.self) + +// CGSGetDisplayList — 能列出被禁用的显示器 +let getCGSList = dlsym(rtld, "CGSGetDisplayList").map { + unsafeBitCast($0, to: GetListFn.self) +} + +// CGDisplayRestoreDisplayConfiguration — 系统默认配置恢复 +let restoreConfig = dlsym(rtld, "CGDisplayRestoreDisplayConfiguration").map { + unsafeBitCast($0, to: RestoreFn.self) +} + +// MARK: - 显示列表 func allDisplays() -> [CGDirectDisplayID] { - if let getCGSList { + if let g = getCGSList { var count: UInt32 = 0 - if getCGSList(0, nil, &count) == .success, count > 0 { + if g(0, nil, &count) == .success, count > 0 { var ids = [CGDirectDisplayID](repeating: 0, count: Int(count)) - if getCGSList(count, &ids, &count) == .success { + if g(count, &ids, &count) == .success { return Array(ids.prefix(Int(count))) } } @@ -36,24 +63,113 @@ func allDisplays() -> [CGDirectDisplayID] { return Array(a.prefix(Int(n))) } -func enable(_ id: CGDirectDisplayID) -> Bool { - var cfg: CGDisplayConfigRef? - guard CGBeginDisplayConfiguration(&cfg) == .success else { return false } - if setEnabled(cfg, id, true) != .success { - CGCancelDisplayConfiguration(cfg) - return false - } - return CGCompleteDisplayConfiguration(cfg, .forSession) == .success + +// MARK: - 主流程 + +// 策略:优先通知正在运行的 ClamOpen 来恢复(它的逻辑已经验证可靠), +// 如果 ClamOpen 没运行或恢复失败,再自己动手。 + +let notifName = NSNotification.Name("com.clamopen.restore-builtin") +DistributedNotificationCenter.default().postNotificationName(notifName, object: nil, deliverImmediately: true) + +// 等待 ClamOpen 处理通知 +Thread.sleep(forTimeInterval: 1.5) + +// 检查内置屏是否已被恢复 +let onlineDisplays = CGOnlineDisplayList() +if onlineDisplays.contains(where: { CGDisplayIsBuiltin($0) != 0 }) { + let msg = "ClamRestore: 通过 ClamOpen 成功恢复内置屏\n" + FileHandle.standardError.write(msg.data(using: .utf8)!) + exit(0) } +// ClamOpen 未运行或恢复失败,自己动手 +let msg = "ClamRestore: ClamOpen 未响应,尝试直接恢复...\n" +FileHandle.standardError.write(msg.data(using: .utf8)!) + let displays = allDisplays() var restored = 0 + for d in displays { let builtin = CGDisplayIsBuiltin(d) != 0 - if enable(d) { + let tag = builtin ? "(内置)" : "(外接)" + + // 跳过已经处于活动状态的显示器 + if CGDisplayIsActive(d) != 0 { + restored += 1 + continue + } + + let maxRetries = builtin ? 5 : 3 + var success = false + var attempt = 0 + + while !success && attempt < maxRetries { + var cfg: CGDisplayConfigRef? + + // 每次重试重新查找内置屏 ID + let currentId: CGDirectDisplayID = builtin + ? (allDisplays().first(where: { CGDisplayIsBuiltin($0) != 0 }) ?? d) + : d + + CGBeginDisplayConfiguration(&cfg) + CGConfigureDisplayMirrorOfDisplay(cfg, currentId, kCGNullDirectDisplay) + let e = setEnabled(cfg, currentId, true) + + if e == .success { + let c = completeConfig(cfg, UInt32(1)) // forceConfiguration + if c == .success { + success = true + } else { + CGCancelDisplayConfiguration(cfg) + } + } else { + CGCancelDisplayConfiguration(cfg) + } + + if !success && attempt < maxRetries - 1 { + Thread.sleep(forTimeInterval: 0.8) + } + attempt += 1 + } + + if !success { + if let rc = restoreConfig, rc(d, UInt32(1)) == .success { + success = true + } + } + + if success { restored += 1 - let tag = builtin ? "(内置)" : "(外接)" - FileHandle.standardError.write("已启用 \(d) \(tag)\n".data(using: .utf8)!) + let m = "已启用 \(d) \(tag)\n" + FileHandle.standardError.write(m.data(using: .utf8)!) + } else { + let m = "未启用 \(d) \(tag)\n" + FileHandle.standardError.write(m.data(using: .utf8)!) } } -FileHandle.standardError.write("ClamRestore: 已恢复 \(restored)/\(displays.count) 台显示器\n".data(using: .utf8)!) + +let finalMsg = "\nClamRestore: 已恢复 \(restored)/\(displays.count) 台显示器\n" +FileHandle.standardError.write(finalMsg.data(using: .utf8)!) + +if restored == 0 { + let help = """ + \n=== 所有显示器都未能恢复 === + 建议: + 1. 检查 ClamOpen 是否在菜单栏运行 + 2. 尝试重启 WindowServer: killall WindowServer + 3. 最后手段:重启 Mac + """ + FileHandle.standardError.write(help.data(using: .utf8)!) + exit(1) +} + +// 辅助函数:在线显示器列表 +func CGOnlineDisplayList() -> [CGDirectDisplayID] { + var count: UInt32 = 0 + CGGetOnlineDisplayList(0, nil, &count) + guard count > 0 else { return [] } + var ids = [CGDirectDisplayID](repeating: 0, count: Int(count)) + CGGetOnlineDisplayList(count, &ids, &count) + return Array(ids.prefix(Int(count))) +} diff --git a/build_app.sh b/build_app.sh index d6131ea..d4712a8 100755 --- a/build_app.sh +++ b/build_app.sh @@ -1,10 +1,18 @@ #!/bin/bash -# 构建 ClamOpen.app(主程序)与 恢复内置屏.app(独立急救工具),并附带图标。 +# Build ClamOpen.app and 恢复内置屏.app using swiftc directly. +# (swift build via SPM is broken on this machine due to +# a corrupted CommandLineTools installation — PackageDescription +# module is missing. Use `xcode-select --install` to fix.) set -euo pipefail ROOT="$(cd "$(dirname "$0")" && pwd)" +SDK="$(xcrun --show-sdk-path)" +TARGET="x86_64-apple-macos12.0" +BUILD_DIR="$ROOT/.build/release" -# 1) 生成图标(缺失时) +mkdir -p "$BUILD_DIR" + +# ---- icons ---- if [[ ! -f "$ROOT/AppIcon.icns" || ! -f "$ROOT/RestoreIcon.icns" ]]; then echo "==> Generating icons ..." swift "$ROOT/make_icon.swift" "$ROOT" @@ -12,18 +20,29 @@ if [[ ! -f "$ROOT/AppIcon.icns" || ! -f "$ROOT/RestoreIcon.icns" ]]; then iconutil -c icns "$ROOT/RestoreIcon.iconset" -o "$ROOT/RestoreIcon.icns" fi -# 2) 编译 -echo "==> Building release binaries ..." -swift build --package-path "$ROOT" -c release -BIN_DIR="$(swift build --package-path "$ROOT" -c release --show-bin-path)" +# ---- compile ---- +echo "==> Building ClamOpen ..." +swiftc -sdk "$SDK" -target "$TARGET" -O \ + -framework AppKit -framework CoreGraphics \ + -o "$BUILD_DIR/ClamOpen" \ + "$ROOT/Sources/ClamOpen/main.swift" \ + "$ROOT/Sources/ClamOpen/DisplayController.swift" \ + "$ROOT/Sources/ClamOpen/AppDelegate.swift" \ + "$ROOT/Sources/ClamOpen/PowerManager.swift" + +echo "==> Building ClamRestore ..." +swiftc -sdk "$SDK" -target "$TARGET" -O \ + -framework CoreGraphics -framework Foundation \ + -o "$BUILD_DIR/ClamRestore" \ + "$ROOT/Sources/ClamRestore/main.swift" -# 3) 组装 .app +# ---- assemble .app ---- make_app() { local exe="$1" appname="$2" plist="$3" icns="$4" local app="$ROOT/$appname.app" rm -rf "$app" mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" - cp "$BIN_DIR/$exe" "$app/Contents/MacOS/$exe" + cp "$BUILD_DIR/$exe" "$app/Contents/MacOS/$exe" cp "$plist" "$app/Contents/Info.plist" [[ -f "$ROOT/$icns" ]] && cp "$ROOT/$icns" "$app/Contents/Resources/$icns" codesign --force --sign - "$app" >/dev/null 2>&1 || echo " (codesign skipped: $appname)"