签名 & 公证: - build/entitlements.mac.plist, build/entitlements.mac.inherit.plist — Electron hardened runtime 所需权限(JIT、unsigned memory、disable library validation) - electron-builder.yml — 启用 hardenedRuntime、entitlements、notarize: true - scripts/after-sign.js — 环境变量 + codesign -d 双重检测,有 Developer ID 时跳过 ad-hoc;无证书时 fallback ad-hoc - .github/workflows/build.yml — 映射签名 secrets(MAC_CERT_P12_BASE64 等),验证步骤断言 Developer ID Authority + TeamIdentifier,遍历双架构 .app 原生热更新: - electron/updater.ts — 从 no-op 重写为完整 electron-updater 实现(autoDownload:false, IPC handlers, 启动 10s 延迟检查) - electron/preload.ts — 恢复 updater IPC bridge - src/components/layout/AppShell.tsx — 运行时检测 native updater,useEffect 监听 updater:status 事件,native 优先 + browser fallback 其他: - README_CN.md — 更新自动更新功能描述 - package.json — 版本号升至 0.23.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
184 lines
6.0 KiB
JavaScript
184 lines
6.0 KiB
JavaScript
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
/**
|
|
* electron-builder afterSign hook — code signing for macOS.
|
|
*
|
|
* When a real Developer ID certificate is available (CSC_LINK or CSC_NAME env
|
|
* vars are set), electron-builder handles signing automatically. This hook only
|
|
* runs a strict verification to confirm the signature is intact.
|
|
*
|
|
* When no certificate is available (local dev builds), falls back to ad-hoc
|
|
* signing so that electron-updater's ShipIt process can still validate the
|
|
* code signature.
|
|
*
|
|
* Ad-hoc signing order (inside-out):
|
|
* 1. All native binaries (.node, .dylib, .so)
|
|
* 2. All Frameworks (*.framework)
|
|
* 3. All Helper apps (*.app inside Frameworks/)
|
|
* 4. The main .app bundle
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execSync } = require('child_process');
|
|
|
|
/**
|
|
* Ad-hoc sign a single path. Failures are logged but non-fatal to avoid
|
|
* breaking builds on edge-case binaries (e.g. debug symbols).
|
|
*/
|
|
function codesign(targetPath) {
|
|
try {
|
|
execSync(`codesign --force --sign - "${targetPath}"`, {
|
|
stdio: 'pipe',
|
|
timeout: 30000,
|
|
});
|
|
} catch (err) {
|
|
console.warn(`[afterSign] Failed to sign ${targetPath}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively collect all files matching the given extensions.
|
|
*/
|
|
function collectFiles(dir, extensions) {
|
|
const results = [];
|
|
if (!fs.existsSync(dir)) return results;
|
|
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
// Don't descend into .app or .framework bundles — they are signed as a unit
|
|
if (entry.name.endsWith('.app') || entry.name.endsWith('.framework')) {
|
|
continue;
|
|
}
|
|
results.push(...collectFiles(fullPath, extensions));
|
|
} else if (entry.isFile()) {
|
|
const ext = path.extname(entry.name);
|
|
if (extensions.includes(ext)) {
|
|
results.push(fullPath);
|
|
}
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Collect bundle directories (.app, .framework) at a given depth.
|
|
*/
|
|
function collectBundles(dir, extension) {
|
|
const results = [];
|
|
if (!fs.existsSync(dir)) return results;
|
|
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (entry.isDirectory() && entry.name.endsWith(extension)) {
|
|
results.push(path.join(dir, entry.name));
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
module.exports = async function afterSign(context) {
|
|
const platform = context.packager.platform.name;
|
|
if (platform !== 'mac') return;
|
|
|
|
const appOutDir = context.appOutDir;
|
|
const appName = context.packager.appInfo.productFilename;
|
|
const appPath = path.join(appOutDir, `${appName}.app`);
|
|
|
|
if (!fs.existsSync(appPath)) {
|
|
console.warn(`[afterSign] macOS app not found at ${appPath}, skipping`);
|
|
return;
|
|
}
|
|
|
|
// ── Detect real (non-ad-hoc) code signature ───────────────────────────
|
|
// Check env vars first (CI path), then probe the actual signature on the
|
|
// .app bundle (covers the case where electron-builder auto-discovered a
|
|
// Developer ID certificate from the local Keychain).
|
|
let hasRealSignature = !!(process.env.CSC_LINK || process.env.CSC_NAME);
|
|
|
|
if (!hasRealSignature) {
|
|
try {
|
|
const info = execSync(`codesign -d --verbose=2 "${appPath}" 2>&1`, {
|
|
stdio: 'pipe',
|
|
timeout: 15000,
|
|
encoding: 'utf-8',
|
|
});
|
|
if (/Authority=Developer ID Application/.test(info)) {
|
|
hasRealSignature = true;
|
|
}
|
|
} catch {
|
|
// codesign -d fails if the bundle is unsigned — that's fine
|
|
}
|
|
}
|
|
|
|
if (hasRealSignature) {
|
|
console.log('[afterSign] Real code signing certificate detected (CSC_LINK/CSC_NAME set or Developer ID signature found).');
|
|
console.log('[afterSign] Skipping ad-hoc signing to preserve Developer ID signature.');
|
|
|
|
try {
|
|
execSync(`codesign --verify --deep --strict --verbose=4 "${appPath}"`, {
|
|
stdio: 'pipe',
|
|
timeout: 60000,
|
|
});
|
|
console.log('[afterSign] Developer ID signature verification passed.');
|
|
} catch (err) {
|
|
console.error('[afterSign] WARNING: Developer ID signature verification FAILED:', err.stderr?.toString() || err.message);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// ── No certificate — ad-hoc signing fallback ─────────────────────────
|
|
console.log(`[afterSign] Ad-hoc signing ${appPath} (individual component signing)...`);
|
|
|
|
const contentsPath = path.join(appPath, 'Contents');
|
|
const frameworksPath = path.join(contentsPath, 'Frameworks');
|
|
let signed = 0;
|
|
|
|
// Step 1: Sign all native binaries (.node, .dylib, .so)
|
|
const nativeBinaries = collectFiles(contentsPath, ['.node', '.dylib', '.so']);
|
|
for (const bin of nativeBinaries) {
|
|
codesign(bin);
|
|
signed++;
|
|
}
|
|
if (nativeBinaries.length > 0) {
|
|
console.log(`[afterSign] Signed ${nativeBinaries.length} native binaries (.node/.dylib/.so)`);
|
|
}
|
|
|
|
// Step 2: Sign all Frameworks
|
|
const frameworks = collectBundles(frameworksPath, '.framework');
|
|
for (const fw of frameworks) {
|
|
codesign(fw);
|
|
signed++;
|
|
}
|
|
if (frameworks.length > 0) {
|
|
console.log(`[afterSign] Signed ${frameworks.length} frameworks`);
|
|
}
|
|
|
|
// Step 3: Sign all Helper apps
|
|
const helperApps = collectBundles(frameworksPath, '.app');
|
|
for (const helper of helperApps) {
|
|
codesign(helper);
|
|
signed++;
|
|
}
|
|
if (helperApps.length > 0) {
|
|
console.log(`[afterSign] Signed ${helperApps.length} helper apps`);
|
|
}
|
|
|
|
// Step 4: Sign the main app bundle
|
|
codesign(appPath);
|
|
signed++;
|
|
|
|
console.log(`[afterSign] Ad-hoc signing complete — ${signed} components signed`);
|
|
|
|
// Verify
|
|
try {
|
|
execSync(`codesign --verify --deep --strict "${appPath}"`, {
|
|
stdio: 'pipe',
|
|
timeout: 30000,
|
|
});
|
|
console.log('[afterSign] Signature verification passed (--deep --strict)');
|
|
} catch (err) {
|
|
console.error('[afterSign] WARNING: Signature verification FAILED:', err.stderr?.toString() || err.message);
|
|
}
|
|
};
|