@@ -24,6 +24,9 @@ const requirementsPath = path.join(projectRoot, 'requirements.txt');
2424const isWin = process . platform === 'win32' ;
2525const isMac = process . platform === 'darwin' ;
2626const 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 ( ) ;
2730const 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+
7795function 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