Skip to content

Commit f1f665a

Browse files
committed
feat(python-env): enhance Python environment preparation with uv and conda support
- Added support for creating Python environments using `uv` in development mode and `conda` in bundle mode. - Updated README with detailed instructions on the new `prepare:python` behavior. - Implemented checks for existing environments to ensure compatibility and prevent failures during setup. - Introduced functions to manage `uv` and `conda` environments more effectively, improving the overall setup process.
1 parent 2d5fc72 commit f1f665a

2 files changed

Lines changed: 93 additions & 13 deletions

File tree

desktop/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@ pnpm run prepare:python
8585
- 本项目桌面端的本地 ASR **默认使用 FunASR ONNX(`funasr-onnx + onnxruntime`**,因此 **不需要安装 `torch`**
8686
- Windows 也使用 FunASR ONNX,无需额外安装 faster-whisper。
8787

88+
补充说明(关于 `prepare:python`):
89+
90+
- **默认(开发模式)**:在 macOS 上会优先使用本机的 `uv` 来创建 `python-env` 并安装依赖(更轻量),无 `uv` 时回退到 venv/pip。
91+
- **打包/发布(bundle 模式)**:如你需要在本机构建可搬运的内置 Python 环境,可执行:
92+
93+
```bash
94+
PREPARE_PYTHON_MODE=bundle pnpm run prepare:python
95+
```
96+
8897
### 启动
8998

9099
```bash

desktop/scripts/prepare-python-env.js

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const requirementsPath = path.join(projectRoot, 'requirements.txt');
2424
const isWin = process.platform === 'win32';
2525
const isMac = process.platform === 'darwin';
2626
const desiredPy = process.env.PYTHON_VERSION || '3.10';
27+
const prepareMode = (process.env.PREPARE_PYTHON_MODE || (process.env.CI === 'true' ? 'bundle' : 'dev')).toLowerCase();
28+
// auto: 开发模式优先使用 uv(若可用),bundle 模式优先使用 conda-pack(历史实现)
29+
const prepareTool = (process.env.PREPARE_PYTHON_TOOL || 'auto').toLowerCase();
2730
const candidateCmds = [
2831
process.env.PYTHON, // 用户显式指定
2932
isWin ? `py -${desiredPy}` : null, // Windows 推荐 py 启动器
@@ -74,6 +77,21 @@ function detectPython() {
7477
return null;
7578
}
7679

80+
function isCondaEnvPrefix(prefix) {
81+
// conda 环境会有 conda-meta/history(比单纯判断 python 可执行文件可靠)
82+
return fs.existsSync(path.join(prefix, 'conda-meta', 'history'));
83+
}
84+
85+
function isVenvPrefix(prefix) {
86+
// venv/virtualenv 会有 pyvenv.cfg
87+
return fs.existsSync(path.join(prefix, 'pyvenv.cfg'));
88+
}
89+
90+
function hasUv() {
91+
const res = runCapture('uv --version', { stdio: 'pipe' });
92+
return res.success;
93+
}
94+
7795
function bootstrapMiniforge() {
7896
if (process.platform !== 'darwin') {
7997
throw new Error(
@@ -147,9 +165,16 @@ function ensureCondaEnv(miniforgePython, { forceRebuild = false } = {}) {
147165
fs.rmSync(venvDir, { recursive: true, force: true });
148166
}
149167

168+
// 关键修复:python-env 可能曾用 venv 创建过(没有 conda-meta),这会导致后续 mamba install 直接失败。
150169
if (fs.existsSync(pythonPath)) {
151-
console.log(`[prepare-python-env] env already exists: ${pythonPath}`);
152-
return;
170+
if (isCondaEnvPrefix(venvDir)) {
171+
console.log(`[prepare-python-env] conda env already exists: ${pythonPath}`);
172+
return;
173+
}
174+
console.warn(
175+
`[prepare-python-env] Detected existing python-env but it is NOT a conda env (missing conda-meta). Rebuilding: ${venvDir}`
176+
);
177+
fs.rmSync(venvDir, { recursive: true, force: true });
153178
}
154179

155180
console.log(`[prepare-python-env] creating conda env (Python ${desiredPy}) at ${venvDir}`);
@@ -283,6 +308,31 @@ function packCondaEnv() {
283308
fs.rmSync(tarPath, { force: true });
284309
}
285310

311+
function ensureUvVenv(pythonSpec, { forceRebuild = false, relocatable = false } = {}) {
312+
const useRelocatable = relocatable ? '--relocatable' : '';
313+
if (fs.existsSync(venvDir) && (forceRebuild || !isVenvPrefix(venvDir))) {
314+
console.log(`[prepare-python-env] (uv) clearing existing env: ${venvDir}`);
315+
// uv venv --clear 会清理目标目录;这里也兼容“目录存在但不是 venv”的情况
316+
run(`uv venv "${venvDir}" --clear ${useRelocatable} --python "${pythonSpec}" --managed-python`);
317+
return;
318+
}
319+
if (fs.existsSync(pythonPath) && isVenvPrefix(venvDir)) {
320+
console.log(`[prepare-python-env] (uv) venv already exists: ${pythonPath}`);
321+
return;
322+
}
323+
console.log(`[prepare-python-env] (uv) creating venv at ${venvDir} (python=${pythonSpec}${relocatable ? ', relocatable' : ''})`);
324+
run(`uv venv "${venvDir}" ${useRelocatable} --python "${pythonSpec}" --managed-python`);
325+
}
326+
327+
function installDepsWithUv() {
328+
if (!fs.existsSync(requirementsPath)) {
329+
throw new Error(`requirements.txt not found at ${requirementsPath}`);
330+
}
331+
console.log('[prepare-python-env] (uv) installing requirements via uv pip ...');
332+
// uv 会自己处理 pip/resolve/缓存;这里显式指定目标 python
333+
run(`uv pip install -r "${requirementsPath}" -p "${pythonPath}"`);
334+
}
335+
286336
/**
287337
* 修复 venv 中的 Python 符号链接为实际文件副本
288338
* venv 创建的符号链接是绝对路径,打包后在其他机器上会失效
@@ -339,20 +389,41 @@ function main() {
339389
pythonCmd = bootstrapMiniforge();
340390
}
341391

392+
const toolAutoPreferUv = prepareTool === 'auto' && prepareMode === 'dev' && hasUv();
393+
const useUv = prepareTool === 'uv' || toolAutoPreferUv;
394+
const useBundle = prepareMode === 'bundle';
395+
342396
if (isMac) {
343-
const updatedPy = ensureCondaEnv(pythonCmd, { forceRebuild });
344-
if (updatedPy) {
345-
pythonCmd = updatedPy;
397+
// macOS:
398+
// - dev: 默认用 uv(若可用)创建 venv 并安装依赖,避免 Miniforge+conda-pack 的笨重流程
399+
// - bundle: 默认使用 conda env + conda-pack,确保可搬运(打包发布)
400+
if (useUv) {
401+
// dev 默认不需要 relocatable;bundle 模式下允许用 uv 的 relocatable venv(实验性)
402+
ensureUvVenv(desiredPy, { forceRebuild, relocatable: useBundle });
403+
installDepsWithUv();
404+
fixPythonSymlinks();
405+
} else if (useBundle) {
406+
const updatedPy = ensureCondaEnv(pythonCmd, { forceRebuild });
407+
if (updatedPy) {
408+
pythonCmd = updatedPy;
409+
}
410+
ensureCondaPackInstalled(pythonCmd);
411+
// 以前这里安装 ffmpeg/av;当前默认 ONNX 推理不需要,且容易引入体积/失败点。
412+
// 如确有需求,可通过 PREPARE_INSTALL_MEDIA_DEPS=1 启用。
413+
if (process.env.PREPARE_INSTALL_MEDIA_DEPS === '1') {
414+
installCondaPackages(['ffmpeg', 'av=11.*']);
415+
}
416+
installDeps();
417+
packCondaEnv();
418+
fixPythonSymlinks();
419+
} else {
420+
// mac dev 非 uv:走普通 venv + pip(比 conda-pack 更轻)
421+
ensureVenv(pythonCmd, { forceRebuild });
422+
installDeps();
423+
fixPythonSymlinks();
346424
}
347-
ensureCondaPackInstalled(pythonCmd);
348-
installCondaPackages([
349-
'ffmpeg',
350-
'av=11.*',
351-
]);
352-
installDeps();
353-
packCondaEnv();
354-
fixPythonSymlinks();
355425
} else {
426+
// Windows/Linux:沿用 venv + pip(可选 uv 也可用,但先保持稳定)
356427
ensureVenv(pythonCmd, { forceRebuild });
357428
installDeps();
358429
fixPythonSymlinks();

0 commit comments

Comments
 (0)