mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-19 07:59:39 +00:00
chroe: prebuilt
This commit is contained in:
parent
48dbae3f1a
commit
ed9d0bfdb6
7 changed files with 1440 additions and 354 deletions
|
|
@ -6,16 +6,30 @@
|
|||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"files": ["dist-electron", "dist", "resources", "!backend", "!dist/images","!**/__pycache__","!server","!docs"],
|
||||
"files": [
|
||||
"dist-electron",
|
||||
"dist",
|
||||
"resources",
|
||||
"!backend",
|
||||
"!dist/images",
|
||||
"!**/__pycache__",
|
||||
"!server",
|
||||
"!docs"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "backend",
|
||||
"from": "backend",
|
||||
"to": "backend",
|
||||
"filter": ["**/*", "!.venv/**/*"]
|
||||
},
|
||||
{
|
||||
"from": "utils",
|
||||
"to": "utils"
|
||||
},
|
||||
{
|
||||
"from": "resources/prebuilt",
|
||||
"to": "prebuilt",
|
||||
"filter": ["**/*"]
|
||||
}
|
||||
],
|
||||
"protocols": [
|
||||
|
|
@ -70,7 +84,7 @@
|
|||
"deleteAppDataOnUninstall": false,
|
||||
"installerIcon": "build/icon.ico",
|
||||
"uninstallerIcon": "build/icon.ico",
|
||||
"installerHeaderIcon": "build/icon.ico",
|
||||
"installerHeaderIcon": "build/icon.ico",
|
||||
"include": "build/installer.nsh"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1359,6 +1359,36 @@ async function createWindow() {
|
|||
// ==================== CHECK IF INSTALLATION IS NEEDED BEFORE LOADING CONTENT ====================
|
||||
log.info('Pre-checking if dependencies need to be installed...');
|
||||
|
||||
// Check if prebuilt dependencies are available (for packaged app)
|
||||
let hasPrebuiltDeps = false;
|
||||
if (app.isPackaged) {
|
||||
const prebuiltBinDir = path.join(process.resourcesPath, 'prebuilt', 'bin');
|
||||
const prebuiltVenvDir = path.join(
|
||||
process.resourcesPath,
|
||||
'prebuilt',
|
||||
'venv'
|
||||
);
|
||||
const uvPath = path.join(
|
||||
prebuiltBinDir,
|
||||
process.platform === 'win32' ? 'uv.exe' : 'uv'
|
||||
);
|
||||
const bunPath = path.join(
|
||||
prebuiltBinDir,
|
||||
process.platform === 'win32' ? 'bun.exe' : 'bun'
|
||||
);
|
||||
const pyvenvCfg = path.join(prebuiltVenvDir, 'pyvenv.cfg');
|
||||
|
||||
hasPrebuiltDeps =
|
||||
fs.existsSync(uvPath) &&
|
||||
fs.existsSync(bunPath) &&
|
||||
fs.existsSync(pyvenvCfg);
|
||||
if (hasPrebuiltDeps) {
|
||||
log.info(
|
||||
'[PRE-CHECK] Prebuilt dependencies found, skipping installation check'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check version and tools status synchronously
|
||||
const currentVersion = app.getVersion();
|
||||
const versionFile = path.join(app.getPath('userData'), 'version.txt');
|
||||
|
|
@ -1380,13 +1410,15 @@ async function createWindow() {
|
|||
const venvPath = getVenvPath(currentVersion);
|
||||
const venvExists = fs.existsSync(venvPath);
|
||||
|
||||
const needsInstallation =
|
||||
!versionExists ||
|
||||
savedVersion !== currentVersion ||
|
||||
!uvExists ||
|
||||
!bunExists ||
|
||||
!installationCompleted ||
|
||||
!venvExists;
|
||||
// If prebuilt deps are available, skip installation
|
||||
const needsInstallation = hasPrebuiltDeps
|
||||
? false
|
||||
: !versionExists ||
|
||||
savedVersion !== currentVersion ||
|
||||
!uvExists ||
|
||||
!bunExists ||
|
||||
!installationCompleted ||
|
||||
!venvExists;
|
||||
|
||||
log.info('Installation check result:', {
|
||||
needsInstallation,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,146 +1,227 @@
|
|||
import { spawn } from 'child_process'
|
||||
import log from 'electron-log'
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import log from 'electron-log';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { app } from 'electron';
|
||||
|
||||
export function getResourcePath() {
|
||||
return path.join(app.getAppPath(), 'resources')
|
||||
return path.join(app.getAppPath(), 'resources');
|
||||
}
|
||||
|
||||
export function getBackendPath() {
|
||||
if (app.isPackaged) {
|
||||
// after packaging, backend is in extraResources
|
||||
return path.join(process.resourcesPath, 'backend')
|
||||
return path.join(process.resourcesPath, 'backend');
|
||||
} else {
|
||||
// development environment
|
||||
return path.join(app.getAppPath(), 'backend')
|
||||
return path.join(app.getAppPath(), 'backend');
|
||||
}
|
||||
}
|
||||
|
||||
export function runInstallScript(scriptPath: string): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const installScriptPath = path.join(getResourcePath(), 'scripts', scriptPath)
|
||||
log.info(`Running script at: ${installScriptPath}`)
|
||||
const installScriptPath = path.join(
|
||||
getResourcePath(),
|
||||
'scripts',
|
||||
scriptPath
|
||||
);
|
||||
log.info(`Running script at: ${installScriptPath}`);
|
||||
|
||||
const nodeProcess = spawn(process.execPath, [installScriptPath], {
|
||||
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
|
||||
})
|
||||
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
|
||||
});
|
||||
|
||||
let stderrOutput = '';
|
||||
|
||||
nodeProcess.stdout.on('data', (data) => {
|
||||
log.info(`Script output: ${data}`)
|
||||
})
|
||||
log.info(`Script output: ${data}`);
|
||||
});
|
||||
|
||||
nodeProcess.stderr.on('data', (data) => {
|
||||
const errorMsg = data.toString();
|
||||
stderrOutput += errorMsg;
|
||||
log.error(`Script error: ${errorMsg}`)
|
||||
})
|
||||
log.error(`Script error: ${errorMsg}`);
|
||||
});
|
||||
|
||||
nodeProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
log.info('Script completed successfully')
|
||||
resolve(true)
|
||||
log.info('Script completed successfully');
|
||||
resolve(true);
|
||||
} else {
|
||||
log.error(`Script exited with code ${code}`)
|
||||
const errorMessage = stderrOutput.trim() || `Script exited with code ${code}`;
|
||||
reject(new Error(errorMessage))
|
||||
log.error(`Script exited with code ${code}`);
|
||||
const errorMessage =
|
||||
stderrOutput.trim() || `Script exited with code ${code}`;
|
||||
reject(new Error(errorMessage));
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function getBinaryName(name: string): Promise<string> {
|
||||
if (process.platform === 'win32') {
|
||||
return `${name}.exe`
|
||||
return `${name}.exe`;
|
||||
}
|
||||
return name
|
||||
return name;
|
||||
}
|
||||
|
||||
export async function getBinaryPath(name?: string): Promise<string> {
|
||||
const binariesDir = path.join(os.homedir(), '.eigent', 'bin')
|
||||
/**
|
||||
* Get path to prebuilt binary (if available in packaged app)
|
||||
*/
|
||||
export function getPrebuiltBinaryPath(name?: string): string | null {
|
||||
if (!app.isPackaged) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure .eigent/bin directory exists
|
||||
if (!fs.existsSync(binariesDir)) {
|
||||
fs.mkdirSync(binariesDir, { recursive: true })
|
||||
const prebuiltBinDir = path.join(process.resourcesPath, 'prebuilt', 'bin');
|
||||
if (!fs.existsSync(prebuiltBinDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return binariesDir
|
||||
return prebuiltBinDir;
|
||||
}
|
||||
|
||||
const binaryName = await getBinaryName(name)
|
||||
return path.join(binariesDir, binaryName)
|
||||
const binaryName = process.platform === 'win32' ? `${name}.exe` : name;
|
||||
const binaryPath = path.join(prebuiltBinDir, binaryName);
|
||||
return fs.existsSync(binaryPath) ? binaryPath : null;
|
||||
}
|
||||
|
||||
export async function getBinaryPath(name?: string): Promise<string> {
|
||||
// First check for prebuilt binary in packaged app
|
||||
if (app.isPackaged) {
|
||||
const prebuiltPath = getPrebuiltBinaryPath(name);
|
||||
if (prebuiltPath) {
|
||||
log.info(`Using prebuilt binary: ${prebuiltPath}`);
|
||||
return prebuiltPath;
|
||||
}
|
||||
}
|
||||
|
||||
const binariesDir = path.join(os.homedir(), '.eigent', 'bin');
|
||||
|
||||
// Ensure .eigent/bin directory exists
|
||||
if (!fs.existsSync(binariesDir)) {
|
||||
fs.mkdirSync(binariesDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return binariesDir;
|
||||
}
|
||||
|
||||
const binaryName = await getBinaryName(name);
|
||||
return path.join(binariesDir, binaryName);
|
||||
}
|
||||
|
||||
export function getCachePath(folder: string): string {
|
||||
const cacheDir = path.join(os.homedir(), '.eigent', 'cache', folder)
|
||||
// For packaged app, try to use prebuilt cache first
|
||||
if (app.isPackaged) {
|
||||
const prebuiltCachePath = path.join(
|
||||
process.resourcesPath,
|
||||
'prebuilt',
|
||||
'cache',
|
||||
folder
|
||||
);
|
||||
if (fs.existsSync(prebuiltCachePath)) {
|
||||
log.info(`Using prebuilt cache: ${prebuiltCachePath}`);
|
||||
return prebuiltCachePath;
|
||||
}
|
||||
}
|
||||
|
||||
const cacheDir = path.join(os.homedir(), '.eigent', 'cache', folder);
|
||||
|
||||
// Ensure cache directory exists
|
||||
if (!fs.existsSync(cacheDir)) {
|
||||
fs.mkdirSync(cacheDir, { recursive: true })
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
}
|
||||
|
||||
return cacheDir
|
||||
return cacheDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to prebuilt venv (if available in packaged app)
|
||||
*/
|
||||
export function getPrebuiltVenvPath(): string | null {
|
||||
if (!app.isPackaged) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prebuiltVenvPath = path.join(process.resourcesPath, 'prebuilt', 'venv');
|
||||
if (fs.existsSync(prebuiltVenvPath)) {
|
||||
const pyvenvCfg = path.join(prebuiltVenvPath, 'pyvenv.cfg');
|
||||
if (fs.existsSync(pyvenvCfg)) {
|
||||
log.info(`Using prebuilt venv: ${prebuiltVenvPath}`);
|
||||
return prebuiltVenvPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getVenvPath(version: string): string {
|
||||
const venvDir = path.join(os.homedir(), '.eigent', 'venvs', `backend-${version}`)
|
||||
|
||||
// Ensure venvs directory exists (parent of the actual venv)
|
||||
const venvsBaseDir = path.dirname(venvDir)
|
||||
if (!fs.existsSync(venvsBaseDir)) {
|
||||
fs.mkdirSync(venvsBaseDir, { recursive: true })
|
||||
// First check for prebuilt venv in packaged app
|
||||
if (app.isPackaged) {
|
||||
const prebuiltVenv = getPrebuiltVenvPath();
|
||||
if (prebuiltVenv) {
|
||||
return prebuiltVenv;
|
||||
}
|
||||
}
|
||||
|
||||
return venvDir
|
||||
const venvDir = path.join(
|
||||
os.homedir(),
|
||||
'.eigent',
|
||||
'venvs',
|
||||
`backend-${version}`
|
||||
);
|
||||
|
||||
// Ensure venvs directory exists (parent of the actual venv)
|
||||
const venvsBaseDir = path.dirname(venvDir);
|
||||
if (!fs.existsSync(venvsBaseDir)) {
|
||||
fs.mkdirSync(venvsBaseDir, { recursive: true });
|
||||
}
|
||||
|
||||
return venvDir;
|
||||
}
|
||||
|
||||
export function getVenvsBaseDir(): string {
|
||||
return path.join(os.homedir(), '.eigent', 'venvs')
|
||||
return path.join(os.homedir(), '.eigent', 'venvs');
|
||||
}
|
||||
|
||||
export async function cleanupOldVenvs(currentVersion: string): Promise<void> {
|
||||
const venvsBaseDir = getVenvsBaseDir()
|
||||
const venvsBaseDir = getVenvsBaseDir();
|
||||
|
||||
// Check if venvs directory exists
|
||||
if (!fs.existsSync(venvsBaseDir)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(venvsBaseDir, { withFileTypes: true })
|
||||
const entries = fs.readdirSync(venvsBaseDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('backend-')) {
|
||||
const versionMatch = entry.name.match(/^backend-(.+)$/)
|
||||
const versionMatch = entry.name.match(/^backend-(.+)$/);
|
||||
if (versionMatch && versionMatch[1] !== currentVersion) {
|
||||
const oldVenvPath = path.join(venvsBaseDir, entry.name)
|
||||
console.log(`Cleaning up old venv: ${oldVenvPath}`)
|
||||
const oldVenvPath = path.join(venvsBaseDir, entry.name);
|
||||
console.log(`Cleaning up old venv: ${oldVenvPath}`);
|
||||
|
||||
try {
|
||||
// Remove old venv directory recursively
|
||||
fs.rmSync(oldVenvPath, { recursive: true, force: true })
|
||||
console.log(`Successfully removed old venv: ${entry.name}`)
|
||||
fs.rmSync(oldVenvPath, { recursive: true, force: true });
|
||||
console.log(`Successfully removed old venv: ${entry.name}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to remove old venv ${entry.name}:`, err)
|
||||
console.error(`Failed to remove old venv ${entry.name}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error during venv cleanup:', err)
|
||||
console.error('Error during venv cleanup:', err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function isBinaryExists(name: string): Promise<boolean> {
|
||||
const cmd = await getBinaryPath(name)
|
||||
const cmd = await getBinaryPath(name);
|
||||
|
||||
return await fs.existsSync(cmd)
|
||||
return await fs.existsSync(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -155,36 +236,36 @@ export function getUvEnv(version: string): Record<string, string> {
|
|||
UV_TOOL_DIR: getCachePath('uv_tool'),
|
||||
UV_PROJECT_ENVIRONMENT: getVenvPath(version),
|
||||
UV_HTTP_TIMEOUT: '300',
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function killProcessByName(name: string): Promise<void> {
|
||||
const platform = process.platform
|
||||
const platform = process.platform;
|
||||
try {
|
||||
if (platform === 'win32') {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// /F = force, /IM = image name
|
||||
const cmd = spawn('taskkill', ['/F', '/IM', `${name}.exe`])
|
||||
const cmd = spawn('taskkill', ['/F', '/IM', `${name}.exe`]);
|
||||
cmd.on('close', (code) => {
|
||||
// code 0 = success, code 128 = process not found (which is fine)
|
||||
if (code === 0 || code === 128) resolve()
|
||||
else reject(new Error(`taskkill exited with code ${code}`))
|
||||
})
|
||||
cmd.on('error', reject)
|
||||
})
|
||||
if (code === 0 || code === 128) resolve();
|
||||
else reject(new Error(`taskkill exited with code ${code}`));
|
||||
});
|
||||
cmd.on('error', reject);
|
||||
});
|
||||
} else {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const cmd = spawn('pkill', ['-9', name])
|
||||
const cmd = spawn('pkill', ['-9', name]);
|
||||
cmd.on('close', (code) => {
|
||||
// code 0 = success, code 1 = no process found (which is fine)
|
||||
if (code === 0 || code === 1) resolve()
|
||||
else reject(new Error(`pkill exited with code ${code}`))
|
||||
})
|
||||
cmd.on('error', reject)
|
||||
})
|
||||
if (code === 0 || code === 1) resolve();
|
||||
else reject(new Error(`pkill exited with code ${code}`));
|
||||
});
|
||||
cmd.on('error', reject);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore errors, just best effort
|
||||
log.warn(`Failed to kill process ${name}:`, err)
|
||||
log.warn(`Failed to kill process ${name}:`, err);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,11 @@
|
|||
"compile-babel": "cd backend && uv run pybabel compile -d lang",
|
||||
"clean-cache": "rimraf node_modules/.vite",
|
||||
"dev": "npm run clean-cache && vite",
|
||||
"build": "npm run compile-babel && tsc && vite build && electron-builder -- --publish always",
|
||||
"build:mac": "npm run compile-babel && tsc && vite build && electron-builder --mac",
|
||||
"build:win": "npm run compile-babel && tsc && vite build && electron-builder --win",
|
||||
"build:all": "npm run compile-babel && tsc && vite build && electron-builder --mac --win",
|
||||
"preinstall-deps": "node scripts/preinstall-deps.js",
|
||||
"build": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder -- --publish always",
|
||||
"build:mac": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --mac",
|
||||
"build:win": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --win",
|
||||
"build:all": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --mac --win",
|
||||
"preview": "vite preview",
|
||||
"pretest": "vite build --mode=test",
|
||||
"test": "vitest run",
|
||||
|
|
|
|||
|
|
@ -35,23 +35,52 @@ export async function downloadWithRedirects(url, destinationPath) {
|
|||
};
|
||||
|
||||
const request = (url) => {
|
||||
https
|
||||
// Support both http and https
|
||||
const httpModule = url.startsWith('https://') ? https : require('http');
|
||||
|
||||
httpModule
|
||||
.get(url, (response) => {
|
||||
if (response.statusCode == 301 || response.statusCode == 302) {
|
||||
request(response.headers.location)
|
||||
return
|
||||
const statusCode = response.statusCode || 0;
|
||||
|
||||
// Handle redirects (301, 302, 307, 308)
|
||||
if (statusCode >= 301 && statusCode <= 308 && response.headers.location) {
|
||||
const redirectUrl = response.headers.location;
|
||||
console.log(`Following redirect to: ${redirectUrl}`);
|
||||
request(redirectUrl);
|
||||
return;
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
safeReject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`))
|
||||
if (statusCode !== 200) {
|
||||
safeReject(new Error(`Download failed: ${statusCode} ${response.statusMessage || 'Unknown error'}`))
|
||||
return
|
||||
}
|
||||
|
||||
const file = fs.createWriteStream(destinationPath)
|
||||
let downloadedBytes = 0
|
||||
const expectedBytes = parseInt(response.headers['content-length'] || '0')
|
||||
const startTime = Date.now()
|
||||
let lastProgressTime = Date.now()
|
||||
|
||||
if (expectedBytes > 0) {
|
||||
console.log(`Downloading ${(expectedBytes / 1024 / 1024).toFixed(2)} MB...`)
|
||||
} else {
|
||||
console.log('Downloading...')
|
||||
}
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
downloadedBytes += chunk.length
|
||||
|
||||
// Show progress every 1 second
|
||||
const now = Date.now()
|
||||
if (now - lastProgressTime >= 1000) {
|
||||
if (expectedBytes > 0) {
|
||||
const percent = ((downloadedBytes / expectedBytes) * 100).toFixed(1)
|
||||
const speed = downloadedBytes / ((now - startTime) / 1000) / 1024 / 1024
|
||||
console.log(`Progress: ${percent}% (${(downloadedBytes / 1024 / 1024).toFixed(2)} MB) - ${speed.toFixed(2)} MB/s`)
|
||||
} else {
|
||||
console.log(`Downloaded: ${(downloadedBytes / 1024 / 1024).toFixed(2)} MB`)
|
||||
}
|
||||
lastProgressTime = now
|
||||
}
|
||||
})
|
||||
|
||||
response.pipe(file)
|
||||
|
|
|
|||
683
scripts/preinstall-deps.js
Normal file
683
scripts/preinstall-deps.js
Normal file
|
|
@ -0,0 +1,683 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Pre-install dependencies script
|
||||
* This script installs all necessary dependencies before packaging the app
|
||||
* so users don't have to wait for installation on first run
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import https from 'https';
|
||||
import http from 'http';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
|
||||
const BIN_DIR = path.join(projectRoot, 'resources', 'prebuilt', 'bin');
|
||||
const VENV_DIR = path.join(projectRoot, 'resources', 'prebuilt', 'venv');
|
||||
const BACKEND_DIR = path.join(projectRoot, 'backend');
|
||||
|
||||
console.log('🚀 Starting pre-installation of dependencies...');
|
||||
console.log(`📦 Binaries will be installed to: ${BIN_DIR}`);
|
||||
console.log(`🐍 Python venv will be installed to: ${VENV_DIR}`);
|
||||
|
||||
// Ensure directories exist
|
||||
fs.mkdirSync(BIN_DIR, { recursive: true });
|
||||
fs.mkdirSync(VENV_DIR, { recursive: true });
|
||||
|
||||
/**
|
||||
* 检测是否配置了代理
|
||||
*/
|
||||
function hasProxy() {
|
||||
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy;
|
||||
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
|
||||
return !!(httpProxy || httpsProxy);
|
||||
}
|
||||
|
||||
const PROXY_DETECTED = hasProxy();
|
||||
if (PROXY_DETECTED) {
|
||||
console.log('🔍 Proxy detected, will use GitHub official sources for better compatibility');
|
||||
console.log(` HTTP_PROXY: ${process.env.HTTP_PROXY || process.env.http_proxy || 'not set'}`);
|
||||
console.log(` HTTPS_PROXY: ${process.env.HTTPS_PROXY || process.env.https_proxy || 'not set'}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证下载的文件是否是有效的 ZIP 文件
|
||||
*/
|
||||
function isValidZip(filePath) {
|
||||
try {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
return buffer.length > 4 &&
|
||||
buffer[0] === 0x50 &&
|
||||
buffer[1] === 0x4B &&
|
||||
buffer[2] === 0x03 &&
|
||||
buffer[3] === 0x04;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证下载的文件是否是有效的 tar.gz 文件
|
||||
*/
|
||||
function isValidTarGz(filePath) {
|
||||
try {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
return buffer.length > 2 &&
|
||||
buffer[0] === 0x1F &&
|
||||
buffer[1] === 0x8B;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件并验证完整性
|
||||
*/
|
||||
async function downloadFileWithValidation(urlsToTry, dest, validateFn, fileType = 'file') {
|
||||
const maxRetries = 2;
|
||||
|
||||
for (const { url, name } of urlsToTry) {
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
console.log(` Trying ${name} (attempt ${attempt + 1}/${maxRetries})`);
|
||||
console.log(` URL: ${url}`);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
const timeout = 180000; // 3 minutes
|
||||
|
||||
const request = protocol.get(url, {
|
||||
timeout: timeout,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||||
}
|
||||
}, (response) => {
|
||||
// 处理重定向
|
||||
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||
const redirectUrl = response.headers.location;
|
||||
console.log(` Following redirect...`);
|
||||
|
||||
const redirectProtocol = redirectUrl.startsWith('https') ? https : http;
|
||||
const redirectRequest = redirectProtocol.get(redirectUrl, {
|
||||
timeout: timeout,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||||
}
|
||||
}, (redirectResponse) => {
|
||||
if (redirectResponse.statusCode === 200) {
|
||||
const file = fs.createWriteStream(dest);
|
||||
let downloadedSize = 0;
|
||||
const totalSize = parseInt(redirectResponse.headers['content-length'] || '0');
|
||||
|
||||
redirectResponse.on('data', (chunk) => {
|
||||
downloadedSize += chunk.length;
|
||||
if (totalSize > 0) {
|
||||
const progress = ((downloadedSize / totalSize) * 100).toFixed(1);
|
||||
process.stdout.write(`\r Progress: ${progress}% (${(downloadedSize / 1024 / 1024).toFixed(2)}MB / ${(totalSize / 1024 / 1024).toFixed(2)}MB)`);
|
||||
}
|
||||
});
|
||||
|
||||
redirectResponse.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
console.log(''); // 新行
|
||||
resolve();
|
||||
});
|
||||
file.on('error', (err) => {
|
||||
file.close();
|
||||
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
reject(new Error(`HTTP ${redirectResponse.statusCode}`));
|
||||
}
|
||||
});
|
||||
|
||||
redirectRequest.on('error', reject);
|
||||
redirectRequest.on('timeout', () => {
|
||||
redirectRequest.destroy();
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fs.createWriteStream(dest);
|
||||
let downloadedSize = 0;
|
||||
const totalSize = parseInt(response.headers['content-length'] || '0');
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
downloadedSize += chunk.length;
|
||||
if (totalSize > 0) {
|
||||
const progress = ((downloadedSize / totalSize) * 100).toFixed(1);
|
||||
process.stdout.write(`\r Progress: ${progress}% (${(downloadedSize / 1024 / 1024).toFixed(2)}MB / ${(totalSize / 1024 / 1024).toFixed(2)}MB)`);
|
||||
}
|
||||
});
|
||||
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
console.log(''); // 新行
|
||||
resolve();
|
||||
});
|
||||
file.on('error', (err) => {
|
||||
file.close();
|
||||
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
request.on('error', (err) => {
|
||||
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
request.on('timeout', () => {
|
||||
request.destroy();
|
||||
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
});
|
||||
|
||||
// 验证下载的文件
|
||||
if (!fs.existsSync(dest)) {
|
||||
throw new Error('Downloaded file does not exist');
|
||||
}
|
||||
|
||||
const fileSize = fs.statSync(dest).size;
|
||||
console.log(` Downloaded file size: ${(fileSize / 1024 / 1024).toFixed(2)}MB`);
|
||||
|
||||
if (fileSize < 1024) {
|
||||
const content = fs.readFileSync(dest, 'utf-8');
|
||||
console.log(` ⚠️ File too small, content: ${content.substring(0, 200)}`);
|
||||
throw new Error('Downloaded file is too small (likely an error page)');
|
||||
}
|
||||
|
||||
if (!validateFn(dest)) {
|
||||
throw new Error(`Downloaded file is not a valid ${fileType}`);
|
||||
}
|
||||
|
||||
console.log(` ✅ Successfully downloaded and validated from ${name}`);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.log(`\n ⚠️ Failed: ${error.message}`);
|
||||
if (fs.existsSync(dest)) {
|
||||
try {
|
||||
fs.unlinkSync(dest);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to download ${fileType} from all mirrors`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Bun 的下载 URL 列表
|
||||
* 如果检测到代理,优先使用 GitHub 官方;否则使用中国镜像
|
||||
*/
|
||||
function getBunUrls(platform, arch) {
|
||||
const filename = `bun-${platform}-${arch}.zip`;
|
||||
const urls = [];
|
||||
|
||||
if (PROXY_DETECTED) {
|
||||
// 有代理时,直接使用 GitHub 官方(代理通常能访问 GitHub)
|
||||
urls.push({
|
||||
url: `https://github.com/oven-sh/bun/releases/latest/download/${filename}`,
|
||||
name: 'GitHub (官方 via proxy)'
|
||||
});
|
||||
|
||||
// 备选:镜像(可能代理访问镜像反而慢)
|
||||
urls.push({
|
||||
url: `https://mirror.ghproxy.com/https://github.com/oven-sh/bun/releases/latest/download/${filename}`,
|
||||
name: 'ghproxy (备选)'
|
||||
});
|
||||
} else {
|
||||
// 无代理时,使用中国镜像
|
||||
urls.push({
|
||||
url: `https://mirror.ghproxy.com/https://github.com/oven-sh/bun/releases/latest/download/${filename}`,
|
||||
name: 'ghproxy.net (GitHub镜像)'
|
||||
});
|
||||
|
||||
urls.push({
|
||||
url: `https://gh-proxy.com/https://github.com/oven-sh/bun/releases/latest/download/${filename}`,
|
||||
name: 'gh-proxy.com'
|
||||
});
|
||||
|
||||
urls.push({
|
||||
url: `https://github.moeyy.xyz/https://github.com/oven-sh/bun/releases/latest/download/${filename}`,
|
||||
name: 'moeyy.xyz (CDN)'
|
||||
});
|
||||
|
||||
urls.push({
|
||||
url: `https://github.com/oven-sh/bun/releases/latest/download/${filename}`,
|
||||
name: 'GitHub (官方)'
|
||||
});
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 UV 的下载 URL 列表
|
||||
*/
|
||||
function getUvUrls(archStr, platformStr) {
|
||||
const filename = `uv-${archStr}-${platformStr}.tar.gz`;
|
||||
const urls = [];
|
||||
|
||||
if (PROXY_DETECTED) {
|
||||
// 有代理时,优先使用 GitHub 官方
|
||||
urls.push({
|
||||
url: `https://github.com/astral-sh/uv/releases/latest/download/${filename}`,
|
||||
name: 'GitHub (官方 via proxy)'
|
||||
});
|
||||
|
||||
urls.push({
|
||||
url: `https://mirror.ghproxy.com/https://github.com/astral-sh/uv/releases/latest/download/${filename}`,
|
||||
name: 'ghproxy (备选)'
|
||||
});
|
||||
} else {
|
||||
// 无代理时,使用中国镜像
|
||||
urls.push({
|
||||
url: `https://mirror.ghproxy.com/https://github.com/astral-sh/uv/releases/latest/download/${filename}`,
|
||||
name: 'ghproxy.net (GitHub镜像)'
|
||||
});
|
||||
|
||||
urls.push({
|
||||
url: `https://gh-proxy.com/https://github.com/astral-sh/uv/releases/latest/download/${filename}`,
|
||||
name: 'gh-proxy.com'
|
||||
});
|
||||
|
||||
urls.push({
|
||||
url: `https://github.com/astral-sh/uv/releases/latest/download/${filename}`,
|
||||
name: 'GitHub (官方)'
|
||||
});
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install uv binary
|
||||
*/
|
||||
async function installUv() {
|
||||
console.log('\n📥 Installing uv...');
|
||||
const uvPath = path.join(BIN_DIR, process.platform === 'win32' ? 'uv.exe' : 'uv');
|
||||
|
||||
if (fs.existsSync(uvPath)) {
|
||||
console.log('✅ uv already installed');
|
||||
return uvPath;
|
||||
}
|
||||
|
||||
// Check manual path
|
||||
const manualUvPath = process.env.MANUAL_UV_PATH;
|
||||
if (manualUvPath && fs.existsSync(manualUvPath)) {
|
||||
console.log(`📋 Using manually provided uv binary: ${manualUvPath}`);
|
||||
fs.copyFileSync(manualUvPath, uvPath);
|
||||
if (process.platform !== 'win32') {
|
||||
fs.chmodSync(uvPath, '755');
|
||||
}
|
||||
return uvPath;
|
||||
}
|
||||
|
||||
// Try pip first
|
||||
const usePipEnv = process.env.USE_PIP_INSTALL_UV;
|
||||
const shouldTryPip = usePipEnv !== 'false';
|
||||
|
||||
if (shouldTryPip) {
|
||||
console.log('\n🐍 Trying to install uv via pip (fastest for China)...');
|
||||
|
||||
try {
|
||||
const pypiMirror = 'https://pypi.tuna.tsinghua.edu.cn/simple';
|
||||
let pipCommand = null;
|
||||
|
||||
try {
|
||||
execSync('pip3 --version', { stdio: 'ignore' });
|
||||
pipCommand = 'pip3';
|
||||
} catch {
|
||||
try {
|
||||
execSync('pip --version', { stdio: 'ignore' });
|
||||
pipCommand = 'pip';
|
||||
} catch {
|
||||
throw new Error('pip not found');
|
||||
}
|
||||
}
|
||||
|
||||
const isMacOS = process.platform === 'darwin';
|
||||
const pipArgs = isMacOS
|
||||
? `install --user --break-system-packages uv -i ${pypiMirror}`
|
||||
: `install --user uv -i ${pypiMirror}`;
|
||||
|
||||
console.log(` Installing via ${pipCommand}...`);
|
||||
execSync(`${pipCommand} ${pipArgs}`, { stdio: 'inherit' });
|
||||
|
||||
// Find installed uv
|
||||
const possiblePaths = [
|
||||
path.join(os.homedir(), '.local', 'bin', 'uv'),
|
||||
path.join(os.homedir(), 'Library', 'Python', '3.11', 'bin', 'uv'),
|
||||
path.join(os.homedir(), 'Library', 'Python', '3.12', 'bin', 'uv'),
|
||||
path.join(os.homedir(), 'Library', 'Python', '3.13', 'bin', 'uv'),
|
||||
'/usr/local/bin/uv',
|
||||
];
|
||||
|
||||
let foundUvPath = null;
|
||||
try {
|
||||
foundUvPath = execSync('which uv', { encoding: 'utf-8' }).trim();
|
||||
} catch {
|
||||
for (const p of possiblePaths) {
|
||||
if (fs.existsSync(p)) {
|
||||
foundUvPath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundUvPath && fs.existsSync(foundUvPath)) {
|
||||
fs.copyFileSync(foundUvPath, uvPath);
|
||||
if (process.platform !== 'win32') {
|
||||
fs.chmodSync(uvPath, '755');
|
||||
}
|
||||
console.log('✅ uv installed via pip');
|
||||
return uvPath;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ pip install failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Download from mirrors
|
||||
console.log('\n📥 Downloading uv...');
|
||||
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
let platformStr, archStr;
|
||||
|
||||
archStr = arch === 'x64' ? 'x86_64' : arch === 'arm64' ? 'aarch64' : arch;
|
||||
|
||||
if (platform === 'darwin') {
|
||||
platformStr = 'apple-darwin';
|
||||
} else if (platform === 'linux') {
|
||||
platformStr = 'unknown-linux-gnu';
|
||||
} else if (platform === 'win32') {
|
||||
platformStr = 'pc-windows-msvc';
|
||||
archStr = 'x86_64';
|
||||
} else {
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
|
||||
const tempFilename = path.join(BIN_DIR, `uv-download-${Date.now()}.tar.gz`);
|
||||
|
||||
console.log(` Platform: ${platform}-${arch}`);
|
||||
|
||||
const urlsToTry = getUvUrls(archStr, platformStr);
|
||||
await downloadFileWithValidation(urlsToTry, tempFilename, isValidTarGz, 'tar.gz');
|
||||
|
||||
// Extract
|
||||
console.log(' Extracting...');
|
||||
const tar = await import('tar');
|
||||
await tar.extract({ file: tempFilename, cwd: BIN_DIR });
|
||||
|
||||
const extractedUvPath = path.join(BIN_DIR, 'uv');
|
||||
if (fs.existsSync(extractedUvPath)) {
|
||||
fs.renameSync(extractedUvPath, uvPath);
|
||||
if (process.platform !== 'win32') {
|
||||
fs.chmodSync(uvPath, '755');
|
||||
}
|
||||
}
|
||||
|
||||
fs.unlinkSync(tempFilename);
|
||||
console.log('✅ uv installed successfully');
|
||||
return uvPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install bun binary
|
||||
*/
|
||||
async function installBun() {
|
||||
console.log('\n📥 Installing bun...');
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
const bunPath = path.join(BIN_DIR, platform === 'win32' ? 'bun.exe' : 'bun');
|
||||
|
||||
if (fs.existsSync(bunPath)) {
|
||||
console.log('✅ bun already installed');
|
||||
return bunPath;
|
||||
}
|
||||
|
||||
// Check manual path
|
||||
const manualBunPath = process.env.MANUAL_BUN_PATH;
|
||||
if (manualBunPath && fs.existsSync(manualBunPath)) {
|
||||
console.log(`📋 Using manually provided bun binary: ${manualBunPath}`);
|
||||
fs.copyFileSync(manualBunPath, bunPath);
|
||||
if (platform !== 'win32') {
|
||||
fs.chmodSync(bunPath, '755');
|
||||
}
|
||||
return bunPath;
|
||||
}
|
||||
|
||||
// Determine platform and architecture
|
||||
let bunPlatform, bunArch;
|
||||
|
||||
if (platform === 'darwin') {
|
||||
bunPlatform = 'darwin';
|
||||
bunArch = arch === 'arm64' ? 'aarch64' : 'x64';
|
||||
} else if (platform === 'linux') {
|
||||
bunPlatform = 'linux';
|
||||
bunArch = arch === 'arm64' ? 'aarch64' : 'x64';
|
||||
} else if (platform === 'win32') {
|
||||
bunPlatform = 'windows';
|
||||
bunArch = 'x64';
|
||||
} else {
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
|
||||
const tempFilename = path.join(BIN_DIR, `bun-download-${Date.now()}.zip`);
|
||||
|
||||
console.log(` Platform: ${bunPlatform}-${bunArch}`);
|
||||
|
||||
const urlsToTry = getBunUrls(bunPlatform, bunArch);
|
||||
await downloadFileWithValidation(urlsToTry, tempFilename, isValidZip, 'ZIP');
|
||||
|
||||
// Extract
|
||||
console.log(' Extracting...');
|
||||
|
||||
try {
|
||||
const AdmZip = (await import('adm-zip')).default;
|
||||
const zip = new AdmZip(tempFilename);
|
||||
const entries = zip.getEntries();
|
||||
|
||||
for (const entry of entries) {
|
||||
const name = entry.entryName;
|
||||
if (name === 'bun' || name === 'bun.exe' || name.endsWith('/bun') || name.endsWith('/bun.exe')) {
|
||||
zip.extractEntryTo(entry, BIN_DIR, false, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (admZipError) {
|
||||
console.log(' Using system unzip...');
|
||||
if (platform === 'win32') {
|
||||
execSync(`powershell -command "Expand-Archive -Path '${tempFilename}' -DestinationPath '${BIN_DIR}' -Force"`, { stdio: 'inherit' });
|
||||
} else {
|
||||
execSync(`unzip -o "${tempFilename}" -d "${BIN_DIR}"`, { stdio: 'inherit' });
|
||||
}
|
||||
}
|
||||
|
||||
if (platform !== 'win32' && fs.existsSync(bunPath)) {
|
||||
fs.chmodSync(bunPath, '755');
|
||||
}
|
||||
|
||||
fs.unlinkSync(tempFilename);
|
||||
|
||||
if (fs.existsSync(bunPath)) {
|
||||
console.log('✅ bun installed successfully');
|
||||
return bunPath;
|
||||
} else {
|
||||
throw new Error('bun binary not found after extraction');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Python dependencies
|
||||
*/
|
||||
async function installPythonDeps(uvPath) {
|
||||
console.log('\n🐍 Installing Python dependencies...');
|
||||
|
||||
const venvPath = VENV_DIR;
|
||||
const cacheDir = path.join(projectRoot, 'resources', 'prebuilt', 'cache', 'uv_cache');
|
||||
const pythonCacheDir = path.join(projectRoot, 'resources', 'prebuilt', 'cache', 'uv_python');
|
||||
const toolCacheDir = path.join(projectRoot, 'resources', 'prebuilt', 'cache', 'uv_tool');
|
||||
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
fs.mkdirSync(pythonCacheDir, { recursive: true });
|
||||
fs.mkdirSync(toolCacheDir, { recursive: true });
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
UV_PYTHON_INSTALL_DIR: pythonCacheDir,
|
||||
UV_TOOL_DIR: toolCacheDir,
|
||||
UV_PROJECT_ENVIRONMENT: venvPath,
|
||||
UV_HTTP_TIMEOUT: '300',
|
||||
};
|
||||
|
||||
const pyvenvCfg = path.join(venvPath, 'pyvenv.cfg');
|
||||
if (fs.existsSync(pyvenvCfg)) {
|
||||
console.log('✅ Python venv exists, syncing...');
|
||||
} else {
|
||||
console.log('📦 Creating Python venv...');
|
||||
}
|
||||
|
||||
const usePypiMirrorEnv = process.env.USE_PYPI_MIRROR;
|
||||
const shouldUseMirror = usePypiMirrorEnv !== 'false';
|
||||
|
||||
const proxyArgs = shouldUseMirror
|
||||
? ['--default-index', 'https://pypi.tuna.tsinghua.edu.cn/simple/']
|
||||
: [];
|
||||
|
||||
if (shouldUseMirror) {
|
||||
console.log(' Using PyPI mirror: https://pypi.tuna.tsinghua.edu.cn/simple/');
|
||||
}
|
||||
|
||||
execSync(
|
||||
`"${uvPath}" sync --no-dev --cache-dir "${cacheDir}" ${proxyArgs.join(' ')}`,
|
||||
{ cwd: BACKEND_DIR, env: env, stdio: 'inherit' }
|
||||
);
|
||||
|
||||
console.log('✅ Python dependencies installed');
|
||||
|
||||
console.log('📝 Compiling babel...');
|
||||
execSync(`"${uvPath}" run pybabel compile -d lang`, {
|
||||
cwd: BACKEND_DIR,
|
||||
env: env,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
console.log('✅ Babel compiled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Install browser toolkit deps
|
||||
*/
|
||||
async function installBrowserToolkitDeps(uvPath, venvPath) {
|
||||
console.log('\n🌐 Installing browser toolkit...');
|
||||
|
||||
try {
|
||||
const libPath = path.join(venvPath, 'lib');
|
||||
if (!fs.existsSync(libPath)) {
|
||||
console.log('⚠️ Skipping browser toolkit');
|
||||
return;
|
||||
}
|
||||
|
||||
const pythonDir = fs.readdirSync(libPath).find(n => n.startsWith('python'));
|
||||
if (!pythonDir) {
|
||||
console.log('⚠️ Skipping browser toolkit');
|
||||
return;
|
||||
}
|
||||
|
||||
const toolkitPath = path.join(libPath, pythonDir, 'site-packages', 'camel', 'toolkits', 'hybrid_browser_toolkit', 'ts');
|
||||
if (!fs.existsSync(toolkitPath)) {
|
||||
console.log('⚠️ Toolkit not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeModulesPath = path.join(toolkitPath, 'node_modules');
|
||||
const distPath = path.join(toolkitPath, 'dist');
|
||||
if (fs.existsSync(nodeModulesPath) && fs.existsSync(distPath)) {
|
||||
console.log('✅ Browser toolkit already installed');
|
||||
return;
|
||||
}
|
||||
|
||||
const npmCacheDir = path.join(venvPath, '.npm-cache');
|
||||
fs.mkdirSync(npmCacheDir, { recursive: true });
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
UV_PROJECT_ENVIRONMENT: venvPath,
|
||||
npm_config_cache: npmCacheDir,
|
||||
};
|
||||
|
||||
let npmCommand = 'npm';
|
||||
try {
|
||||
execSync('npm --version', { stdio: 'ignore' });
|
||||
} catch {
|
||||
npmCommand = `"${uvPath}" run npm`;
|
||||
}
|
||||
|
||||
console.log('📦 Installing npm deps...');
|
||||
execSync(`${npmCommand} install`, { cwd: toolkitPath, env: env, stdio: 'inherit' });
|
||||
|
||||
console.log('🔨 Building TS...');
|
||||
execSync(`${npmCommand} run build`, { cwd: toolkitPath, env: env, stdio: 'inherit' });
|
||||
|
||||
console.log('🎭 Installing Playwright...');
|
||||
try {
|
||||
const npxCommand = npmCommand === 'npm' ? 'npx' : `"${uvPath}" run npx`;
|
||||
execSync(`${npxCommand} playwright install`, {
|
||||
cwd: toolkitPath,
|
||||
env: env,
|
||||
stdio: 'inherit',
|
||||
timeout: 600000
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('⚠️ Playwright install failed (non-critical)');
|
||||
}
|
||||
|
||||
console.log('✅ Browser toolkit installed');
|
||||
} catch (error) {
|
||||
console.error('❌ Browser toolkit failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
async function main() {
|
||||
try {
|
||||
const uvPath = await installUv();
|
||||
await installBun();
|
||||
await installPythonDeps(uvPath);
|
||||
await installBrowserToolkitDeps(uvPath, VENV_DIR);
|
||||
|
||||
console.log('\n✅ All dependencies installed!');
|
||||
console.log(`📦 Binaries: ${BIN_DIR}`);
|
||||
console.log(`🐍 Python venv: ${VENV_DIR}`);
|
||||
} catch (error) {
|
||||
console.error('\n❌ Failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Loading…
Add table
Add a link
Reference in a new issue