diff --git a/de_shell/js/main/pythonEnv.test.ts b/de_shell/js/main/pythonEnv.test.ts index a531f28..3c71f7a 100644 --- a/de_shell/js/main/pythonEnv.test.ts +++ b/de_shell/js/main/pythonEnv.test.ts @@ -25,10 +25,12 @@ import { dirname, join } from 'path' import { tmpdir } from 'os' import { createHash } from 'crypto' import { configureShell } from './config.ts' +import { parseUvLine } from './envProgress.ts' import { findUv, resolvePythonEnv, hasNvidiaGpu, readWheelhouse, findWheelhouse, wheelhouseCandidates, lockStamp, buildSyncArgs, offlinePlan, effectiveExtras, gpuEnvOverride, platformMatches, fileUrl, checkWheelhouseExtras, venvPython, clearBrokenEnv, + withoutPythonDownloadLines, } from './pythonEnv.ts' configureShell({ @@ -284,6 +286,44 @@ test('offlinePlan builds the python, venv, export, pip-install sequence', () => assert.equal(plan.install.at(-1), venvPython(join('/ud', 'python-env'))) }) +/** uv 0.13.4's output for the wheelhouse's interpreter step, piped (de-shell #31). */ +const UV_PYTHON_FROM_MIRROR = + 'Downloading cpython-3.13.12-windows-x86_64-none (download) (20.7MiB)\n' + + ' Downloaded cpython-3.13.12-windows-x86_64-none (download)\n' + + 'Installed Python 3.13.12 in 1.22s\n' + + ' + cpython-3.13.12-windows-x86_64-none\n' + +function relayed(chunks: string[]): string { + const out: string[] = [] + const python = withoutPythonDownloadLines((s) => out.push(s)) + for (const chunk of chunks) python.relay(chunk) + python.flush() + return out.join('') +} + +test('the wheelhouse interpreter step drops uv\'s download lines and keeps the rest', () => { + assert.equal(relayed([UV_PYTHON_FROM_MIRROR]), + 'Installed Python 3.13.12 in 1.22s\n + cpython-3.13.12-windows-x86_64-none\n') +}) + +test('a download line split across chunks is still dropped whole', () => { + const cut = UV_PYTHON_FROM_MIRROR.indexOf('(download) (20.7MiB)') + assert.equal(relayed([UV_PYTHON_FROM_MIRROR.slice(0, cut), UV_PYTHON_FROM_MIRROR.slice(cut)]), + 'Installed Python 3.13.12 in 1.22s\n + cpython-3.13.12-windows-x86_64-none\n') +}) + +test('an unfinished last line reaches the progress stream at flush', () => { + assert.equal(relayed(['error: no Python in the mirror']), 'error: no Python in the mirror') +}) + +test('the setup window reads the offline interpreter step as an install, never a download', () => { + const lines = ('[env-setup] installing Python 3.13.12 from the wheelhouse\n' + + relayed([UV_PYTHON_FROM_MIRROR])).split('\n') + const events = lines.map(parseUvLine).filter((e) => e !== null) + assert.equal(events[0]?.step, 'Installing Python 3.13.12 from the wheelhouse') + assert.deepEqual(events.filter((e) => e.phase === 'downloading'), []) +}) + test('checkWheelhouseExtras names the missing extra', () => { const m = { ...goodManifest, extras: [] } assert.throws(() => checkWheelhouseExtras(['gpu'], m), /built without the 'gpu' extra/) diff --git a/de_shell/js/main/pythonEnv.ts b/de_shell/js/main/pythonEnv.ts index 6d0ad93..cf455d5 100644 --- a/de_shell/js/main/pythonEnv.ts +++ b/de_shell/js/main/pythonEnv.ts @@ -625,6 +625,39 @@ function runUv( }) } +/** uv's words for copying an interpreter from its install mirror. */ +const PYTHON_DOWNLOAD_LINE = /^ *Download(?:ing|ed) cpython-/ + +/** + * A progress relay for the wheelhouse's interpreter step. Its mirror is the + * wheelhouse's `python/` folder on disk, but uv words the copy as a download + * ("Downloading cpython-…", "Downloaded cpython-…"), and the setup window would + * show it as one. Those lines are dropped and the rest passed on. Whole lines + * only: a chunk's unfinished last line is held until its end arrives, or `flush`. + */ +export function withoutPythonDownloadLines(onProgress?: (line: string) => void): { + relay: (chunk: string) => void + flush: () => void +} { + let held = '' + const pass = (text: string) => { + const kept = text.split(/(?<=\n)/).filter((line) => !PYTHON_DOWNLOAD_LINE.test(line)).join('') + if (kept) onProgress?.(kept) + } + return { + relay(chunk) { + const text = held + chunk + const end = text.lastIndexOf('\n') + 1 + held = text.slice(end) + if (end) pass(text.slice(0, end)) + }, + flush() { + if (held) pass(held) + held = '' + }, + } +} + /** * Install the locked torch release into the managed env with the backend * resolved for THIS machine (`--torch-backend=auto`: uv probes the NVIDIA @@ -772,7 +805,14 @@ async function setupEnv(o: { onProgress?.(`[env-setup] offline install from wheelhouse ${dir}\n`) const plan = offlinePlan({ wheelhouse: dir, manifest, envDir, userData: join(envDir, '..'), extras }) mkdirSync(join(envDir, '..'), { recursive: true }) - await runUv(projectDir, envDir, plan.python, appVersion, onProgress, plan.env) + onProgress?.(`[env-setup] installing Python ${manifest.python} from the wheelhouse +`) + const python = withoutPythonDownloadLines(onProgress) + try { + await runUv(projectDir, envDir, plan.python, appVersion, python.relay, plan.env) + } finally { + python.flush() + } await runUv(projectDir, envDir, plan.venv, appVersion, onProgress, plan.env) await runUv(projectDir, envDir, plan.export, appVersion, onProgress, plan.env) await runUv(projectDir, envDir, plan.install, appVersion, onProgress, plan.env) diff --git a/upcoming_changes/32.bugfix.rst b/upcoming_changes/32.bugfix.rst new file mode 100644 index 0000000..c5b7b92 --- /dev/null +++ b/upcoming_changes/32.bugfix.rst @@ -0,0 +1 @@ +An offline install's setup window said it was downloading Python, although the interpreter is copied from the wheelhouse; it now says it is installing Python from the wheelhouse.