223 lines
6.6 KiB
JavaScript
223 lines
6.6 KiB
JavaScript
#!/usr/bin/env node
|
||
'use strict';
|
||
|
||
const { spawnSync } = require('child_process');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const DEFAULT_TARGET = 'bun-linux-x64-baseline';
|
||
const DEFAULT_OUTDIR = 'dist-exe';
|
||
const DEFAULT_NAME = 'cc-web';
|
||
|
||
function parseArgs(argv) {
|
||
const options = {
|
||
target: DEFAULT_TARGET,
|
||
outdir: DEFAULT_OUTDIR,
|
||
name: DEFAULT_NAME,
|
||
};
|
||
|
||
for (let index = 0; index < argv.length; index += 1) {
|
||
const arg = argv[index];
|
||
const readValue = (name) => {
|
||
const inlinePrefix = `${name}=`;
|
||
if (arg.startsWith(inlinePrefix)) return arg.slice(inlinePrefix.length);
|
||
if (arg === name && index + 1 < argv.length) {
|
||
index += 1;
|
||
return argv[index];
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const target = readValue('--target');
|
||
if (target !== null) {
|
||
options.target = target;
|
||
continue;
|
||
}
|
||
|
||
const outdir = readValue('--outdir');
|
||
if (outdir !== null) {
|
||
options.outdir = outdir;
|
||
continue;
|
||
}
|
||
|
||
const name = readValue('--name');
|
||
if (name !== null) {
|
||
options.name = name;
|
||
continue;
|
||
}
|
||
|
||
if (arg === '-h' || arg === '--help') {
|
||
options.help = true;
|
||
continue;
|
||
}
|
||
|
||
throw new Error(`未知参数: ${arg}`);
|
||
}
|
||
|
||
return options;
|
||
}
|
||
|
||
function printHelp() {
|
||
console.log(`用法:
|
||
npm run build:single-exe
|
||
node scripts/build-single-exe.js --target bun-linux-x64-baseline --outdir dist-exe --name cc-web
|
||
|
||
说明:
|
||
默认 target 是 ${DEFAULT_TARGET},用于兼容 CentOS 7 这类老 glibc Linux x64 系统。
|
||
该命令只打包 cc-web 服务本体;Claude/Codex CLI 仍在运行时从宿主机 PATH 或 CLAUDE_PATH/CODEX_PATH 调用。
|
||
构建完成后会同时生成 dist-exe/<target>/ 和 dist-exe/cc-web-<target>.tar.gz。`);
|
||
}
|
||
|
||
function isWindowsTarget(target) {
|
||
return /^bun-windows(?:-|$)/.test(String(target || ''));
|
||
}
|
||
|
||
function resolveBinaryName(target, name) {
|
||
return isWindowsTarget(target) && !name.endsWith('.exe') ? `${name}.exe` : name;
|
||
}
|
||
|
||
function copyFileIfExists(source, destination) {
|
||
if (!fs.existsSync(source)) return false;
|
||
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
||
fs.copyFileSync(source, destination);
|
||
return true;
|
||
}
|
||
|
||
function copyDirIfExists(source, destination) {
|
||
if (!fs.existsSync(source)) return false;
|
||
fs.cpSync(source, destination, { recursive: true, force: true });
|
||
return true;
|
||
}
|
||
|
||
function writeRunningGuide(releaseDir, target, binaryName) {
|
||
const runCommand = isWindowsTarget(target) ? binaryName : `./${binaryName}`;
|
||
const content = `# cc-web single executable 运行说明
|
||
|
||
这个目录是 Bun single executable 发布包,面向 CentOS 7 / 老 glibc Linux 运行。
|
||
|
||
## 直接运行
|
||
|
||
\`\`\`bash
|
||
chmod +x ${binaryName}
|
||
PORT=8002 CC_WEB_PASSWORD='请改成强密码' ${runCommand}
|
||
\`\`\`
|
||
|
||
## 调用宿主机 Claude/Codex CLI
|
||
|
||
cc-web 不内置 Claude/Codex CLI。运行时会继续从宿主机 PATH 查找:
|
||
|
||
\`\`\`bash
|
||
export CLAUDE_PATH=/usr/local/bin/claude
|
||
export CODEX_PATH=/usr/local/bin/codex
|
||
${runCommand}
|
||
\`\`\`
|
||
|
||
如果不设置上述变量,默认命令名仍是 \`claude\` 和 \`codex\`。
|
||
|
||
## 目录说明
|
||
|
||
- \`public/\`:前端静态资源
|
||
- \`config/\`:运行时配置,首次启动会写入登录和通知配置
|
||
- \`sessions/\`:会话数据
|
||
- \`logs/\`:运行日志
|
||
|
||
不要把已有生产环境的 \`config/\`、\`sessions/\` 误删或覆盖。
|
||
`;
|
||
fs.writeFileSync(path.join(releaseDir, 'RUNNING.md'), content, 'utf8');
|
||
}
|
||
|
||
function copyRuntimeAssets(projectRoot, releaseDir, target, binaryName) {
|
||
copyDirIfExists(path.join(projectRoot, 'public'), path.join(releaseDir, 'public'));
|
||
|
||
for (const file of ['.env.example', 'README.md', 'README.en.md', 'CHANGELOG.md', 'package.json']) {
|
||
copyFileIfExists(path.join(projectRoot, file), path.join(releaseDir, file));
|
||
}
|
||
|
||
for (const dir of ['.codex/skills', '.codex/prompts', '.agents/skills', '.agents/prompts']) {
|
||
copyDirIfExists(path.join(projectRoot, dir), path.join(releaseDir, dir));
|
||
}
|
||
|
||
for (const dir of ['config', 'sessions', 'logs']) {
|
||
fs.mkdirSync(path.join(releaseDir, dir), { recursive: true });
|
||
}
|
||
|
||
writeRunningGuide(releaseDir, target, binaryName);
|
||
}
|
||
|
||
function runBunBuild(projectRoot, target, outfile) {
|
||
const bunCommand = process.versions.bun ? process.execPath : (process.env.BUN_BIN || 'bun');
|
||
const args = [
|
||
'build',
|
||
'--compile',
|
||
'--no-compile-autoload-dotenv',
|
||
`--target=${target}`,
|
||
`--outfile=${outfile}`,
|
||
path.join(projectRoot, 'server.js'),
|
||
];
|
||
|
||
console.log(`[build:single-exe] ${bunCommand} ${args.join(' ')}`);
|
||
const result = spawnSync(bunCommand, args, {
|
||
cwd: projectRoot,
|
||
env: process.env,
|
||
stdio: 'inherit',
|
||
});
|
||
|
||
if (result.error && result.error.code === 'ENOENT') {
|
||
throw new Error('未找到 bun。请先在构建机安装 Bun,或通过 BUN_BIN 指定 bun 可执行文件。');
|
||
}
|
||
if (result.error) throw result.error;
|
||
if (result.status !== 0) {
|
||
throw new Error(`Bun 编译失败,退出码: ${result.status}`);
|
||
}
|
||
}
|
||
|
||
function createArchive(outdir, target, name) {
|
||
const archivePath = path.join(outdir, `${name}-${target}.tar.gz`);
|
||
fs.rmSync(archivePath, { force: true });
|
||
const result = spawnSync('tar', ['-C', outdir, '-czf', archivePath, target], {
|
||
stdio: 'inherit',
|
||
});
|
||
if (result.error && result.error.code === 'ENOENT') {
|
||
throw new Error('未找到 tar,无法生成发布压缩包。');
|
||
}
|
||
if (result.error) throw result.error;
|
||
if (result.status !== 0) {
|
||
throw new Error(`生成发布压缩包失败,退出码: ${result.status}`);
|
||
}
|
||
return archivePath;
|
||
}
|
||
|
||
function main() {
|
||
const options = parseArgs(process.argv.slice(2));
|
||
if (options.help) {
|
||
printHelp();
|
||
return;
|
||
}
|
||
|
||
const projectRoot = path.resolve(__dirname, '..');
|
||
const target = options.target || DEFAULT_TARGET;
|
||
const binaryName = resolveBinaryName(target, options.name || DEFAULT_NAME);
|
||
const outdir = path.resolve(projectRoot, options.outdir || DEFAULT_OUTDIR);
|
||
const releaseDir = path.join(outdir, target);
|
||
const outfile = path.join(releaseDir, binaryName);
|
||
|
||
fs.rmSync(releaseDir, { recursive: true, force: true });
|
||
fs.mkdirSync(releaseDir, { recursive: true });
|
||
|
||
runBunBuild(projectRoot, target, outfile);
|
||
if (!isWindowsTarget(target)) fs.chmodSync(outfile, 0o755);
|
||
copyRuntimeAssets(projectRoot, releaseDir, target, binaryName);
|
||
const archivePath = createArchive(outdir, target, options.name || DEFAULT_NAME);
|
||
|
||
console.log(`[build:single-exe] 发布目录: ${releaseDir}`);
|
||
console.log(`[build:single-exe] 可执行文件: ${outfile}`);
|
||
console.log(`[build:single-exe] 发布压缩包: ${archivePath}`);
|
||
}
|
||
|
||
try {
|
||
main();
|
||
} catch (err) {
|
||
console.error(`[build:single-exe] ERROR: ${err.message || err}`);
|
||
process.exit(1);
|
||
}
|