refactor(signing): 原生侧签名标识符改由 signing.env codegen 生成(单一真相源)

承接 scripts/signing.env(shell 单源):Xcode/Swift 侧的 Team ID/Bundle ID/App Group
等无法 source 变量、只能字面量,故用 codegen 保持单源。

- 新增 scripts/gen-signing.mjs:从 signing.env 读 3 基值,用**结构化正则**(匹配
  `DEVELOPMENT_TEAM = X`/app group 形状等,不含任何旧字面值)回填 16 个原生文件的
  标识符;--check 为 drift 模式。因不含旧值,codegen 自身无痕、未来再迁账号仍生效。
- ci/check-codegen-drift.sh 追加签名 drift 闸(gen-signing.mjs --check)。
- 全量迁到美国身份:Team 44WULXM6SV / 实体 Yanmei AI LLC / Bundle com.yanmeiai.pangolin
  (+.PacketTunnel)/ App Group macOS 原生 44WULXM6SV.com.yanmeiai.pangolin、iOS
  group.com.yanmeiai.pangolin。清理注释/ORGANIZATIONNAME 等 Apple 侧残留痕迹。
- CLAUDE.md 同步新身份 + 指向 signing.env 单源。

验证:plutil lint 6 份 entitlements/plist 全 OK;macOS/iOS pbxproj `xcodebuild -list`
可解析、Runner build settings 解析为新 Team/Bundle;gen-signing.mjs --check 幂等 0 drift;
完整 codegen drift 闸(token/l10n/proto-i18n/signing)全绿。

注:未动 Android 包名 com.pangolin.pangolin_vpn(Play 身份、自签、非 Apple 账号痕迹,
改动影响大另议)与 Windows/桌面少量元数据串。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
This commit is contained in:
wangjia
2026-09-06 20:14:30 +08:00
parent 6618929092
commit 3fb645ed40
20 changed files with 230 additions and 68 deletions
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env node
// gen-signing.mjs — 从 scripts/signing.env(单一真相源)回填所有原生文件里的
// Apple 签名标识符(Team ID / Bundle ID / App Group / 签名主体)。
//
// 为什么用 codegen 而非硬编码:Apple 工具链要求这些值以字面量出现在 pbxproj /
// entitlements / plist / swift 里,无法像 shell 那样 source 一个变量;又分散在
// 十几处,迁账号极易漏。本脚本让 signing.env 成为唯一手改点,一键回填全部字面量。
//
// 关键设计:所有匹配都是**结构化正则**(匹配 `DEVELOPMENT_TEAM = X`、native app
// group 形状等),**不含任何旧的具体值**——所以①本脚本自身无「中国账号痕迹」;
// ②未来再迁账号(改 signing.env 里的值)仍能正确回填,不依赖旧字面量。
//
// 用法:
// node scripts/gen-signing.mjs 回填(就地改文件)
// node scripts/gen-signing.mjs --check drift 闸:若有文件会被改动则退出 1(不改文件)
//
// 注:源码注释里遗留的旧标识符是内部字符串、不进产物;本脚本只管功能性字面量
// (会被编译/签名进产物的),注释去痕由一次性清理处理。
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..');
const CLIENT = join(ROOT, 'client');
// ── 读 signing.env 的 3 个基值(去引号),派生其余 ───────────────────────────
function readEnv() {
const txt = readFileSync(join(HERE, 'signing.env'), 'utf8');
const pick = (k) => {
const m = txt.match(new RegExp('^' + k + '="?([^"\\n]+)"?\\s*$', 'm'));
if (!m) throw new Error(`signing.env 缺 ${k}`);
return m[1].trim();
};
const TEAM = pick('SIGNING_TEAM_ID');
const ENTITY = pick('SIGNING_ENTITY');
const APP = pick('SIGNING_APP_BUNDLE_ID');
return {
TEAM, ENTITY, APP,
EXT: `${APP}.PacketTunnel`,
MAC_GROUP: `${TEAM}.${APP}`,
IOS_GROUP: `group.${APP}`,
};
}
const V = readEnv();
// ── 每文件的结构化替换表(路径相对 client/)──────────────────────────────────
// 每条 [正则, 替换串]。正则只描述「形状」,替换用派生值填入。
const TABLE = {
// ---- 通用:两个 pbxproj 的 Team + Bundle ID(按后缀区分 app/ext/tests)----
'macos/Runner.xcodeproj/project.pbxproj': [
[/DEVELOPMENT_TEAM = [A-Za-z0-9]+;/g, `DEVELOPMENT_TEAM = ${V.TEAM};`],
[/PRODUCT_BUNDLE_IDENTIFIER = [\w.]+\.PacketTunnel;/g, `PRODUCT_BUNDLE_IDENTIFIER = ${V.EXT};`],
[/PRODUCT_NAME = [\w.]+\.PacketTunnel;/g, `PRODUCT_NAME = ${V.EXT};`],
[/PRODUCT_BUNDLE_IDENTIFIER = [\w.]+\.RunnerTests;/g, `PRODUCT_BUNDLE_IDENTIFIER = ${V.APP}.RunnerTests;`],
],
'ios/Runner.xcodeproj/project.pbxproj': [
[/DEVELOPMENT_TEAM = [A-Za-z0-9]+;/g, `DEVELOPMENT_TEAM = ${V.TEAM};`],
[/PRODUCT_BUNDLE_IDENTIFIER = [\w.]+\.PacketTunnel;/g, `PRODUCT_BUNDLE_IDENTIFIER = ${V.EXT};`],
// 主 app:不以 .PacketTunnel 结尾的那条(负向前瞻,避免误改扩展行)。
[/PRODUCT_BUNDLE_IDENTIFIER = (?![\w.]*\.PacketTunnel;)[\w.]+;/g, `PRODUCT_BUNDLE_IDENTIFIER = ${V.APP};`],
],
// ---- macOS xcconfig(主 app Bundle ID + 版权)----
'macos/Runner/Configs/AppInfo.xcconfig': [
[/^PRODUCT_BUNDLE_IDENTIFIER = .+$/m, `PRODUCT_BUNDLE_IDENTIFIER = ${V.APP}`],
[/^PRODUCT_COPYRIGHT = .+$/m, `PRODUCT_COPYRIGHT = Copyright © 2026 ${V.ENTITY}. All rights reserved.`],
],
// ---- macOS entitlements:app-identifier 前缀 + native app group ----
'macos/Runner/Release.entitlements': [
[/\$\(AppIdentifierPrefix\)[\w.]+/g, `$(AppIdentifierPrefix)${V.APP}`],
[/<string>[A-Z0-9]{10}\.[\w.]+<\/string>/g, `<string>${V.MAC_GROUP}</string>`],
],
'macos/Runner/DebugProfile.entitlements': [
[/\$\(AppIdentifierPrefix\)[\w.]+/g, `$(AppIdentifierPrefix)${V.APP}`],
[/<string>group\.[\w.]+<\/string>/g, `<string>${V.IOS_GROUP}</string>`],
],
'macos/PacketTunnel/PacketTunnel.entitlements': [
[/<string>[A-Z0-9]{10}\.[\w.]+<\/string>/g, `<string>${V.MAC_GROUP}</string>`],
],
// ---- macOS PacketTunnel Info.plist:NEMachServiceName = app group + .PacketTunnel ----
'macos/PacketTunnel/Info.plist': [
[/<string>[A-Z0-9]{10}\.[\w.]+\.PacketTunnel<\/string>/g, `<string>${V.MAC_GROUP}.PacketTunnel</string>`],
],
// ---- macOS Swift / 登录助手脚本 ----
'macos/PacketTunnel/PacketTunnelProvider.swift': [
[/subsystem: "[\w.]+"/g, `subsystem: "${V.EXT}"`],
[/appGroup = "[\w.]+"/g, `appGroup = "${V.MAC_GROUP}"`],
],
'macos/Runner/VpnChannel.swift': [
[/subsystem: "[\w.]+"/g, `subsystem: "${V.APP}"`],
[/tunnelBundleId = "[\w.]+"/g, `tunnelBundleId = "${V.EXT}"`],
],
'macos/Runner/MainFlutterWindow.swift': [
[/loginHelperID = "[\w.]+"/g, `loginHelperID = "${V.APP}.LoginHelper"`],
],
'macos/build_login_helper.sh': [
[/HELPER_ID="[\w.]+"/g, `HELPER_ID="${V.APP}.LoginHelper"`],
],
// ---- iOS entitlements(group. 前缀 app group)----
'ios/Runner/Runner.entitlements': [
[/<string>group\.[\w.]+<\/string>/g, `<string>${V.IOS_GROUP}</string>`],
],
'ios/PacketTunnel/PacketTunnel.entitlements': [
[/<string>group\.[\w.]+<\/string>/g, `<string>${V.IOS_GROUP}</string>`],
],
// ---- iOS Swift ----
'ios/Runner/VpnManager.swift': [
[/extensionBundleId = "[\w.]+"/g, `extensionBundleId = "${V.EXT}"`],
[/appGroup = "[\w.]+"/g, `appGroup = "${V.IOS_GROUP}"`],
],
'ios/PacketTunnel/PacketTunnelProvider.swift': [
[/subsystem: "[\w.]+"/g, `subsystem: "${V.EXT}"`],
[/appGroup = "[\w.]+"/g, `appGroup = "${V.IOS_GROUP}"`],
],
'ios/PacketTunnel/MemoryMonitor.swift': [
[/subsystem: String = "[\w.]+"/g, `subsystem: String = "${V.EXT}"`],
],
};
// ── 执行 ─────────────────────────────────────────────────────────────────────
const check = process.argv.includes('--check');
let changed = 0;
const changedFiles = [];
for (const [rel, rules] of Object.entries(TABLE)) {
const path = join(CLIENT, rel);
const before = readFileSync(path, 'utf8');
let after = before;
for (const [re, repl] of rules) after = after.replace(re, repl);
if (after !== before) {
changed++;
changedFiles.push(rel);
if (!check) writeFileSync(path, after);
}
}
if (check) {
if (changed) {
console.error(`❌ signing codegen drift:${changed} 个文件与 signing.env 不一致:`);
for (const f of changedFiles) console.error(' - ' + f);
console.error(' 跑 `node scripts/gen-signing.mjs` 回填后重试。');
process.exit(1);
}
console.log('✅ signing codegen 无 drift(原生文件与 signing.env 一致)');
} else {
console.log(`signing codegen 完成:回填 ${changed} 个文件` + (changed ? ':' : '(已一致,无改动)'));
for (const f of changedFiles) console.log(' - ' + f);
console.log(` TEAM=${V.TEAM} APP=${V.APP} MAC_GROUP=${V.MAC_GROUP} IOS_GROUP=${V.IOS_GROUP}`);
}
+9 -5
View File
@@ -1,11 +1,15 @@
# signing.env — Apple 签名身份的【单一真相源】(shell 侧)
#
# 迁移开发者账号 / 改 Bundle ID 时,改这一个文件;三个签名脚本
# (local_test.sh / ci/compile-macos.sh / ci/compile-ios.sh)全部 source 它,
# 不再各自硬编码。非机密(Team ID / 实体名 / Bundle ID 本就编进产物、公开),入 git。
# 迁移账号 / 改 Bundle ID 时,改这一个文件就够】,然后跑一次:
# node scripts/gen-signing.mjs
# 它会把这里的值回填进所有原生文件(pbxproj/entitlements/plist/swift)的字面量
# ——Apple 工具链要求这些值以字面量出现、无法 source 变量,故用 codegen 保持单源。
#
# ⚠️ Xcode 侧另有等价单一真相源 client/Signing.xcconfig(格式不同、内容须一致),
# 由 ci/check-signing-config.sh 一致性闸比对,防两处漂移。改这里也要改那里。
# 消费方:
# - 三个签名脚本(local_test.sh / ci/compile-macos.sh / ci/compile-ios.sh)直接 source 本文件;
# - 原生文件(Xcode/Swift)由 scripts/gen-signing.mjs 从本文件回填(勿手改那些标识符)。
# drift 闸:ci/check-codegen-drift.sh 跑 `gen-signing.mjs --check`,防改了 env 没重生成 /
# 手改了原生标识符。非机密(Team ID / 实体名 / Bundle ID 本就编进产物、公开),入 git。
# 10 位 Team ID(developer.apple.com → Membership details)。
SIGNING_TEAM_ID="44WULXM6SV"