From a9af6c634c44825daac65496ae6b3ee517bea983 Mon Sep 17 00:00:00 2001 From: liangxu Date: Fri, 1 May 2026 16:02:14 +0800 Subject: [PATCH] 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) --- .gitignore | 2 + apps/desktop-client/DESIGN.md | 2 + .../build/entitlements.mac.inherit.plist | 11 ++ .../build/entitlements.mac.plist | 16 +++ apps/desktop-client/electron-builder.yml | 19 ++- apps/desktop-client/package.json | 78 +------------ apps/desktop-client/scripts/ci-import-cert.sh | 63 ++++++++++ apps/desktop-client/scripts/sign-mac.sh | 110 ++++++++++++++++++ apps/desktop-client/scripts/sign-setup.sh | 105 +++++++++++++++++ package.json | 4 + pnpm-lock.yaml | 12 +- 11 files changed, 336 insertions(+), 86 deletions(-) create mode 100644 apps/desktop-client/build/entitlements.mac.inherit.plist create mode 100644 apps/desktop-client/build/entitlements.mac.plist create mode 100755 apps/desktop-client/scripts/ci-import-cert.sh create mode 100755 apps/desktop-client/scripts/sign-mac.sh create mode 100755 apps/desktop-client/scripts/sign-setup.sh diff --git a/.gitignore b/.gitignore index ab96dff..6f6c3aa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ coverage/ dist/ .env .env.local +.env.signing +apps/*/.env.signing apps/*/dist/ apps/*/out/ apps/*/release/ diff --git a/apps/desktop-client/DESIGN.md b/apps/desktop-client/DESIGN.md index 3a70171..b774243 100644 --- a/apps/desktop-client/DESIGN.md +++ b/apps/desktop-client/DESIGN.md @@ -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 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 + +rnzy-benw-ynai-ftbl \ No newline at end of file diff --git a/apps/desktop-client/build/entitlements.mac.inherit.plist b/apps/desktop-client/build/entitlements.mac.inherit.plist new file mode 100644 index 0000000..667e0db --- /dev/null +++ b/apps/desktop-client/build/entitlements.mac.inherit.plist @@ -0,0 +1,11 @@ + + + + + com.apple.security.cs.allow-jit + com.apple.security.cs.allow-unsigned-executable-memory + com.apple.security.cs.allow-dyld-environment-variables + com.apple.security.cs.disable-library-validation + com.apple.security.inherit + + diff --git a/apps/desktop-client/build/entitlements.mac.plist b/apps/desktop-client/build/entitlements.mac.plist new file mode 100644 index 0000000..65aab31 --- /dev/null +++ b/apps/desktop-client/build/entitlements.mac.plist @@ -0,0 +1,16 @@ + + + + + com.apple.security.cs.allow-jit + com.apple.security.cs.allow-unsigned-executable-memory + com.apple.security.cs.allow-dyld-environment-variables + com.apple.security.cs.disable-library-validation + com.apple.security.device.audio-input + com.apple.security.device.camera + com.apple.security.network.client + com.apple.security.network.server + com.apple.security.files.user-selected.read-write + com.apple.security.automation.apple-events + + diff --git a/apps/desktop-client/electron-builder.yml b/apps/desktop-client/electron-builder.yml index d52ec47..709bec2 100644 --- a/apps/desktop-client/electron-builder.yml +++ b/apps/desktop-client/electron-builder.yml @@ -35,18 +35,25 @@ files: - "!**/node_modules/**/*.{c,cc,cpp,h,hpp,cmake,m4,am,sh}" mac: category: public.app-category.productivity - identity: null - notarize: false - hardenedRuntime: false + # identity 不写死:让 electron-builder 自动从 CSC_NAME 环境变量或钥匙串里挑 + # 注意:写 `identity: null` 是"明确不签名",不要这么写 + notarize: false # 我们用 notarytool 自己跑,比 electron-builder 内置更稳 + hardenedRuntime: true gatekeeperAssess: false + entitlements: build/entitlements.mac.plist + entitlementsInherit: build/entitlements.mac.inherit.plist electronLanguages: - en - zh_CN + extendInfo: + NSCameraUsageDescription: 用于视频会议和录屏功能 + NSMicrophoneUsageDescription: 用于音频采集 + NSAppleEventsUsageDescription: 用于自动化控制其他应用 target: - target: dmg - arch: [arm64] - - target: zip - arch: [arm64] + arch: [arm64, universal] + # - target: zip + # arch: [arm64, universal] dmg: format: ULFO contents: diff --git a/apps/desktop-client/package.json b/apps/desktop-client/package.json index deaedfa..6c996db 100644 --- a/apps/desktop-client/package.json +++ b/apps/desktop-client/package.json @@ -14,6 +14,8 @@ "dev": "unset ELECTRON_RUN_AS_NODE && electron-vite dev --inspect=9229", "build": "electron-vite build", "package:mac": "electron-builder --mac --arm64", + "sign:setup": "bash scripts/sign-setup.sh", + "sign:mac": "bash scripts/sign-mac.sh", "test": "vitest run", "typecheck": "vue-tsc --noEmit" }, @@ -46,81 +48,5 @@ "vue": "^3.5.31", "vue-router": "^4.5.1", "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 } } diff --git a/apps/desktop-client/scripts/ci-import-cert.sh b/apps/desktop-client/scripts/ci-import-cert.sh new file mode 100755 index 0000000..10a4105 --- /dev/null +++ b/apps/desktop-client/scripts/ci-import-cert.sh @@ -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 diff --git a/apps/desktop-client/scripts/sign-mac.sh b/apps/desktop-client/scripts/sign-mac.sh new file mode 100755 index 0000000..8ce1474 --- /dev/null +++ b/apps/desktop-client/scripts/sign-mac.sh @@ -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 拦截。" diff --git a/apps/desktop-client/scripts/sign-setup.sh b/apps/desktop-client/scripts/sign-setup.sh new file mode 100755 index 0000000..8d2abe7 --- /dev/null +++ b/apps/desktop-client/scripts/sign-setup.sh @@ -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 </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" <