chore(desktop-client): add macOS code-signing and notarization scaffolding
- Move the build config out of package.json into electron-builder.yml so
the mac block can carry hardenedRuntime, entitlements, universal arch,
and usage-description Info.plist keys. Drop identity:null (which
forces unsigned builds) and let CSC_NAME or the keychain decide.
- Add entitlements plist files and three helper scripts: sign-setup.sh
(one-time keychain prep with notarytool store-credentials),
sign-mac.sh (sign + notarize + staple, supports keychain or CI env
vars), and ci-import-cert.sh (CI keychain bootstrap).
- Ignore .env.signing so local credentials don't sneak into the repo,
and pin @emnapi/{core,runtime} as devDependencies so universal builds
resolve them deterministically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@ coverage/
|
|||||||
dist/
|
dist/
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
.env.signing
|
||||||
|
apps/*/.env.signing
|
||||||
apps/*/dist/
|
apps/*/dist/
|
||||||
apps/*/out/
|
apps/*/out/
|
||||||
apps/*/release/
|
apps/*/release/
|
||||||
|
|||||||
@@ -365,3 +365,5 @@ The color system is almost entirely achromatic — dark backgrounds with white/g
|
|||||||
5. Brand indigo (`#5e6ad2` / `#7170ff`) is the only chromatic color — everything else is grayscale
|
5. Brand indigo (`#5e6ad2` / `#7170ff`) is the only chromatic color — everything else is grayscale
|
||||||
6. Borders are always semi-transparent white, never solid dark colors on dark backgrounds
|
6. Borders are always semi-transparent white, never solid dark colors on dark backgrounds
|
||||||
7. Berkeley Mono for any code or technical content, Inter Variable for everything else
|
7. Berkeley Mono for any code or technical content, Inter Variable for everything else
|
||||||
|
|
||||||
|
rnzy-benw-ynai-ftbl
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key><true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key><true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key><true/>
|
||||||
|
<key>com.apple.security.inherit</key><true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key><true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key><true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key><true/>
|
||||||
|
<key>com.apple.security.device.audio-input</key><true/>
|
||||||
|
<key>com.apple.security.device.camera</key><true/>
|
||||||
|
<key>com.apple.security.network.client</key><true/>
|
||||||
|
<key>com.apple.security.network.server</key><true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key><true/>
|
||||||
|
<key>com.apple.security.automation.apple-events</key><true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -35,18 +35,25 @@ files:
|
|||||||
- "!**/node_modules/**/*.{c,cc,cpp,h,hpp,cmake,m4,am,sh}"
|
- "!**/node_modules/**/*.{c,cc,cpp,h,hpp,cmake,m4,am,sh}"
|
||||||
mac:
|
mac:
|
||||||
category: public.app-category.productivity
|
category: public.app-category.productivity
|
||||||
identity: null
|
# identity 不写死:让 electron-builder 自动从 CSC_NAME 环境变量或钥匙串里挑
|
||||||
notarize: false
|
# 注意:写 `identity: null` 是"明确不签名",不要这么写
|
||||||
hardenedRuntime: false
|
notarize: false # 我们用 notarytool 自己跑,比 electron-builder 内置更稳
|
||||||
|
hardenedRuntime: true
|
||||||
gatekeeperAssess: false
|
gatekeeperAssess: false
|
||||||
|
entitlements: build/entitlements.mac.plist
|
||||||
|
entitlementsInherit: build/entitlements.mac.inherit.plist
|
||||||
electronLanguages:
|
electronLanguages:
|
||||||
- en
|
- en
|
||||||
- zh_CN
|
- zh_CN
|
||||||
|
extendInfo:
|
||||||
|
NSCameraUsageDescription: 用于视频会议和录屏功能
|
||||||
|
NSMicrophoneUsageDescription: 用于音频采集
|
||||||
|
NSAppleEventsUsageDescription: 用于自动化控制其他应用
|
||||||
target:
|
target:
|
||||||
- target: dmg
|
- target: dmg
|
||||||
arch: [arm64]
|
arch: [arm64, universal]
|
||||||
- target: zip
|
# - target: zip
|
||||||
arch: [arm64]
|
# arch: [arm64, universal]
|
||||||
dmg:
|
dmg:
|
||||||
format: ULFO
|
format: ULFO
|
||||||
contents:
|
contents:
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
"dev": "unset ELECTRON_RUN_AS_NODE && electron-vite dev --inspect=9229",
|
"dev": "unset ELECTRON_RUN_AS_NODE && electron-vite dev --inspect=9229",
|
||||||
"build": "electron-vite build",
|
"build": "electron-vite build",
|
||||||
"package:mac": "electron-builder --mac --arm64",
|
"package:mac": "electron-builder --mac --arm64",
|
||||||
|
"sign:setup": "bash scripts/sign-setup.sh",
|
||||||
|
"sign:mac": "bash scripts/sign-mac.sh",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"typecheck": "vue-tsc --noEmit"
|
"typecheck": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
@@ -46,81 +48,5 @@
|
|||||||
"vue": "^3.5.31",
|
"vue": "^3.5.31",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vue-tsc": "^3.2.6"
|
"vue-tsc": "^3.2.6"
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"appId": "com.geo.rankly.desktop",
|
|
||||||
"productName": "省心推",
|
|
||||||
"copyright": "Copyright © 2026 shengxintui.com. All Rights Reserved.",
|
|
||||||
"directories": {
|
|
||||||
"output": "release",
|
|
||||||
"buildResources": "build"
|
|
||||||
},
|
|
||||||
"compression": "maximum",
|
|
||||||
"removePackageScripts": true,
|
|
||||||
"removePackageKeywords": true,
|
|
||||||
"asar": true,
|
|
||||||
"asarUnpack": [
|
|
||||||
"**/node_modules/better-sqlite3/build/Release/**",
|
|
||||||
"**/node_modules/better-sqlite3/lib/**",
|
|
||||||
"**/node_modules/bindings/**",
|
|
||||||
"**/node_modules/file-uri-to-path/**",
|
|
||||||
"**/node_modules/playwright-core/lib/**",
|
|
||||||
"**/node_modules/playwright-core/bin/**"
|
|
||||||
],
|
|
||||||
"files": [
|
|
||||||
"out/**/*",
|
|
||||||
"package.json",
|
|
||||||
"!**/*.map",
|
|
||||||
"!**/*.ts",
|
|
||||||
"!**/*.tsx",
|
|
||||||
"!**/*.md",
|
|
||||||
"!**/*.markdown",
|
|
||||||
"!**/{test,tests,__tests__,__mocks__,example,examples,docs,doc,demo,samples,man}/**",
|
|
||||||
"!**/{LICENSE,LICENSE.*,license,CHANGELOG,CHANGELOG.*,README,README.*,HISTORY,HISTORY.*,AUTHORS,CONTRIBUTING}*",
|
|
||||||
"!**/.*",
|
|
||||||
"!**/node_modules/.bin/**",
|
|
||||||
"!**/node_modules/*/{Makefile,Gulpfile.js,Gruntfile.js,CMakeLists.txt,*.coffee,*.flow,*.c,*.cc,*.cpp,*.h,*.hpp,*.gyp,*.gypi,*.tgz,binding.gyp}",
|
|
||||||
"!**/node_modules/**/{*.d.ts,tsconfig*.json,jest.config*,rollup.config*,webpack.config*,vite.config*}",
|
|
||||||
"!**/node_modules/better-sqlite3/{deps,src,docs,benchmark}/**",
|
|
||||||
"!**/node_modules/better-sqlite3/{test_extension.c,download.sh,*.gyp,*.gypi}",
|
|
||||||
"!**/node_modules/playwright-core/{types,ThirdPartyNotices.txt}/**",
|
|
||||||
"!**/node_modules/**/*.{c,cc,cpp,h,hpp,cmake,m4,am,sh}"
|
|
||||||
],
|
|
||||||
"mac": {
|
|
||||||
"category": "public.app-category.productivity",
|
|
||||||
"identity": null,
|
|
||||||
"electronLanguages": ["en", "zh_CN"],
|
|
||||||
"target": [
|
|
||||||
{ "target": "dmg", "arch": ["arm64"] },
|
|
||||||
{ "target": "zip", "arch": ["arm64"] }
|
|
||||||
],
|
|
||||||
"notarize": false,
|
|
||||||
"hardenedRuntime": false,
|
|
||||||
"gatekeeperAssess": false
|
|
||||||
},
|
|
||||||
"dmg": {
|
|
||||||
"format": "ULFO",
|
|
||||||
"contents": [
|
|
||||||
{ "x": 130, "y": 220 },
|
|
||||||
{ "x": 410, "y": 220, "type": "link", "path": "/Applications" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"win": {
|
|
||||||
"target": [
|
|
||||||
{ "target": "nsis", "arch": ["x64"] }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nsis": {
|
|
||||||
"oneClick": false,
|
|
||||||
"perMachine": false,
|
|
||||||
"allowToChangeInstallationDirectory": true
|
|
||||||
},
|
|
||||||
"linux": {
|
|
||||||
"target": [
|
|
||||||
{ "target": "AppImage", "arch": ["x64"] }
|
|
||||||
],
|
|
||||||
"category": "Office"
|
|
||||||
},
|
|
||||||
"publish": null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# CI 专用:把 base64 编码的 .p12 证书导入到一个临时钥匙串
|
||||||
|
# 仅在 CI 环境调用,本地不要跑(会污染你的登录钥匙串体验)
|
||||||
|
#
|
||||||
|
# 必需环境变量:
|
||||||
|
# CSC_LINK - base64 编码的 .p12 文件内容
|
||||||
|
# CSC_KEY_PASSWORD - .p12 文件的密码
|
||||||
|
# KEYCHAIN_PASSWORD - 临时钥匙串自身的密码(任意字符串,例如 secrets.GITHUB_TOKEN 截断都行)
|
||||||
|
#
|
||||||
|
# 副作用:创建 ~/Library/Keychains/build.keychain-db,已加入搜索路径并解锁
|
||||||
|
# 销毁:脚本注册了 trap,进程退出时自动 delete-keychain
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${CSC_LINK:?CI 必须提供 CSC_LINK (base64 .p12)}"
|
||||||
|
: "${CSC_KEY_PASSWORD:?CI 必须提供 CSC_KEY_PASSWORD}"
|
||||||
|
: "${KEYCHAIN_PASSWORD:?CI 必须提供 KEYCHAIN_PASSWORD(用于临时钥匙串自身)}"
|
||||||
|
|
||||||
|
KEYCHAIN_NAME="build.keychain"
|
||||||
|
CERT_PATH="$RUNNER_TEMP/build_certificate.p12"
|
||||||
|
[ -z "${RUNNER_TEMP:-}" ] && CERT_PATH="/tmp/build_certificate.p12"
|
||||||
|
|
||||||
|
# 1. 解码证书到临时文件
|
||||||
|
echo "$CSC_LINK" | base64 --decode > "$CERT_PATH"
|
||||||
|
|
||||||
|
# 2. 创建临时钥匙串
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
|
||||||
|
security set-keychain-settings -lut 21600 "$KEYCHAIN_NAME" # 6 小时不锁
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
|
||||||
|
|
||||||
|
# 3. 导入 .p12(允许 codesign 和 productsign 不弹密码框)
|
||||||
|
security import "$CERT_PATH" \
|
||||||
|
-P "$CSC_KEY_PASSWORD" \
|
||||||
|
-A -t cert -f pkcs12 \
|
||||||
|
-k "$KEYCHAIN_NAME"
|
||||||
|
|
||||||
|
# 4. 关键:允许 codesign 不交互访问私钥
|
||||||
|
security set-key-partition-list \
|
||||||
|
-S apple-tool:,apple:,codesign: \
|
||||||
|
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME" >/dev/null
|
||||||
|
|
||||||
|
# 5. 把临时钥匙串加进搜索路径(放第一位,登录钥匙串保留作 fallback)
|
||||||
|
security list-keychain -d user -s "$KEYCHAIN_NAME" $(security list-keychain -d user | tr -d '"')
|
||||||
|
|
||||||
|
# 6. 立即清理证书文件
|
||||||
|
rm -f "$CERT_PATH"
|
||||||
|
|
||||||
|
# 7. 输出实际可用的 identity 名(让 sign-mac.sh 拿到精确的 CSC_NAME)
|
||||||
|
IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_NAME" \
|
||||||
|
| grep "Developer ID Application" | head -n1 \
|
||||||
|
| sed -E 's/.*"(Developer ID Application: [^"]+)".*/\1/')
|
||||||
|
|
||||||
|
if [ -z "$IDENTITY" ]; then
|
||||||
|
echo "✗ 导入后未在临时钥匙串里找到 Developer ID Application" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ 已导入证书到临时钥匙串: $KEYCHAIN_NAME"
|
||||||
|
echo "✓ Identity: $IDENTITY"
|
||||||
|
|
||||||
|
# 在 GitHub Actions 里把 CSC_NAME 写入后续步骤的 env
|
||||||
|
if [ -n "${GITHUB_ENV:-}" ]; then
|
||||||
|
echo "CSC_NAME=$IDENTITY" >> "$GITHUB_ENV"
|
||||||
|
echo "✓ 已写入 \$GITHUB_ENV:CSC_NAME"
|
||||||
|
fi
|
||||||
Executable
+110
@@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 一键:构建 → 签名 → 公证 → 装订 → 验证
|
||||||
|
# 用法: pnpm sign:mac
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[1m'; N=$'\033[0m'
|
||||||
|
ok() { echo " ${G}✓${N} $1"; }
|
||||||
|
fail() { echo " ${R}✗${N} $1"; }
|
||||||
|
step() { echo ""; echo "${B}── $1 ──${N}"; }
|
||||||
|
|
||||||
|
# ── 前置检查(本地/CI 双模式)─────────────────────────────────────
|
||||||
|
# 本地模式:读 .env.signing,公证走钥匙串 profile
|
||||||
|
# CI 模式:靠环境变量 CSC_NAME / APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID
|
||||||
|
NOTARY_ARGS=()
|
||||||
|
if [ -f .env.signing ] && [ -z "${CI:-}" ]; then
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
set -a; source .env.signing; set +a
|
||||||
|
: "${CSC_NAME:?.env.signing 缺少 CSC_NAME}"
|
||||||
|
: "${APPLE_TEAM_ID:?.env.signing 缺少 APPLE_TEAM_ID}"
|
||||||
|
: "${NOTARY_KEYCHAIN_PROFILE:?.env.signing 缺少 NOTARY_KEYCHAIN_PROFILE}"
|
||||||
|
NOTARY_ARGS=(--keychain-profile "$NOTARY_KEYCHAIN_PROFILE")
|
||||||
|
ok "模式: 本地(钥匙串 profile)"
|
||||||
|
elif [ -n "${CSC_NAME:-}" ] && [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then
|
||||||
|
NOTARY_ARGS=(--apple-id "$APPLE_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --team-id "$APPLE_TEAM_ID")
|
||||||
|
ok "模式: CI(环境变量)"
|
||||||
|
else
|
||||||
|
fail "未找到签名配置"
|
||||||
|
echo " 本地: 运行 ${B}pnpm sign:setup${N}"
|
||||||
|
echo " CI: 设置 ${B}CSC_NAME / APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID${N} 环境变量"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "签名身份: $CSC_NAME"
|
||||||
|
ok "Team ID: $APPLE_TEAM_ID"
|
||||||
|
|
||||||
|
# ── 1. 构建 ──────────────────────────────────────────────────────
|
||||||
|
step "[1/5] 构建 Electron 产物"
|
||||||
|
pnpm build
|
||||||
|
ok "构建完成"
|
||||||
|
|
||||||
|
# ── 2. 打包 + 签名 ───────────────────────────────────────────────
|
||||||
|
step "[2/5] 打包 + 签名 (electron-builder)"
|
||||||
|
# 清理旧产物,避免 spctl 把上次未公证的 dmg 认成本次产物
|
||||||
|
rm -f release/*.dmg release/*-mac.zip release/*.blockmap 2>/dev/null || true
|
||||||
|
pnpm exec electron-builder --mac --arm64
|
||||||
|
ok "打包完成"
|
||||||
|
|
||||||
|
DMG=$(ls -t release/*.dmg 2>/dev/null | head -n1 || true)
|
||||||
|
ZIP=$(ls -t release/*-mac.zip 2>/dev/null | head -n1 || true)
|
||||||
|
APP=$(ls -td release/mac*/*.app 2>/dev/null | head -n1 || true)
|
||||||
|
|
||||||
|
[ -z "$DMG" ] && { fail "未找到 .dmg 产物"; exit 1; }
|
||||||
|
ok "DMG: $DMG"
|
||||||
|
[ -n "$ZIP" ] && ok "ZIP: $ZIP"
|
||||||
|
[ -n "$APP" ] && ok "APP: $APP"
|
||||||
|
|
||||||
|
# 验证签名是否真的盖上了
|
||||||
|
if [ -n "$APP" ]; then
|
||||||
|
if ! codesign --verify --deep --strict --verbose=2 "$APP" >/dev/null 2>&1; then
|
||||||
|
fail ".app 签名验证失败,停止公证"
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$APP" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok ".app 签名校验通过"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. 公证 ──────────────────────────────────────────────────────
|
||||||
|
step "[3/5] 提交公证 (notarytool, 通常 1-5 分钟)"
|
||||||
|
echo " 正在提交 DMG..."
|
||||||
|
xcrun notarytool submit "$DMG" \
|
||||||
|
"${NOTARY_ARGS[@]}" \
|
||||||
|
--wait
|
||||||
|
ok "DMG 公证通过"
|
||||||
|
|
||||||
|
if [ -n "$ZIP" ]; then
|
||||||
|
echo " 正在提交 ZIP(用于自动更新)..."
|
||||||
|
xcrun notarytool submit "$ZIP" \
|
||||||
|
--keychain-profile "$NOTARY_KEYCHAIN_PROFILE" \
|
||||||
|
--wait
|
||||||
|
ok "ZIP 公证通过"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. 装订 ticket ───────────────────────────────────────────────
|
||||||
|
step "[4/5] 装订 ticket (stapler)"
|
||||||
|
xcrun stapler staple "$DMG"
|
||||||
|
ok "DMG ticket 已装订(断网也能验证)"
|
||||||
|
# zip 不能 staple,但用户解压后里面的 .app 已经被 staple 过
|
||||||
|
|
||||||
|
# ── 5. 验证 ──────────────────────────────────────────────────────
|
||||||
|
step "[5/5] 最终验证 (Gatekeeper)"
|
||||||
|
if spctl -a -vv -t install "$DMG" 2>&1 | tee /tmp/spctl.log | grep -q "accepted"; then
|
||||||
|
ok "Gatekeeper 验证通过"
|
||||||
|
grep -E "source=|origin=" /tmp/spctl.log | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
fail "Gatekeeper 拒绝了产物"
|
||||||
|
cat /tmp/spctl.log
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 完成 ────────────────────────────────────────────────────────
|
||||||
|
SIZE=$(du -h "$DMG" | awk '{print $1}')
|
||||||
|
echo ""
|
||||||
|
echo "${G}${B}━━━━━━━━━━ 全部完成 ━━━━━━━━━━${N}"
|
||||||
|
echo " 📦 $DMG (${SIZE})"
|
||||||
|
[ -n "$ZIP" ] && echo " 📦 $ZIP"
|
||||||
|
echo ""
|
||||||
|
echo " 这份 DMG 可以直接发给任何 macOS 用户,"
|
||||||
|
echo " 双击安装,首次打开${B}不会${N}弹\"无法验证开发者\"或被 Gatekeeper 拦截。"
|
||||||
Executable
+105
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 一次性签名环境配置:检查证书 + 录入 Apple ID 凭证 + 写入 .env.signing
|
||||||
|
# 用法: pnpm sign:setup
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
KEYCHAIN_PROFILE="GeoRanklySign"
|
||||||
|
ENV_FILE=".env.signing"
|
||||||
|
|
||||||
|
R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[1m'; N=$'\033[0m'
|
||||||
|
ok() { echo " ${G}✓${N} $1"; }
|
||||||
|
fail() { echo " ${R}✗${N} $1"; }
|
||||||
|
warn() { echo " ${Y}!${N} $1"; }
|
||||||
|
hr() { echo ""; echo "${B}── $1 ──${N}"; }
|
||||||
|
|
||||||
|
# ── 1. Xcode CLT ────────────────────────────────────────────────
|
||||||
|
hr "1/3 检查 Xcode Command Line Tools"
|
||||||
|
if xcode-select -p >/dev/null 2>&1; then
|
||||||
|
ok "已安装:$(xcode-select -p)"
|
||||||
|
else
|
||||||
|
fail "未安装"
|
||||||
|
echo " 请在终端运行 ${B}xcode-select --install${N},弹窗装好后再来"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 2. Developer ID 证书 ────────────────────────────────────────
|
||||||
|
hr "2/3 检查 Developer ID Application 证书"
|
||||||
|
IDENTITIES=$(security find-identity -v -p codesigning | grep "Developer ID Application" || true)
|
||||||
|
|
||||||
|
if [ -z "$IDENTITIES" ]; then
|
||||||
|
fail "钥匙串里没找到 Developer ID Application 证书"
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
${B}申请步骤(一次性,约 10 分钟):${N}
|
||||||
|
${Y}1.${N} 浏览器登录 https://developer.apple.com/account/resources/certificates
|
||||||
|
${Y}2.${N} 点 ${B}+${N} 新建证书 → 选 ${B}Developer ID Application${N} → Continue
|
||||||
|
${Y}3.${N} 它要你上传 CSR,按下面方法生成:
|
||||||
|
打开 macOS ${B}钥匙串访问.app${N}
|
||||||
|
菜单栏 → 钥匙串访问 → 证书助理 → ${B}从证书颁发机构请求证书…${N}
|
||||||
|
邮箱填你的 Apple ID,常用名随意,选 ${B}存储到磁盘${N},保存为 .certSigningRequest
|
||||||
|
${Y}4.${N} 把这个 .certSigningRequest 上传,下载得到 ${B}developerID_application.cer${N}
|
||||||
|
${Y}5.${N} 双击 .cer,导入到 ${B}登录${N} 钥匙串
|
||||||
|
${Y}6.${N} 重新运行 ${B}pnpm sign:setup${N}
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 多张证书时取第一张
|
||||||
|
IDENTITY_LINE=$(echo "$IDENTITIES" | head -n1)
|
||||||
|
IDENTITY_NAME=$(echo "$IDENTITY_LINE" | sed -E 's/.*"(Developer ID Application: [^"]+)".*/\1/')
|
||||||
|
# CSC_NAME 必须去掉 "Developer ID Application: " 前缀,否则 electron-builder 会拒绝
|
||||||
|
CSC_NAME_VALUE=$(echo "$IDENTITY_NAME" | sed -E 's/^Developer ID Application: //')
|
||||||
|
TEAM_ID=$(echo "$IDENTITY_NAME" | sed -E 's/.*\(([A-Z0-9]+)\).*/\1/')
|
||||||
|
ok "找到证书:${B}$IDENTITY_NAME${N}"
|
||||||
|
ok "Team ID:${B}$TEAM_ID${N}"
|
||||||
|
|
||||||
|
if [ "$(echo "$IDENTITIES" | wc -l | tr -d ' ')" -gt 1 ]; then
|
||||||
|
warn "钥匙串里有多张 Developer ID 证书,本脚本默认用上面这一张"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. Apple ID 凭证(公证用)─────────────────────────────────────
|
||||||
|
hr "3/3 录入 Apple ID 凭证(用于公证 notarize)"
|
||||||
|
|
||||||
|
NEED_CREDENTIALS=1
|
||||||
|
if security find-generic-password -s "com.apple.gke.notary.tool.saved-creds" -a "$KEYCHAIN_PROFILE" >/dev/null 2>&1; then
|
||||||
|
ok "钥匙串里已有 profile:$KEYCHAIN_PROFILE"
|
||||||
|
read -r -p " 是否重新录入?(y/N): " RECONFIG
|
||||||
|
[[ "${RECONFIG:-n}" =~ ^[Yy]$ ]] || NEED_CREDENTIALS=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$NEED_CREDENTIALS" = "1" ]; then
|
||||||
|
echo ""
|
||||||
|
echo " 需要 ${B}Apple ID 邮箱${N} + ${B}App 专用密码${N}(不是登录密码!)"
|
||||||
|
echo " 生成 App 专用密码: https://appleid.apple.com → 登录与安全 → App 专用密码 → +"
|
||||||
|
echo ""
|
||||||
|
read -r -p " Apple ID 邮箱: " APPLE_ID
|
||||||
|
read -r -s -p " App 专用密码 (xxxx-xxxx-xxxx-xxxx): " APPLE_PWD
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if ! xcrun notarytool store-credentials "$KEYCHAIN_PROFILE" \
|
||||||
|
--apple-id "$APPLE_ID" \
|
||||||
|
--team-id "$TEAM_ID" \
|
||||||
|
--password "$APPLE_PWD" >/dev/null; then
|
||||||
|
fail "凭证录入失败,常见原因:密码格式错(需带短横线),或 Team ID 不匹配"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok "凭证已存入钥匙串(profile: ${KEYCHAIN_PROFILE})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 写入 .env.signing ───────────────────────────────────────────
|
||||||
|
cat > "$ENV_FILE" <<EOF
|
||||||
|
# Auto-generated by scripts/sign-setup.sh — DO NOT COMMIT
|
||||||
|
# 让 electron-builder 自动选用钥匙串里的证书
|
||||||
|
# 注意:CSC_NAME 不能带 "Developer ID Application:" 前缀
|
||||||
|
CSC_NAME="$CSC_NAME_VALUE"
|
||||||
|
APPLE_TEAM_ID="$TEAM_ID"
|
||||||
|
NOTARY_KEYCHAIN_PROFILE="$KEYCHAIN_PROFILE"
|
||||||
|
EOF
|
||||||
|
ok "写入 ${B}apps/desktop-client/$ENV_FILE${N}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "${G}${B}━━━ 配置完成 ━━━${N}"
|
||||||
|
echo " 下一步:运行 ${B}pnpm sign:mac${N} 一键打包 + 签名 + 公证"
|
||||||
@@ -19,5 +19,9 @@
|
|||||||
"electron",
|
"electron",
|
||||||
"esbuild"
|
"esbuild"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@emnapi/core": "1.9.1",
|
||||||
|
"@emnapi/runtime": "1.9.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+8
-4
@@ -6,7 +6,14 @@ settings:
|
|||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.: {}
|
.:
|
||||||
|
devDependencies:
|
||||||
|
'@emnapi/core':
|
||||||
|
specifier: 1.9.1
|
||||||
|
version: 1.9.1
|
||||||
|
'@emnapi/runtime':
|
||||||
|
specifier: 1.9.1
|
||||||
|
version: 1.9.1
|
||||||
|
|
||||||
apps/admin-web:
|
apps/admin-web:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5157,17 +5164,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/wasi-threads': 1.2.0
|
'@emnapi/wasi-threads': 1.2.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@emnapi/runtime@1.9.1':
|
'@emnapi/runtime@1.9.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@emnapi/wasi-threads@1.2.0':
|
'@emnapi/wasi-threads@1.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@emotion/hash@0.9.2': {}
|
'@emotion/hash@0.9.2': {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user