Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/lib/components/dialogs/ToolboxManagerDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -80,8 +81,13 @@
let activeOverrideRow = $state<string | null>(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 {
Expand Down Expand Up @@ -508,6 +514,17 @@
network requests, read clipboard data, or consume CPU and memory.
</p>
<p>Only continue if you trust the source.</p>
{#if isWebRuntime}
<div class="web-note">
<Icon name="info" size={14} />
<span>
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
<code>pip install pathview</code> desktop app.
</span>
</div>
{/if}
<div class="source-recap">
{#if resolvedSource?.type === 'pypi'}
<code>pip install {resolvedSource.pkg}{resolvedSource.version ? `==${resolvedSource.version}` : ''}</code>
Expand Down Expand Up @@ -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;
Expand Down
43 changes: 41 additions & 2 deletions src/lib/toolbox/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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)})`);
Expand Down
24 changes: 22 additions & 2 deletions src/lib/toolbox/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"}


Expand Down
Loading