mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-24 05:26:42 +00:00
Co-authored-by: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com> Co-authored-by: Wendong-Fan <w3ndong.fan@gmail.com>
1231 lines
39 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
});
|
|
}
|