From 15c1aa5fb106a9234db2845d637c53b7520f10d3 Mon Sep 17 00:00:00 2001 From: Michael Spilman <130705596+TheDrOnos@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:42:49 -0500 Subject: [PATCH 1/2] Offline install: say Python comes from the wheelhouse, not a download MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The offline interpreter step runs `uv python install ` with UV_PYTHON_INSTALL_MIRROR pointing at the wheelhouse's python/ folder. uv words that copy as a download ("Downloading cpython-…", "Downloaded cpython-…"), runUv relayed it unchanged, and the setup window showed "Downloading cpython-3.13.12-…" as its step, with the Download pill lit, although nothing is downloaded. The wheelhouse branch of setupEnv now announces the step as "[env-setup] installing Python from the wheelhouse" and relays uv's output through withoutPythonDownloadLines, which drops those two lines and passes the rest ("Installed Python …"). It works on whole lines, holding a chunk's unfinished last line until its end arrives or the step ends, so a line split across chunks is still dropped whole. The online path keeps uv's wording, which is true there. Tests (pythonEnv.test.ts), on uv 0.13.4's real output for this step: - the download lines are dropped and the rest kept; - a download line split across chunks is dropped whole; - an unfinished last line still reaches the stream at flush; - through parseUvLine, the step reads "Installing Python 3.13.12 from the wheelhouse" and no event is in the downloading phase. With the filter disabled, the first, second and fourth fail (26 passed, 3 failed); all of them failed before the function existed. Checks: npm run typecheck clean; npm run test:unit 179 passed. --- de_shell/js/main/pythonEnv.test.ts | 40 ++++++++++++++++++++++++++++ de_shell/js/main/pythonEnv.ts | 42 +++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) 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) From c6168acf3253bc8363d24cd1c0623d91676718ad Mon Sep 17 00:00:00 2001 From: Michael Spilman <130705596+TheDrOnos@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:45:15 -0500 Subject: [PATCH 2/2] news: fragment for #32 --- upcoming_changes/32.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 upcoming_changes/32.bugfix.rst 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.