diff --git a/src/lib/components/dialogs/ToolboxManagerDialog.svelte b/src/lib/components/dialogs/ToolboxManagerDialog.svelte index 898ca599..25122af6 100644 --- a/src/lib/components/dialogs/ToolboxManagerDialog.svelte +++ b/src/lib/components/dialogs/ToolboxManagerDialog.svelte @@ -2,6 +2,7 @@ import { onDestroy } from 'svelte'; import Icon from '$lib/components/icons/Icon.svelte'; import { tooltip } from '$lib/components/Tooltip.svelte'; + import { getBackendType } from '$lib/pyodide/backend'; import DialogShell from './shared/DialogShell.svelte'; import { TOOLBOX_CATALOG, @@ -80,8 +81,13 @@ let activeOverrideRow = $state(null); const SHAPE_OPTIONS = ['pill', 'rect', 'mixed'] as const; + // True when running in the browser (Pyodide) backend: installs are then + // limited to pure-Python / Pyodide-compatible packages. + let isWebRuntime = $state(false); + $effect(() => { if (!open) return; + isWebRuntime = getBackendType() === 'pyodide'; if (editing) { startEdit(editing); } else { @@ -508,6 +514,17 @@ network requests, read clipboard data, or consume CPU and memory.

Only continue if you trust the source.

+ {#if isWebRuntime} +
+ + + You're using the PathView web app. Installs run through Pyodide in the browser, + so only pure-Python toolboxes (or packages Pyodide ships pre-built) work here. + For toolboxes with compiled dependencies, use the standalone + pip install pathview desktop app. + +
+ {/if}
{#if resolvedSource?.type === 'pypi'} pip install {resolvedSource.pkg}{resolvedSource.version ? `==${resolvedSource.version}` : ''} @@ -940,6 +957,30 @@ color: var(--text-muted); } + /* Web-runtime (Pyodide) install limitation notice on the trust step */ + .web-note { + display: flex; + gap: var(--space-sm); + padding: var(--space-sm) var(--space-md); + background: var(--accent-bg); + border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); + border-radius: var(--radius-sm); + font-size: var(--font-base); + line-height: 1.5; + color: var(--text-muted); + } + + .web-note :global(svg) { + flex-shrink: 0; + margin-top: 2px; + color: var(--accent); + } + + .web-note code { + font-family: var(--font-mono); + color: var(--text-muted); + } + .spinner-row { display: flex; align-items: center; diff --git a/src/lib/toolbox/installer.ts b/src/lib/toolbox/installer.ts index f24627f0..d768596b 100644 --- a/src/lib/toolbox/installer.ts +++ b/src/lib/toolbox/installer.ts @@ -78,6 +78,40 @@ function pyStr(s: string): string { return JSON.stringify(s); } +/** + * Turn a raw micropip failure into a web-version-aware message. + * + * The web app runs Python through Pyodide, which can only install + * pure-Python wheels (or packages Pyodide ships pre-built). Toolboxes with + * compiled/native code fail here even though they install fine in the + * standalone (pip-backed) PathView. `_pv_install_micropip` tags those + * failures with `PV_INCOMPATIBLE`, so we give a useful hint instead of + * surfacing a raw traceback. Genuine failures pass through unchanged. + */ +function reframePyodideInstallError(spec: string, err: unknown): Error { + const raw = err instanceof Error ? err.message : String(err); + if (!raw.includes('PV_INCOMPATIBLE')) { + // Network error, bad spec, etc. — pass through, just strip our tag. + return new Error(raw.replace(/PV_INSTALL_ERROR:\s*/, '')); + } + const detail = raw.split('PV_INCOMPATIBLE:').pop()?.trim() || raw; + return new Error( + `"${spec}" can't be installed in the PathView web app.\n` + + `\n` + + `The web version runs Python in your browser via Pyodide, which can\n` + + `only install pure-Python packages (or packages Pyodide ships\n` + + `pre-built). This toolbox needs compiled or native code that isn't\n` + + `available in the browser.\n` + + `\n` + + `To use it, install the standalone PathView desktop app:\n` + + ` pip install pathview\n` + + ` pathview\n` + + `It runs a real Python environment and can install any pip package.\n` + + `\n` + + `micropip: ${detail}` + ); +} + /** * Install a package. Skips when `importPath` is given and the module is * already importable (saves a round-trip + download). @@ -94,8 +128,13 @@ export async function installPackage(spec: string, importPath?: string): Promise } const backend = getBackendType(); if (backend === 'pyodide') { - // runPythonAsync supports top-level await for micropip.install - await exec(`await _pv_install_micropip(${pyStr(spec)})`); + // runPythonAsync supports top-level await for micropip.install. + // Reframe Pyodide-incompatibility failures into an actionable hint. + try { + await exec(`await _pv_install_micropip(${pyStr(spec)})`); + } catch (e) { + throw reframePyodideInstallError(spec, e); + } } else { // Flask / remote: real CPython, use subprocess pip (sync) await exec(`_pv_install_pip(${pyStr(spec)})`); diff --git a/src/lib/toolbox/python.ts b/src/lib/toolbox/python.ts index 2ebcac21..22c368fa 100644 --- a/src/lib/toolbox/python.ts +++ b/src/lib/toolbox/python.ts @@ -34,9 +34,29 @@ def _pv_already_installed(import_path): async def _pv_install_micropip(spec): - """Pyodide-side install via micropip (top-level await).""" + """Pyodide-side install via micropip (top-level await). + + micropip can only install pure-Python wheels (or packages Pyodide + ships pre-built), so toolboxes with compiled/native code fail here + even though they install fine in the standalone (pip-backed) build. + On failure we classify the error and prefix it with PV_INCOMPATIBLE + (browser-runtime limitation) or PV_INSTALL_ERROR (genuine failure) + so the JS side can show a useful hint instead of a raw traceback.""" import micropip - await micropip.install(spec, keep_going=True) + try: + await micropip.install(spec, keep_going=True) + except Exception as e: + msg = str(e) + low = msg.lower() + incompatible = ( + "pure python" in low + or "can't find" in low + or "cannot find" in low + or "no matching distribution" in low + or "no known package" in low + ) + tag = "PV_INCOMPATIBLE" if incompatible else "PV_INSTALL_ERROR" + raise RuntimeError(tag + ": " + msg) return {"ok": True, "spec": spec, "via": "micropip"}