Files
CipherTalk/scripts/clean-locales.js
cxuanAI 7393496353 fix(macos): 打包后自动构造 WCDB.framework,修复 dyld 加载失败
libwcdb_api.dylib 编译时硬链接 @rpath/WCDB.framework/Versions/<ver>/WCDB,
但 native 产物把 WCDB 主二进制扁平化为 libWCDB.dylib 放在 resources/macos/,
extraResources 复制后落在 Contents/Resources/resources/macos/。dyld 在运行时
按 framework 路径找不到主二进制,导致 mac 装包后 GUI 解密入口报:

    WCDB 初始化异常: Failed to load shared library
    Library not loaded: @rpath/WCDB.framework/Versions/2.1.15/WCDB
    Reason: tried: '<App>/Contents/Frameworks/WCDB.framework/Versions/2.1.15/WCDB' (no such file)

App 实际无法工作。

新增 scripts/setup-macos-wcdb-framework.js,由 clean-locales.js 在 afterPack
钩子里调用,打包后自动:

1. 从 libWCDB.dylib 的 install_name 解析期望的 framework 版本号(解析失败回退 "A")
2. 在 Contents/Frameworks/WCDB.framework/Versions/<ver>/WCDB 处放置同一个二进制
3. 创建 Versions/Current 与顶层 WCDB 软链
4. 对 framework 内主二进制和 framework 目录做 ad-hoc 重签名

不删除原 libWCDB.dylib,保留作为后向兼容来源。

MACOS_PORT_GUIDE.md 第 10 节补充该机制说明。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 11:35:02 +08:00

128 lines
4.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const fs = require('fs');
const path = require('path');
const { Arch } = require('electron-builder');
const { setupMacosWcdbFramework } = require('./setup-macos-wcdb-framework');
const IMAGE_NATIVE_PREFIX = 'ciphertalk-image-native-';
const IMAGE_NATIVE_SUFFIX = '.node';
function resolveNativePlatform(electronPlatformName) {
if (electronPlatformName === 'darwin') return 'macos';
if (electronPlatformName === 'win32') return 'win32';
if (electronPlatformName === 'linux') return 'linux';
return electronPlatformName;
}
function resolveNativeArch(arch) {
if (typeof arch === 'string') return arch;
if (typeof arch === 'number' && Arch[arch]) return Arch[arch];
return process.arch;
}
function uniqueExistingDirs(candidates) {
return Array.from(new Set(candidates)).filter((targetPath) => fs.existsSync(targetPath));
}
function rewriteNativeManifest(manifestPath, targetKey) {
if (!fs.existsSync(manifestPath)) return;
try {
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const nextActiveBinaries = {};
if (manifest.activeBinaries && manifest.activeBinaries[targetKey]) {
nextActiveBinaries[targetKey] = manifest.activeBinaries[targetKey];
}
manifest.activeBinaries = nextActiveBinaries;
manifest.platforms = Object.keys(nextActiveBinaries);
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
console.log(`已收敛 image native manifest 到当前平台: ${targetKey}`);
} catch (error) {
console.warn(`收敛 image native manifest 失败: ${manifestPath}`, error);
}
}
function pruneImageNativeAddons(context) {
const platformDir = resolveNativePlatform(context.electronPlatformName);
const archDir = resolveNativeArch(context.arch);
const targetFileName = `${IMAGE_NATIVE_PREFIX}${platformDir}-${archDir}${IMAGE_NATIVE_SUFFIX}`;
const targetKey = `${platformDir}-${archDir}`;
const productName = context.packager?.appInfo?.productFilename || 'CipherTalk';
const resourceRoots = uniqueExistingDirs([
path.join(context.appOutDir, 'resources'),
path.join(context.appOutDir, 'Contents', 'Resources'),
path.join(context.appOutDir, `${productName}.app`, 'Contents', 'Resources')
]);
for (const resourceRoot of resourceRoots) {
for (const nativeDir of [
path.join(resourceRoot, 'resources', 'wedecrypt'),
path.join(resourceRoot, 'wedecrypt')
]) {
if (!fs.existsSync(nativeDir)) continue;
const nativeFiles = fs.readdirSync(nativeDir)
.filter((file) => file.startsWith(IMAGE_NATIVE_PREFIX) && file.endsWith(IMAGE_NATIVE_SUFFIX));
if (nativeFiles.length === 0) continue;
if (!nativeFiles.includes(targetFileName)) {
console.warn(`未找到当前平台 image native addon跳过裁剪: ${targetFileName}`);
continue;
}
let deletedCount = 0;
for (const file of nativeFiles) {
if (file === targetFileName) continue;
fs.rmSync(path.join(nativeDir, file), { force: true });
deletedCount++;
}
rewriteNativeManifest(path.join(nativeDir, 'manifest.json'), targetKey);
console.log(`已裁剪 image native addon仅保留 ${targetFileName},删除 ${deletedCount} 个无关文件。`);
}
}
}
exports.default = async function (context) {
// context.appOutDir 是打包后的临时解压目录
const localesDir = path.join(context.appOutDir, 'locales');
if (fs.existsSync(localesDir)) {
console.log('正在清理多余的 Chromium 语言包...');
const files = fs.readdirSync(localesDir);
// 只保留中文(简体/繁体)和英文
const whitelist = [
'zh-CN.pak',
'en-US.pak'
];
let deletedCount = 0;
for (const file of files) {
if (file.endsWith('.pak') && !whitelist.includes(file)) {
fs.unlinkSync(path.join(localesDir, file));
deletedCount++;
}
}
console.log(`已删除 ${deletedCount} 个无关语言包,仅保留中英文。`);
}
pruneImageNativeAddons(context);
if (context.electronPlatformName === 'darwin') {
const productName = context.packager?.appInfo?.productFilename || 'CipherTalk';
const launcherCandidates = [
path.join(context.appOutDir, 'ciphertalk-mcp'),
path.join(context.appOutDir, `${productName}.app`, 'Contents', 'MacOS', 'ciphertalk-mcp')
];
for (const launcherPath of launcherCandidates) {
if (!fs.existsSync(launcherPath)) continue;
fs.chmodSync(launcherPath, 0o755);
console.log(`已确保 macOS MCP 启动器可执行: ${launcherPath}`);
break;
}
setupMacosWcdbFramework(context);
}
};