Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/desktop load prebuilt packages separately #833

Merged
merged 3 commits into from
Apr 3, 2024
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
82 changes: 42 additions & 40 deletions packages/desktop/bin-src/dump_artifacts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import fetch from "node-fetch";
import { loadPyodide, type PyodideInterface } from "pyodide";
import { parseRequirementsTxt, verifyRequirements } from "@stlite/common";
import { makePyodideUrl } from "./url";
import { PyodideBuiltinPackagesData } from "./pyodide_packages";
import { PyodidePrebuiltPackagesData } from "./pyodide_packages";
import { dumpManifest } from "./manifest";

// @ts-ignore
Expand Down Expand Up @@ -76,16 +76,16 @@ async function copyBuildDirectory(options: CopyBuildDirectoryOptions) {
await fsExtra.copy(sourceDir, options.copyTo);
}

interface InspectUsedBuiltinPackagesOptions {
interface InspectUsedPrebuiltPackagesOptions {
requirements: string[];
}
/**
* Get the list of the built-in packages used by the given requirements.
* Get the list of the prebuilt packages used by the given requirements.
* These package files (`pyodide/*.whl`) will be vendored in the app executable
* and loaded at runtime to avoid problems such as https://github.com/whitphx/stlite/issues/558
*/
async function inspectUsedBuiltinPackages(
options: InspectUsedBuiltinPackagesOptions
async function inspectUsedPrebuiltPackages(
options: InspectUsedPrebuiltPackagesOptions
): Promise<string[]> {
if (options.requirements.length === 0) {
return [];
Expand Down Expand Up @@ -147,7 +147,7 @@ async function installPackages(

interface CreateSitePackagesSnapshotOptions {
requirements: string[];
usedBuiltinPackages: string[];
usedPrebuiltPackages: string[];
saveTo: string;
}
async function createSitePackagesSnapshot(
Expand All @@ -160,16 +160,16 @@ async function createSitePackagesSnapshot(
await ensureLoadPackage(pyodide, "micropip");
const micropip = pyodide.pyimport("micropip");

const pyodideBuiltinPackagesData =
await PyodideBuiltinPackagesData.getInstance();
const pyodidePrebuiltPackagesData =
await PyodidePrebuiltPackagesData.getInstance();

const mockedPackages: string[] = [];
if (options.usedBuiltinPackages.length > 0) {
if (options.usedPrebuiltPackages.length > 0) {
console.log(
"Mocking builtin packages so that they will not be included in the site-packages snapshot because these will be installed from the vendored wheel files at runtime..."
"Mocking prebuilt packages so that they will not be included in the site-packages snapshot because these will be installed from the vendored wheel files at runtime..."
);
options.usedBuiltinPackages.forEach((pkg) => {
const packageInfo = pyodideBuiltinPackagesData.getPackageInfoByName(pkg);
options.usedPrebuiltPackages.forEach((pkg) => {
const packageInfo = pyodidePrebuiltPackagesData.getPackageInfoByName(pkg);
if (packageInfo == null) {
throw new Error(`Package ${pkg} is not found in the lock file.`);
}
Expand Down Expand Up @@ -236,35 +236,35 @@ async function readRequirements(
return parseRequirementsTxt(requirementsTxtData);
}

async function writeRequirements(
requirementsTxtPath: string,
requirements: string[]
async function writePrebuiltPackagesTxt(
prebuiltPackagesTxtPath: string,
prebuiltPackages: string[]
): Promise<void> {
const requirementsTxtData = requirements.join("\n");
await fsPromises.writeFile(requirementsTxtPath, requirementsTxtData, {
const prebuiltPackagesTxtData = prebuiltPackages.join("\n");
await fsPromises.writeFile(prebuiltPackagesTxtPath, prebuiltPackagesTxtData, {
encoding: "utf-8",
});
}

interface DownloadPyodideBuiltinPackageWheelsOptions {
interface DownloadPyodidePrebuiltPackageWheelsOptions {
packages: string[];
destDir: string;
}
async function downloadPyodideBuiltinPackageWheels(
options: DownloadPyodideBuiltinPackageWheelsOptions
async function downloadPyodidePrebuiltPackageWheels(
options: DownloadPyodidePrebuiltPackageWheelsOptions
) {
const pyodideBuiltinPackagesData =
await PyodideBuiltinPackagesData.getInstance();
const usedBuiltInPackages = options.packages.map((pkgName) =>
pyodideBuiltinPackagesData.getPackageInfoByName(pkgName)
const pyodidePrebuiltPackagesData =
await PyodidePrebuiltPackagesData.getInstance();
const usedPrebuiltPackages = options.packages.map((pkgName) =>
pyodidePrebuiltPackagesData.getPackageInfoByName(pkgName)
);
const usedBuiltinPackageUrls = usedBuiltInPackages.map((pkg) =>
const usedPrebuiltPackageUrls = usedPrebuiltPackages.map((pkg) =>
makePyodideUrl(pkg.file_name)
);

console.log("Downloading the used built-in packages...");
console.log("Downloading the used prebuilt packages...");
await Promise.all(
usedBuiltinPackageUrls.map(async (pkgUrl) => {
usedPrebuiltPackageUrls.map(async (pkgUrl) => {
const dstPath = path.resolve(
options.destDir,
"./pyodide",
Expand Down Expand Up @@ -336,32 +336,34 @@ yargs(hideBin(process.argv))
}
verifyRequirements(requirements);

const usedBuiltinPackages = await inspectUsedBuiltinPackages({
const usedPrebuiltPackages = await inspectUsedPrebuiltPackages({
requirements: requirements,
});
console.log("The built-in packages loaded for the given requirements:");
console.log(usedBuiltinPackages);
console.log("The prebuilt packages loaded for the given requirements:");
console.log(usedPrebuiltPackages);

await copyBuildDirectory({ copyTo: destDir, keepOld: args.keepOldBuild });
await createSitePackagesSnapshot({
requirements: requirements,
usedBuiltinPackages,
usedPrebuiltPackages,
saveTo: path.resolve(destDir, "./site-packages-snapshot.tar.gz"), // This path will be loaded in the `readSitePackagesSnapshot` handler in electron/main.ts.
});
// The `requirements.txt` file will be needed to call `micropip.install()` at runtime.
// The Pyodide-built packages will be vendored in the build artifact as wheel files
// and `micropip.install()` will install them at runtime,
// while the packages downloaded from PyPI will have been included in the site-packages snapshot.
await writeRequirements(
path.resolve(destDir, "./requirements.txt"), // This path will be loaded in the `readRequirements` handler in electron/main.ts.
usedBuiltinPackages
// These prebuilt packages will be vendored in the build artifact by `downloadPyodidePrebuiltPackageWheels()`
// and the package names will be saved in the `./prebuilt-packages.txt` file
// so that they will be read and passed to `pyodide.loadPackage()` at runtime to install them from the vendored files.
// While the packages downloaded from PyPI at build time will have been shipped in the site-packages snapshot by `createSitePackagesSnapshot()`,
// the prebuilt packages must be installed at runtime by `pyodide.loadPackage()` or `micropip.install()`
// to avoid problems such as https://github.com/whitphx/stlite/issues/564.
await writePrebuiltPackagesTxt(
path.resolve(destDir, "./prebuilt-packages.txt"), // This path will be loaded in the `readRequirements` handler in electron/main.ts.
usedPrebuiltPackages
);
await copyStreamlitAppDirectory({
sourceDir: args.appHomeDirSource,
copyTo: path.resolve(destDir, "./streamlit_app"), // This path will be loaded in the `readStreamlitAppDirectory` handler in electron/main.ts.
});
await downloadPyodideBuiltinPackageWheels({
packages: usedBuiltinPackages,
await downloadPyodidePrebuiltPackageWheels({
packages: usedPrebuiltPackages,
destDir,
});
await dumpManifest({
Expand Down
12 changes: 6 additions & 6 deletions packages/desktop/bin-src/dump_artifacts/pyodide_packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ interface PackageInfo {
file_name: string;
depends: string[];
}
export class PyodideBuiltinPackagesData {
private static _instance: PyodideBuiltinPackagesData;
export class PyodidePrebuiltPackagesData {
private static _instance: PyodidePrebuiltPackagesData;
private _data: Record<string, PackageInfo> | null = null;

private constructor() {}

private static async loadPyodideBuiltinPackageData(): Promise<
private static async loadPyodidePrebuiltPackageData(): Promise<
Record<string, PackageInfo>
> {
const url = makePyodideUrl("pyodide-lock.json");
Expand All @@ -25,10 +25,10 @@ export class PyodideBuiltinPackagesData {
return resJson.packages;
}

static async getInstance(): Promise<PyodideBuiltinPackagesData> {
static async getInstance(): Promise<PyodidePrebuiltPackagesData> {
if (this._instance == null) {
this._instance = new PyodideBuiltinPackagesData();
this._instance._data = await this.loadPyodideBuiltinPackageData();
this._instance = new PyodidePrebuiltPackagesData();
this._instance._data = await this.loadPyodidePrebuiltPackageData();
}
return this._instance;
}
Expand Down
22 changes: 14 additions & 8 deletions packages/desktop/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,25 @@ const createWindow = async () => {
);
return fsPromises.readFile(archiveFilePath);
});
ipcMain.handle("readRequirements", async (ev): Promise<string[]> => {
ipcMain.handle("readPrebuiltPackageNames", async (ev): Promise<string[]> => {
if (!isValidIpcSender(ev.senderFrame)) {
throw new Error("Invalid IPC sender");
}

const requirementsTxtPath = path.resolve(__dirname, "../requirements.txt");
const requirementsTxtData = await fsPromises.readFile(requirementsTxtPath, {
encoding: "utf-8",
});
return requirementsTxtData
const prebuiltPackagesTxtPath = path.resolve(
__dirname,
"../prebuilt-packages.txt"
);
const prebuiltPackagesTxtData = await fsPromises.readFile(
prebuiltPackagesTxtPath,
{
encoding: "utf-8",
}
);
return prebuiltPackagesTxtData
.split("\n")
.map((r) => r.trim())
.filter((r) => r.length > 0); // Assuming that the input `requirements.txt` file is generated by `dump-artifacts.js` and can be parsed with this simple logic.
.filter((r) => r.length > 0); // Assuming that the input file is generated by `dump-artifacts.js` so can be parsed with this simple logic.
});
ipcMain.handle(
"readStreamlitAppDirectory",
Expand All @@ -101,7 +107,7 @@ const createWindow = async () => {

mainWindow.on("closed", () => {
ipcMain.removeHandler("readSitePackagesSnapshot");
ipcMain.removeHandler("readRequirements");
ipcMain.removeHandler("readPrebuiltPackageNames");
ipcMain.removeHandler("readStreamlitAppDirectory");
});

Expand Down
3 changes: 2 additions & 1 deletion packages/desktop/electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export type AppConfig = typeof appConfig;
const archivesAPI = {
readSitePackagesSnapshot: () =>
ipcRenderer.invoke("readSitePackagesSnapshot"),
readRequirements: () => ipcRenderer.invoke("readRequirements"),
readPrebuiltPackageNames: () =>
ipcRenderer.invoke("readPrebuiltPackageNames"),
readStreamlitAppDirectory: () =>
ipcRenderer.invoke("readStreamlitAppDirectory"),
};
Expand Down
11 changes: 8 additions & 3 deletions packages/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ function App() {

Promise.all([
window.archivesAPI.readSitePackagesSnapshot(),
window.archivesAPI.readRequirements(),
window.archivesAPI.readPrebuiltPackageNames(),
window.archivesAPI.readStreamlitAppDirectory(),
]).then(
([sitePackagesSnapshotFileBin, requirements, streamlitAppFiles]) => {
([
sitePackagesSnapshotFileBin,
prebuiltPackageNames,
streamlitAppFiles,
]) => {
if (unmounted) {
return;
}
Expand All @@ -52,7 +56,8 @@ function App() {
},
},
archives: [],
requirements,
requirements: [],
prebuiltPackageNames,
mountedSitePackagesSnapshotFilePath,
pyodideUrl,
idbfsMountpoints: window.appConfig.idbfsMountpoints,
Expand Down
8 changes: 8 additions & 0 deletions packages/kernel/src/kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ export interface StliteKernelOptions {
*/
requirements: string[];

/**
* A list of prebuilt package names to be install at the booting-up phase via `pyodide.loadPackage()`.
* These packages basically can be installed via the `requirements` option,
* but some are only installable via this option such as `openssl`.
*/
prebuiltPackageNames: string[];

/**
* Files to mount.
*/
Expand Down Expand Up @@ -195,6 +202,7 @@ export class StliteKernel {
files: options.files,
archives: options.archives,
requirements: options.requirements,
prebuiltPackageNames: options.prebuiltPackageNames,
pyodideUrl: options.pyodideUrl,
wheels,
mountedSitePackagesSnapshotFilePath:
Expand Down
1 change: 1 addition & 0 deletions packages/kernel/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
files: Record<string, EmscriptenFile | EmscriptenFileUrl>;
archives: Array<PyodideArchive | PyodideArchiveUrl>;
requirements: string[];
prebuiltPackageNames: string[];
pyodideUrl?: string;
wheels?: {
stliteServer: string;
Expand Down Expand Up @@ -89,7 +90,7 @@
data: {
path: string;
data: string | ArrayBufferView;
opts?: Record<string, any>;

Check warning on line 93 in packages/kernel/src/types.ts

View workflow job for this annotation

GitHub Actions / test-kernel

Unexpected any. Specify a different type
};
}
export interface InMessageFileRename extends InMessageBase {
Expand Down Expand Up @@ -170,7 +171,7 @@
interface ReplyMessageBase {
type: string;
error?: Error;
data?: any;

Check warning on line 174 in packages/kernel/src/types.ts

View workflow job for this annotation

GitHub Actions / test-kernel

Unexpected any. Specify a different type
}
export interface ReplyMessageHttpResponse extends ReplyMessageBase {
type: "http:response";
Expand Down
6 changes: 6 additions & 0 deletions packages/kernel/src/worker-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
loadPyodide = pyodideModule.loadPyodide;
} else {
importScripts(pyodideUrl);
loadPyodide = (self as any).loadPyodide;

Check warning on line 31 in packages/kernel/src/worker-runtime.ts

View workflow job for this annotation

GitHub Actions / test-kernel

Unexpected any. Specify a different type
}
return loadPyodide({ ...loadPyodideOptions, indexURL: indexUrl });
}
Expand All @@ -55,7 +55,7 @@

let pyodide: Pyodide.PyodideInterface;

let httpServer: any;

Check warning on line 58 in packages/kernel/src/worker-runtime.ts

View workflow job for this annotation

GitHub Actions / test-kernel

Unexpected any. Specify a different type

const initDataPromiseDelegate = new PromiseDelegate<WorkerInitialData>();

Expand All @@ -80,6 +80,7 @@
files,
archives,
requirements,
prebuiltPackageNames: prebuiltPackages,
wheels,
mountedSitePackagesSnapshotFilePath,
pyodideUrl = defaultPyodideUrl,
Expand Down Expand Up @@ -209,6 +210,11 @@
// ===
// Also, this must be after restoring the snapshot because the snapshot may contain the site-packages.
postProgressMessage("Installing packages.");

console.debug("Installing the prebuilt packages:", prebuiltPackages);
await pyodide.loadPackage(prebuiltPackages);
console.debug("Installed the prebuilt packages");

await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
if (wheels) {
Expand Down Expand Up @@ -454,7 +460,7 @@

httpServer.start_websocket(
path,
(messageProxy: any, binary: boolean) => {

Check warning on line 463 in packages/kernel/src/worker-runtime.ts

View workflow job for this annotation

GitHub Actions / test-kernel

Unexpected any. Specify a different type
// XXX: Now there is no session mechanism

if (binary) {
Expand Down Expand Up @@ -502,8 +508,8 @@

const onResponse = (
statusCode: number,
_headers: any,

Check warning on line 511 in packages/kernel/src/worker-runtime.ts

View workflow job for this annotation

GitHub Actions / test-kernel

Unexpected any. Specify a different type
_body: any

Check warning on line 512 in packages/kernel/src/worker-runtime.ts

View workflow job for this annotation

GitHub Actions / test-kernel

Unexpected any. Specify a different type
) => {
const headers = _headers.toJs();
const body = _body.toJs();
Expand Down
20 changes: 19 additions & 1 deletion packages/mountable/src/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,12 @@ describe("parseMountOptions()", () => {
},
},
requirements: [],
prebuiltPackageNames: [],
archives: [],
});
});

it("fills `command`, `entrypoint`, and `requirements` fields and converts the `files` into the canonical form", () => {
it("fills `entrypoint`, and `requirements` fields and converts the `files` into the canonical form", () => {
const { kernelOptions } = parseMountOptions({
files: {
"streamlit_app.py": "foo",
Expand All @@ -82,6 +83,7 @@ describe("parseMountOptions()", () => {
expect(kernelOptions).toEqual({
entrypoint: "streamlit_app.py",
requirements: [],
prebuiltPackageNames: [],
files: {
"streamlit_app.py": {
data: "foo",
Expand Down Expand Up @@ -120,6 +122,7 @@ describe("parseMountOptions()", () => {
expect(kernelOptions).toEqual({
entrypoint: "streamlit_app.py",
requirements: [],
prebuiltPackageNames: [],
files: {},
archives: [
{
Expand Down Expand Up @@ -151,6 +154,7 @@ describe("parseMountOptions()", () => {
expect(kernelOptions).toEqual({
entrypoint: "foo.py",
requirements: [],
prebuiltPackageNames: [],
files: {
"streamlit_app.py": {
data: "foo",
Expand All @@ -166,6 +170,20 @@ describe("parseMountOptions()", () => {
});
expect(kernelOptions).toEqual({
requirements: ["matplotlib"],
prebuiltPackageNames: [],
entrypoint: "streamlit_app.py",
files: {},
archives: [],
});
});

it("preserves the `prebuiltPackageNames` option if specified", () => {
const { kernelOptions } = parseMountOptions({
prebuiltPackageNames: ["openssl"],
});
expect(kernelOptions).toEqual({
requirements: [],
prebuiltPackageNames: ["openssl"],
entrypoint: "streamlit_app.py",
files: {},
archives: [],
Expand Down
Loading
Loading