mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-29 21:53:43 +00:00
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:
@@ -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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import AppKit
|
||||
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
/// LSUIElement 菜单栏形态的硬性要求:关闭最后一个窗口后进程必须保留。
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user