From 3f052e403f508c37ab798e88850ca4972b79a8df Mon Sep 17 00:00:00 2001 From: Qiu Yuzhou Date: Sun, 20 Sep 2026 09:00:29 +0800 Subject: [PATCH] feat(ng2): add 2.0 project skeleton and signing baseline (#22) XcodeGen-managed SwiftUI menu bar app in ShadowsocksX-NG2/: LSUIElement, inherited bundle id com.qiuyuzhou.ShadowsocksX-NG, macOS 15 arm64, Hardened Runtime + Developer ID signing, empty entitlements dict (no App Sandbox; Release disables base entitlement injection). Placeholder status menu and main window; unit test target wired into the app scheme. Test bundles sign with the Apple Development cert so Hardened Runtime library validation accepts injection. Generated .xcodeproj and plists are gitignored; project.yml is the source of truth. Legacy/ untouched. Closes #22 --- .gitignore | 7 +++ ShadowsocksX-NG2/App/AppDelegate.swift | 8 +++ ShadowsocksX-NG2/App/MainApp.swift | 18 ++++++ .../App/PlaceholderMainWindow.swift | 20 ++++++ .../App/PlaceholderStatusMenu.swift | 24 ++++++++ ShadowsocksX-NG2/README.md | 26 ++++++++ ShadowsocksX-NG2/Tests/SkeletonTests.swift | 20 ++++++ ShadowsocksX-NG2/project.yml | 61 +++++++++++++++++++ 8 files changed, 184 insertions(+) create mode 100644 ShadowsocksX-NG2/App/AppDelegate.swift create mode 100644 ShadowsocksX-NG2/App/MainApp.swift create mode 100644 ShadowsocksX-NG2/App/PlaceholderMainWindow.swift create mode 100644 ShadowsocksX-NG2/App/PlaceholderStatusMenu.swift create mode 100644 ShadowsocksX-NG2/Tests/SkeletonTests.swift create mode 100644 ShadowsocksX-NG2/project.yml diff --git a/.gitignore b/.gitignore index 76937b9d..65ba25db 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,10 @@ Legacy/ShadowsocksX-NG/v2ray-plugin/v2ray-plugin # New project build artifacts ShadowsocksX-NG2/.build/ ShadowsocksX-NG2/DerivedData/ + +#### +# NG2 generated build files (project.yml is the source of truth; run `xcodegen generate`) +ShadowsocksX-NG2/*.xcodeproj +ShadowsocksX-NG2/App/Info.plist +ShadowsocksX-NG2/App/ShadowsocksX-NG2.entitlements +ShadowsocksX-NG2/Tests/Info.plist diff --git a/ShadowsocksX-NG2/App/AppDelegate.swift b/ShadowsocksX-NG2/App/AppDelegate.swift new file mode 100644 index 00000000..f649962d --- /dev/null +++ b/ShadowsocksX-NG2/App/AppDelegate.swift @@ -0,0 +1,8 @@ +import AppKit + +final class AppDelegate: NSObject, NSApplicationDelegate { + /// LSUIElement 菜单栏形态的硬性要求:关闭最后一个窗口后进程必须保留。 + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + false + } +} diff --git a/ShadowsocksX-NG2/App/MainApp.swift b/ShadowsocksX-NG2/App/MainApp.swift new file mode 100644 index 00000000..3a960f85 --- /dev/null +++ b/ShadowsocksX-NG2/App/MainApp.swift @@ -0,0 +1,18 @@ +import SwiftUI + +@main +struct ShadowsocksXNG2App: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + + var body: some Scene { + MenuBarExtra("ShadowsocksX-NG 2.0", systemImage: "network") { + PlaceholderStatusMenu() + } + .menuBarExtraStyle(.menu) + + Window("ShadowsocksX-NG 2.0", id: "main") { + PlaceholderMainWindow() + } + .defaultLaunchBehavior(.suppressed) + } +} diff --git a/ShadowsocksX-NG2/App/PlaceholderMainWindow.swift b/ShadowsocksX-NG2/App/PlaceholderMainWindow.swift new file mode 100644 index 00000000..6209ae2a --- /dev/null +++ b/ShadowsocksX-NG2/App/PlaceholderMainWindow.swift @@ -0,0 +1,20 @@ +import SwiftUI + +/// 占位主窗口,由后续工单替换为服务器与订阅管理界面。 +struct PlaceholderMainWindow: View { + var body: some View { + VStack(spacing: 12) { + Image(systemName: "network") + .font(.system(size: 44)) + .foregroundStyle(.secondary) + Text("ShadowsocksX-NG 2.0") + .font(.title2.bold()) + Text("这是工程骨架的占位主窗口,后续工单将逐个替换为真实的管理界面。") + .font(.callout) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + .padding(32) + .frame(width: 420, height: 240) + } +} diff --git a/ShadowsocksX-NG2/App/PlaceholderStatusMenu.swift b/ShadowsocksX-NG2/App/PlaceholderStatusMenu.swift new file mode 100644 index 00000000..6b5e5868 --- /dev/null +++ b/ShadowsocksX-NG2/App/PlaceholderStatusMenu.swift @@ -0,0 +1,24 @@ +import AppKit +import SwiftUI + +/// 占位状态菜单,由后续工单替换为真实的代理控制菜单。 +struct PlaceholderStatusMenu: View { + @Environment(\.openWindow) private var openWindow + + var body: some View { + Text("代理未运行(占位状态)") + + Divider() + + Button("打开主窗口…") { + NSApp.activate() + openWindow(id: "main") + } + + Divider() + + Button("退出 ShadowsocksX-NG 2.0") { + NSApp.terminate(nil) + } + } +} diff --git a/ShadowsocksX-NG2/README.md b/ShadowsocksX-NG2/README.md index 2010d641..5083cb8f 100644 --- a/ShadowsocksX-NG2/README.md +++ b/ShadowsocksX-NG2/README.md @@ -5,3 +5,29 @@ ShadowsocksX-NG 的现代化重写版本。新工程的全部源码与构建配 依赖的二进制工具(ss-local、privoxy、kcptun、v2ray-plugin 等)不再以子模块方式在仓库内构建,而是直接复制外部项目发布好的可执行文件。 旧版工程源码位于仓库根目录的 [`Legacy/`](../Legacy) 下,仅供参考,不保证可构建。 + +## 工程结构 + +- `project.yml` — XcodeGen 工程定义,唯一事实来源。`.xcodeproj` 与 `App/Info.plist`、`*.entitlements`、`Tests/Info.plist` 都是生成物(已 gitignore),改工程结构一律改 `project.yml` 后重新生成。 +- `App/` — app target 源码(SwiftUI 菜单栏 app,LSUIElement)。 +- `Tests/` — 单元测试 target,随 `ShadowsocksX-NG2` scheme 运行。 + +## 签名与运行形态基线 + +- 继承 bundle id `com.qiuyuzhou.ShadowsocksX-NG`;macOS 15+ / arm64。 +- Hardened Runtime + Developer ID 签名(`DEVELOPMENT_TEAM` / 证书见 `project.yml`);不启用 App Sandbox,entitlements 为空 dict。 +- 测试 target 用 Apple Development 证书签名:宿主 app 开启 Hardened Runtime 后 library validation 要求被注入的 xctest 包同 Team 签名。 +- Release 配置关闭 `CODE_SIGN_INJECT_BASE_ENTITLEMENTS`,保证发布产物 entitlements 只来自项目文件。 + +## 构建与测试 + +```bash +cd ShadowsocksX-NG2 +xcodegen generate +# 构建 +xcodebuild -project ShadowsocksX-NG2.xcodeproj -scheme ShadowsocksX-NG2 \ + -configuration Debug -destination 'platform=macOS,arch=arm64' build +# 运行单元测试 +xcodebuild -project ShadowsocksX-NG2.xcodeproj -scheme ShadowsocksX-NG2 \ + -destination 'platform=macOS,arch=arm64' test +``` diff --git a/ShadowsocksX-NG2/Tests/SkeletonTests.swift b/ShadowsocksX-NG2/Tests/SkeletonTests.swift new file mode 100644 index 00000000..1c04e46d --- /dev/null +++ b/ShadowsocksX-NG2/Tests/SkeletonTests.swift @@ -0,0 +1,20 @@ +import XCTest +@testable import ShadowsocksX_NG2 + +final class SkeletonTests: XCTestCase { + // 以下两个 Bundle.main 断言依赖测试包以 app 为 TEST_HOST 注入(target dependency); + // 若移除该依赖,Bundle.main 不再是 app bundle,断言会失效。 + + func testMenuBarAppDoesNotTerminateAfterLastWindowClosed() { + let delegate = AppDelegate() + XCTAssertFalse(delegate.applicationShouldTerminateAfterLastWindowClosed(.shared)) + } + + func testAppBundleIDInheritedFromLegacy() { + XCTAssertEqual(Bundle.main.bundleIdentifier, "com.qiuyuzhou.ShadowsocksX-NG") + } + + func testAppIsMenuBarAgent() { + XCTAssertEqual(Bundle.main.object(forInfoDictionaryKey: "LSUIElement") as? Bool, true) + } +} diff --git a/ShadowsocksX-NG2/project.yml b/ShadowsocksX-NG2/project.yml new file mode 100644 index 00000000..2e911776 --- /dev/null +++ b/ShadowsocksX-NG2/project.yml @@ -0,0 +1,61 @@ +name: ShadowsocksX-NG2 +options: + createIntermediateGroups: true + developmentLanguage: zh-Hans + deploymentTarget: + macOS: "15.0" +settings: + base: + ARCHS: arm64 + SWIFT_VERSION: "5.0" + CODE_SIGN_STYLE: Manual + DEVELOPMENT_TEAM: S878RH3PA8 + ENABLE_HARDENED_RUNTIME: YES + configs: + Release: + # 发布基线:签名 entitlements 必须只来自项目 entitlements 文件(保持最小)。 + CODE_SIGN_INJECT_BASE_ENTITLEMENTS: NO +targets: + ShadowsocksX-NG2: + type: application + platform: macOS + sources: + - path: App + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.qiuyuzhou.ShadowsocksX-NG + PRODUCT_NAME: ShadowsocksX-NG2 + CODE_SIGN_IDENTITY: "Developer ID Application" + MARKETING_VERSION: "2.0.0" + CURRENT_PROJECT_VERSION: 1 + info: + path: App/Info.plist + properties: + CFBundleShortVersionString: $(MARKETING_VERSION) + CFBundleVersion: $(CURRENT_PROJECT_VERSION) + LSMinimumSystemVersion: "15.0" + LSUIElement: true + NSPrincipalClass: NSApplication + LSApplicationCategoryType: public.app-category.utilities + entitlements: + path: App/ShadowsocksX-NG2.entitlements + properties: {} + scheme: + testTargets: + - ShadowsocksX-NG2Tests + ShadowsocksX-NG2Tests: + type: bundle.unit-test + platform: macOS + sources: + - path: Tests + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.qiuyuzhou.ShadowsocksX-NG.Tests + # 必须与宿主 app 同 Team 签名:Hardened Runtime 的 library validation 拒绝 + # 加载不同 Team ID 的非平台库,ad-hoc("-")签名的 xctest 包无法注入。 + CODE_SIGN_IDENTITY: "Apple Development" + info: + path: Tests/Info.plist + properties: {} + dependencies: + - target: ShadowsocksX-NG2