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
This commit is contained in:
Qiu Yuzhou
2026-09-20 09:00:29 +08:00
parent 40bb5c9638
commit 3f052e403f
8 changed files with 184 additions and 0 deletions
+7
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
import AppKit
final class AppDelegate: NSObject, NSApplicationDelegate {
/// LSUIElement 菜单栏形态的硬性要求:关闭最后一个窗口后进程必须保留。
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
false
}
}
+18
View File
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
}
+26
View File
@@ -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
```
@@ -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)
}
}
+61
View File
@@ -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