From 69571c1306a4ba4c46c53bfb9edbb05846c5e627 Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Sat, 25 Apr 2026 09:29:33 +0200 Subject: [PATCH] fix: pass wheel filename through extraction chain to fix micropip install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit micropip requires the full PEP 427 wheel filename (name-version-py-abi-plat.whl) — writing the file as bincio.whl caused InvalidWheelFilename. The wheel URL from /api/wheel/version now provides the basename; it flows through fetchWheelBase64 → extractFile → WebView where the file is written with the correct name and _wheel_path is set as a Pyodide global before PY_INSTALL_WHEEL runs. --- mobile/app/(tabs)/import.tsx | 17 +++++++++++------ mobile/extraction/PyodideWebView.tsx | 16 ++++++++++------ mobile/extraction/extractActivity.ts | 3 ++- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/mobile/app/(tabs)/import.tsx b/mobile/app/(tabs)/import.tsx index 459679a..879bdc4 100644 --- a/mobile/app/(tabs)/import.tsx +++ b/mobile/app/(tabs)/import.tsx @@ -97,12 +97,13 @@ export default function ImportScreen() { // allows local-network HTTP (NSAllowsLocalNetworking=true in Info.plist). const instanceUrl = await getInstanceUrl(dbCtx); setState({ status: 'loading', msg: 'Fetching Bincio engine…' }); - const wheelBase64 = await fetchWheelBase64(instanceUrl); + const { base64: wheelBase64, filename: wheelFilename } = await fetchWheelBase64(instanceUrl); const result = await extractFile( name, base64, wheelBase64, + wheelFilename, (msg) => setState({ status: 'loading', msg }), ); @@ -215,21 +216,25 @@ async function getInstanceUrl(db: ReturnType): Promise< } // In-memory cache so repeated imports in one session don't re-download the wheel. -let _cachedWheelBase64: string | null = null; +let _cachedWheel: { base64: string; filename: string } | null = null; -async function fetchWheelBase64(instanceUrl: string): Promise { - if (_cachedWheelBase64) return _cachedWheelBase64; +async function fetchWheelBase64(instanceUrl: string): Promise<{ base64: string; filename: string }> { + if (_cachedWheel) return _cachedWheel; const base = instanceUrl || 'https://bincio.org'; // Ask the instance for the canonical wheel URL (handles both dev and prod layouts). let wheelUrl = `${base}/api/wheel/download`; + let wheelFilename = 'bincio-0.1.0-py3-none-any.whl'; try { const vr = await fetch(`${base}/api/wheel/version`, { signal: AbortSignal.timeout(5000) }); if (vr.ok) { const d = await vr.json() as { api_url?: string; url?: string }; const path = d.api_url ?? d.url ?? '/api/wheel/download'; wheelUrl = path.startsWith('http') ? path : `${base}${path}`; + // Extract the filename from the URL path (last segment after final /) + const urlBasename = wheelUrl.split('/').pop() ?? ''; + if (urlBasename.endsWith('.whl')) wheelFilename = urlBasename; } } catch {} @@ -237,8 +242,8 @@ async function fetchWheelBase64(instanceUrl: string): Promise { const resp = await fetch(wheelUrl); if (!resp.ok) throw new Error(`Could not download Bincio engine (${resp.status}). Is the instance running?`); const buf = await resp.arrayBuffer(); - _cachedWheelBase64 = arrayBufferToBase64(buf); - return _cachedWheelBase64; + _cachedWheel = { base64: arrayBufferToBase64(buf), filename: wheelFilename }; + return _cachedWheel; } function arrayBufferToBase64(buf: ArrayBuffer): string { diff --git a/mobile/extraction/PyodideWebView.tsx b/mobile/extraction/PyodideWebView.tsx index f4994f5..1a4aed7 100644 --- a/mobile/extraction/PyodideWebView.tsx +++ b/mobile/extraction/PyodideWebView.tsx @@ -13,9 +13,10 @@ const PY_INSTALL_PACKAGES = [ // emfs:// is Pyodide's Emscripten-FS URL scheme — the only reliable way to // install a wheel from bytes without an http/https URL (blob: URLs are not // recognised by micropip and cause an InvalidRequirement parse error). +// _wheel_path is set as a Pyodide global before this runs. const PY_INSTALL_WHEEL = [ 'import micropip', - 'await micropip.install("emfs:///tmp/bincio.whl", deps=False)', + 'await micropip.install("emfs://" + _wheel_path, deps=False)', ].join('\n'); const PY_EXTRACT = [ @@ -90,10 +91,11 @@ var initError = null; })(); window._bincioExtract = async function(params) { - var reqId = params.reqId; - var filename = params.filename; - var base64 = params.base64; - var wheelBase64 = params.wheelBase64; // pre-fetched by React Native (avoids ATS/HTTP issues) + var reqId = params.reqId; + var filename = params.filename; + var base64 = params.base64; + var wheelBase64 = params.wheelBase64; // pre-fetched by React Native (avoids ATS/HTTP issues) + var wheelFilename = params.wheelFilename; // e.g. "bincio-0.1.0-py3-none-any.whl" function post(m) { _post(Object.assign({}, m, { reqId: reqId })); } @@ -118,7 +120,9 @@ window._bincioExtract = async function(params) { if (!wheelReady) { post({ type: 'progress', msg: 'Loading Bincio…' }); var wheelBytes = Uint8Array.from(atob(wheelBase64), function(c) { return c.charCodeAt(0); }); - pyodide.FS.writeFile('/tmp/bincio.whl', wheelBytes); + var wheelPath = '/tmp/' + wheelFilename; + pyodide.FS.writeFile(wheelPath, wheelBytes); + pyodide.globals.set('_wheel_path', wheelPath); await pyodide.runPythonAsync(_PY_INSTALL_WHEEL); wheelReady = true; } diff --git a/mobile/extraction/extractActivity.ts b/mobile/extraction/extractActivity.ts index e21bc89..9e1c121 100644 --- a/mobile/extraction/extractActivity.ts +++ b/mobile/extraction/extractActivity.ts @@ -60,6 +60,7 @@ export function extractFile( filename: string, base64: string, wheelBase64: string, + wheelFilename: string, onStatus: (msg: string) => void = () => {}, ): Promise { if (isExtracting) return Promise.reject(new Error('Another extraction is already in progress')); @@ -69,7 +70,7 @@ export function extractFile( isExtracting = true; const reqId = String(++reqCounter); - const args = JSON.stringify({ reqId, filename, base64, wheelBase64 }); + const args = JSON.stringify({ reqId, filename, base64, wheelBase64, wheelFilename }); return new Promise((resolve, reject) => { pending.set(reqId, {