diff --git a/ShadowsocksX-NG2/App/MainApp.swift b/ShadowsocksX-NG2/App/MainApp.swift index 2d2ff063..b7d9caf9 100644 --- a/ShadowsocksX-NG2/App/MainApp.swift +++ b/ShadowsocksX-NG2/App/MainApp.swift @@ -63,11 +63,10 @@ struct ShadowsocksXNG2App: App { } } -/// 随窗激活策略的真实 NSApp 落点(hermetic:单测 host 不触碰激活策略)。 +/// 随窗激活策略的真实 NSApp 落点。 @MainActor private final class NSAppWindowActivationApplier: WindowActivationPolicyApplying { func apply(_ policy: NSApplication.ActivationPolicy) { - guard !ApplicationDependencies.isUnitTesting else { return } NSApp.setActivationPolicy(policy) } } @@ -83,7 +82,6 @@ private struct StatusMenuLabel: View { Image(systemName: "network") .accessibilityLabel("ShadowsocksX-NG2") .task { - guard !ApplicationDependencies.isUnitTesting else { return } await control.resyncOnLaunch() } } @@ -196,10 +194,8 @@ private struct AppComposition { } } -/// The test host still loads the app executable because this target is a macOS -/// SwiftUI application. Its bootstrap must nevertheless be hermetic: starting -/// XCTest must not read the user's catalog, settings, activation state, Legacy -/// defaults, login-item registration, runtime files, or production Keychain. +/// 组合根依赖集(仅生产形态):单测不再以本 app 为宿主(去宿主化,ADR 0021), +/// 装配永远面向真实存储、系统服务与用户默认域。 @MainActor private struct ApplicationDependencies { let credentials: CredentialStoring @@ -217,18 +213,7 @@ private struct ApplicationDependencies { let diagnosticReportExporter: any DiagnosticReportExporter let configurationGroupFileExporter: any ConfigurationGroupFileExporter - static var isUnitTesting: Bool { - let environment = ProcessInfo.processInfo.environment - return environment["XCTestConfigurationFilePath"] != nil - || environment["XCTestSessionIdentifier"] != nil - } - static func make() -> ApplicationDependencies { - guard isUnitTesting else { return makeProduction() } - return makeTesting() - } - - private static func makeProduction() -> ApplicationDependencies { // ADR 0017 初版文件形态的迁移(发布链从未包含,仅存量开发机):文件不存在 // 即空操作,随每次启动调用无害。 SilentLaunchStore.migrateLegacyFileIfPresent( @@ -257,97 +242,4 @@ private struct ApplicationDependencies { configurationGroupFileExporter: AppKitConfigurationGroupFileExporter()) } - private static func makeTesting() -> ApplicationDependencies { - let directory = FileManager.default.temporaryDirectory - .appendingPathComponent("ShadowsocksX-NG2-test-host-\(UUID().uuidString)", isDirectory: true) - try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - - // 测试宿主不得触碰生产 defaults 域:走独立套件并在装配时清空(封闭性公约)。 - let silentLaunchSuite = "ShadowsocksX-NG2-test-host-silent-launch" - UserDefaults.standard.removePersistentDomain(forName: silentLaunchSuite) - let silentLaunchStore = SilentLaunchStore( - defaults: UserDefaults(suiteName: silentLaunchSuite)!) - - let credentials = EphemeralCredentialStore() - let catalogFileStore = CatalogFileStore( - fileURL: directory.appendingPathComponent("catalog.json")) - let settingsStore = ProxySettingsFileStore( - fileURL: directory.appendingPathComponent("settings.json"), - legacyListenFileURL: directory.appendingPathComponent("listen-settings.json")) - let restoredSettings = ProxySettingsFileStore.restored(store: settingsStore) - let marker = EphemeralLegacyImportMarkerStore() - let legacyImportService = LegacyImportService( - source: EmptyLegacySnapshotProvider(), - catalogStore: catalogFileStore, - credentials: credentials, - marker: marker) - - return ApplicationDependencies( - credentials: credentials, - catalogFileStore: catalogFileStore, - activationFileStore: ActivationStateFileStore( - fileURL: directory.appendingPathComponent("activation.json")), - runtimeFileStore: RuntimeFileStore( - fileURL: directory.appendingPathComponent("sslocal-active.json")), - settingsStore: settingsStore, - settingsRestore: restoredSettings, - listenRestore: RestoredListenSettings( - settings: restoredSettings.settings.listen, unreadableError: nil), - legacyImportService: legacyImportService, - launchAgent: NoopLaunchAgentService(), - loginService: NoopLaunchAtLoginService(), - silentLaunchStore: silentLaunchStore, - textClipboard: InMemoryTextClipboard(), - diagnosticReportExporter: InMemoryDiagnosticReportExporter(), - configurationGroupFileExporter: InMemoryConfigurationGroupFileExporter()) - } -} - -private final class EphemeralCredentialStore: CredentialStoring { - private var values: [CredentialReference: String] = [:] - private let lock = NSLock() - - func save(_ secret: String, for reference: CredentialReference) throws { - lock.lock() - defer { lock.unlock() } - values[reference] = secret - } - - func secret(for reference: CredentialReference) throws -> String? { - lock.lock() - defer { lock.unlock() } - return values[reference] - } - - func delete(_ reference: CredentialReference) throws { - lock.lock() - defer { lock.unlock() } - values.removeValue(forKey: reference) - } -} - -private struct EmptyLegacySnapshotProvider: LegacySnapshotProviding { - func readSnapshot() throws -> LegacySnapshot? { nil } -} - -private final class EphemeralLegacyImportMarkerStore: LegacyImportMarkerStoring { - private var completed = false - - func isCompleted() throws -> Bool { completed } - - func setCompleted(_ completed: Bool) throws { - self.completed = completed - } -} - -private struct NoopLaunchAgentService: LaunchAgentControlling { - var status: LaunchAgentStatus { .notRegistered } - func register() throws {} - func unregister() throws {} -} - -private struct NoopLaunchAtLoginService: LaunchAtLoginControlling { - var status: LoginItemStatus { .notRegistered } - func register() throws {} - func unregister() throws {} } diff --git a/ShadowsocksX-NG2/App/ProxyRuntimeController+Mode.swift b/ShadowsocksX-NG2/App/ProxyRuntimeController+Mode.swift index 23ec9942..134f8430 100644 --- a/ShadowsocksX-NG2/App/ProxyRuntimeController+Mode.swift +++ b/ShadowsocksX-NG2/App/ProxyRuntimeController+Mode.swift @@ -133,15 +133,16 @@ extension ProxyRuntimeController { /// (issue #63/#64)。快照缺失/损坏/版本不匹配时抛错,调用方必须失败 /// 并保留旧 ACL,不得静默退化成全局(issue #63 AC4)。 func chinaDirectRules() throws -> [ProxyRule] { - let geolocation = try BuiltinRuleCatalog.loadGeolocationCN() - let chinaIPv4 = try BuiltinRuleCatalog.loadChinaIPv4() + let geolocation = try BuiltinRuleCatalog.loadGeolocationCN(from: appBundle) + let chinaIPv4 = try BuiltinRuleCatalog.loadChinaIPv4(from: appBundle) return BuiltinRuleCatalog.chinaDirectRules(from: [geolocation, chinaIPv4]) } /// GFWList 候选(issue #65):可准确表达且未被更宽代理规则遮蔽的规则 /// (含未遮蔽例外),参与 `bypass_all` ACL 编译。 func gfwlistRules() throws -> [ProxyRule] { - BuiltinRuleCatalog.gfwlistRules(from: try BuiltinRuleCatalog.loadGFWList()) + BuiltinRuleCatalog.gfwlistRules( + from: try BuiltinRuleCatalog.loadGFWList(from: appBundle)) } private func deployModeTransition( diff --git a/ShadowsocksX-NG2/App/ProxyRuntimeController+SystemProxyGate.swift b/ShadowsocksX-NG2/App/ProxyRuntimeController+SystemProxyGate.swift index 5a1da728..52db8593 100644 --- a/ShadowsocksX-NG2/App/ProxyRuntimeController+SystemProxyGate.swift +++ b/ShadowsocksX-NG2/App/ProxyRuntimeController+SystemProxyGate.swift @@ -149,7 +149,7 @@ extension ProxyRuntimeController { private func ensureHelperAvailableForApply() async -> Bool { switch systemProxyHelper.status { case .approved: - if Self.helperRegistrationDrifted() { + if helperRegistrationDrifted() { do { try systemProxyHelper.unregister() // launchd 对节流中(spawn scheduled)job 的移除是异步的:立即重注 @@ -157,7 +157,7 @@ extension ProxyRuntimeController { // runtime(10s)节流窗再重注;间隔内收敛以旧定义失败呈超时。 try? await Task.sleep(nanoseconds: helperRefreshDelayNanoseconds) try systemProxyHelper.register() - Self.storeHelperRegistrationStamp() + storeHelperRegistrationStamp() } catch { // 刷新失败不记指纹,下次启动重试;状态退回后由下方分支接手。 RuntimeLog.emit(.systemProxyHelperRegisterFailed(detail: describe(error))) @@ -170,7 +170,7 @@ extension ProxyRuntimeController { case .notRegistered: do { try systemProxyHelper.register() - Self.storeHelperRegistrationStamp() + storeHelperRegistrationStamp() } catch { RuntimeLog.emit(.systemProxyHelperRegisterFailed(detail: describe(error))) } @@ -185,14 +185,16 @@ extension ProxyRuntimeController { return false } - private static func helperRegistrationDrifted(defaults: UserDefaults = .standard) -> Bool { + private func helperRegistrationDrifted(defaults: UserDefaults = .standard) -> Bool { SystemProxyHelperRegistrationStamp.drifted( - plistData: SystemProxyHelperIdentity.launchDaemonPlistData, + plistData: SystemProxyHelperIdentity.launchDaemonPlistData(in: appBundle), storedStamp: defaults.string(forKey: SystemProxyHelperRegistrationStamp.defaultsKey)) } - private static func storeHelperRegistrationStamp(defaults: UserDefaults = .standard) { - guard let plistData = SystemProxyHelperIdentity.launchDaemonPlistData else { return } + private func storeHelperRegistrationStamp(defaults: UserDefaults = .standard) { + guard let plistData = SystemProxyHelperIdentity.launchDaemonPlistData(in: appBundle) else { + return + } defaults.set( SystemProxyHelperRegistrationStamp.stamp(plistData), forKey: SystemProxyHelperRegistrationStamp.defaultsKey) diff --git a/ShadowsocksX-NG2/App/ProxyRuntimeController.swift b/ShadowsocksX-NG2/App/ProxyRuntimeController.swift index 8661ec5b..4b05a307 100644 --- a/ShadowsocksX-NG2/App/ProxyRuntimeController.swift +++ b/ShadowsocksX-NG2/App/ProxyRuntimeController.swift @@ -94,6 +94,10 @@ final class ProxyRuntimeController: ObservableObject { let systemProxyNetworkChangeMonitor: SystemProxyNetworkChangeMonitoring let firewallChecker: FirewallStatusChecking let firewallExecutableURLs: [URL] + /// 宿主 app bundle 缝(生产为 Bundle.main):内置规则快照与 LaunchDaemon + /// 清单等打包资源的定位基准。去宿主化(ADR 0021)后单测运行于 xctest + /// runner,`Bundle.main` 不再是 app,经此缝注入构建产物 bundle。 + let appBundle: Bundle let firewallPollIntervalNanoseconds: UInt64 let launchHealthTimeoutSeconds: TimeInterval /// 注册清单漂移重注时,注销与重注的间隔(launchd 对节流中 job 的移除异步)。 @@ -126,6 +130,7 @@ final class ProxyRuntimeController: ObservableObject { listenRestore: RestoredListenSettings = ListenSettingsFileStore.restored(), settingsStore: ProxySettingsStoring = ProxySettingsFileStore(), customRuleStore: CustomRuleStore = CustomRuleStore(), + appBundle: Bundle = .main, settingsRestore: RestoredProxySettings? = nil, agent: LaunchAgentControlling = SMAppLaunchAgentService(), probe: EndpointProbing = SystemEndpointProbe(), @@ -149,6 +154,7 @@ final class ProxyRuntimeController: ObservableObject { self.plugins = plugins self.settingsStore = settingsStore self.customRuleStore = customRuleStore + self.appBundle = appBundle let restoredSettings = settingsRestore ?? RestoredProxySettings( diff --git a/ShadowsocksX-NG2/Domain/SystemProxyHelperContract.swift b/ShadowsocksX-NG2/Domain/SystemProxyHelperContract.swift index 7ecfae3b..b3568239 100644 --- a/ShadowsocksX-NG2/Domain/SystemProxyHelperContract.swift +++ b/ShadowsocksX-NG2/Domain/SystemProxyHelperContract.swift @@ -20,9 +20,11 @@ enum SystemProxyHelperIdentity { /// App bundle 内 LaunchDaemon 清单内容(注册漂移指纹的基准)。helper 进程 /// 内没有 bundle,返回 nil,调用方按「无基准不动作」处理。Contents/Library - /// 不是资源目录,Bundle 资源 API 搜不到,按固定路径直读。 - static var launchDaemonPlistData: Data? { - let url = Bundle.main.bundleURL + /// 不是资源目录,Bundle 资源 API 搜不到,按固定路径直读。bundle 参数生产 + /// 默认 Bundle.main;去宿主化(ADR 0021)后单测经控制器的 appBundle 缝注入 + /// 构建产物 bundle。 + static func launchDaemonPlistData(in bundle: Bundle = .main) -> Data? { + let url = bundle.bundleURL .appendingPathComponent("Contents/Library/LaunchDaemons") .appendingPathComponent(plistName) return try? Data(contentsOf: url) diff --git a/ShadowsocksX-NG2/Domain/SystemProxyHelperListenerDelegate.swift b/ShadowsocksX-NG2/Domain/SystemProxyHelperListenerDelegate.swift index 87acc926..ece5b2d2 100644 --- a/ShadowsocksX-NG2/Domain/SystemProxyHelperListenerDelegate.swift +++ b/ShadowsocksX-NG2/Domain/SystemProxyHelperListenerDelegate.swift @@ -6,11 +6,18 @@ import Security /// 不校验 Team ID 或 UID。校验基于连接对端 pid 的 SecCode 查询。 final class SystemProxyHelperListenerDelegate: NSObject, NSXPCListenerDelegate { private let engine: SystemProxyHelperEngine + private let expectedClientIdentifier: String /// 引擎由进程入口显式注入:特权写入器 SystemProxyWriter 属 helper 专属 - /// (不进共享编译单元),测试注入假写入器。 - init(engine: SystemProxyHelperEngine) { + /// (不进共享编译单元),测试注入假写入器。期望客户端身份生产默认为 2.0 + /// bundle identifier;去宿主化(ADR 0021)后单测运行于 xctest runner, + /// 连接方身份随宿主改变,测试注入本进程实际签名 identifier。 + init( + engine: SystemProxyHelperEngine, + expectedClientIdentifier: String = SystemProxyHelperIdentity.clientCodeSigningIdentifier + ) { self.engine = engine + self.expectedClientIdentifier = expectedClientIdentifier super.init() } @@ -20,7 +27,7 @@ final class SystemProxyHelperListenerDelegate: NSObject, NSXPCListenerDelegate { ) -> Bool { guard SystemProxyClientValidator.clientSigningIdentifier(newConnection) - == SystemProxyHelperIdentity.clientCodeSigningIdentifier + == expectedClientIdentifier else { return false } newConnection.exportedInterface = NSXPCInterface(with: SystemProxyHelperControlling.self) newConnection.exportedObject = engine diff --git a/ShadowsocksX-NG2/Tests/AppArtifact.swift b/ShadowsocksX-NG2/Tests/AppArtifact.swift index 8191120c..ae47830a 100644 --- a/ShadowsocksX-NG2/Tests/AppArtifact.swift +++ b/ShadowsocksX-NG2/Tests/AppArtifact.swift @@ -1,23 +1,35 @@ import Foundation -/// 构建产物定位:沿测试 bundle 落盘位置向上查找 app 产物。覆盖两种布局—— -/// TEST_HOST 过渡期 `.xctest` 嵌在 app bundle 的 `Contents/PlugIns/` 内, -/// 去宿主化后 `.xctest` 与 `.app` 平级同在 `Build/Products//`。 -/// 测试需要 app 产物(嵌入二进制、Info.plist 打包面断言)时一律经此定位, -/// 不得用 `Bundle.main`(去宿主化后它是 xctest runner 的 bundle)。 +/// 构建产物定位:从测试 bundle 落盘位置向上逐层查找 app 产物。覆盖两种布局 +/// ——TEST_HOST 过渡期 `.xctest` 嵌于 app bundle 的 `Contents/PlugIns/`(app +/// 是祖先),去宿主化后 `.xctest` 与 `.app` 平级同在 `Build/Products//` +/// (app 是某层祖先的子项)。测试需要 app 产物(嵌入二进制、Info.plist 打包面 +/// 断言)时一律经此定位,不得用 `Bundle.main`(去宿主化后它是 xctest runner +/// 的 bundle)。 enum AppArtifact { static let bundleURL: URL = { - var directory = Bundle(for: Marker.self).bundleURL + let testBundleURL = Bundle(for: Marker.self).bundleURL + var directory = testBundleURL while directory.path != "/" { - directory.deleteLastPathComponent() if directory.lastPathComponent == "ShadowsocksX-NG2.app" { return directory } + directory.deleteLastPathComponent() + let candidate = directory.appendingPathComponent( + "ShadowsocksX-NG2.app", isDirectory: true) + if FileManager.default.fileExists(atPath: candidate.path) { + return candidate + } } // 定位失败时返回原目录让用例以「产物缺失」失败并暴露实际路径,不炸进程。 - return Bundle(for: Marker.self).bundleURL + return testBundleURL }() + /// `bundleURL` 的 Bundle 形态:注入按 Bundle 取资源的生产缝(如 + /// ProxyRuntimeController 的内置规则快照 bundle)。定位失败时退回测试 + /// bundle,让资源缺失以用例失败呈现。 + static let bundle: Bundle = Bundle(url: bundleURL) ?? Bundle(for: Marker.self) + /// `Bundle(for:)` 需要类对象;marker 只为取得测试 bundle 的落盘位置。 private final class Marker {} } diff --git a/ShadowsocksX-NG2/Tests/ProxyControlWorkflowIntegrationTests.swift b/ShadowsocksX-NG2/Tests/ProxyControlWorkflowIntegrationTests.swift index 8fe6b236..704839d1 100644 --- a/ShadowsocksX-NG2/Tests/ProxyControlWorkflowIntegrationTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxyControlWorkflowIntegrationTests.swift @@ -69,6 +69,7 @@ final class ProxyControlWorkflowIntegrationTests: XCTestCase { listenRestore: RestoredListenSettings( settings: ActivationFixture.listen, unreadableError: nil), settingsStore: settingsStore ?? InMemoryProxySettingsStore(), + appBundle: AppArtifact.bundle, settingsRestore: RestoredProxySettings( settings: settings ?? ProxySettings(listen: ActivationFixture.listen), unreadableError: nil), diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerModeTests.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerModeTests.swift index b28f2b93..5bfc5652 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerModeTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerModeTests.swift @@ -121,6 +121,7 @@ final class CatalogRuntimeSnapshotIntegrationTests: XCTestCase { listenRestore: RestoredListenSettings( settings: ActivationFixture.listen, unreadableError: nil), settingsStore: InMemoryProxySettingsStore(), + appBundle: AppArtifact.bundle, settingsRestore: RestoredProxySettings( settings: settings ?? ProxySettings(listen: ActivationFixture.listen), unreadableError: nil), diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift index 55064f93..a8a00cac 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift @@ -126,6 +126,7 @@ final class ProxyRuntimeControllerTests: XCTestCase { settingsStore: settingsStore ?? InMemoryProxySettingsStore(), customRuleStore: customRuleStore ?? CustomRuleStore(fileURL: runtime.directory.appendingPathComponent("custom-rules.json")), + appBundle: AppArtifact.bundle, settingsRestore: restored, agent: agent, probe: probe, diff --git a/ShadowsocksX-NG2/Tests/SettingsWorkflowTests.swift b/ShadowsocksX-NG2/Tests/SettingsWorkflowTests.swift index da6b1616..b2173ae7 100644 --- a/ShadowsocksX-NG2/Tests/SettingsWorkflowTests.swift +++ b/ShadowsocksX-NG2/Tests/SettingsWorkflowTests.swift @@ -59,6 +59,7 @@ final class SettingsWorkflowTests: XCTestCase { listenRestore: RestoredListenSettings( settings: ActivationFixture.listen, unreadableError: nil), settingsStore: settingsStore, + appBundle: AppArtifact.bundle, agent: agent, probe: ProxyRuntimeFixture.FakeProbe.reachable(), systemProxy: systemProxy, diff --git a/ShadowsocksX-NG2/Tests/SystemProxyHelperXPCRoundTripTests.swift b/ShadowsocksX-NG2/Tests/SystemProxyHelperXPCRoundTripTests.swift index 69f8b17f..e8834919 100644 --- a/ShadowsocksX-NG2/Tests/SystemProxyHelperXPCRoundTripTests.swift +++ b/ShadowsocksX-NG2/Tests/SystemProxyHelperXPCRoundTripTests.swift @@ -1,4 +1,5 @@ import Foundation +import Security import XCTest @testable import ShadowsocksX_NG2 @@ -6,8 +7,9 @@ import XCTest /// 真实 XPC 装配层的契约 round-trip(issue #71):匿名监听器挂真实 listener /// delegate(注入假特权写入器,不触真实系统设置)+ listenerEndpoint 连接,覆盖 /// fakes 与纯逻辑单测覆盖不到的层:NSXPCInterface 方法签名、客户端签名 -/// identifier 校验、Data payload 传输与 XPC 内部队列应答路径。测试宿主是 -/// app bundle,客户端 identifier 与 delegate 期望一致,无需 launchd。 +/// identifier 校验、Data payload 传输与 XPC 内部队列应答路径。去宿主化 +/// (ADR 0021)后连接方是 xctest runner 本进程,delegate 期望身份按本进程 +/// 实际签名 identifier 注入,无需 launchd。 /// 注意:NSXPCListener.delegate 是弱引用,delegate 必须由测试强持有。 final class SystemProxyHelperXPCRoundTripTests: XCTestCase { func testTypedApplyAndClearRoundTripThroughRealXPCConnection() async throws { @@ -17,7 +19,9 @@ final class SystemProxyHelperXPCRoundTripTests: XCTestCase { case .clear: return .cleared } }) - let delegate = SystemProxyHelperListenerDelegate(engine: engine) + let delegate = SystemProxyHelperListenerDelegate( + engine: engine, + expectedClientIdentifier: try XCTUnwrap(Self.currentProcessSigningIdentifier())) let listener = NSXPCListener.anonymous() listener.delegate = delegate listener.resume() @@ -35,8 +39,7 @@ final class SystemProxyHelperXPCRoundTripTests: XCTestCase { let configuration = SystemProxyConfiguration( socks: .init(host: "127.0.0.1", port: 1086), - http: .init(host: "127.0.0.1", port: 1087), - https: .init(host: "127.0.0.1", port: 1087), + http: .init(host: "127.0.0.1", port: 1087), https: .init(host: "127.0.0.1", port: 1087), exceptions: ["localhost"]) let payload = try SystemProxyHelperWire.encodeConfiguration(configuration) @@ -66,4 +69,19 @@ final class SystemProxyHelperXPCRoundTripTests: XCTestCase { await fulfillment(of: [clearReplied], timeout: 10) XCTAssertEqual(clearedResponse, .cleared) } + + /// 本进程的代码签名 identifier(与生产 SystemProxyClientValidator 同一 + /// SecCode 查询路径,对自身用 SecCodeCopySelf 取 code)。 + private static func currentProcessSigningIdentifier() -> String? { + var code: SecCode? + guard SecCodeCopySelf([], &code) == errSecSuccess, let code else { return nil } + var staticCode: SecStaticCode? + guard SecCodeCopyStaticCode(code, [], &staticCode) == errSecSuccess, let staticCode + else { return nil } + var information: CFDictionary? + guard SecCodeCopySigningInformation(staticCode, SecCSFlags(), &information) == errSecSuccess, + let information + else { return nil } + return (information as NSDictionary)[kSecCodeInfoIdentifier] as? String + } } diff --git a/ShadowsocksX-NG2/Tests/UnreadableListenSettingsTests.swift b/ShadowsocksX-NG2/Tests/UnreadableListenSettingsTests.swift index 627d1bed..26538a26 100644 --- a/ShadowsocksX-NG2/Tests/UnreadableListenSettingsTests.swift +++ b/ShadowsocksX-NG2/Tests/UnreadableListenSettingsTests.swift @@ -45,6 +45,7 @@ final class UnreadableListenSettingsTests: XCTestCase { plugins: ActivationFixture.plugins, listenRestore: RestoredListenSettings( settings: SslocalListenSettings(), unreadableError: listenUnreadable), + appBundle: AppArtifact.bundle, settingsRestore: settingsRestore, agent: agent, probe: ProxyRuntimeFixture.FakeProbe.reachable(), diff --git a/ShadowsocksX-NG2/project.yml b/ShadowsocksX-NG2/project.yml index 8a9e3632..6d8821c1 100644 --- a/ShadowsocksX-NG2/project.yml +++ b/ShadowsocksX-NG2/project.yml @@ -85,6 +85,10 @@ targets: base: PRODUCT_BUNDLE_IDENTIFIER: com.qiuyuzhou.ShadowsocksX-NG2 PRODUCT_NAME: ShadowsocksX-NG2 + # 模块名让位给 Core 镜像库(ShadowsocksX_NG2):两者 swiftmodule 都落 + # 共享 Products 目录,重名会互相覆盖;app 可执行自身不 import 任何 + # 消费方,改名零影响。 + PRODUCT_MODULE_NAME: ShadowsocksX_NG2App CODE_SIGN_IDENTITY: "Developer ID Application" ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon MARKETING_VERSION: "2.0.0" @@ -118,6 +122,29 @@ targets: copy: destination: wrapper subpath: Contents/MacOS + ShadowsocksX-NG2Core: + type: library.static + platform: macOS + # 测试专用 app 源码镜像(去宿主化,ADR 0021):与 app target 同源编译 + # (除 @main 组合根 MainApp.swift),模块名沿用 ShadowsocksX_NG2,全部 + # 测试的 @testable import 零改动。测试 bundle 改链本库后不再生成 + # TEST_HOST:xcodebuild test 只构建 app、不再启动 app 进程,与安装版 + # 运行实例零接触。同源双编译沿用 Agent/Helper 共享 Domain 文件的既定 + # 模式;Core 只被测试 target 依赖,发布链与普通构建不编译它。 + sources: + - path: App + excludes: + - "MainApp.swift" + - "Info.plist" + - "ShadowsocksX-NG2.entitlements" + - "Assets.xcassets" + - path: Domain + settings: + base: + PRODUCT_NAME: ShadowsocksX-NG2Core + PRODUCT_MODULE_NAME: ShadowsocksX_NG2 + # 静态库产物不签名(app 的 DevID 身份不适用)。 + CODE_SIGNING_ALLOWED: NO ShadowsocksX-NG2Agent: type: tool platform: macOS @@ -177,7 +204,9 @@ targets: path: Tests/Info.plist properties: {} dependencies: - - target: ShadowsocksX-NG2 + # 去宿主化(ADR 0021):单测链 Core 静态库镜像而非 app 可执行,测试在 + # 独立 xctest runner 中运行,宿主 app 不再被启动。 + - target: ShadowsocksX-NG2Core ShadowsocksX-NG2SmokeTests: type: bundle.unit-test platform: macOS @@ -194,7 +223,9 @@ targets: path: Tests/Smoke/Info.plist properties: {} dependencies: - - target: ShadowsocksX-NG2 + # 去宿主化(ADR 0021):smoke 同样链 Core 镜像库;真实 sslocal/agent 从 + # scheme 构建的 app 产物取(AppArtifact),宿主 app 不再被启动。 + - target: ShadowsocksX-NG2Core schemes: # 真实进程冒烟独立 scheme(task test:smoke):拉起真实 sslocal,按需执行; # 发布门槛要求与 packaging-gate 一起全绿。