Skip to content

Commit 553c3ff

Browse files
committed
Add web-runtime guard and hint for non-Pyodide-compatible toolboxes
1 parent d44da90 commit 553c3ff

3 files changed

Lines changed: 104 additions & 4 deletions

File tree

src/lib/components/dialogs/ToolboxManagerDialog.svelte

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { onDestroy } from 'svelte';
33
import Icon from '$lib/components/icons/Icon.svelte';
44
import { tooltip } from '$lib/components/Tooltip.svelte';
5+
import { getBackendType } from '$lib/pyodide/backend';
56
import DialogShell from './shared/DialogShell.svelte';
67
import {
78
TOOLBOX_CATALOG,
@@ -80,8 +81,13 @@
8081
let activeOverrideRow = $state<string | null>(null);
8182
const SHAPE_OPTIONS = ['pill', 'rect', 'mixed'] as const;
8283
84+
// True when running in the browser (Pyodide) backend: installs are then
85+
// limited to pure-Python / Pyodide-compatible packages.
86+
let isWebRuntime = $state(false);
87+
8388
$effect(() => {
8489
if (!open) return;
90+
isWebRuntime = getBackendType() === 'pyodide';
8591
if (editing) {
8692
startEdit(editing);
8793
} else {
@@ -508,6 +514,17 @@
508514
network requests, read clipboard data, or consume CPU and memory.
509515
</p>
510516
<p>Only continue if you trust the source.</p>
517+
{#if isWebRuntime}
518+
<div class="web-note">
519+
<Icon name="info" size={14} />
520+
<span>
521+
You're using the PathView web app. Installs run through Pyodide in the browser,
522+
so only pure-Python toolboxes (or packages Pyodide ships pre-built) work here.
523+
For toolboxes with compiled dependencies, use the standalone
524+
<code>pip install pathview</code> desktop app.
525+
</span>
526+
</div>
527+
{/if}
511528
<div class="source-recap">
512529
{#if resolvedSource?.type === 'pypi'}
513530
<code>pip install {resolvedSource.pkg}{resolvedSource.version ? `==${resolvedSource.version}` : ''}</code>
@@ -940,6 +957,30 @@
940957
color: var(--text-muted);
941958
}
942959
960+
/* Web-runtime (Pyodide) install limitation notice on the trust step */
961+
.web-note {
962+
display: flex;
963+
gap: var(--space-sm);
964+
padding: var(--space-sm) var(--space-md);
965+
background: var(--accent-bg);
966+
border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
967+
border-radius: var(--radius-sm);
968+
font-size: var(--font-base);
969+
line-height: 1.5;
970+
color: var(--text-muted);
971+
}
972+
973+
.web-note :global(svg) {
974+
flex-shrink: 0;
975+
margin-top: 2px;
976+
color: var(--accent);
977+
}
978+
979+
.web-note code {
980+
font-family: var(--font-mono);
981+
color: var(--text-muted);
982+
}
983+
943984
.spinner-row {
944985
display: flex;
945986
align-items: center;

src/lib/toolbox/installer.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,40 @@ function pyStr(s: string): string {
7878
return JSON.stringify(s);
7979
}
8080

81+
/**
82+
* Turn a raw micropip failure into a web-version-aware message.
83+
*
84+
* The web app runs Python through Pyodide, which can only install
85+
* pure-Python wheels (or packages Pyodide ships pre-built). Toolboxes with
86+
* compiled/native code fail here even though they install fine in the
87+
* standalone (pip-backed) PathView. `_pv_install_micropip` tags those
88+
* failures with `PV_INCOMPATIBLE`, so we give a useful hint instead of
89+
* surfacing a raw traceback. Genuine failures pass through unchanged.
90+
*/
91+
function reframePyodideInstallError(spec: string, err: unknown): Error {
92+
const raw = err instanceof Error ? err.message : String(err);
93+
if (!raw.includes('PV_INCOMPATIBLE')) {
94+
// Network error, bad spec, etc. — pass through, just strip our tag.
95+
return new Error(raw.replace(/PV_INSTALL_ERROR:\s*/, ''));
96+
}
97+
const detail = raw.split('PV_INCOMPATIBLE:').pop()?.trim() || raw;
98+
return new Error(
99+
`"${spec}" can't be installed in the PathView web app.\n` +
100+
`\n` +
101+
`The web version runs Python in your browser via Pyodide, which can\n` +
102+
`only install pure-Python packages (or packages Pyodide ships\n` +
103+
`pre-built). This toolbox needs compiled or native code that isn't\n` +
104+
`available in the browser.\n` +
105+
`\n` +
106+
`To use it, install the standalone PathView desktop app:\n` +
107+
` pip install pathview\n` +
108+
` pathview\n` +
109+
`It runs a real Python environment and can install any pip package.\n` +
110+
`\n` +
111+
`micropip: ${detail}`
112+
);
113+
}
114+
81115
/**
82116
* Install a package. Skips when `importPath` is given and the module is
83117
* already importable (saves a round-trip + download).
@@ -94,8 +128,13 @@ export async function installPackage(spec: string, importPath?: string): Promise
94128
}
95129
const backend = getBackendType();
96130
if (backend === 'pyodide') {
97-
// runPythonAsync supports top-level await for micropip.install
98-
await exec(`await _pv_install_micropip(${pyStr(spec)})`);
131+
// runPythonAsync supports top-level await for micropip.install.
132+
// Reframe Pyodide-incompatibility failures into an actionable hint.
133+
try {
134+
await exec(`await _pv_install_micropip(${pyStr(spec)})`);
135+
} catch (e) {
136+
throw reframePyodideInstallError(spec, e);
137+
}
99138
} else {
100139
// Flask / remote: real CPython, use subprocess pip (sync)
101140
await exec(`_pv_install_pip(${pyStr(spec)})`);

src/lib/toolbox/python.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,29 @@ def _pv_already_installed(import_path):
3434
3535
3636
async def _pv_install_micropip(spec):
37-
"""Pyodide-side install via micropip (top-level await)."""
37+
"""Pyodide-side install via micropip (top-level await).
38+
39+
micropip can only install pure-Python wheels (or packages Pyodide
40+
ships pre-built), so toolboxes with compiled/native code fail here
41+
even though they install fine in the standalone (pip-backed) build.
42+
On failure we classify the error and prefix it with PV_INCOMPATIBLE
43+
(browser-runtime limitation) or PV_INSTALL_ERROR (genuine failure)
44+
so the JS side can show a useful hint instead of a raw traceback."""
3845
import micropip
39-
await micropip.install(spec, keep_going=True)
46+
try:
47+
await micropip.install(spec, keep_going=True)
48+
except Exception as e:
49+
msg = str(e)
50+
low = msg.lower()
51+
incompatible = (
52+
"pure python" in low
53+
or "can't find" in low
54+
or "cannot find" in low
55+
or "no matching distribution" in low
56+
or "no known package" in low
57+
)
58+
tag = "PV_INCOMPATIBLE" if incompatible else "PV_INSTALL_ERROR"
59+
raise RuntimeError(tag + ": " + msg)
4060
return {"ok": True, "spec": spec, "via": "micropip"}
4161
4262

0 commit comments

Comments
 (0)