eigent/electron/main/install-deps.ts
Tong Chen ad44e59485
Fix add llvmlite to mac intel (#1205)
Co-authored-by: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com>
Co-authored-by: Wendong-Fan <w3ndong.fan@gmail.com>
2026-02-11 17:56:57 +08:00

1231 lines
39 KiB
TypeScript

// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { spawn } from 'child_process';
import { app, BrowserWindow } from 'electron';
import log from 'electron-log';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
cleanupOldVenvs,
getBackendPath,
getBinaryPath,
getCachePath,
getPrebuiltPythonDir,
getPrebuiltTerminalVenvPath,
getTerminalVenvPath,
getUvEnv,
getVenvPath,
getVenvPythonPath,
isBinaryExists,
runInstallScript,
TERMINAL_BASE_PACKAGES,
} from './utils/process';
import { safeMainWindowSend } from './utils/safeWebContentsSend';
const userData = app.getPath('userData');
const versionFile = path.join(userData, 'version.txt');
export type PromiseReturnType = {
message: string;
success: boolean;
};
interface checkInstallProps {
win: BrowserWindow | null;
forceInstall?: boolean;
}
// Read last run version and install dependencies on update
export const checkAndInstallDepsOnUpdate = async ({
win,
forceInstall = false,
}: checkInstallProps): Promise<PromiseReturnType> => {
const currentVersion = app.getVersion();
let savedVersion = '';
// Check if prebuilt dependencies are available
const hasPrebuiltDeps = (): boolean => {
if (!app.isPackaged) {
return false;
}
const prebuiltBinDir = path.join(process.resourcesPath, 'prebuilt', 'bin');
const prebuiltDir = path.join(process.resourcesPath, 'prebuilt');
const prebuiltVenvDir = path.join(prebuiltDir, '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');
const hasBinaries = fs.existsSync(uvPath) && fs.existsSync(bunPath);
const hasVenv = fs.existsSync(pyvenvCfg);
if (hasBinaries && hasVenv) {
log.info(
'[DEPS INSTALL] Prebuilt dependencies found, skipping installation'
);
return true;
}
return false;
};
const checkInstallOperations = {
getSavedVersion: (): boolean => {
// Check if version file exists
const versionExists = fs.existsSync(versionFile);
if (versionExists) {
log.info('[DEPS INSTALL] start check version', { currentVersion });
savedVersion = fs.readFileSync(versionFile, 'utf-8').trim();
log.info('[DEPS INSTALL] read saved version', { savedVersion });
} else {
log.info('[DEPS INSTALL] version file not exist, will create new file');
}
return versionExists;
},
handleUpdateNotification: (versionExists: boolean) => {
if (win && !win.isDestroyed()) {
win.webContents.send('update-notification', {
type: 'version-update',
currentVersion,
previousVersion: versionExists ? savedVersion : 'none',
reason: !versionExists
? 'version file not exist'
: 'version not match',
});
} else {
log.warn(
'[DEPS INSTALL] Cannot send update notification - window not available'
);
}
},
createVersionFile: () => {
fs.writeFileSync(versionFile, currentVersion);
log.info('[DEPS INSTALL] version file updated', { currentVersion });
},
};
return new Promise(async (resolve, _reject) => {
try {
// If prebuilt dependencies are available, use them and skip main installation
if (hasPrebuiltDeps()) {
log.info(
'[DEPS INSTALL] Using prebuilt dependencies, creating version file'
);
checkInstallOperations.createVersionFile();
// Check if prebuilt terminal venv exists
const prebuiltTerminalVenv = getPrebuiltTerminalVenvPath();
if (prebuiltTerminalVenv) {
log.info(
'[DEPS INSTALL] Using prebuilt terminal venv:',
prebuiltTerminalVenv
);
} else {
// Create terminal base venv if not prebuilt
log.info(
'[DEPS INSTALL] Creating terminal base venv (not prebuilt)...'
);
try {
uv_path = await getBinaryPath('uv');
const terminalResult =
await installTerminalBaseVenv(currentVersion);
if (!terminalResult.success) {
log.warn(
'[DEPS INSTALL] Terminal base venv installation failed, but continuing...',
terminalResult.message
);
} else {
log.info(
'[DEPS INSTALL] Terminal base venv created successfully'
);
}
} catch (error) {
log.warn(
'[DEPS INSTALL] Failed to create terminal base venv:',
error
);
}
}
resolve({ message: 'Using prebuilt dependencies', success: true });
return;
}
// Clean up cache in production environment BEFORE any checks
// This ensures users always get fresh dependencies in production
if (app.isPackaged) {
log.info(
'[CACHE CLEANUP] Production environment detected, cleaning cache before dependency check...'
);
cleanupCacheInProduction();
}
const versionExists: boolean = checkInstallOperations.getSavedVersion();
// Check if command tools are installed
const uvExists = await isBinaryExists('uv');
const bunExists = await isBinaryExists('bun');
const toolsMissing = !uvExists || !bunExists;
// If version file does not exist or version does not match, reinstall dependencies
// Or if command tools are missing, need to install them
if (
forceInstall ||
!versionExists ||
savedVersion !== currentVersion ||
toolsMissing
) {
if (toolsMissing) {
log.info(
'[DEPS INSTALL] Command tools missing, starting installation...',
{
uvExists,
bunExists,
}
);
} else {
log.info(
'[DEPS INSTALL] version changed, prepare to reinstall uv dependencies...',
{
currentVersion,
savedVersion: versionExists ? savedVersion : 'none',
reason: !versionExists
? 'version file not exist'
: 'version not match',
}
);
}
// Notify frontend to update
checkInstallOperations.handleUpdateNotification(versionExists);
// Install dependencies (version.txt will be updated AFTER successful install)
const result = await installDependencies(currentVersion);
if (!result.success) {
log.error(' install dependencies failed');
resolve({
message: `Install dependencies failed, msg ${result.message}`,
success: false,
});
return;
}
// Update version file ONLY after successful installation
checkInstallOperations.createVersionFile();
resolve({
message: 'Dependencies installed successfully after update',
success: true,
});
log.info('[DEPS INSTALL] install dependencies complete');
return;
} else {
log.info(
'[DEPS INSTALL] version not changed and tools installed, skip install dependencies',
{ currentVersion }
);
resolve({
message:
'Version not changed and tools installed, skipped installation',
success: true,
});
return;
}
} catch (error) {
log.error(' check version and install dependencies error:', error);
resolve({ message: `Error checking version: ${error}`, success: false });
return;
}
});
};
/**
* Check if command line tools are installed, install if not
*/
export async function installCommandTool(): Promise<PromiseReturnType> {
try {
const ensureInstalled = async (
toolName: 'uv' | 'bun',
scriptName: string
): Promise<PromiseReturnType> => {
if (await isBinaryExists(toolName)) {
return { message: `${toolName} already installed`, success: true };
}
console.log(`start install ${toolName}`);
try {
await runInstallScript(scriptName);
const installed = await isBinaryExists(toolName);
if (installed) {
safeMainWindowSend('install-dependencies-log', {
type: 'stdout',
data: `${toolName} installed successfully`,
});
return {
message: `${toolName} installed successfully`,
success: true,
};
} else {
const errorMsg = `${toolName} installation failed: binary not found after installation`;
safeMainWindowSend('install-dependencies-complete', {
success: false,
code: 2,
error: errorMsg,
});
return {
message: errorMsg,
success: false,
};
}
} catch (scriptError) {
const errorMsg = `${toolName} installation failed: ${
scriptError instanceof Error
? scriptError.message
: String(scriptError)
}`;
safeMainWindowSend('install-dependencies-complete', {
success: false,
code: 2,
error: errorMsg,
});
return {
message: errorMsg,
success: false,
};
}
};
const uvResult = await ensureInstalled('uv', 'install-uv.js');
if (!uvResult.success) {
return { message: uvResult.message, success: false };
}
const bunResult = await ensureInstalled('bun', 'install-bun.js');
if (!bunResult.success) {
return { message: bunResult.message, success: false };
}
return { message: 'Command tools installed successfully', success: true };
} catch (error) {
const errorMessage = `Command tool installation failed: ${error}`;
log.error(
'[DEPS INSTALL] Exception during command tool installation:',
error
);
safeMainWindowSend('install-dependencies-complete', {
success: false,
code: 2,
error: errorMessage,
});
return { message: errorMessage, success: false };
}
}
let uv_path: string;
const backendPath = getBackendPath();
// Ensure backend directory exists
if (!fs.existsSync(backendPath)) {
log.info(`Creating backend directory: ${backendPath}`);
fs.mkdirSync(backendPath, { recursive: true });
}
const installingLockPath = path.join(backendPath, 'uv_installing.lock');
const installedLockPath = path.join(backendPath, 'uv_installed.lock');
// const proxyArgs = ['--default-index', 'https://pypi.tuna.tsinghua.edu.cn/simple']
const proxyArgs = [
'--default-index',
'https://mirrors.aliyun.com/pypi/simple/',
];
/**
* Get current installation status by checking lock files
* @returns Object with installation status information
*/
export async function getInstallationStatus(): Promise<{
isInstalling: boolean;
hasLockFile: boolean;
installedExists: boolean;
}> {
try {
const installingExists = fs.existsSync(installingLockPath);
const installedExists = fs.existsSync(installedLockPath);
// If installing lock exists, installation is in progress
// If installed lock exists, installation completed previously
return {
isInstalling: installingExists,
hasLockFile: installingExists || installedExists,
installedExists: installedExists,
};
} catch (error) {
console.error(
'[getInstallationStatus] Error checking installation status:',
error
);
return {
isInstalling: false,
hasLockFile: false,
installedExists: false,
};
}
}
class InstallLogs {
private node_process;
private version: string;
constructor(extraArgs: string[], version: string) {
console.log('start install dependencies', extraArgs, 'version:', version);
this.version = version;
this.node_process = spawn(
uv_path,
[
'sync',
'--no-dev',
'--cache-dir',
getCachePath('uv_cache'),
...extraArgs,
],
{
cwd: backendPath,
env: {
...process.env,
...getUvEnv(version),
},
}
);
}
/**Display filtered logs based on severity */
displayFilteredLogs(data: String) {
if (!data) return;
const msg = data.toString().trimEnd();
if (
msg.toLowerCase().includes('error') ||
msg.toLowerCase().includes('traceback')
) {
log.error(`BACKEND: [DEPS INSTALL] ${msg}`);
safeMainWindowSend('install-dependencies-log', {
type: 'stderr',
data: data.toString(),
});
} else {
log.info(`BACKEND: [DEPS INSTALL] ${msg}`);
safeMainWindowSend('install-dependencies-log', {
type: 'stdout',
data: data.toString(),
});
}
}
/**Handle stdout data */
onStdout() {
this.node_process.stdout.on('data', (data: any) => {
this.displayFilteredLogs(data);
});
}
/**Handle stderr data */
onStderr() {
this.node_process.stderr.on('data', (data: any) => {
this.displayFilteredLogs(data);
});
}
/**Handle process close event */
onClose(resolveInner: (code: number | null) => void) {
this.node_process.on('close', resolveInner);
}
/**
* Set installing Lock Path
* Creates uv_installing.lock file to indicate installation in progress
* Creates backend directory if not exists
*/
static setLockPath() {
if (!fs.existsSync(backendPath)) {
fs.mkdirSync(backendPath, { recursive: true });
}
fs.writeFileSync(installingLockPath, '');
}
/**Clean installing Lock Path */
static cleanLockPath() {
if (fs.existsSync(installingLockPath)) {
fs.unlinkSync(installingLockPath);
}
}
}
/**
* Clean up cache directory
* This ensures users get fresh dependencies
* Note: Only call this in production environment (caller should check app.isPackaged)
*/
function cleanupCacheInProduction(): void {
try {
const cacheBaseDir = path.join(os.homedir(), '.eigent', 'cache');
if (!fs.existsSync(cacheBaseDir)) {
log.info(
'[CACHE CLEANUP] Cache directory does not exist, nothing to clean'
);
return;
}
log.info('[CACHE CLEANUP] Cleaning cache directory:', cacheBaseDir);
fs.rmSync(cacheBaseDir, { recursive: true, force: true });
log.info('[CACHE CLEANUP] Cache directory cleaned successfully');
fs.mkdirSync(cacheBaseDir, { recursive: true });
log.info('[CACHE CLEANUP] Empty cache directory recreated');
} catch (error) {
log.error('[CACHE CLEANUP] Failed to clean cache directory:', error);
}
}
const runInstall = (extraArgs: string[], version: string) => {
const installLogs = new InstallLogs(extraArgs, version);
return new Promise<PromiseReturnType>((resolveInner, rejectInner) => {
try {
installLogs.onStdout();
installLogs.onStderr();
installLogs.onClose((code) => {
console.log('install dependencies end', code === 0);
InstallLogs.cleanLockPath();
resolveInner({
message:
code === 0
? 'Installation completed successfully'
: `Installation failed with code ${code}`,
success: code === 0,
});
});
} catch (err) {
log.error('run install failed', err);
// Clean up uv_installing.lock file if installation fails
InstallLogs.cleanLockPath();
rejectInner({ message: `Installation failed: ${err}`, success: false });
}
});
};
/**
* Find Python executable in prebuilt Python directory
* UV stores Python installations in directories like: cpython-3.11.x+.../install/bin/python
*/
function findPrebuiltPythonExecutable(): string | null {
const prebuiltPythonDir = getPrebuiltPythonDir();
if (!prebuiltPythonDir) {
return null;
}
// Look for Python executable in the prebuilt directory
// UV stores Python in subdirectories like: cpython-3.11.x+.../install/bin/python
const possiblePaths: string[] = [];
// First, try common direct paths
possiblePaths.push(
path.join(prebuiltPythonDir, 'install', 'bin', 'python'),
path.join(prebuiltPythonDir, 'install', 'python.exe'),
path.join(prebuiltPythonDir, 'bin', 'python'),
path.join(prebuiltPythonDir, 'python.exe')
);
// Then, search in subdirectories (UV stores Python in versioned directories)
try {
if (fs.existsSync(prebuiltPythonDir)) {
const entries = fs.readdirSync(prebuiltPythonDir, {
withFileTypes: true,
});
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('cpython-')) {
const subDir = path.join(prebuiltPythonDir, entry.name);
possiblePaths.push(
path.join(subDir, 'install', 'bin', 'python'),
path.join(subDir, 'install', 'python.exe'),
path.join(subDir, 'bin', 'python'),
path.join(subDir, 'python.exe')
);
}
}
}
} catch (error) {
log.warn('[DEPS INSTALL] Error searching for prebuilt Python:', error);
}
for (const pythonPath of possiblePaths) {
if (fs.existsSync(pythonPath)) {
log.info(
`[DEPS INSTALL] Found prebuilt Python executable: ${pythonPath}`
);
return pythonPath;
}
}
log.info(
'[DEPS INSTALL] Prebuilt Python directory found but executable not found, will use UV_PYTHON_INSTALL_DIR'
);
return null;
}
/**
* Install terminal base venv with common packages for terminal tasks.
* This is a lightweight venv separate from the backend venv.
*/
async function installTerminalBaseVenv(
version: string
): Promise<PromiseReturnType> {
const terminalVenvPath = getTerminalVenvPath(version);
const pythonPath =
process.platform === 'win32'
? path.join(terminalVenvPath, 'Scripts', 'python.exe')
: path.join(terminalVenvPath, 'bin', 'python');
// Marker file to indicate packages were installed successfully
const installedMarker = path.join(terminalVenvPath, '.packages_installed');
// Check if terminal base venv already exists and packages are installed
if (fs.existsSync(pythonPath) && fs.existsSync(installedMarker)) {
log.info(
'[DEPS INSTALL] Terminal base venv already exists with packages, skipping creation'
);
return { message: 'Terminal base venv already exists', success: true };
}
// If python exists but marker doesn't, packages may not be installed - need to reinstall
const needsPackageInstall =
fs.existsSync(pythonPath) && !fs.existsSync(installedMarker);
if (needsPackageInstall) {
log.info(
'[DEPS INSTALL] Terminal venv exists but packages not installed, installing packages...'
);
} else {
log.info('[DEPS INSTALL] Creating terminal base venv...');
}
safeMainWindowSend('install-dependencies-log', {
type: 'stdout',
data: needsPackageInstall
? 'Installing missing packages in terminal environment...\n'
: 'Creating terminal base environment...\n',
});
try {
// Get UV environment variables (includes prebuilt Python if available)
const uvEnv = getUvEnv(version);
// Create the venv using uv (skip if only need package install)
if (!needsPackageInstall) {
// Try to use prebuilt Python directly if available
const prebuiltPython = findPrebuiltPythonExecutable();
const venvArgs = prebuiltPython
? ['venv', '--python', prebuiltPython, terminalVenvPath]
: ['venv', '--python', '3.11', terminalVenvPath];
await new Promise<void>((resolve, reject) => {
const createVenv = spawn(uv_path, venvArgs, {
env: {
...process.env,
...uvEnv,
},
});
createVenv.stdout.on('data', (data) => {
log.info(`[DEPS INSTALL] terminal venv: ${data}`);
});
createVenv.stderr.on('data', (data) => {
log.info(`[DEPS INSTALL] terminal venv: ${data}`);
});
createVenv.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(
new Error(`Failed to create terminal venv, exit code: ${code}`)
);
}
});
createVenv.on('error', reject);
});
}
// Install base packages
log.info('[DEPS INSTALL] Installing terminal base packages...');
safeMainWindowSend('install-dependencies-log', {
type: 'stdout',
data: `Installing packages: ${TERMINAL_BASE_PACKAGES.join(', ')}...\n`,
});
await new Promise<void>((resolve, reject) => {
const installPkgs = spawn(
uv_path,
['pip', 'install', '--python', pythonPath, ...TERMINAL_BASE_PACKAGES],
{
env: {
...process.env,
...uvEnv,
},
}
);
installPkgs.stdout.on('data', (data) => {
log.info(`[DEPS INSTALL] terminal packages: ${data}`);
safeMainWindowSend('install-dependencies-log', {
type: 'stdout',
data: data.toString(),
});
});
installPkgs.stderr.on('data', (data) => {
log.info(`[DEPS INSTALL] terminal packages: ${data}`);
safeMainWindowSend('install-dependencies-log', {
type: 'stdout',
data: data.toString(),
});
});
installPkgs.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(
new Error(`Failed to install terminal packages, exit code: ${code}`)
);
}
});
installPkgs.on('error', reject);
});
// Create marker file to indicate successful installation
fs.writeFileSync(installedMarker, new Date().toISOString());
log.info('[DEPS INSTALL] Terminal base venv created successfully');
return {
message: 'Terminal base venv created successfully',
success: true,
};
} catch (error) {
log.error('[DEPS INSTALL] Failed to create terminal base venv:', error);
return {
message: `Failed to create terminal base venv: ${error}`,
success: false,
};
}
}
export async function installDependencies(
version: string
): Promise<PromiseReturnType> {
uv_path = await getBinaryPath('uv');
const venvPath = getVenvPath(version);
const handleInstallOperations = {
spawnBabel: (message: 'mirror' | 'main' = 'main') => {
fs.writeFileSync(installedLockPath, '');
log.info('[DEPS INSTALL] Script completed successfully');
console.log(
`Install Dependencies completed ${message} for version ${version}`
);
console.log(`Virtual environment path: ${venvPath}`);
const pythonPath = getVenvPythonPath(venvPath);
spawn(
pythonPath,
['-m', 'babel.messages.frontend', 'compile', '-d', 'lang'],
{
cwd: backendPath,
env: { ...process.env },
}
);
},
notifyInstallDependenciesPage: (): boolean => {
const success = safeMainWindowSend('install-dependencies-start');
if (!success) {
log.warn(
'[DEPS INSTALL] Main window not available, continuing installation without UI updates'
);
}
return success;
},
installHybridBrowserDependencies: async (): Promise<boolean> => {
try {
// Find the hybrid_browser_toolkit ts directory in the virtual environment
// Need to determine the Python version to construct the correct path
let sitePackagesPath: string | null = null;
const libPath = path.join(venvPath, 'lib');
// Try to find the site-packages directory (it varies by Python version)
if (fs.existsSync(libPath)) {
const libContents = fs.readdirSync(libPath);
const pythonDir = libContents.find((name) =>
name.startsWith('python')
);
if (pythonDir) {
sitePackagesPath = path.join(libPath, pythonDir, 'site-packages');
}
}
if (!sitePackagesPath || !fs.existsSync(sitePackagesPath)) {
log.warn(
'[DEPS INSTALL] site-packages directory not found in venv, skipping npm install'
);
return true; // Not an error if the venv structure is different
}
const toolkitPath = path.join(
sitePackagesPath,
'camel',
'toolkits',
'hybrid_browser_toolkit',
'ts'
);
if (!fs.existsSync(toolkitPath)) {
log.warn(
'[DEPS INSTALL] hybrid_browser_toolkit ts directory not found at ' +
toolkitPath +
', skipping npm install'
);
return true; // Not an error if the toolkit isn't installed
}
// Check if npm dependencies are already installed
const npmMarkerPath = path.join(
toolkitPath,
'.npm_dependencies_installed'
);
const nodeModulesPath = path.join(toolkitPath, 'node_modules');
const distPath = path.join(toolkitPath, 'dist');
// Check if marker exists and verify version
if (
fs.existsSync(npmMarkerPath) &&
fs.existsSync(nodeModulesPath) &&
fs.existsSync(distPath)
) {
try {
const markerContent = JSON.parse(
fs.readFileSync(npmMarkerPath, 'utf-8')
);
if (markerContent.version === version) {
log.info(
'[DEPS INSTALL] hybrid_browser_toolkit npm dependencies already installed for current version, skipping...'
);
return true;
} else {
log.info(
'[DEPS INSTALL] npm dependencies installed for different version, will reinstall...'
);
// Clean up old installation
fs.unlinkSync(npmMarkerPath);
}
} catch (error) {
log.warn(
'[DEPS INSTALL] Could not read npm marker file, will reinstall...',
error
);
// If we can't read the marker, assume we need to reinstall
}
}
log.info(
'[DEPS INSTALL] Installing hybrid_browser_toolkit npm dependencies...'
);
safeMainWindowSend('install-dependencies-log', {
type: 'stdout',
data: 'Installing browser toolkit dependencies...\n',
});
// Try to find npm - first try system npm, then try uv run npm
let npmCommand: string[];
const testNpm = spawn('npm', ['--version'], { shell: true });
const npmExists = await new Promise<boolean>((resolve) => {
testNpm.on('close', (code) => resolve(code === 0));
testNpm.on('error', () => resolve(false));
});
if (npmExists) {
// Use system npm directly
npmCommand = ['npm'];
log.info('[DEPS INSTALL] Using system npm for installation');
} else {
// Try uv run npm (might not work if nodejs-wheel isn't properly set up)
// Quote the path to handle spaces in username on Windows
npmCommand = [`"${uv_path}"`, 'run', 'npm'];
log.info('[DEPS INSTALL] Attempting to use uv run npm');
}
// Run npm install
const npmCacheDir = path.join(venvPath, '.npm-cache');
if (!fs.existsSync(npmCacheDir)) {
fs.mkdirSync(npmCacheDir, { recursive: true });
}
const npmInstall = spawn(
npmCommand[0],
[...npmCommand.slice(1), 'install'],
{
cwd: toolkitPath,
env: {
...process.env,
UV_PROJECT_ENVIRONMENT: venvPath,
npm_config_cache: npmCacheDir,
},
shell: true, // Important for Windows
}
);
await new Promise<void>((resolve, reject) => {
if (npmInstall.stdout) {
npmInstall.stdout.on('data', (data) => {
log.info(`[DEPS INSTALL] npm install: ${data}`);
safeMainWindowSend('install-dependencies-log', {
type: 'stdout',
data: data.toString(),
});
});
}
if (npmInstall.stderr) {
npmInstall.stderr.on('data', (data) => {
log.warn(`[DEPS INSTALL] npm install stderr: ${data}`);
safeMainWindowSend('install-dependencies-log', {
type: 'stderr',
data: data.toString(),
});
});
}
npmInstall.on('close', (code) => {
if (code === 0) {
log.info('[DEPS INSTALL] npm install completed successfully');
resolve();
} else {
log.error(`[DEPS INSTALL] npm install failed with code ${code}`);
reject(new Error(`npm install failed with code ${code}`));
}
});
npmInstall.on('error', (err) => {
log.error(`[DEPS INSTALL] npm install process error: ${err}`);
reject(err);
});
});
// Run npm build (use the same npm command as install)
log.info(
'[DEPS INSTALL] Building hybrid_browser_toolkit TypeScript...'
);
safeMainWindowSend('install-dependencies-log', {
type: 'stdout',
data: 'Building browser toolkit TypeScript...\n',
});
const buildArgs =
npmCommand[0] === 'npm'
? ['run', 'build']
: [...npmCommand.slice(1), 'run', 'build'];
const npmBuild = spawn(npmCommand[0], buildArgs, {
cwd: toolkitPath,
env: {
...process.env,
UV_PROJECT_ENVIRONMENT: venvPath,
npm_config_cache: npmCacheDir,
},
shell: true, // Important for Windows
});
await new Promise<void>((resolve, reject) => {
if (npmBuild.stdout) {
npmBuild.stdout.on('data', (data) => {
log.info(`[DEPS INSTALL] npm build: ${data}`);
safeMainWindowSend('install-dependencies-log', {
type: 'stdout',
data: data.toString(),
});
});
}
if (npmBuild.stderr) {
npmBuild.stderr.on('data', (data) => {
// TypeScript build warnings are common, don't treat as errors
log.info(`[DEPS INSTALL] npm build output: ${data}`);
safeMainWindowSend('install-dependencies-log', {
type: 'stdout',
data: data.toString(),
});
});
}
npmBuild.on('close', (code) => {
if (code === 0) {
log.info(
'[DEPS INSTALL] TypeScript build completed successfully'
);
resolve();
} else {
log.error(
`[DEPS INSTALL] TypeScript build failed with code ${code}`
);
reject(new Error(`TypeScript build failed with code ${code}`));
}
});
npmBuild.on('error', (err) => {
log.error(`[DEPS INSTALL] npm build process error: ${err}`);
reject(err);
});
});
// Optionally install Playwright browsers
try {
log.info('[DEPS INSTALL] Installing Playwright browsers...');
const npxCommand =
npmCommand[0] === 'npm' ? ['npx'] : [`"${uv_path}"`, 'run', 'npx'];
const playwrightInstall = spawn(
npxCommand[0],
[...npxCommand.slice(1), 'playwright', 'install'],
{
cwd: toolkitPath,
env: {
...process.env,
UV_PROJECT_ENVIRONMENT: venvPath,
},
shell: true,
}
);
await new Promise<void>((resolve) => {
playwrightInstall.on('close', (code) => {
if (code === 0) {
log.info(
'[DEPS INSTALL] Playwright browsers installed successfully'
);
// Create marker file
const markerPath = path.join(
toolkitPath,
'.playwright_installed'
);
fs.writeFileSync(markerPath, 'installed');
} else {
log.warn(
'[DEPS INSTALL] Playwright installation failed, but continuing anyway'
);
}
resolve();
});
playwrightInstall.on('error', (err) => {
log.warn(
'[DEPS INSTALL] Playwright installation process error:',
err
);
resolve(); // Non-critical, continue
});
});
} catch (error) {
log.warn(
'[DEPS INSTALL] Failed to install Playwright browsers:',
error
);
// Non-critical, continue
}
// Create marker file to indicate npm dependencies are installed
fs.writeFileSync(
npmMarkerPath,
JSON.stringify({
installedAt: new Date().toISOString(),
version: version,
})
);
log.info('[DEPS INSTALL] Created npm dependencies marker file');
log.info(
'[DEPS INSTALL] hybrid_browser_toolkit dependencies installed successfully'
);
return true;
} catch (error) {
log.error(
'[DEPS INSTALL] Failed to install hybrid_browser_toolkit dependencies:',
error
);
// Don't fail the entire installation if this fails
return false;
}
},
};
return new Promise<PromiseReturnType>(async (resolve, _reject) => {
console.log('start install dependencies');
const mainWindowAvailable =
handleInstallOperations.notifyInstallDependenciesPage();
if (!mainWindowAvailable) {
log.info(
'[DEPS INSTALL] Proceeding with installation without UI notifications'
);
}
const isInstalCommandTool = await installCommandTool();
if (!isInstalCommandTool.success) {
log.error(
'[DEPS INSTALL] Command tool installation failed:',
isInstalCommandTool.message
);
safeMainWindowSend('install-dependencies-complete', {
success: false,
code: 2,
error:
isInstalCommandTool.message || 'Command tool installation failed',
});
resolve({ message: 'Command tool installation failed', success: false });
return;
}
// Set Installing Lock Files
InstallLogs.setLockPath();
// Clean up npm dependencies marker when reinstalling Python deps
// This ensures npm deps are reinstalled when Python environment changes
try {
let sitePackagesPath: string | null = null;
const libPath = path.join(venvPath, 'lib');
if (fs.existsSync(libPath)) {
const libContents = fs.readdirSync(libPath);
const pythonDir = libContents.find((name) => name.startsWith('python'));
if (pythonDir) {
sitePackagesPath = path.join(libPath, pythonDir, 'site-packages');
}
}
if (sitePackagesPath) {
const npmMarkerPath = path.join(
sitePackagesPath,
'camel',
'toolkits',
'hybrid_browser_toolkit',
'ts',
'.npm_dependencies_installed'
);
if (fs.existsSync(npmMarkerPath)) {
fs.unlinkSync(npmMarkerPath);
log.info(
'[DEPS INSTALL] Removed npm dependencies marker for fresh installation'
);
}
}
} catch (error) {
log.warn('[DEPS INSTALL] Could not clean npm marker file:', error);
// Non-critical, continue
}
// try default install
const installSuccess = await runInstall([], version);
if (installSuccess.success) {
// Install terminal base venv (lightweight venv for terminal tasks)
log.info('[DEPS INSTALL] Installing terminal base venv...');
const terminalResult = await installTerminalBaseVenv(version);
if (!terminalResult.success) {
log.warn(
'[DEPS INSTALL] Terminal base venv installation failed, but continuing...',
terminalResult.message
);
}
// Install hybrid_browser_toolkit npm dependencies after Python packages are installed
log.info(
'[DEPS INSTALL] Installing hybrid_browser_toolkit dependencies...'
);
await handleInstallOperations.installHybridBrowserDependencies();
handleInstallOperations.spawnBabel();
// Clean up old venvs after successful installation
log.info('[DEPS INSTALL] Cleaning up old virtual environments...');
await cleanupOldVenvs(version);
log.info('[DEPS INSTALL] Old venvs cleanup completed');
resolve({
message: 'Dependencies installed successfully',
success: true,
});
return;
}
// try mirror install
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
let mirrorInstallSuccess: PromiseReturnType = {
message: '',
success: false,
};
mirrorInstallSuccess =
timezone === 'Asia/Shanghai'
? await runInstall(proxyArgs, version)
: await runInstall([], version);
if (mirrorInstallSuccess.success) {
// Install terminal base venv (lightweight venv for terminal tasks)
log.info('[DEPS INSTALL] Installing terminal base venv...');
const terminalResult = await installTerminalBaseVenv(version);
if (!terminalResult.success) {
log.warn(
'[DEPS INSTALL] Terminal base venv installation failed, but continuing...',
terminalResult.message
);
}
// Install hybrid_browser_toolkit npm dependencies after Python packages are installed
log.info(
'[DEPS INSTALL] Installing hybrid_browser_toolkit dependencies...'
);
await handleInstallOperations.installHybridBrowserDependencies();
handleInstallOperations.spawnBabel('mirror');
// Clean up old venvs after successful installation
log.info('[DEPS INSTALL] Cleaning up old virtual environments...');
await cleanupOldVenvs(version);
log.info('[DEPS INSTALL] Old venvs cleanup completed');
resolve({
message: 'Dependencies installed successfully with mirror',
success: true,
});
} else {
log.error('Both default and mirror install failed');
safeMainWindowSend('install-dependencies-complete', {
success: false,
error: 'Both default and mirror install failed',
});
resolve({
message: 'Both default and mirror install failed',
success: false,
});
}
});
}