From a5dcbc632eb4e6922fb23b93fcb38ba85c3712ec Mon Sep 17 00:00:00 2001 From: Sunwenzhi58 <2514832692@qq.com> Date: Tue, 18 Aug 2026 20:34:18 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat(macOS):=20=E6=B7=BB=E5=8A=A0=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=9B=AE=E5=BD=95=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Lithe/Models/Settings/AppSettings.swift | 19 +++++++ Sources/Lithe/Views/App/SettingsView.swift | 41 ++++++++++++- Tests/LitheTests/AppSettingsTests.swift | 21 +++++++ ...-17-cross-platform-logging-requirements.md | 57 +++++++++++++++++++ 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 docs/superpowers/specs/2026-08-17-cross-platform-logging-requirements.md diff --git a/Sources/Lithe/Models/Settings/AppSettings.swift b/Sources/Lithe/Models/Settings/AppSettings.swift index 5469979c0..83359215f 100644 --- a/Sources/Lithe/Models/Settings/AppSettings.swift +++ b/Sources/Lithe/Models/Settings/AppSettings.swift @@ -22,6 +22,7 @@ final class AppSettings: ObservableObject { static let javaLanguageServerJDKPath = "settings.javaLanguageServerJDKPath" static let commitMessageAI = "settings.commitMessageAI" static let keyboardShortcutOverrides = "settings.keyboardShortcutOverrides" + static let customLogDirectory = "settings.customLogDirectory" } private struct KeyboardShortcutOverridesPayload: Codable { @@ -77,6 +78,7 @@ final class AppSettings: ObservableObject { didSet { saveCommitMessageAI() } } @Published private(set) var keyboardShortcutOverrides: [String: [KeyboardShortcutBinding]] + @Published private(set) var customLogDirectory: URL? private var fileVisibilityRulesObservers: [UUID: () -> Void] = [:] @@ -110,6 +112,10 @@ final class AppSettings: ObservableObject { ) ?? .ask javaLanguageServerJDKPath = defaults.string(forKey: Key.javaLanguageServerJDKPath) ?? "" keyboardShortcutOverrides = Self.loadKeyboardShortcutOverrides(from: defaults) + customLogDirectory = defaults.string(forKey: Key.customLogDirectory).flatMap { path in + guard !path.isEmpty else { return nil } + return URL(fileURLWithPath: path, isDirectory: true).standardizedFileURL + } if let data = defaults.data(forKey: Key.commitMessageAI), let saved = try? JSONDecoder().decode(CommitMessageAISettings.self, from: data) { commitMessageAI = saved @@ -121,6 +127,18 @@ final class AppSettings: ObservableObject { var terminalShellPath: String? { terminalShell.path } + var defaultLogDirectory: URL { + FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] + .appendingPathComponent("Logs/Lithe", isDirectory: true) + } + + var logDirectory: URL { customLogDirectory ?? defaultLogDirectory } + + func setCustomLogDirectory(_ url: URL?) { + customLogDirectory = url?.standardizedFileURL + defaults.set(customLogDirectory?.path, forKey: Key.customLogDirectory) + } + var fileVisibilityRules: FileVisibilityRules { FileVisibilityRules( hiddenDirectoryNames: hiddenDirectoryNames, @@ -162,6 +180,7 @@ final class AppSettings: ObservableObject { projectOpenBehavior = .ask javaLanguageServerJDKPath = "" commitMessageAI = .default + setCustomLogDirectory(nil) setKeyboardShortcutOverrides([:]) } diff --git a/Sources/Lithe/Views/App/SettingsView.swift b/Sources/Lithe/Views/App/SettingsView.swift index b3e572c76..74811870c 100644 --- a/Sources/Lithe/Views/App/SettingsView.swift +++ b/Sources/Lithe/Views/App/SettingsView.swift @@ -1,4 +1,3 @@ -import AppKit import SwiftUI import LitheCoreContracts import LitheGitModule @@ -168,7 +167,7 @@ struct SettingsView: View { private func searchTerms(for category: SettingsCategory) -> [String] { switch category { case .general: - ["General", "Appearance", "Color theme", "Appearance mode", "Language", "Projects", "Files", "Version control"] + ["General", "Appearance", "Color theme", "Appearance mode", "Language", "Projects", "Files", "Version control", "Logs", "Log directory"] case .editor: ["Editor", "Display", "Editor tabs", "Font size", "Indentation", "Tab width"] case .keymap: @@ -368,6 +367,32 @@ struct SettingsView: View { .buttonStyle(LithePrimaryButtonStyle()) } } + + group("Logs") { + logDirectoryRow("Default directory", url: settings.defaultLogDirectory) + logDirectoryRow("Selected directory", url: settings.logDirectory) + + HStack(spacing: 8) { + Button { + guard let directory = model.platformUI.chooseDirectory( + title: "Choose Log Directory", + prompt: "Choose" + ) else { return } + settings.setCustomLogDirectory(directory) + } label: { + Label("Choose Directory", systemImage: "folder.badge.plus") + } + .buttonStyle(LitheSecondaryButtonStyle()) + + Button { + settings.setCustomLogDirectory(nil) + } label: { + Label("Restore Default", systemImage: "arrow.counterclockwise") + } + .buttonStyle(LitheSecondaryButtonStyle()) + .disabled(settings.customLogDirectory == nil) + } + } } } @@ -1063,6 +1088,18 @@ struct SettingsView: View { .frame(minHeight: 28) } + private func logDirectoryRow(_ title: String, url: URL) -> some View { + VStack(alignment: .leading, spacing: 5) { + Text(LocalizedStringKey(title)) + .foregroundStyle(LitheTheme.secondaryText) + Text(url.path) + .font(.system(size: 11, design: .monospaced)) + .textSelection(.enabled) + .lineLimit(2) + .fixedSize(horizontal: false, vertical: true) + } + } + private func syncAIProviderDraft() { aiAPIKeyDraft = model.activeCommitMessageAPIKey } diff --git a/Tests/LitheTests/AppSettingsTests.swift b/Tests/LitheTests/AppSettingsTests.swift index 0977a1892..a28b5b384 100644 --- a/Tests/LitheTests/AppSettingsTests.swift +++ b/Tests/LitheTests/AppSettingsTests.swift @@ -28,6 +28,27 @@ struct AppSettingsTests { #expect(settings.autoSave) #expect(AppSettings(store: store).autoSave) } + + @Test + func customLogDirectoryPersistsAndCanBeRestoredToDefault() { + let store = AppSettingsTestStore() + let settings = AppSettings(store: store) + let customDirectory = URL(fileURLWithPath: "/tmp/lithe-test-logs", isDirectory: true) + + #expect(settings.customLogDirectory == nil) + #expect(settings.logDirectory == settings.defaultLogDirectory) + + settings.setCustomLogDirectory(customDirectory) + + let restored = AppSettings(store: store) + #expect(restored.customLogDirectory == customDirectory.standardizedFileURL) + #expect(restored.logDirectory == customDirectory.standardizedFileURL) + + restored.restoreDefaults() + + #expect(restored.customLogDirectory == nil) + #expect(AppSettings(store: store).logDirectory == restored.defaultLogDirectory) + } } private final class AppSettingsTestStore: KeyValueStore, @unchecked Sendable { diff --git a/docs/superpowers/specs/2026-08-17-cross-platform-logging-requirements.md b/docs/superpowers/specs/2026-08-17-cross-platform-logging-requirements.md new file mode 100644 index 000000000..611638676 --- /dev/null +++ b/docs/superpowers/specs/2026-08-17-cross-platform-logging-requirements.md @@ -0,0 +1,57 @@ +# Lithe macOS 日志目录设置需求 + +## 背景 + +macOS 设置界面没有告诉用户日志默认保存在哪里,也不能修改日志保存目录。 + +本需求只完善 macOS 日志目录的查看和配置,不重建日志系统,也不涉及 Windows 端。 + +## 目标 + +在设置中增加日志目录设置,让用户能够: + +- 查看系统默认日志目录; +- 查看当前选定的日志目录; +- 选择新的日志目录; +- 恢复默认日志目录; +- 复制完整目录路径。 + +## 目录行为 + +### 默认目录 + +默认目录是 macOS 用户日志目录下的 `Lithe` 目录。设置页面展示系统解析后的真实绝对路径,不能硬编码开发机路径。 + +### 自定义目录 + +用户通过系统目录选择器选择目录。选择成功后保存配置,应用重启后继续显示该选择。该设置不影响其他配置。 + +“恢复默认目录”只清除自定义日志目录。“恢复全部默认设置”也应清除该配置。 + +## 界面要求 + +日志设置页面显示默认目录和当前选定目录,并提供“选择目录”“恢复默认”操作。 + +长路径可以换行或缩略显示,但必须支持复制完整路径。 + +## 不在范围内 + +本次不做: + +- 新建结构化日志协议; +- 重写日志 writer、文件 sink 或导出逻辑; +- 新增文件轮换和 ownership manifest; +- 新增 Run、Build、Test、Debug、LSP、Terminal 生命周期日志; +- 新增应用、窗口、工作区或文件事件日志; +- 新增帧率采样或性能日志; +- 修改日志格式、日志级别或日志内容; +- 自动上传、云端查看或崩溃转储。 + +## 验收标准 + +1. 设置页显示真实默认目录和当前选定目录。 +2. 用户可以选择自定义目录,重启后配置仍然存在。 +3. 用户可以恢复默认目录。 +4. “恢复全部默认设置”会清除自定义日志目录。 +5. 用户可以复制完整路径。 +6. 没有引入新的日志协议、业务埋点、帧率监控或日志写入管线。 From 49827b714786d02441289d203fbe4cbb4c032d0b Mon Sep 17 00:00:00 2001 From: Sunwenzhi58 <2514832692@qq.com> Date: Tue, 18 Aug 2026 20:58:15 +0800 Subject: [PATCH 2/8] =?UTF-8?q?fix(macOS):=20=E8=A1=A5=E5=85=85=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=9B=AE=E5=BD=95=E8=AE=BE=E7=BD=AE=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Resources/zh-Hans.lproj/Localizable.strings | 7 +++++++ Tests/LitheTests/AppLocalizationTests.swift | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Resources/zh-Hans.lproj/Localizable.strings b/Resources/zh-Hans.lproj/Localizable.strings index c1fb47ecd..e49be9c7f 100644 --- a/Resources/zh-Hans.lproj/Localizable.strings +++ b/Resources/zh-Hans.lproj/Localizable.strings @@ -178,6 +178,13 @@ "The interface language changes immediately. English is the default." = "界面语言会立即生效。默认语言为英文。"; "Files" = "文件"; "Save changed files automatically" = "自动保存已修改的文件"; +"Logs" = "日志"; +"Log directory" = "日志目录"; +"Default directory" = "默认目录"; +"Selected directory" = "当前选择的目录"; +"Choose Directory" = "选择目录"; +"Choose Log Directory" = "选择日志目录"; +"Restore Default" = "恢复默认"; "Save after" = "保存延迟"; "Hidden paths" = "隐藏路径"; "One entry per line. Directory names hide matching folders; file entries support * and ?." = "每行输入一项。目录名用于隐藏匹配的文件夹;文件条目支持 * 和 ?。"; diff --git a/Tests/LitheTests/AppLocalizationTests.swift b/Tests/LitheTests/AppLocalizationTests.swift index 6bb5d4b2f..3ef5099e5 100644 --- a/Tests/LitheTests/AppLocalizationTests.swift +++ b/Tests/LitheTests/AppLocalizationTests.swift @@ -37,6 +37,19 @@ struct AppLocalizationTests { ) } + @Test + func simplifiedChineseResourcesCoverLogDirectorySettings() throws { + let translations = try simplifiedChineseTranslations() + + #expect(translations["Logs"] == "日志") + #expect(translations["Log directory"] == "日志目录") + #expect(translations["Default directory"] == "默认目录") + #expect(translations["Selected directory"] == "当前选择的目录") + #expect(translations["Choose Directory"] == "选择目录") + #expect(translations["Choose Log Directory"] == "选择日志目录") + #expect(translations["Restore Default"] == "恢复默认") + } + @Test func simplifiedChineseResourcesCoverGitHubPullRequests() throws { let translations = try simplifiedChineseTranslations() From 7321d53de66ca8c4a1c81e47fb310982c0ccf6d9 Mon Sep 17 00:00:00 2001 From: Sunwenzhi58 <2514832692@qq.com> Date: Tue, 18 Aug 2026 21:00:37 +0800 Subject: [PATCH 3/8] =?UTF-8?q?style(macOS):=20=E8=B0=83=E6=95=B4=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=9B=AE=E5=BD=95=E9=80=89=E6=8B=A9=E6=8E=A7=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Lithe/Views/App/SettingsView.swift | 74 +++++++++++++++------- 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/Sources/Lithe/Views/App/SettingsView.swift b/Sources/Lithe/Views/App/SettingsView.swift index 74811870c..111f2ee4e 100644 --- a/Sources/Lithe/Views/App/SettingsView.swift +++ b/Sources/Lithe/Views/App/SettingsView.swift @@ -369,10 +369,19 @@ struct SettingsView: View { } group("Logs") { - logDirectoryRow("Default directory", url: settings.defaultLogDirectory) - logDirectoryRow("Selected directory", url: settings.logDirectory) + Text("Log directory") + .font(.system(size: 11.5, weight: .medium)) + + HStack(spacing: 10) { + Text(settings.logDirectory.path) + .font(.system(size: 13, weight: .medium)) + .lineLimit(1) + .truncationMode(.middle) + .textSelection(.enabled) + .help(settings.logDirectory.path) + + Spacer(minLength: 8) - HStack(spacing: 8) { Button { guard let directory = model.platformUI.chooseDirectory( title: "Choose Log Directory", @@ -380,18 +389,49 @@ struct SettingsView: View { ) else { return } settings.setCustomLogDirectory(directory) } label: { - Label("Choose Directory", systemImage: "folder.badge.plus") + Image(systemName: "folder") + .font(.system(size: 16, weight: .regular)) + .frame(width: 26, height: 26) } - .buttonStyle(LitheSecondaryButtonStyle()) + .buttonStyle(.plain) + .foregroundStyle(LitheTheme.secondaryText) + .contentShape(Rectangle()) + .lithePointer() + .help("Choose Directory") + } + .padding(.horizontal, 12) + .frame(maxWidth: .infinity, minHeight: 46, maxHeight: 46) + .background(LitheTheme.inputBackground) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .overlay { + RoundedRectangle(cornerRadius: 6) + .stroke(LitheTheme.inputBorder, lineWidth: 1) + } - Button { - settings.setCustomLogDirectory(nil) - } label: { - Label("Restore Default", systemImage: "arrow.counterclockwise") + HStack(spacing: 6) { + Text("Default directory") + .foregroundStyle(LitheTheme.secondaryText) + Text(settings.defaultLogDirectory.path) + .foregroundStyle(LitheTheme.tertiaryText) + .lineLimit(1) + .truncationMode(.middle) + .textSelection(.enabled) + .help(settings.defaultLogDirectory.path) + + Spacer(minLength: 8) + + if settings.customLogDirectory != nil { + Button { + settings.setCustomLogDirectory(nil) + } label: { + Text("Restore Default") + } + .buttonStyle(.plain) + .foregroundStyle(LitheTheme.accent) + .lithePointer() } - .buttonStyle(LitheSecondaryButtonStyle()) - .disabled(settings.customLogDirectory == nil) } + .font(LitheTheme.smallFont) } } } @@ -1088,18 +1128,6 @@ struct SettingsView: View { .frame(minHeight: 28) } - private func logDirectoryRow(_ title: String, url: URL) -> some View { - VStack(alignment: .leading, spacing: 5) { - Text(LocalizedStringKey(title)) - .foregroundStyle(LitheTheme.secondaryText) - Text(url.path) - .font(.system(size: 11, design: .monospaced)) - .textSelection(.enabled) - .lineLimit(2) - .fixedSize(horizontal: false, vertical: true) - } - } - private func syncAIProviderDraft() { aiAPIKeyDraft = model.activeCommitMessageAPIKey } From 721736eb9b6f8af2f71b72dc2559d94e3711b5ff Mon Sep 17 00:00:00 2001 From: Sunwenzhi58 <2514832692@qq.com> Date: Tue, 18 Aug 2026 21:49:47 +0800 Subject: [PATCH 4/8] =?UTF-8?q?fix(macOS):=20=E6=8E=A5=E5=85=A5=E5=8F=AF?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA=E7=9B=AE?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Ports/LogDirectoryProviding.swift | 5 ++ Sources/Lithe/LitheApp.swift | 36 ++++++++++++- .../Lithe/Models/Settings/AppSettings.swift | 21 ++++++-- .../Logging/MacApplicationLogWriter.swift | 32 ++++++++++++ .../Logging/MacLogDirectoryProvider.swift | 8 +++ .../Platform/MacOS/MacServiceContainer.swift | 8 +++ Tests/LitheTests/AppSettingsTests.swift | 16 ++++++ .../MacApplicationLogWriterTests.swift | 51 +++++++++++++++++++ .../LitheTests/TestLogDirectoryProvider.swift | 12 +++++ ...-17-cross-platform-logging-requirements.md | 11 ++-- 10 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 Sources/Lithe/Core/Ports/LogDirectoryProviding.swift create mode 100644 Sources/Lithe/Platform/MacOS/Logging/MacApplicationLogWriter.swift create mode 100644 Sources/Lithe/Platform/MacOS/Logging/MacLogDirectoryProvider.swift create mode 100644 Tests/LitheTests/MacApplicationLogWriterTests.swift create mode 100644 Tests/LitheTests/TestLogDirectoryProvider.swift diff --git a/Sources/Lithe/Core/Ports/LogDirectoryProviding.swift b/Sources/Lithe/Core/Ports/LogDirectoryProviding.swift new file mode 100644 index 000000000..836f960fd --- /dev/null +++ b/Sources/Lithe/Core/Ports/LogDirectoryProviding.swift @@ -0,0 +1,5 @@ +import Foundation + +protocol LogDirectoryProviding { + var defaultLogDirectory: URL { get } +} diff --git a/Sources/Lithe/LitheApp.swift b/Sources/Lithe/LitheApp.swift index 6be8d2660..8de14e9ea 100644 --- a/Sources/Lithe/LitheApp.swift +++ b/Sources/Lithe/LitheApp.swift @@ -133,11 +133,27 @@ struct LitheApp: App { @StateObject private var memoryUsageMonitor: MemoryUsageMonitor @StateObject private var frameRateMonitor = FrameRateMonitor() @StateObject private var updateChecker = UpdateChecker() + private let applicationLogWriter: MacApplicationLogWriter init() { - MacBundledFontRegistry.registerFonts() let store = MacUserDefaultsStore() - let settings = AppSettings(store: store) + let settings = AppSettings( + store: store, + logDirectoryProvider: MacServiceContainer.makeLogDirectoryProvider() + ) + let applicationLogWriter = MacServiceContainer.makeApplicationLogWriter() + if !Self.redirectApplicationLogs(applicationLogWriter, to: settings.logDirectory), + settings.customLogDirectory != nil { + settings.setCustomLogDirectory(nil) + _ = Self.redirectApplicationLogs(applicationLogWriter, to: settings.defaultLogDirectory) + } + settings.addLogDirectoryObserver { [weak settings] directory in + guard !Self.redirectApplicationLogs(applicationLogWriter, to: directory), + settings?.customLogDirectory != nil else { return } + settings?.setCustomLogDirectory(nil) + } + self.applicationLogWriter = applicationLogWriter + MacBundledFontRegistry.registerFonts() let processRegistry = ManagedProcessRegistry() let moduleStore = MacModuleConfigurationStore(store: store) let pluginRuntimeRecovery = MacPluginRuntimeRecoveryCoordinator() @@ -185,6 +201,22 @@ struct LitheApp: App { } } + private static func redirectApplicationLogs( + _ writer: MacApplicationLogWriter, + to directory: URL + ) -> Bool { + do { + try writer.redirect(to: directory) + return true + } catch { + let message = "Could not redirect Lithe logs to \(directory.path): \(error.localizedDescription)\n" + if let data = message.data(using: .utf8) { + FileHandle.standardError.write(data) + } + return false + } + } + private var model: AppModel { projectSessions.activeModel } var body: some Scene { diff --git a/Sources/Lithe/Models/Settings/AppSettings.swift b/Sources/Lithe/Models/Settings/AppSettings.swift index 83359215f..be7be26a0 100644 --- a/Sources/Lithe/Models/Settings/AppSettings.swift +++ b/Sources/Lithe/Models/Settings/AppSettings.swift @@ -33,6 +33,7 @@ final class AppSettings: ObservableObject { } private let defaults: any KeyValueStore + private let logDirectoryProvider: any LogDirectoryProviding @Published var colorTheme: AppColorTheme { didSet { @@ -81,9 +82,14 @@ final class AppSettings: ObservableObject { @Published private(set) var customLogDirectory: URL? private var fileVisibilityRulesObservers: [UUID: () -> Void] = [:] + private var logDirectoryObservers: [UUID: (URL) -> Void] = [:] - init(store: any KeyValueStore) { + init( + store: any KeyValueStore, + logDirectoryProvider: any LogDirectoryProviding + ) { self.defaults = store + self.logDirectoryProvider = logDirectoryProvider colorTheme = AppColorTheme( rawValue: defaults.string(forKey: Key.colorTheme) ?? "" ) ?? .lithe @@ -128,8 +134,7 @@ final class AppSettings: ObservableObject { var terminalShellPath: String? { terminalShell.path } var defaultLogDirectory: URL { - FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] - .appendingPathComponent("Logs/Lithe", isDirectory: true) + logDirectoryProvider.defaultLogDirectory } var logDirectory: URL { customLogDirectory ?? defaultLogDirectory } @@ -137,6 +142,16 @@ final class AppSettings: ObservableObject { func setCustomLogDirectory(_ url: URL?) { customLogDirectory = url?.standardizedFileURL defaults.set(customLogDirectory?.path, forKey: Key.customLogDirectory) + for observer in logDirectoryObservers.values { + observer(logDirectory) + } + } + + @discardableResult + func addLogDirectoryObserver(_ observer: @escaping (URL) -> Void) -> UUID { + let id = UUID() + logDirectoryObservers[id] = observer + return id } var fileVisibilityRules: FileVisibilityRules { diff --git a/Sources/Lithe/Platform/MacOS/Logging/MacApplicationLogWriter.swift b/Sources/Lithe/Platform/MacOS/Logging/MacApplicationLogWriter.swift new file mode 100644 index 000000000..c69a49f5b --- /dev/null +++ b/Sources/Lithe/Platform/MacOS/Logging/MacApplicationLogWriter.swift @@ -0,0 +1,32 @@ +import Darwin +import Foundation + +final class MacApplicationLogWriter { + static let fileName = "lithe.log" + + private let targetFileDescriptor: Int32 + + init(targetFileDescriptor: Int32 = STDERR_FILENO) { + self.targetFileDescriptor = targetFileDescriptor + } + + func redirect(to directory: URL) throws { + try FileManager.default.createDirectory( + at: directory, + withIntermediateDirectories: true + ) + let logURL = directory.appendingPathComponent(Self.fileName, isDirectory: false) + let descriptor = logURL.withUnsafeFileSystemRepresentation { path in + guard let path else { return -1 } + return open(path, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR) + } + guard descriptor >= 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + defer { close(descriptor) } + + guard dup2(descriptor, targetFileDescriptor) >= 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + } +} diff --git a/Sources/Lithe/Platform/MacOS/Logging/MacLogDirectoryProvider.swift b/Sources/Lithe/Platform/MacOS/Logging/MacLogDirectoryProvider.swift new file mode 100644 index 000000000..98161e48c --- /dev/null +++ b/Sources/Lithe/Platform/MacOS/Logging/MacLogDirectoryProvider.swift @@ -0,0 +1,8 @@ +import Foundation + +struct MacLogDirectoryProvider: LogDirectoryProviding { + var defaultLogDirectory: URL { + FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] + .appendingPathComponent("Logs/Lithe", isDirectory: true) + } +} diff --git a/Sources/Lithe/Platform/MacOS/MacServiceContainer.swift b/Sources/Lithe/Platform/MacOS/MacServiceContainer.swift index 0d88d68f1..f4682835c 100644 --- a/Sources/Lithe/Platform/MacOS/MacServiceContainer.swift +++ b/Sources/Lithe/Platform/MacOS/MacServiceContainer.swift @@ -38,6 +38,14 @@ final class MacServiceContainer { let runConfigurationStore: MacRunConfigurationStore let moduleLifecycleCoordinator: ModuleLifecycleCoordinator + static func makeLogDirectoryProvider() -> any LogDirectoryProviding { + MacLogDirectoryProvider() + } + + static func makeApplicationLogWriter() -> MacApplicationLogWriter { + MacApplicationLogWriter() + } + init( store: any KeyValueStore, settings: AppSettings, diff --git a/Tests/LitheTests/AppSettingsTests.swift b/Tests/LitheTests/AppSettingsTests.swift index a28b5b384..f7ae14f34 100644 --- a/Tests/LitheTests/AppSettingsTests.swift +++ b/Tests/LitheTests/AppSettingsTests.swift @@ -49,6 +49,22 @@ struct AppSettingsTests { #expect(restored.customLogDirectory == nil) #expect(AppSettings(store: store).logDirectory == restored.defaultLogDirectory) } + + @Test + func logDirectoryObserversReceiveCustomAndRestoredDirectories() { + let settings = AppSettings(store: AppSettingsTestStore()) + let customDirectory = URL(fileURLWithPath: "/tmp/lithe-observed-logs", isDirectory: true) + var observedDirectories: [URL] = [] + settings.addLogDirectoryObserver { observedDirectories.append($0) } + + settings.setCustomLogDirectory(customDirectory) + settings.setCustomLogDirectory(nil) + + #expect(observedDirectories == [ + customDirectory.standardizedFileURL, + settings.defaultLogDirectory + ]) + } } private final class AppSettingsTestStore: KeyValueStore, @unchecked Sendable { diff --git a/Tests/LitheTests/MacApplicationLogWriterTests.swift b/Tests/LitheTests/MacApplicationLogWriterTests.swift new file mode 100644 index 000000000..36098e46d --- /dev/null +++ b/Tests/LitheTests/MacApplicationLogWriterTests.swift @@ -0,0 +1,51 @@ +import Darwin +import Foundation +import Testing +@testable import Lithe + +@Suite("macOS application log writer") +struct MacApplicationLogWriterTests { + @Test + func changingDirectoryMovesSubsequentStandardErrorOutputToTheSelection() throws { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + let originalTarget = root.appendingPathComponent("original-target") + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) + #expect(FileManager.default.createFile(atPath: originalTarget.path, contents: nil)) + let targetHandle = try FileHandle(forWritingTo: originalTarget) + defer { + try? targetHandle.close() + try? FileManager.default.removeItem(at: root) + } + + let writer = MacApplicationLogWriter(targetFileDescriptor: targetHandle.fileDescriptor) + let defaultDirectory = root.appendingPathComponent("default", isDirectory: true) + let selectedDirectory = root.appendingPathComponent("selected", isDirectory: true) + + try writer.redirect(to: defaultDirectory) + try targetHandle.write(contentsOf: Data("default log line\n".utf8)) + try targetHandle.synchronize() + + try writer.redirect(to: selectedDirectory) + try targetHandle.write(contentsOf: Data("selected log line\n".utf8)) + try targetHandle.synchronize() + + let defaultContents = try String( + contentsOf: defaultDirectory.appendingPathComponent("lithe.log"), + encoding: .utf8 + ) + let selectedContents = try String( + contentsOf: selectedDirectory.appendingPathComponent("lithe.log"), + encoding: .utf8 + ) + #expect(defaultContents == "default log line\n") + #expect(selectedContents == "selected log line\n") + } + + @Test + func defaultDirectoryProviderUsesTheMacUserLogsDirectory() { + let directory = MacLogDirectoryProvider().defaultLogDirectory + + #expect(directory.path.hasSuffix("/Library/Logs/Lithe")) + } +} diff --git a/Tests/LitheTests/TestLogDirectoryProvider.swift b/Tests/LitheTests/TestLogDirectoryProvider.swift new file mode 100644 index 000000000..05bf59205 --- /dev/null +++ b/Tests/LitheTests/TestLogDirectoryProvider.swift @@ -0,0 +1,12 @@ +import Foundation +@testable import Lithe + +struct TestLogDirectoryProvider: LogDirectoryProviding { + let defaultLogDirectory = URL(fileURLWithPath: "/test/default-logs", isDirectory: true) +} + +extension AppSettings { + convenience init(store: any KeyValueStore) { + self.init(store: store, logDirectoryProvider: TestLogDirectoryProvider()) + } +} diff --git a/docs/superpowers/specs/2026-08-17-cross-platform-logging-requirements.md b/docs/superpowers/specs/2026-08-17-cross-platform-logging-requirements.md index 611638676..be21878cb 100644 --- a/docs/superpowers/specs/2026-08-17-cross-platform-logging-requirements.md +++ b/docs/superpowers/specs/2026-08-17-cross-platform-logging-requirements.md @@ -4,7 +4,7 @@ macOS 设置界面没有告诉用户日志默认保存在哪里,也不能修改日志保存目录。 -本需求只完善 macOS 日志目录的查看和配置,不重建日志系统,也不涉及 Windows 端。 +本需求只完善 macOS 日志目录的查看和配置,并将应用现有的标准错误输出写入该目录,不涉及 Windows 端或业务日志扩展。 ## 目标 @@ -24,7 +24,9 @@ macOS 设置界面没有告诉用户日志默认保存在哪里,也不能修 ### 自定义目录 -用户通过系统目录选择器选择目录。选择成功后保存配置,应用重启后继续显示该选择。该设置不影响其他配置。 +用户通过系统目录选择器选择目录。选择成功后保存配置,应用标准错误输出立即切换到该目录中的 `lithe.log`,应用重启后继续使用该选择。该设置不影响其他配置。 + +如果自定义目录无法创建或打开,应用清除该选择并恢复默认目录,界面不得继续把失败目录显示为当前日志目录。 “恢复默认目录”只清除自定义日志目录。“恢复全部默认设置”也应清除该配置。 @@ -39,7 +41,7 @@ macOS 设置界面没有告诉用户日志默认保存在哪里,也不能修 本次不做: - 新建结构化日志协议; -- 重写日志 writer、文件 sink 或导出逻辑; +- 新建结构化日志 writer、业务 sink 或导出逻辑; - 新增文件轮换和 ownership manifest; - 新增 Run、Build、Test、Debug、LSP、Terminal 生命周期日志; - 新增应用、窗口、工作区或文件事件日志; @@ -54,4 +56,5 @@ macOS 设置界面没有告诉用户日志默认保存在哪里,也不能修 3. 用户可以恢复默认目录。 4. “恢复全部默认设置”会清除自定义日志目录。 5. 用户可以复制完整路径。 -6. 没有引入新的日志协议、业务埋点、帧率监控或日志写入管线。 +6. 标准错误输出写入当前目录的 `lithe.log`,切换目录后新输出写入新目录。 +7. 没有引入新的结构化日志协议、业务埋点或帧率监控。 From 03d87a191182606ab427b2e5a3d08ebb31ae451c Mon Sep 17 00:00:00 2001 From: Sunwenzhi58 <2514832692@qq.com> Date: Tue, 18 Aug 2026 22:00:18 +0800 Subject: [PATCH 5/8] =?UTF-8?q?fix(macOS):=20=E4=BF=AE=E6=AD=A3=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E6=96=87=E4=BB=B6=E6=8F=8F=E8=BF=B0=E7=AC=A6=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Platform/MacOS/Logging/MacApplicationLogWriter.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Lithe/Platform/MacOS/Logging/MacApplicationLogWriter.swift b/Sources/Lithe/Platform/MacOS/Logging/MacApplicationLogWriter.swift index c69a49f5b..0c428655d 100644 --- a/Sources/Lithe/Platform/MacOS/Logging/MacApplicationLogWriter.swift +++ b/Sources/Lithe/Platform/MacOS/Logging/MacApplicationLogWriter.swift @@ -16,8 +16,8 @@ final class MacApplicationLogWriter { withIntermediateDirectories: true ) let logURL = directory.appendingPathComponent(Self.fileName, isDirectory: false) - let descriptor = logURL.withUnsafeFileSystemRepresentation { path in - guard let path else { return -1 } + let descriptor: Int32 = logURL.withUnsafeFileSystemRepresentation { path in + guard let path else { return Int32(-1) } return open(path, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR) } guard descriptor >= 0 else { From 182b1f7bdabc5442c8576023f4c376cbf12104d3 Mon Sep 17 00:00:00 2001 From: Sunwenzhi58 <2514832692@qq.com> Date: Tue, 18 Aug 2026 22:33:22 +0800 Subject: [PATCH 6/8] =?UTF-8?q?fix(macOS):=20=E9=81=BF=E5=85=8D=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E9=87=8D=E5=AE=9A=E5=90=91=E6=B5=8B=E8=AF=95=E9=98=BB?= =?UTF-8?q?=E5=A1=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MacApplicationLogWriterTests.swift | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/Tests/LitheTests/MacApplicationLogWriterTests.swift b/Tests/LitheTests/MacApplicationLogWriterTests.swift index 36098e46d..b400737e3 100644 --- a/Tests/LitheTests/MacApplicationLogWriterTests.swift +++ b/Tests/LitheTests/MacApplicationLogWriterTests.swift @@ -11,24 +11,25 @@ struct MacApplicationLogWriterTests { .appendingPathComponent(UUID().uuidString, isDirectory: true) let originalTarget = root.appendingPathComponent("original-target") try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - #expect(FileManager.default.createFile(atPath: originalTarget.path, contents: nil)) - let targetHandle = try FileHandle(forWritingTo: originalTarget) + let targetDescriptor: Int32 = originalTarget.withUnsafeFileSystemRepresentation { path in + guard let path else { return Int32(-1) } + return open(path, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR) + } + #expect(targetDescriptor >= 0) defer { - try? targetHandle.close() + close(targetDescriptor) try? FileManager.default.removeItem(at: root) } - let writer = MacApplicationLogWriter(targetFileDescriptor: targetHandle.fileDescriptor) + let writer = MacApplicationLogWriter(targetFileDescriptor: targetDescriptor) let defaultDirectory = root.appendingPathComponent("default", isDirectory: true) let selectedDirectory = root.appendingPathComponent("selected", isDirectory: true) try writer.redirect(to: defaultDirectory) - try targetHandle.write(contentsOf: Data("default log line\n".utf8)) - try targetHandle.synchronize() + try write("default log line\n", to: targetDescriptor) try writer.redirect(to: selectedDirectory) - try targetHandle.write(contentsOf: Data("selected log line\n".utf8)) - try targetHandle.synchronize() + try write("selected log line\n", to: targetDescriptor) let defaultContents = try String( contentsOf: defaultDirectory.appendingPathComponent("lithe.log"), @@ -48,4 +49,14 @@ struct MacApplicationLogWriterTests { #expect(directory.path.hasSuffix("/Library/Logs/Lithe")) } + + private func write(_ value: String, to descriptor: Int32) throws { + let data = Data(value.utf8) + let written = data.withUnsafeBytes { bytes in + Darwin.write(descriptor, bytes.baseAddress, bytes.count) + } + guard written == data.count, fsync(descriptor) == 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + } } From 7a7280d1051cdd1b9ac802d9eb710b8e68f9685c Mon Sep 17 00:00:00 2001 From: Sunwenzhi58 <2514832692@qq.com> Date: Tue, 18 Aug 2026 22:46:03 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix(macOS):=20=E9=9A=94=E7=A6=BB=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E5=86=99=E5=85=A5=E4=B8=8E=E6=B5=8B=E8=AF=95=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Lithe/LitheApp.swift | 21 +++++++++-- .../Logging/MacApplicationLogWriter.swift | 35 +++++++++++++++---- .../MacOS/UI/MacBundledFontRegistry.swift | 13 +++++-- .../MacApplicationLogWriterTests.swift | 25 +++---------- ...-17-cross-platform-logging-requirements.md | 6 ++-- 5 files changed, 64 insertions(+), 36 deletions(-) diff --git a/Sources/Lithe/LitheApp.swift b/Sources/Lithe/LitheApp.swift index 8de14e9ea..852eaf0d8 100644 --- a/Sources/Lithe/LitheApp.swift +++ b/Sources/Lithe/LitheApp.swift @@ -153,7 +153,9 @@ struct LitheApp: App { settings?.setCustomLogDirectory(nil) } self.applicationLogWriter = applicationLogWriter - MacBundledFontRegistry.registerFonts() + MacBundledFontRegistry.registerFonts { message in + Self.appendApplicationLog(applicationLogWriter, message: message) + } let processRegistry = ManagedProcessRegistry() let moduleStore = MacModuleConfigurationStore(store: store) let pluginRuntimeRecovery = MacPluginRuntimeRecoveryCoordinator() @@ -187,8 +189,7 @@ struct LitheApp: App { _memoryUsageMonitor = StateObject(wrappedValue: MemoryUsageMonitor( startedAt: litheProcessLaunchDate, baselineReporter: { marker in - guard let data = (marker + "\n").data(using: .utf8) else { return } - FileHandle.standardError.write(data) + Self.appendApplicationLog(applicationLogWriter, message: marker + "\n") }, logsPerformanceBaseline: ProcessInfo.processInfo.environment["LITHE_PERFORMANCE_BASELINE"] == "1", processRegistry: processRegistry, @@ -217,6 +218,20 @@ struct LitheApp: App { } } + private static func appendApplicationLog( + _ writer: MacApplicationLogWriter, + message: String + ) { + do { + try writer.append(message) + } catch { + let fallback = "Could not write Lithe log: \(error.localizedDescription)\n" + if let data = fallback.data(using: .utf8) { + FileHandle.standardError.write(data) + } + } + } + private var model: AppModel { projectSessions.activeModel } var body: some Scene { diff --git a/Sources/Lithe/Platform/MacOS/Logging/MacApplicationLogWriter.swift b/Sources/Lithe/Platform/MacOS/Logging/MacApplicationLogWriter.swift index 0c428655d..c40670940 100644 --- a/Sources/Lithe/Platform/MacOS/Logging/MacApplicationLogWriter.swift +++ b/Sources/Lithe/Platform/MacOS/Logging/MacApplicationLogWriter.swift @@ -4,17 +4,36 @@ import Foundation final class MacApplicationLogWriter { static let fileName = "lithe.log" - private let targetFileDescriptor: Int32 - - init(targetFileDescriptor: Int32 = STDERR_FILENO) { - self.targetFileDescriptor = targetFileDescriptor - } + private let lock = NSLock() + private var directory: URL? func redirect(to directory: URL) throws { try FileManager.default.createDirectory( at: directory, withIntermediateDirectories: true ) + let logURL = directory.appendingPathComponent(Self.fileName, isDirectory: false) + let descriptor: Int32 = logURL.withUnsafeFileSystemRepresentation { path in + guard let path else { return Int32(-1) } + return open(path, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR) + } + guard descriptor >= 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + close(descriptor) + + lock.lock() + self.directory = directory + lock.unlock() + } + + func append(_ message: String) throws { + lock.lock() + defer { lock.unlock() } + guard let directory else { + throw CocoaError(.fileNoSuchFile) + } + let logURL = directory.appendingPathComponent(Self.fileName, isDirectory: false) let descriptor: Int32 = logURL.withUnsafeFileSystemRepresentation { path in guard let path else { return Int32(-1) } @@ -25,7 +44,11 @@ final class MacApplicationLogWriter { } defer { close(descriptor) } - guard dup2(descriptor, targetFileDescriptor) >= 0 else { + let data = Data(message.utf8) + let written = data.withUnsafeBytes { bytes in + Darwin.write(descriptor, bytes.baseAddress, bytes.count) + } + guard written == data.count else { throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) } } diff --git a/Sources/Lithe/Platform/MacOS/UI/MacBundledFontRegistry.swift b/Sources/Lithe/Platform/MacOS/UI/MacBundledFontRegistry.swift index 749114018..0f734e1fb 100644 --- a/Sources/Lithe/Platform/MacOS/UI/MacBundledFontRegistry.swift +++ b/Sources/Lithe/Platform/MacOS/UI/MacBundledFontRegistry.swift @@ -11,6 +11,13 @@ enum MacBundledFontRegistry { ] static func registerFonts(bundle: Bundle = .main) { + registerFonts(bundle: bundle, reporter: report) + } + + static func registerFonts( + bundle: Bundle = .main, + reporter: (String) -> Void + ) { guard bundle.url(forResource: "JetBrainsMono-Regular", withExtension: "ttf", subdirectory: "Fonts") != nil else { return } @@ -21,7 +28,7 @@ enum MacBundledFontRegistry { withExtension: "ttf", subdirectory: "Fonts" ) else { - report("Missing bundled font: \(font.resource).ttf") + reporter("Lithe font registration: Missing bundled font: \(font.resource).ttf\n") continue } @@ -29,14 +36,14 @@ enum MacBundledFontRegistry { guard CTFontManagerRegisterFontsForURL(url as CFURL, .process, ®istrationError) else { let detail = registrationError?.takeRetainedValue().localizedDescription ?? "Unknown CoreText error" - report("Could not register \(font.resource).ttf: \(detail)") + reporter("Lithe font registration: Could not register \(font.resource).ttf: \(detail)\n") continue } } } private static func report(_ message: String) { - guard let data = ("Lithe font registration: \(message)\n").data(using: .utf8) else { return } + guard let data = message.data(using: .utf8) else { return } FileHandle.standardError.write(data) } } diff --git a/Tests/LitheTests/MacApplicationLogWriterTests.swift b/Tests/LitheTests/MacApplicationLogWriterTests.swift index b400737e3..bf1eb68ba 100644 --- a/Tests/LitheTests/MacApplicationLogWriterTests.swift +++ b/Tests/LitheTests/MacApplicationLogWriterTests.swift @@ -1,4 +1,3 @@ -import Darwin import Foundation import Testing @testable import Lithe @@ -6,30 +5,23 @@ import Testing @Suite("macOS application log writer") struct MacApplicationLogWriterTests { @Test - func changingDirectoryMovesSubsequentStandardErrorOutputToTheSelection() throws { + func changingDirectoryMovesSubsequentApplicationLogOutputToTheSelection() throws { let root = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) - let originalTarget = root.appendingPathComponent("original-target") try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - let targetDescriptor: Int32 = originalTarget.withUnsafeFileSystemRepresentation { path in - guard let path else { return Int32(-1) } - return open(path, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR) - } - #expect(targetDescriptor >= 0) defer { - close(targetDescriptor) try? FileManager.default.removeItem(at: root) } - let writer = MacApplicationLogWriter(targetFileDescriptor: targetDescriptor) + let writer = MacApplicationLogWriter() let defaultDirectory = root.appendingPathComponent("default", isDirectory: true) let selectedDirectory = root.appendingPathComponent("selected", isDirectory: true) try writer.redirect(to: defaultDirectory) - try write("default log line\n", to: targetDescriptor) + try writer.append("default log line\n") try writer.redirect(to: selectedDirectory) - try write("selected log line\n", to: targetDescriptor) + try writer.append("selected log line\n") let defaultContents = try String( contentsOf: defaultDirectory.appendingPathComponent("lithe.log"), @@ -50,13 +42,4 @@ struct MacApplicationLogWriterTests { #expect(directory.path.hasSuffix("/Library/Logs/Lithe")) } - private func write(_ value: String, to descriptor: Int32) throws { - let data = Data(value.utf8) - let written = data.withUnsafeBytes { bytes in - Darwin.write(descriptor, bytes.baseAddress, bytes.count) - } - guard written == data.count, fsync(descriptor) == 0 else { - throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) - } - } } diff --git a/docs/superpowers/specs/2026-08-17-cross-platform-logging-requirements.md b/docs/superpowers/specs/2026-08-17-cross-platform-logging-requirements.md index be21878cb..b688006ea 100644 --- a/docs/superpowers/specs/2026-08-17-cross-platform-logging-requirements.md +++ b/docs/superpowers/specs/2026-08-17-cross-platform-logging-requirements.md @@ -4,7 +4,7 @@ macOS 设置界面没有告诉用户日志默认保存在哪里,也不能修改日志保存目录。 -本需求只完善 macOS 日志目录的查看和配置,并将应用现有的标准错误输出写入该目录,不涉及 Windows 端或业务日志扩展。 +本需求只完善 macOS 日志目录的查看和配置,并将应用现有的字体注册和性能基线诊断写入该目录,不涉及 Windows 端或业务日志扩展。 ## 目标 @@ -24,7 +24,7 @@ macOS 设置界面没有告诉用户日志默认保存在哪里,也不能修 ### 自定义目录 -用户通过系统目录选择器选择目录。选择成功后保存配置,应用标准错误输出立即切换到该目录中的 `lithe.log`,应用重启后继续使用该选择。该设置不影响其他配置。 +用户通过系统目录选择器选择目录。选择成功后保存配置,应用诊断输出立即切换到该目录中的 `lithe.log`,应用重启后继续使用该选择。该设置不影响其他配置。 如果自定义目录无法创建或打开,应用清除该选择并恢复默认目录,界面不得继续把失败目录显示为当前日志目录。 @@ -56,5 +56,5 @@ macOS 设置界面没有告诉用户日志默认保存在哪里,也不能修 3. 用户可以恢复默认目录。 4. “恢复全部默认设置”会清除自定义日志目录。 5. 用户可以复制完整路径。 -6. 标准错误输出写入当前目录的 `lithe.log`,切换目录后新输出写入新目录。 +6. 现有字体注册和性能基线诊断写入当前目录的 `lithe.log`,切换目录后新输出写入新目录。 7. 没有引入新的结构化日志协议、业务埋点或帧率监控。 From 215f463a0b99ff1720fcce0f6bfc12b477c24fd5 Mon Sep 17 00:00:00 2001 From: Sunwenzhi58 <2514832692@qq.com> Date: Tue, 18 Aug 2026 23:07:25 +0800 Subject: [PATCH 8/8] =?UTF-8?q?ci(macOS):=20=E6=94=BE=E5=AE=BD=20Swift=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=B6=85=E6=97=B6=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-macos.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index ffdffb8eb..4a9f185a7 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -81,7 +81,7 @@ jobs: needs: changes if: needs.changes.outputs.full == 'true' runs-on: macos-14 - timeout-minutes: 20 + timeout-minutes: 30 steps: - name: Check out source @@ -104,12 +104,12 @@ jobs: - name: Run Swift tests id: swift-tests-primary continue-on-error: true - timeout-minutes: 8 + timeout-minutes: 12 run: ./scripts/test-macos.sh - name: Retry Swift tests after a failed attempt if: steps.swift-tests-primary.outcome == 'failure' - timeout-minutes: 8 + timeout-minutes: 12 run: ./scripts/test-macos.sh --skip-build rust-tests: