Skip to content

Commit 1ac09da

Browse files
committed
upgrade pyodide to 0.29.3
1 parent da96bd3 commit 1ac09da

File tree

5 files changed

+332
-75
lines changed

5 files changed

+332
-75
lines changed

internals/scripts/InstallMNE.mjs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Downloads MNE-Python and its pure-Python dependencies from PyPI as wheel
4+
* files for offline use with Pyodide. Binary dependencies (numpy, scipy,
5+
* matplotlib, pandas) are already included via the `pyodide` npm package and
6+
* do NOT need to be downloaded here.
7+
*
8+
* Downloaded wheels are saved to:
9+
* src/renderer/utils/pyodide/src/packages/
10+
*
11+
* A manifest.json is written there so the web worker knows which filenames
12+
* to pass to micropip.install() at startup.
13+
*
14+
* Usage: node internals/scripts/InstallMNE.mjs
15+
*/
16+
17+
import fs from 'fs';
18+
import https from 'https';
19+
import path from 'path';
20+
import chalk from 'chalk';
21+
22+
const PACKAGES_DIR = path.resolve(
23+
'src/renderer/utils/pyodide/src/packages'
24+
);
25+
const MANIFEST_FILE = path.join(PACKAGES_DIR, 'manifest.json');
26+
27+
/**
28+
* Pure-Python packages required by MNE that are not bundled with Pyodide.
29+
* Each entry is resolved against the PyPI JSON API to find the latest
30+
* pure-Python wheel (py3-none-any or py2.py3-none-any).
31+
*/
32+
const PACKAGES_TO_DOWNLOAD = [
33+
'mne',
34+
'pooch',
35+
'tqdm',
36+
'platformdirs',
37+
];
38+
39+
// ---------------------------------------------------------------------------
40+
// Network helpers
41+
// ---------------------------------------------------------------------------
42+
43+
function httpsGet(url) {
44+
return new Promise((resolve, reject) => {
45+
const req = https.get(url, { headers: { 'User-Agent': 'BrainWaves-installer/1.0' } }, (res) => {
46+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
47+
resolve(httpsGet(res.headers.location));
48+
return;
49+
}
50+
if (res.statusCode !== 200) {
51+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
52+
return;
53+
}
54+
let body = '';
55+
res.setEncoding('utf8');
56+
res.on('data', (chunk) => { body += chunk; });
57+
res.on('end', () => resolve(body));
58+
res.on('error', reject);
59+
});
60+
req.on('error', reject);
61+
});
62+
}
63+
64+
function downloadBinary(url, dest) {
65+
return new Promise((resolve, reject) => {
66+
const doGet = (reqUrl) => {
67+
https.get(reqUrl, { headers: { 'User-Agent': 'BrainWaves-installer/1.0' } }, (res) => {
68+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
69+
doGet(res.headers.location);
70+
return;
71+
}
72+
if (res.statusCode !== 200) {
73+
reject(new Error(`HTTP ${res.statusCode} for ${reqUrl}`));
74+
return;
75+
}
76+
const file = fs.createWriteStream(dest);
77+
res.pipe(file);
78+
file.on('finish', () => file.close(resolve));
79+
file.on('error', (err) => { fs.unlink(dest, () => {}); reject(err); });
80+
}).on('error', (err) => { fs.unlink(dest, () => {}); reject(err); });
81+
};
82+
doGet(url);
83+
});
84+
}
85+
86+
// ---------------------------------------------------------------------------
87+
// PyPI helpers
88+
// ---------------------------------------------------------------------------
89+
90+
/**
91+
* Returns the best pure-Python wheel for the latest release of `packageName`.
92+
* Preference: py3-none-any > py2.py3-none-any > *-none-any
93+
*/
94+
async function resolvePureWheel(packageName) {
95+
const raw = await httpsGet(`https://pypi.org/pypi/${packageName}/json`);
96+
const data = JSON.parse(raw);
97+
const version = data.info.version;
98+
const urls = data.urls; // files for the latest release
99+
100+
const wheels = urls.filter((f) => f.filename.endsWith('.whl'));
101+
102+
const ranked = [
103+
wheels.find((f) => f.filename.endsWith('-py3-none-any.whl')),
104+
wheels.find((f) => f.filename.endsWith('-py2.py3-none-any.whl')),
105+
wheels.find((f) => f.filename.includes('-none-any.whl')),
106+
].filter(Boolean);
107+
108+
if (ranked.length === 0) {
109+
throw new Error(
110+
`No pure-Python wheel found for ${packageName} ${version}. ` +
111+
`Binary packages must come from the Pyodide npm bundle.`
112+
);
113+
}
114+
115+
return { version, wheel: ranked[0] };
116+
}
117+
118+
// ---------------------------------------------------------------------------
119+
// Main
120+
// ---------------------------------------------------------------------------
121+
122+
async function installPackage(packageName, manifest) {
123+
process.stdout.write(chalk.blue(` ${packageName}: `));
124+
125+
let version, wheel;
126+
try {
127+
({ version, wheel } = await resolvePureWheel(packageName));
128+
} catch (err) {
129+
console.log(chalk.red(`FAILED — ${err.message}`));
130+
return;
131+
}
132+
133+
const dest = path.join(PACKAGES_DIR, wheel.filename);
134+
135+
if (fs.existsSync(dest)) {
136+
console.log(chalk.gray(`${version} already present, skipping`));
137+
manifest[packageName] = { version, filename: wheel.filename };
138+
return;
139+
}
140+
141+
try {
142+
await downloadBinary(wheel.url, dest);
143+
console.log(chalk.green(`${version} downloaded`));
144+
manifest[packageName] = { version, filename: wheel.filename };
145+
} catch (err) {
146+
console.log(chalk.red(`FAILED — ${err.message}`));
147+
if (fs.existsSync(dest)) fs.unlinkSync(dest);
148+
}
149+
}
150+
151+
async function main() {
152+
fs.mkdirSync(PACKAGES_DIR, { recursive: true });
153+
154+
// Preserve any previously downloaded packages in the manifest.
155+
let manifest = {};
156+
if (fs.existsSync(MANIFEST_FILE)) {
157+
try {
158+
manifest = JSON.parse(fs.readFileSync(MANIFEST_FILE, 'utf8'));
159+
} catch {
160+
manifest = {};
161+
}
162+
}
163+
164+
console.log(chalk.blue.bold('Downloading MNE-Python wheels from PyPI…'));
165+
for (const pkg of PACKAGES_TO_DOWNLOAD) {
166+
await installPackage(pkg, manifest);
167+
}
168+
169+
fs.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2));
170+
console.log(chalk.green.bold('\nAll MNE wheels ready.'));
171+
console.log(chalk.gray(`Manifest → ${MANIFEST_FILE}`));
172+
}
173+
174+
main().catch((err) => {
175+
console.error(chalk.red('Fatal error:'), err);
176+
process.exit(1);
177+
});

internals/scripts/InstallPyodide.js

Lines changed: 0 additions & 66 deletions
This file was deleted.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Downloads the Pyodide core tarball from GitHub releases and extracts it
4+
* into the renderer's public directory so Vite serves the runtime as a static
5+
* asset at /pyodide/… (Vite publicDir → src/renderer/utils/pyodide/src/).
6+
*
7+
* The "core" tarball is ~40 MB and includes the runtime plus the most
8+
* commonly used scientific packages (numpy, scipy, matplotlib, pandas, …).
9+
* It is much smaller than the full Pyodide build (~500 MB).
10+
*
11+
* Usage: node internals/scripts/InstallPyodide.mjs
12+
* Runs automatically via the postinstall npm hook.
13+
*/
14+
15+
import fs from 'fs';
16+
import https from 'https';
17+
import path from 'path';
18+
import { pipeline } from 'stream/promises';
19+
import chalk from 'chalk';
20+
import bz2 from 'unbzip2-stream';
21+
import tar from 'tar-fs';
22+
23+
const PYODIDE_VERSION = '0.29.3';
24+
const TARBALL_NAME = `pyodide-core-${PYODIDE_VERSION}.tar.bz2`;
25+
const TARBALL_URL = `https://github.com/pyodide/pyodide/releases/download/${PYODIDE_VERSION}/${TARBALL_NAME}`;
26+
27+
// Vite publicDir root — everything here is served verbatim by the dev server.
28+
const PUBLIC_ROOT = path.resolve('src/renderer/utils/pyodide/src');
29+
// The tarball extracts into a `pyodide/` subdirectory, which ends up at:
30+
// src/renderer/utils/pyodide/src/pyodide/ → served at /pyodide/
31+
const DEST_DIR = path.join(PUBLIC_ROOT, 'pyodide');
32+
const VERSION_FILE = path.join(DEST_DIR, '.pyodide-version');
33+
34+
// ---------------------------------------------------------------------------
35+
// Network helpers (follow redirects)
36+
// ---------------------------------------------------------------------------
37+
38+
function httpsGetResponse(url) {
39+
return new Promise((resolve, reject) => {
40+
https
41+
.get(url, { headers: { 'User-Agent': 'BrainWaves-installer/1.0' } }, (res) => {
42+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
43+
resolve(httpsGetResponse(res.headers.location));
44+
} else {
45+
resolve(res);
46+
}
47+
})
48+
.on('error', reject);
49+
});
50+
}
51+
52+
// ---------------------------------------------------------------------------
53+
// Main
54+
// ---------------------------------------------------------------------------
55+
56+
async function main() {
57+
// Skip if this exact version is already extracted.
58+
if (
59+
fs.existsSync(VERSION_FILE) &&
60+
fs.readFileSync(VERSION_FILE, 'utf8').trim() === PYODIDE_VERSION
61+
) {
62+
console.log(
63+
chalk.green.bold(`Pyodide ${PYODIDE_VERSION} already installed, skipping.`)
64+
);
65+
return;
66+
}
67+
68+
fs.mkdirSync(PUBLIC_ROOT, { recursive: true });
69+
70+
const tarballPath = path.join(PUBLIC_ROOT, TARBALL_NAME);
71+
72+
// Download the tarball if not already cached.
73+
if (!fs.existsSync(tarballPath)) {
74+
console.log(
75+
chalk.blue.bold(`Downloading Pyodide ${PYODIDE_VERSION} core tarball…`)
76+
);
77+
const res = await httpsGetResponse(TARBALL_URL);
78+
if (res.statusCode !== 200) {
79+
throw new Error(`Failed to download tarball: HTTP ${res.statusCode}`);
80+
}
81+
await pipeline(res, fs.createWriteStream(tarballPath));
82+
console.log(chalk.gray(` Saved → ${tarballPath}`));
83+
} else {
84+
console.log(chalk.gray(` Tarball already cached, skipping download.`));
85+
}
86+
87+
// Extract the tarball. The archive contains a top-level `pyodide/`
88+
// directory, so extracting into PUBLIC_ROOT gives us PUBLIC_ROOT/pyodide/.
89+
console.log(chalk.blue.bold(`Extracting…`));
90+
await pipeline(
91+
fs.createReadStream(tarballPath),
92+
bz2(),
93+
tar.extract(PUBLIC_ROOT)
94+
);
95+
96+
// Stamp the installed version and clean up the cached tarball.
97+
fs.writeFileSync(VERSION_FILE, PYODIDE_VERSION);
98+
fs.unlinkSync(tarballPath);
99+
100+
console.log(
101+
chalk.green.bold(`Pyodide ${PYODIDE_VERSION} installed successfully.`)
102+
);
103+
}
104+
105+
main().catch((err) => {
106+
console.error(chalk.red('Fatal error:'), err);
107+
process.exit(1);
108+
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"package-mac": "npm run build && electron-builder build --mac",
1616
"package-linux": "npm run build && electron-builder build --linux",
1717
"package-win": "npm run build && electron-builder build --win --x64",
18-
"postinstall": "electron-builder install-app-deps && node internals/scripts/InstallPyodide.js && node internals/scripts/patchDeps.mjs",
18+
"postinstall": "electron-builder install-app-deps && node internals/scripts/InstallPyodide.mjs && node internals/scripts/InstallMNE.mjs && node internals/scripts/patchDeps.mjs",
1919
"lint": "cross-env NODE_ENV=development eslint . --cache",
2020
"lint-fix": "npm run lint -- --fix",
2121
"lint-styles": "stylelint '**/*.*(css|scss)'",

0 commit comments

Comments
 (0)