fix: pass wheel filename through extraction chain to fix micropip install

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.
This commit is contained in:
Davide Scaini
2026-04-25 09:29:33 +02:00
parent ef45d4f4bb
commit 69571c1306
3 changed files with 23 additions and 13 deletions
+11 -6
View File
@@ -97,12 +97,13 @@ export default function ImportScreen() {
// allows local-network HTTP (NSAllowsLocalNetworking=true in Info.plist). // allows local-network HTTP (NSAllowsLocalNetworking=true in Info.plist).
const instanceUrl = await getInstanceUrl(dbCtx); const instanceUrl = await getInstanceUrl(dbCtx);
setState({ status: 'loading', msg: 'Fetching Bincio engine…' }); setState({ status: 'loading', msg: 'Fetching Bincio engine…' });
const wheelBase64 = await fetchWheelBase64(instanceUrl); const { base64: wheelBase64, filename: wheelFilename } = await fetchWheelBase64(instanceUrl);
const result = await extractFile( const result = await extractFile(
name, name,
base64, base64,
wheelBase64, wheelBase64,
wheelFilename,
(msg) => setState({ status: 'loading', msg }), (msg) => setState({ status: 'loading', msg }),
); );
@@ -215,21 +216,25 @@ async function getInstanceUrl(db: ReturnType<typeof useSQLiteContext>): Promise<
} }
// In-memory cache so repeated imports in one session don't re-download the wheel. // 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<string> { async function fetchWheelBase64(instanceUrl: string): Promise<{ base64: string; filename: string }> {
if (_cachedWheelBase64) return _cachedWheelBase64; if (_cachedWheel) return _cachedWheel;
const base = instanceUrl || 'https://bincio.org'; const base = instanceUrl || 'https://bincio.org';
// Ask the instance for the canonical wheel URL (handles both dev and prod layouts). // Ask the instance for the canonical wheel URL (handles both dev and prod layouts).
let wheelUrl = `${base}/api/wheel/download`; let wheelUrl = `${base}/api/wheel/download`;
let wheelFilename = 'bincio-0.1.0-py3-none-any.whl';
try { try {
const vr = await fetch(`${base}/api/wheel/version`, { signal: AbortSignal.timeout(5000) }); const vr = await fetch(`${base}/api/wheel/version`, { signal: AbortSignal.timeout(5000) });
if (vr.ok) { if (vr.ok) {
const d = await vr.json() as { api_url?: string; url?: string }; const d = await vr.json() as { api_url?: string; url?: string };
const path = d.api_url ?? d.url ?? '/api/wheel/download'; const path = d.api_url ?? d.url ?? '/api/wheel/download';
wheelUrl = path.startsWith('http') ? path : `${base}${path}`; 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 {} } catch {}
@@ -237,8 +242,8 @@ async function fetchWheelBase64(instanceUrl: string): Promise<string> {
const resp = await fetch(wheelUrl); const resp = await fetch(wheelUrl);
if (!resp.ok) throw new Error(`Could not download Bincio engine (${resp.status}). Is the instance running?`); if (!resp.ok) throw new Error(`Could not download Bincio engine (${resp.status}). Is the instance running?`);
const buf = await resp.arrayBuffer(); const buf = await resp.arrayBuffer();
_cachedWheelBase64 = arrayBufferToBase64(buf); _cachedWheel = { base64: arrayBufferToBase64(buf), filename: wheelFilename };
return _cachedWheelBase64; return _cachedWheel;
} }
function arrayBufferToBase64(buf: ArrayBuffer): string { function arrayBufferToBase64(buf: ArrayBuffer): string {
+6 -2
View File
@@ -13,9 +13,10 @@ const PY_INSTALL_PACKAGES = [
// emfs:// is Pyodide's Emscripten-FS URL scheme — the only reliable way to // 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 // install a wheel from bytes without an http/https URL (blob: URLs are not
// recognised by micropip and cause an InvalidRequirement parse error). // recognised by micropip and cause an InvalidRequirement parse error).
// _wheel_path is set as a Pyodide global before this runs.
const PY_INSTALL_WHEEL = [ const PY_INSTALL_WHEEL = [
'import micropip', 'import micropip',
'await micropip.install("emfs:///tmp/bincio.whl", deps=False)', 'await micropip.install("emfs://" + _wheel_path, deps=False)',
].join('\n'); ].join('\n');
const PY_EXTRACT = [ const PY_EXTRACT = [
@@ -94,6 +95,7 @@ window._bincioExtract = async function(params) {
var filename = params.filename; var filename = params.filename;
var base64 = params.base64; var base64 = params.base64;
var wheelBase64 = params.wheelBase64; // pre-fetched by React Native (avoids ATS/HTTP issues) 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 })); } function post(m) { _post(Object.assign({}, m, { reqId: reqId })); }
@@ -118,7 +120,9 @@ window._bincioExtract = async function(params) {
if (!wheelReady) { if (!wheelReady) {
post({ type: 'progress', msg: 'Loading Bincio…' }); post({ type: 'progress', msg: 'Loading Bincio…' });
var wheelBytes = Uint8Array.from(atob(wheelBase64), function(c) { return c.charCodeAt(0); }); 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); await pyodide.runPythonAsync(_PY_INSTALL_WHEEL);
wheelReady = true; wheelReady = true;
} }
+2 -1
View File
@@ -60,6 +60,7 @@ export function extractFile(
filename: string, filename: string,
base64: string, base64: string,
wheelBase64: string, wheelBase64: string,
wheelFilename: string,
onStatus: (msg: string) => void = () => {}, onStatus: (msg: string) => void = () => {},
): Promise<ExtractionResult> { ): Promise<ExtractionResult> {
if (isExtracting) return Promise.reject(new Error('Another extraction is already in progress')); if (isExtracting) return Promise.reject(new Error('Another extraction is already in progress'));
@@ -69,7 +70,7 @@ export function extractFile(
isExtracting = true; isExtracting = true;
const reqId = String(++reqCounter); const reqId = String(++reqCounter);
const args = JSON.stringify({ reqId, filename, base64, wheelBase64 }); const args = JSON.stringify({ reqId, filename, base64, wheelBase64, wheelFilename });
return new Promise<ExtractionResult>((resolve, reject) => { return new Promise<ExtractionResult>((resolve, reject) => {
pending.set(reqId, { pending.set(reqId, {