Files
peoplelib/build-portable.js

208 lines
7.6 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 { spawnSync } = require('child_process');
const ROOT = __dirname;
const pkg = require('./package.json');
const PRODUCT = pkg.productName || 'PeopleLib';
const TARGET = `${PRODUCT}-windows-x64`;
const REQUESTED_OUT = process.env.PEOPLELIB_OUT_DIR
? path.resolve(process.env.PEOPLELIB_OUT_DIR)
: path.join(ROOT, 'dist', TARGET);
const OUT = REQUESTED_OUT;
const APP = path.join(OUT, 'resources', 'app');
function rimraf(p) { if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true }); }
function clearOutput(dir) {
if (!fs.existsSync(dir)) return;
const executable = path.join(dir, `${PRODUCT}.exe`);
const probe = executable + '.build-lock-check';
if (fs.existsSync(executable)) {
try {
fs.renameSync(executable, probe);
fs.renameSync(probe, executable);
} catch (error) {
if (!fs.existsSync(executable) && fs.existsSync(probe)) {
try { fs.renameSync(probe, executable); } catch (restoreError) { /* report the original lock error */ }
}
if (error && (error.code === 'EPERM' || error.code === 'EBUSY' || error.code === 'EACCES')) {
throw new Error(`无法清理固定输出目录 ${dir},请先关闭其中正在运行的 ${PRODUCT}.exe 后重试`);
}
throw error;
}
}
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.name === 'data') continue;
const target = path.join(dir, entry.name);
try {
fs.rmSync(target, { recursive: true, force: true });
} catch (error) {
if (error && (error.code === 'EPERM' || error.code === 'EBUSY')) {
throw new Error(`无法清理固定输出目录 ${dir},请先关闭其中正在运行的 ${PRODUCT}.exe 后重试`);
}
throw error;
}
}
}
function ensureElectronRuntime() {
const electronDir = path.join(ROOT, 'node_modules', 'electron');
const distDir = path.join(electronDir, 'dist');
const executable = path.join(distDir, 'electron.exe');
const versionFile = path.join(distDir, 'version');
const expected = String(pkg.devDependencies && pkg.devDependencies.electron || '').replace(/^v/, '');
const installed = fs.existsSync(versionFile)
? fs.readFileSync(versionFile, 'utf8').trim().replace(/^v/, '')
: '';
if (installed === expected && fs.existsSync(executable)) return distDir;
const installer = path.join(electronDir, 'install.js');
if (!fs.existsSync(installer)) throw new Error('缺少 Electron 安装脚本,请先运行 npm ci');
console.log(`准备 Electron ${expected} 运行时...`);
const result = spawnSync(process.execPath, [installer], {
cwd: electronDir,
env: {
...process.env,
ELECTRON_MIRROR: process.env.ELECTRON_MIRROR
|| process.env.npm_config_electron_mirror
|| 'https://npmmirror.com/mirrors/electron/'
},
stdio: 'inherit'
});
if (result.error) throw result.error;
if (result.status !== 0) throw new Error(`Electron 运行时安装失败(退出码 ${result.status}`);
const prepared = fs.existsSync(versionFile)
? fs.readFileSync(versionFile, 'utf8').trim().replace(/^v/, '')
: '';
if (prepared !== expected || !fs.existsSync(executable)) {
throw new Error(`Electron 运行时版本无效(期望 ${expected || '未知'},实际 ${prepared || '缺失'}`);
}
return distDir;
}
function copyDir(src, dst, skip) {
fs.mkdirSync(dst, { recursive: true });
for (const e of fs.readdirSync(src, { withFileTypes: true })) {
if (skip && skip(e)) continue;
const s = path.join(src, e.name);
const d = path.join(dst, e.name);
if (e.isDirectory()) copyDir(s, d, skip);
else fs.copyFileSync(s, d);
}
}
function pruneLocales(dir) {
const keep = new Set(['zh-CN.pak', 'en-US.pak']);
for (const name of fs.readdirSync(dir)) {
if (!keep.has(name)) fs.unlinkSync(path.join(dir, name));
}
}
function skipDevFiles(e) {
const name = e.name.toLowerCase();
if (e.isDirectory()) return false;
if (/\.(d\.ts|d\.ts\.map|ts|tsx|map|flow)$/.test(name)) return true;
if (/^(readme|changelog|history|license|licence|notice|authors|contributing|security)/.test(name)) return true;
if (/\.(md|markdown)$/.test(name)) return true;
return false;
}
function copyUndici(dst) {
const src = path.join(ROOT, 'node_modules', 'undici');
fs.mkdirSync(dst, { recursive: true });
for (const name of ['package.json', 'index.js', 'index-fetch.js', 'LICENSE']) {
const file = path.join(src, name);
if (fs.existsSync(file)) fs.copyFileSync(file, path.join(dst, name));
}
copyDir(path.join(src, 'lib'), path.join(dst, 'lib'), skipDevFiles);
}
function copyFoliate(dst) {
const src = path.join(ROOT, 'node_modules', 'foliate-js');
fs.mkdirSync(path.join(dst, 'vendor'), { recursive: true });
for (const name of ['package.json', 'LICENSE', 'mobi.js']) {
fs.copyFileSync(path.join(src, name), path.join(dst, name));
}
fs.copyFileSync(
path.join(src, 'vendor', 'fflate.js'),
path.join(dst, 'vendor', 'fflate.js')
);
}
async function build() {
const electronDist = ensureElectronRuntime();
console.log('清理固定输出目录(保留 data...');
clearOutput(OUT);
console.log('复制 Electron 运行时...');
copyDir(electronDist, OUT);
console.log('精简语言包...');
pruneLocales(path.join(OUT, 'locales'));
console.log('重命名可执行文件...');
const executable = path.join(OUT, PRODUCT + '.exe');
fs.renameSync(path.join(OUT, 'electron.exe'), executable);
rimraf(path.join(OUT, 'resources', 'default_app.asar'));
console.log(`应用 ${PRODUCT} 图标...`);
const { rcedit } = await import('rcedit');
await rcedit(executable, {
icon: path.join(ROOT, 'icons', 'dist', 'book-ai-dark.ico'),
'file-version': pkg.version,
'product-version': pkg.version,
'version-string': {
ProductName: PRODUCT,
FileDescription: PRODUCT,
InternalName: PRODUCT,
OriginalFilename: `${PRODUCT}.exe`
}
});
console.log('组装 app 源码...');
fs.mkdirSync(APP, { recursive: true });
fs.copyFileSync(path.join(ROOT, 'main.js'), path.join(APP, 'main.js'));
fs.copyFileSync(path.join(ROOT, 'preload.js'), path.join(APP, 'preload.js'));
fs.copyFileSync(
path.join(ROOT, 'manga-online-preload.js'),
path.join(APP, 'manga-online-preload.js')
);
copyDir(path.join(ROOT, 'src'), path.join(APP, 'src'), (e) => e.name.startsWith('_test'));
const iconDir = path.join(APP, 'icons', 'dist');
fs.mkdirSync(iconDir, { recursive: true });
for (const name of ['book-ai-dark.ico', 'book-ai-light.ico']) {
fs.copyFileSync(path.join(ROOT, 'icons', 'dist', name), path.join(iconDir, name));
}
for (const theme of ['dark', 'light']) {
const themeDir = path.join(iconDir, theme);
fs.mkdirSync(themeDir, { recursive: true });
fs.copyFileSync(
path.join(ROOT, 'icons', 'dist', theme, 'icon-32.png'),
path.join(themeDir, 'icon-32.png')
);
}
copyUndici(path.join(APP, 'node_modules', 'undici'));
copyFoliate(path.join(APP, 'node_modules', 'foliate-js'));
fs.writeFileSync(path.join(APP, 'package.json'), JSON.stringify({
name: pkg.name, version: pkg.version, description: pkg.description,
productName: PRODUCT,
main: 'main.js', author: pkg.author, license: pkg.license,
dependencies: {
undici: pkg.dependencies.undici,
'foliate-js': pkg.dependencies['foliate-js']
}
}, null, 2));
console.log('\n构建完成:');
console.log(' 目录:', OUT);
console.log(' 可执行文件:', executable);
}
build().catch((error) => {
console.error('构建失败:', error && error.message ? error.message : error);
process.exitCode = 1;
});