82 lines
4.3 KiB
JavaScript
82 lines
4.3 KiB
JavaScript
// npm run backend:setup —— 用 uv 建 / 同步后端 Python 环境(.venv,Python 3.12)
|
||
//
|
||
// - 有 backend/requirements.lock(uv pip compile 生成、带哈希)时按锁精确同步(会卸掉锁外的包)
|
||
// - 没有锁时按 requirements.txt + requirements-build.txt 安装
|
||
// - 找不到 uv 时用系统 Python 的 pip 把 uv 装进 native/build/uv-bootstrap,不动系统环境
|
||
// - 结束时核对 browser-use 0.13.10 和 PyInstaller 版本
|
||
import { spawn, spawnSync } from 'node:child_process';
|
||
import { existsSync } from 'node:fs';
|
||
import { mkdir } from 'node:fs/promises';
|
||
import { homedir } from 'node:os';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||
const win = process.platform === 'win32';
|
||
const python = path.join(root, '.venv', win ? 'Scripts/python.exe' : 'bin/python');
|
||
const lock = path.join(root, 'backend/requirements.lock');
|
||
const requirements = path.join(root, 'backend/requirements.txt');
|
||
const buildRequirements = path.join(root, 'backend/requirements-build.txt');
|
||
const bootstrap = path.join(root, 'native/build/uv-bootstrap');
|
||
const uvName = win ? 'uv.exe' : 'uv';
|
||
|
||
function run(command, args) {
|
||
return new Promise((resolve, reject) => {
|
||
const child = spawn(command, args, { cwd: root, stdio: 'inherit', windowsHide: true });
|
||
child.once('error', reject);
|
||
child.once('exit', code => (code === 0 ? resolve() : reject(new Error(`${command} ${args.join(' ')} 退出码 ${code}`))));
|
||
});
|
||
}
|
||
|
||
function available(command, args) {
|
||
return spawnSync(command, args, { encoding: 'utf8', windowsHide: true }).status === 0;
|
||
}
|
||
|
||
async function findUv() {
|
||
const override = process.argv.slice(2).find(argument => argument.startsWith('--uv='));
|
||
if (override) {
|
||
const executable = path.resolve(override.slice('--uv='.length));
|
||
if (!available(executable, ['--version'])) throw new Error(`指定的 uv 不可用: ${executable}`);
|
||
return executable;
|
||
}
|
||
const candidates = [
|
||
'uv',
|
||
path.join(homedir(), '.local/bin', uvName),
|
||
path.join(homedir(), '.cargo/bin', uvName),
|
||
...['bin', 'Scripts', 'uv'].map(folder => path.join(bootstrap, folder, uvName)),
|
||
];
|
||
for (const executable of candidates) if (available(executable, ['--version'])) return executable;
|
||
// 只把 uv 装到本项目忽略目录里,不改系统 Python 的 site-packages
|
||
const pythons = win ? [['py', ['-3.12']], ['python', []]] : [['python3.12', []], ['python3', []]];
|
||
const found = pythons.find(([command, args]) => available(command, [...args, '-m', 'pip', '--version']));
|
||
if (!found) throw new Error('没有 uv,也没有带 pip 的 Python 3.12,先装其中一个再跑 backend:setup');
|
||
await mkdir(bootstrap, { recursive: true });
|
||
await run(found[0], [...found[1], '-m', 'pip', '--isolated', '--disable-pip-version-check', 'install',
|
||
'--no-compile', '--no-deps', '--target', bootstrap, 'uv>=0.12']);
|
||
const bundled = ['bin', 'Scripts', 'uv'].map(folder => path.join(bootstrap, folder, uvName)).find(exe => available(exe, ['--version']));
|
||
if (!bundled) throw new Error('uv 引导安装没有产出可执行文件');
|
||
return bundled;
|
||
}
|
||
|
||
try {
|
||
const uv = await findUv();
|
||
console.log(`[backend:setup] uv: ${uv}`);
|
||
if (!existsSync(python)) await run(uv, ['venv', '--python', '3.12', path.join(root, '.venv')]);
|
||
const version = spawnSync(python, ['-c', 'import sys; assert sys.version_info[:2] == (3, 12), sys.version'], { encoding: 'utf8', windowsHide: true });
|
||
if (version.status !== 0) throw new Error(`.venv 必须是 Python 3.12:${version.stderr.trim()}`);
|
||
if (existsSync(lock)) {
|
||
await run(uv, ['pip', 'sync', '--python', python, '--require-hashes', lock]);
|
||
} else {
|
||
console.warn('[backend:setup] 没有 backend/requirements.lock,按 requirements.txt 安装(建议之后运行 npm run backend:lock)');
|
||
await run(uv, ['pip', 'install', '--python', python, '-r', requirements, '-r', buildRequirements]);
|
||
}
|
||
await run(python, ['-c', [
|
||
'import importlib.metadata as m',
|
||
"assert m.version('browser-use') == '0.13.10', m.version('browser-use')",
|
||
"print('[backend:setup] browser-use', m.version('browser-use'), '/ PyInstaller', m.version('pyinstaller'), '/ psycopg', m.version('psycopg'))",
|
||
].join('; ')]);
|
||
} catch (error) {
|
||
console.error(`[backend:setup] 失败:${error.message}`);
|
||
process.exitCode = 1;
|
||
}
|