根因:CI 上没有 Apple Developer 证书,electron-builder 跳过签名, 生成完全未签名的 .app。electron-updater 的 ShipIt 进程在应用更新时 校验 code signature,未签名的 app 被拒绝: "Code signature did not pass validation: 代码未能满足指定的代码要求" 修复: 1. scripts/after-pack.js — 在所有文件修改(better-sqlite3 替换) 完成后,对 macOS .app 执行 ad-hoc 签名(codesign --force --deep -s -)。 Ad-hoc 签名不需要证书,但能通过 ShipIt 的基本校验。 如果将来配置了真实证书,electron-builder 的签名步骤会覆盖 ad-hoc。 2. .github/workflows/build.yml — macOS 构建添加 CSC_IDENTITY_AUTO_DISCOVERY=false,防止 electron-builder 尝试查找不存在的证书时产生不可预期的行为。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
151 lines
6.0 KiB
JavaScript
151 lines
6.0 KiB
JavaScript
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
/**
|
|
* electron-builder afterPack hook.
|
|
*
|
|
* The standard @electron/rebuild step only rebuilds native modules found
|
|
* in the `files` config. Since better-sqlite3 enters the app through
|
|
* extraResources (via .next/standalone/), it gets skipped.
|
|
*
|
|
* This hook:
|
|
* 1. Explicitly rebuilds better-sqlite3 for the target Electron ABI
|
|
* 2. Copies the rebuilt .node into all locations within standalone resources
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execSync } = require('child_process');
|
|
|
|
module.exports = async function afterPack(context) {
|
|
const appOutDir = context.appOutDir;
|
|
const arch = context.arch;
|
|
// electron-builder arch enum: 1=x64, 3=arm64, etc.
|
|
const archName = arch === 3 ? 'arm64' : arch === 1 ? 'x64' : arch === 0 ? 'ia32' : String(arch);
|
|
const platform = context.packager.platform.name; // 'mac', 'windows', 'linux'
|
|
|
|
// Get Electron version from packager config or from installed package
|
|
const electronVersion =
|
|
context.electronVersion ||
|
|
context.packager?.config?.electronVersion ||
|
|
require(path.join(process.cwd(), 'node_modules', 'electron', 'package.json')).version;
|
|
|
|
console.log(`[afterPack] Electron ${electronVersion}, arch=${archName}, platform=${platform}`);
|
|
|
|
// Step 1: Explicitly rebuild better-sqlite3 for the target Electron version
|
|
const projectDir = process.cwd();
|
|
console.log('[afterPack] Rebuilding better-sqlite3 for Electron ABI...');
|
|
|
|
try {
|
|
// Use @electron/rebuild via npx (it's a dependency of electron-builder)
|
|
const rebuildCmd = `npx electron-rebuild -f -o better-sqlite3 -v ${electronVersion} -a ${archName}`;
|
|
console.log(`[afterPack] Running: ${rebuildCmd}`);
|
|
execSync(rebuildCmd, {
|
|
cwd: projectDir,
|
|
stdio: 'inherit',
|
|
timeout: 120000,
|
|
});
|
|
console.log('[afterPack] Rebuild completed successfully');
|
|
} catch (err) {
|
|
console.error('[afterPack] Failed to rebuild better-sqlite3:', err.message);
|
|
// Try alternative: use @electron/rebuild programmatically
|
|
try {
|
|
const { rebuild } = require('@electron/rebuild');
|
|
await rebuild({
|
|
buildPath: projectDir,
|
|
electronVersion: electronVersion,
|
|
arch: archName,
|
|
onlyModules: ['better-sqlite3'],
|
|
force: true,
|
|
});
|
|
console.log('[afterPack] Rebuild via @electron/rebuild API succeeded');
|
|
} catch (err2) {
|
|
console.error('[afterPack] @electron/rebuild API also failed:', err2.message);
|
|
throw new Error('Cannot rebuild better-sqlite3 for Electron ABI');
|
|
}
|
|
}
|
|
|
|
// Step 2: Verify the rebuilt .node file
|
|
const rebuiltSource = path.join(
|
|
projectDir, 'node_modules', 'better-sqlite3', 'build', 'Release', 'better_sqlite3.node'
|
|
);
|
|
|
|
if (!fs.existsSync(rebuiltSource)) {
|
|
throw new Error(`[afterPack] Rebuilt better_sqlite3.node not found at ${rebuiltSource}`);
|
|
}
|
|
|
|
const sourceStats = fs.statSync(rebuiltSource);
|
|
console.log(`[afterPack] Rebuilt .node file: ${rebuiltSource} (${sourceStats.size} bytes, mtime: ${sourceStats.mtime.toISOString()})`);
|
|
|
|
// Step 3: Find and replace all better_sqlite3.node in standalone resources
|
|
// macOS: <appOutDir>/CodePilot.app/Contents/Resources/standalone/...
|
|
// Windows/Linux: <appOutDir>/resources/standalone/...
|
|
const searchRoots = [
|
|
path.join(appOutDir, 'CodePilot.app', 'Contents', 'Resources', 'standalone'),
|
|
path.join(appOutDir, 'Contents', 'Resources', 'standalone'),
|
|
path.join(appOutDir, 'resources', 'standalone'),
|
|
];
|
|
|
|
let replaced = 0;
|
|
|
|
function walkAndReplace(dir) {
|
|
if (!fs.existsSync(dir)) return;
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
walkAndReplace(fullPath);
|
|
} else if (entry.name === 'better_sqlite3.node') {
|
|
const beforeSize = fs.statSync(fullPath).size;
|
|
fs.copyFileSync(rebuiltSource, fullPath);
|
|
const afterSize = fs.statSync(fullPath).size;
|
|
console.log(`[afterPack] Replaced ${fullPath} (${beforeSize} -> ${afterSize} bytes)`);
|
|
replaced++;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const root of searchRoots) {
|
|
walkAndReplace(root);
|
|
}
|
|
|
|
if (replaced > 0) {
|
|
console.log(`[afterPack] Successfully replaced ${replaced} better_sqlite3.node file(s) with Electron ABI build`);
|
|
} else {
|
|
console.warn('[afterPack] WARNING: No better_sqlite3.node files found in standalone resources!');
|
|
for (const root of searchRoots) {
|
|
if (fs.existsSync(root)) {
|
|
console.log(`[afterPack] Contents of ${root}:`, fs.readdirSync(root).slice(0, 20));
|
|
} else {
|
|
console.log(`[afterPack] Path does not exist: ${root}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 4: Ad-hoc code sign on macOS for auto-update compatibility.
|
|
// electron-updater's ShipIt process validates code signatures when applying
|
|
// updates. Without at least an ad-hoc signature, the update fails with:
|
|
// "Code signature did not pass validation: 代码未能满足指定的代码要求"
|
|
// Ad-hoc signing (codesign -s -) creates a valid local signature without
|
|
// requiring an Apple Developer certificate.
|
|
// This runs AFTER all file modifications (better-sqlite3 replacement above)
|
|
// so the signature covers the final app state. If a real certificate is
|
|
// available, electron-builder's signing step will override this.
|
|
if (platform === 'mac') {
|
|
const appName = context.packager.appInfo.productFilename;
|
|
const appPath = path.join(appOutDir, `${appName}.app`);
|
|
|
|
if (fs.existsSync(appPath)) {
|
|
console.log(`[afterPack] Ad-hoc signing ${appPath} for auto-update compatibility...`);
|
|
try {
|
|
execSync(`codesign --force --deep -s - "${appPath}"`, {
|
|
stdio: 'inherit',
|
|
timeout: 120000,
|
|
});
|
|
console.log('[afterPack] Ad-hoc signing completed successfully');
|
|
} catch (err) {
|
|
console.warn('[afterPack] Ad-hoc signing failed (non-fatal):', err.message);
|
|
}
|
|
} else {
|
|
console.warn(`[afterPack] macOS app not found at ${appPath}, skipping ad-hoc signing`);
|
|
}
|
|
}
|
|
};
|