Intégration de Basthon
CapyCode exécute Python, SQL et OCaml avec les noyaux Basthon officiels, installés depuis npm (@basthon/kernel-python3, kernel-sql, kernel-ocaml, tous à la même version), et utilisés tels quels : CapyCode n'hérite d'aucune classe Basthon et ne modifie pas leur comportement.
Isolation
Chaque noyau tourne dans le worker « sandboxé » de Basthon : un worker lancé depuis une URL data:, donc d'origine opaque. Le code de l'élève ne peut lire ni l'IndexedDB de l'IDE (projet, dépôt Git), ni son localStorage, ni ses cookies. Ses requêtes réseau vers les fichiers du noyau passent par un petit worker proxy, qui n'autorise que ce dossier.
En pointillés rouges : ce qui s'exécute hors de l'origine de l'application.
Aucun noyau ne tourne en mode « legacy » (dans la page).
Publication des noyaux : vite-plugin-basthon.ts
Les paquets Basthon sont prévus pour webpack. Chaque noyau crée son worker avec new Worker(new URL('./comlink-worker.js', import.meta.url), { sandboxed: true }) : ce worker est un script classique chargé par importScripts depuis l'URL data:, et il cherche ses fichiers dans <son dossier>/<version>/<langage>/. Vite ne sait produire ni des workers classiques depuis les dépendances en développement, ni un import.meta.url utilisable dans un worker sandboxé.
Le plugin reproduit donc la disposition webpack, noyau par noyau, avec esbuild :
basthon/<dossier>/<langage>/kernel.js classes du thread principal (module ES)
basthon/<dossier>/<langage>/comlink-worker.js worker du noyau (script classique)
basthon/<dossier>/<langage>/comlink-proxy.js proxy réseau du worker isolé
basthon/<dossier>/python3/<version>/python3/… lib/dist du paquet (basthon.tar, pyodide-lock.json, modules)
basthon/<dossier>/sql/sql-wasm.wasm
basthon/<dossier>/ocaml/__kernel__.jsLe <dossier> vaut <version de Basthon>-<empreinte> au build (par exemple 0.88.0-1348febd), l'empreinte étant calculée sur tous les fichiers publiés ; il vaut dev en développement. Une nouvelle version des noyaux, ou un changement de leur construction, change donc leurs URL : le service worker et les navigateurs les gardent en cache sans jamais servir une version périmée (voir Déploiement). Le plugin le transmet à l'application et au service worker par la constante __BASTHON_DIR__ (define de Vite, déclarée dans src/build-constants.d.ts).
- En développement, ces fichiers sont construits en mémoire au démarrage et servis par le serveur de Vite, avec un vrai 404 pour les fichiers absents.
- Au build, ils sont écrits dans
dist/basthon/<dossier>/. - L'application charge
basthon/<dossier>/<noyau>/kernel.jspar import dynamique (createKernel,src/services/kernels.ts) : Vite ne voit jamais le code des noyaux. Le langage qui utilise chaque noyau est décrit dans le registre des langages.
Mettre Basthon à jour revient à changer la version des paquets @basthon/* : rien n'est copié dans le dépôt.
Pyodide n'est pas embarqué : le noyau Python le charge depuis cdn.basthon.fr (jsDelivr en secours), comme les autres interfaces Basthon.
Démarrage rapide : l'instantané mémoire de Pyodide
Depuis Basthon 0.84, le noyau Python prend un instantané (snapshot) de la mémoire de Pyodide juste après l'avoir chargé, avant Basthon et avant toute évaluation. Les démarrages suivants restaurent cet instantané au lieu de réinitialiser Python : chaque python …, chaque exécution et chaque prompt démarre un noyau neuf en ~1 s au lieu de ~4 s. L'instantané (~20 Mo) est gardé dans l'IndexedDB de la page (base basthon-python3), d'une visite à l'autre ; seule la toute première visite paie le démarrage complet.
CapyCode n'a rien à faire pour en profiter, mais deux choix en découlent :
- Une instance de noyau par démarrage.
KernelSession.restart()crée une nouvelle instance plutôt que d'appelerkernel.restart(): Basthon rejoue au redémarrage les fichiers déjà donnés au noyau, ce qui ferait réapparaître des fichiers supprimés du workspace. La nouvelle instance relit l'instantané dans IndexedDB (quelques dizaines de millisecondes). - L'instantané est dans l'origine de CapyCode, à côté du workspace :
purgeWorkspacene l'efface pas, et il est partagé par le terminal, les prompts et le bouton Exécuter. Basthon le vérifie (version et build de Pyodide) et le refait s'il ne correspond plus.
Sessions et exécution
src/services/runtime.ts définit la KernelSession : au plus un noyau démarré, plus le routage de ses événements. Basthon diffuse ses événements (eval.output, eval.display, eval.input) sur tout le document : chaque session marque ses évaluations et ne traite que les siennes, puis les transmet à la console qui l'affiche.
Le store runtime tient les sessions dans un SessionPool (même fichier), par clé : le langage pour un prompt, terminal:<id> pour un terminal. Il orchestre l'exécution et confie l'affichage au store console (revealPrompt, promptIO, graphicsSink, appendResult) : où vont les sorties, quand ouvrir une console graphique ou déplier le panneau.
| Action | Session | Noyau |
|---|---|---|
Bouton Exécuter (Ctrl+Entrée) | celle du prompt du langage | neuf à chaque exécution ; le prompt garde l'état final, comme python -i |
| Ligne saisie dans un prompt | celle du prompt (un par langage) | persistant |
python … dans un terminal | celle de ce terminal (une par terminal) | neuf à chaque commande, comme un processus |
python seul dans un terminal | celle du prompt Python | bascule vers le prompt Python (ouvert sur un noyau neuf s'il n'existe pas encore) |
L'interpréteur interactif
L'interpréteur interactif de CapyCode est le prompt Python (la « console Python » du panneau inférieur), pas le terminal. python sans programme y bascule (runtimeStore.showPrompt) : c'est la seule commande du terminal qui fait quitter le terminal. Les programmes lancés par python … écrivent dans le terminal, à la suite de la commande, et leur état n'est pas repris par le prompt (noyaux distincts). python -i script.py exécute le script dans le terminal et rappelle que l'interpréteur est le prompt ; c'est le bouton ▶ Exécuter qui laisse le prompt sur l'état final d'un programme.
Ce que demande chaque langage est décrit par son runtime (voir Langages) : copier les fichiers du workspace, afficher ou non le résultat d'un script, le mettre en forme. Pour un script Python, un prélude (pythonScriptPrelude) reproduit python <script> <args> : os.chdir, sys.argv, dossier du script en tête de sys.path, __file__. Les fichiers du workspace sont copiés en binaire dans le système de fichiers du noyau, ce qui permet les imports locaux (import utils) et la lecture de fichiers de données.
Entrées et sorties
- Sorties : au fil de l'eau, dans le prompt ou le terminal.
input(): la session reçoiteval.input, la console passe en mode saisie, la réponse est renvoyée au noyau. Ctrl+C l'interrompt (KeyboardInterrupt) ; Ctrl+D sur une saisie vide signale la fin des entrées (END_OF_INPUTde Basthon) etinput()lèveEOFError, comme dans un vrai terminal.- Messages d'erreur : un script est évalué sous son nom (option
filenamedeevalAsync,scriptFilename) : le chemin du fichier pour le bouton Exécuter etpython fichier.py,<string>pourpython -c, comme CPython. Les tracebacks montrent doncFile "/workspace/main.py", line 3au lieu d'un nom interne (<basthon-input-…>), que gardent les lignes saisies dans la console. - Arrêter un programme (boucle infinie) : Basthon ne sait pas interrompre un calcul ; la session arrête le noyau, ce qui termine proprement l'évaluation en cours.
- Sorties riches (
eval.display) :KernelOutput.vueles affiche comme l'interface Basthon. Le PNG va dans une image ; tout contenu actif (SVG de turtle, HTML, iframes dynamiques, canevas matplotlib ou p5, fenêtres tkinter) va dans une iframe sandboxée sansallow-same-origin. - Caméra (webcam de p5) : Basthon demande l'accord de l'utilisateur à chaque demande du code, par
window.confirmsi l'interface ne fournit pas sa fenêtre. La session remplace donckernel.confirmCamera, prévu par Basthon pour cela, par la fonction que lui donne leSessionPool: le storeruntimey passe une fenêtredialogs.confirmtraduite. Sans elle, la caméra est refusée. - Iframes du noyau (
kernel-iframe: matplotlib, p5, tkinter) : elles dialoguent avec le worker par un port, et avec l'interface par un second port (« gui ») qui sert à leur taille et aux messages que le worker attend (la réponse à un dialogue tkinter, transmise parpostIFrameMessage, car le worker ne lit pas son propre port pendant qu'il attend). Un port ne se transmet qu'une fois : les consoles graphiques restent donc montées quand on change de console (v-show), sinon l'iframe recréée serait coupée du noyau.
Déploiement
- Pas de COOP/COEP, comme la console Basthon de Capytale : sans
SharedArrayBuffer, lesinput()passent par l'API réseau de Basthon. Si COEP est activé un jour, les fichiersbasthon/devront être servis avecCross-Origin-Resource-Policy: cross-origin, car les workers isolés ont une origine opaque. - Le serveur doit répondre 404 aux fichiers absents sous
basthon/: le noyau Python sonde un Pyodide local avant de se rabattre surcdn.basthon.fr. - Hors-ligne : les fichiers
basthon/chargés par la page sont mis en cache par le service worker à leur première utilisation (cachebasthon-<dossier>, les précédents sont supprimés) ; ceux que charge le worker isolé (comlink-worker.js, Pyodide) ne passent pas par lui.
Tests
Les vrais noyaux ne tournent pas sous Vitest. tests/helpers/fake-kernel.ts reproduit le contrat utilisé par CapyCode (événements sur le document, evalAsync, more, complete, putFile, eval.input) ; les tests vérifient la logique de CapyCode, pas Basthon. Toute modification de l'intégration se vérifie aussi dans le navigateur.
Évolutions possibles côté Basthon
- Nom de fichier des tracebacks : Basthon nomme chaque évaluation
<basthon-input-…>; une option deevalpermettrait d'afficher le chemin du script. - Interruption d'un calcul sans redémarrer le noyau.