diff --git a/jupyterlite_xeus/add_on.py b/jupyterlite_xeus/add_on.py index 39f77da..72964ce 100644 --- a/jupyterlite_xeus/add_on.py +++ b/jupyterlite_xeus/add_on.py @@ -1,4 +1,5 @@ """a JupyterLite addon for creating the env for xeus kernels""" + import json import os from pathlib import Path @@ -18,7 +19,7 @@ SHARE_LABEXTENSIONS, UTF8, ) -from traitlets import Bool, List, Unicode +from traitlets import Bool, Callable, List, Unicode from .create_conda_env import ( create_conda_env_from_env_file, @@ -102,6 +103,13 @@ class XeusAddon(FederatedExtensionAddon): description="A comma-separated list of mount points, in the form : to mount in the wasm prefix", ) + package_url_factory = Callable( + None, + allow_none=True, + config=True, + description="Factory to generate package download URL from package metadata. This is used to load python packages from external host", + ) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.xeus_output_dir = Path(self.manager.output_dir) / "xeus" @@ -285,6 +293,9 @@ def pack_prefix(self, kernel_dir): else: pack_kwargs["file_filters"] = pkg_file_filter_from_yaml(DEFAULT_CONFIG_PATH) + if self.package_url_factory is not None: + pack_kwargs["package_url_factory"] = self.package_url_factory + pack_env( env_prefix=self.prefix, relocate_prefix="/", @@ -342,7 +353,8 @@ def pack_prefix(self, kernel_dir): # Pack JupyterLite content if enabled # If we only build a voici output, mount jupyterlite content into the kernel by default if self.mount_jupyterlite_content or ( - list(self.manager.apps) == ["voici"] and self.mount_jupyterlite_content is None + list(self.manager.apps) == ["voici"] + and self.mount_jupyterlite_content is None ): contents_dir = self.manager.output_dir / "files" diff --git a/src/index.ts b/src/index.ts index 44d2864..459e78d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { IBroadcastChannelWrapper } from '@jupyterlite/contents'; import { IKernel, IKernelSpecs } from '@jupyterlite/kernel'; import { WebWorkerKernel } from './web_worker_kernel'; +import { IEmpackEnvMetaFile } from './tokens'; function getJson(url: string) { const json_url = URLExt.join(PageConfig.getBaseUrl(), url); @@ -29,59 +30,89 @@ try { throw err; } -const plugins = kernel_list.map((kernel): JupyterLiteServerPlugin => { - return { - id: `@jupyterlite/xeus-${kernel}:register`, - autoStart: true, - requires: [IKernelSpecs], - optional: [IServiceWorkerManager, IBroadcastChannelWrapper], - activate: ( - app: JupyterLiteServer, - kernelspecs: IKernelSpecs, - serviceWorker?: IServiceWorkerManager, - broadcastChannel?: IBroadcastChannelWrapper - ) => { - // Fetch kernel spec - const kernelspec = getJson('xeus/kernels/' + kernel + '/kernel.json'); - kernelspec.name = kernel; - kernelspec.dir = kernel; - for (const [key, value] of Object.entries(kernelspec.resources)) { - kernelspec.resources[key] = URLExt.join( - PageConfig.getBaseUrl(), - value as string - ); - } - - const contentsManager = app.serviceManager.contents; - - kernelspecs.register({ - spec: kernelspec, - create: async (options: IKernel.IOptions): Promise => { - const mountDrive = !!( - (serviceWorker?.enabled && broadcastChannel?.enabled) || - crossOriginIsolated +const plugins = kernel_list.map( + (kernel): JupyterLiteServerPlugin => { + return { + id: `@jupyterlite/xeus-${kernel}:register`, + autoStart: true, + requires: [IKernelSpecs], + optional: [ + IServiceWorkerManager, + IBroadcastChannelWrapper, + IEmpackEnvMetaFile + ], + activate: ( + app: JupyterLiteServer, + kernelspecs: IKernelSpecs, + serviceWorker?: IServiceWorkerManager, + broadcastChannel?: IBroadcastChannelWrapper, + empackEnvMetaFile?: IEmpackEnvMetaFile + ) => { + // Fetch kernel spec + const kernelspec = getJson('xeus/kernels/' + kernel + '/kernel.json'); + kernelspec.name = kernel; + kernelspec.dir = kernel; + for (const [key, value] of Object.entries(kernelspec.resources)) { + kernelspec.resources[key] = URLExt.join( + PageConfig.getBaseUrl(), + value as string ); + } - if (mountDrive) { - console.info( - `${kernelspec.name} contents will be synced with Jupyter Contents` - ); - } else { - console.warn( - `${kernelspec.name} contents will NOT be synced with Jupyter Contents` + const contentsManager = app.serviceManager.contents; + kernelspecs.register({ + spec: kernelspec, + create: async (options: IKernel.IOptions): Promise => { + const mountDrive = !!( + (serviceWorker?.enabled && broadcastChannel?.enabled) || + crossOriginIsolated ); + + if (mountDrive) { + console.info( + `${kernelspec.name} contents will be synced with Jupyter Contents` + ); + } else { + console.warn( + `${kernelspec.name} contents will NOT be synced with Jupyter Contents` + ); + } + const link = empackEnvMetaFile + ? await empackEnvMetaFile.getLink(kernelspec) + : ''; + + return new WebWorkerKernel({ + ...options, + contentsManager, + mountDrive, + kernelSpec: kernelspec, + empackEnvMetaLink: link + }); } + }); + } + }; + } +); - return new WebWorkerKernel({ - ...options, - contentsManager, - mountDrive, - kernelSpec: kernelspec - }); - } - }); - } - }; -}); +const empackEnvMetaPlugin: JupyterLiteServerPlugin = { + id: '@jupyterlite/xeus:empack-env-meta', + autoStart: true, + provides: IEmpackEnvMetaFile, + activate: (): IEmpackEnvMetaFile => { + return { + getLink: async (kernelspec: Record) => { + const kernelName = kernelspec.name; + const kernel_root_url = URLExt.join( + PageConfig.getBaseUrl(), + `xeus/kernels/${kernelName}` + ); + return `${kernel_root_url}`; + } + }; + } +}; + +plugins.push(empackEnvMetaPlugin); export default plugins; diff --git a/src/tokens.ts b/src/tokens.ts index bf41bdc..1c222b5 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -12,6 +12,7 @@ import { } from '@jupyterlite/contents'; import { IWorkerKernel } from '@jupyterlite/kernel'; +import { Token } from '@lumino/coreutils'; /** * An interface for Xeus workers. @@ -85,5 +86,17 @@ export namespace IXeusWorkerKernel { baseUrl: string; kernelSpec: any; mountDrive: boolean; + empackEnvMetaLink?: string; } } + +export interface IEmpackEnvMetaFile { + /** + * Get empack_env_meta link. + */ + getLink: (kernelspec: Record) => Promise; +} + +export const IEmpackEnvMetaFile = new Token( + '@jupyterlite/xeus:IEmpackEnvMetaFile' +); diff --git a/src/web_worker_kernel.ts b/src/web_worker_kernel.ts index c0b34cd..719af94 100644 --- a/src/web_worker_kernel.ts +++ b/src/web_worker_kernel.ts @@ -29,17 +29,24 @@ export class WebWorkerKernel implements IKernel { * @param options The instantiation options for a new WebWorkerKernel */ constructor(options: WebWorkerKernel.IOptions) { - const { id, name, sendMessage, location, kernelSpec, contentsManager } = - options; + const { + id, + name, + sendMessage, + location, + kernelSpec, + contentsManager, + empackEnvMetaLink + } = options; this._id = id; this._name = name; this._location = location; this._kernelSpec = kernelSpec; this._contentsManager = contentsManager; this._sendMessage = sendMessage; + this._empackEnvMetaLink = empackEnvMetaLink; this._worker = this.initWorker(options); this._remoteKernel = this.initRemote(options); - this.initFileSystem(options); } @@ -99,11 +106,13 @@ export class WebWorkerKernel implements IKernel { }; remote = wrap(this._worker) as Remote; } + remote .initialize({ kernelSpec: this._kernelSpec, baseUrl: PageConfig.getBaseUrl(), - mountDrive: options.mountDrive + mountDrive: options.mountDrive, + empackEnvMetaLink: this._empackEnvMetaLink }) .then(this._ready.resolve.bind(this._ready)); @@ -290,6 +299,7 @@ export class WebWorkerKernel implements IKernel { | undefined = undefined; private _parent: KernelMessage.IMessage | undefined = undefined; private _ready = new PromiseDelegate(); + private _empackEnvMetaLink: string | undefined; } /** @@ -303,5 +313,6 @@ export namespace WebWorkerKernel { contentsManager: Contents.IManager; mountDrive: boolean; kernelSpec: any; + empackEnvMetaLink?: string | undefined; } } diff --git a/src/worker.ts b/src/worker.ts index 60c91b4..4d47d06 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -103,7 +103,7 @@ export class XeusRemoteKernel { } async initialize(options: IXeusWorkerKernel.IOptions): Promise { - const { baseUrl, kernelSpec } = options; + const { baseUrl, kernelSpec, empackEnvMetaLink } = options; // location of the kernel binary on the server const binary_js = URLExt.join(baseUrl, kernelSpec.argv[0]); const binary_wasm = binary_js.replace('.js', '.wasm'); @@ -125,10 +125,9 @@ export class XeusRemoteKernel { // This function is usually implemented in the pre/post.js // in the emscripten build of that kernel if (globalThis.Module['async_init'] !== undefined) { - const kernel_root_url = URLExt.join( - baseUrl, - `xeus/kernels/${kernelSpec.dir}` - ); + const kernel_root_url = empackEnvMetaLink + ? empackEnvMetaLink + : URLExt.join(baseUrl, `xeus/kernels/${kernelSpec.dir}`); const pkg_root_url = URLExt.join(baseUrl, 'xeus/kernel_packages'); const verbose = true; await globalThis.Module['async_init'](