Files
cc-web/scripts/build-single-exe.js

223 lines
6.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.

#!/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);
}