Files
zn-ai/electron/utils/uv-setup.ts
DEV_DSW 4c61e93c3e Add unit tests for skill capabilities, skill planner, and UV setup
- Implement tests for random ID generation, ensuring preference for crypto.randomUUID.
- Create tests for runtime context capabilities, validating the injection of enabled skill capabilities.
- Add tests for skill capability parsing, including classification and command example extraction.
- Introduce tests for the skill planner, verifying tool call planning based on user requests and attachment requirements.
- Establish tests for UV setup, ensuring proper handling of Python installation scenarios and environment checks.
2026-04-24 17:02:59 +08:00

230 lines
6.3 KiB
TypeScript

import { execSync, spawn } from 'node:child_process';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { app } from 'electron';
import logManager from '@electron/service/logger';
import { getUvMirrorEnv } from './uv-env';
type UvResolutionSource = 'bundled' | 'path' | 'bundled-fallback' | 'missing';
type UvResolution = {
bin: string;
source: UvResolutionSource;
bundledPath: string;
};
function getBundledUvPath(): string {
const platform = process.platform;
const arch = process.arch;
const target = `${platform}-${arch}`;
const binName = platform === 'win32' ? 'uv.exe' : 'uv';
if (app.isPackaged) {
return join(process.resourcesPath, 'bin', binName);
}
return join(process.cwd(), 'resources', 'bin', target, binName);
}
function findUvInPathSync(): string | null {
try {
const command = process.platform === 'win32' ? 'where.exe uv' : 'which uv';
const output = execSync(command, {
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 5000,
windowsHide: true,
encoding: 'utf8',
});
const resolved = output
.split(/\r?\n/)
.map((line) => line.trim())
.find(Boolean);
return resolved || null;
} catch {
return null;
}
}
function resolveUvBin(): UvResolution {
const bundled = getBundledUvPath();
if (app.isPackaged) {
if (existsSync(bundled)) {
return { bin: bundled, source: 'bundled', bundledPath: bundled };
}
logManager.warn(`Bundled uv binary not found at ${bundled}, falling back to system PATH`);
}
const fromPath = findUvInPathSync();
if (fromPath) {
return { bin: fromPath, source: 'path', bundledPath: bundled };
}
if (existsSync(bundled)) {
return { bin: bundled, source: 'bundled-fallback', bundledPath: bundled };
}
return { bin: bundled, source: 'missing', bundledPath: bundled };
}
function buildMissingUvError(resolution: UvResolution): Error {
return new Error(
`uv is required for managed Python setup but is unavailable.\n` +
` expected bundled binary: ${resolution.bundledPath}\n` +
` platform: ${process.platform}/${process.arch}\n` +
` searched PATH: yes`,
);
}
export async function checkUvInstalled(): Promise<boolean> {
const { bin, source } = resolveUvBin();
if (source === 'bundled' || source === 'bundled-fallback') {
return existsSync(bin);
}
if (source === 'path') {
return Boolean(bin);
}
return false;
}
export async function installUv(): Promise<void> {
const resolution = resolveUvBin();
if (resolution.source === 'missing') {
throw buildMissingUvError(resolution);
}
logManager.info('uv is available and ready to use');
}
export async function isPythonReady(): Promise<boolean> {
const resolution = resolveUvBin();
if (resolution.source === 'missing') {
return false;
}
const uvBin = resolution.bin;
return await new Promise<boolean>((resolve) => {
try {
const child = spawn(uvBin, ['python', 'find', '3.12'], {
windowsHide: true,
});
child.on('close', (code) => resolve(code === 0));
child.on('error', () => resolve(false));
} catch {
resolve(false);
}
});
}
async function runPythonInstall(
uvBin: string,
env: Record<string, string | undefined>,
label: string,
): Promise<void> {
return await new Promise<void>((resolve, reject) => {
const stderrChunks: string[] = [];
const stdoutChunks: string[] = [];
const child = spawn(uvBin, ['python', 'install', '3.12'], {
env,
windowsHide: true,
});
child.stdout?.on('data', (data) => {
const line = data.toString().trim();
if (line) {
stdoutChunks.push(line);
logManager.debug(`[python-setup:${label}] stdout: ${line}`);
}
});
child.stderr?.on('data', (data) => {
const line = data.toString().trim();
if (line) {
stderrChunks.push(line);
logManager.info(`[python-setup:${label}] stderr: ${line}`);
}
});
child.on('close', (code) => {
if (code === 0) {
resolve();
return;
}
const stderr = stderrChunks.join('\n');
const stdout = stdoutChunks.join('\n');
const detail = stderr || stdout || '(no output captured)';
reject(new Error(
`Python installation failed with code ${code} [${label}]\n` +
` uv binary: ${uvBin}\n` +
` platform: ${process.platform}/${process.arch}\n` +
` output: ${detail}`,
));
});
child.on('error', (error) => {
reject(new Error(
`Python installation spawn error [${label}]: ${error.message}\n` +
` uv binary: ${uvBin}\n` +
` platform: ${process.platform}/${process.arch}`,
));
});
});
}
export async function setupManagedPython(): Promise<void> {
const resolution = resolveUvBin();
if (resolution.source === 'missing') {
throw buildMissingUvError(resolution);
}
const { bin: uvBin, source } = resolution;
const uvEnv = await getUvMirrorEnv();
const hasMirror = Object.keys(uvEnv).length > 0;
logManager.info(
`Setting up managed Python 3.12 ` +
`(uv=${uvBin}, source=${source}, arch=${process.arch}, mirror=${hasMirror})`,
);
const baseEnv: Record<string, string | undefined> = { ...process.env };
try {
await runPythonInstall(uvBin, { ...baseEnv, ...uvEnv }, hasMirror ? 'mirror' : 'default');
} catch (firstError) {
logManager.warn('Python install attempt 1 failed:', firstError);
if (!hasMirror) {
throw firstError;
}
logManager.info('Retrying Python install without mirror...');
try {
await runPythonInstall(uvBin, baseEnv, 'no-mirror');
} catch (secondError) {
logManager.error('Python install attempt 2 (no mirror) also failed:', secondError);
throw secondError;
}
}
try {
const findPath = await new Promise<string>((resolve) => {
const child = spawn(uvBin, ['python', 'find', '3.12'], {
env: { ...process.env, ...uvEnv },
windowsHide: true,
});
let output = '';
child.stdout?.on('data', (data) => {
output += data;
});
child.on('close', () => resolve(output.trim()));
child.on('error', () => resolve(''));
});
if (findPath) {
logManager.info(`Managed Python 3.12 installed at: ${findPath}`);
}
} catch (error) {
logManager.warn('Could not determine Python path after install:', error);
}
}