Fichiers manifest de MicroPython

Résumé

MicroPython propose une fonctionnalité qui permet de « figer » (freeze) du code Python dans le micrologiciel, en alternative au chargement du code depuis le système de fichiers.

Cela présente les avantages suivants :

  • le code est précompilé en bytecode, ce qui évite d’avoir à compiler les sources Python au moment du chargement.

  • le bytecode peut être exécuté directement depuis la ROM (c.-à-d. la mémoire flash) plutôt que d’être copié en RAM. De même, tous les objets constants (chaînes, tuples, etc.) sont également chargés depuis la ROM. Cela peut libérer beaucoup plus de mémoire pour votre application.

  • sur les appareils dépourvus de système de fichiers, c’est la seule façon de charger du code Python.

Pendant le développement, le figement (freezing) n’est généralement pas recommandé car il ralentit considérablement votre cycle de développement, chaque mise à jour nécessitant de reflasher l’intégralité du micrologiciel. Il peut toutefois rester utile de figer sélectivement certaines dépendances qui changent rarement (comme des bibliothèques tierces).

La façon de lister les fichiers Python à figer dans le micrologiciel passe par un « manifest », c’est-à-dire un fichier Python qui sera interprété par le processus de compilation. En général, vous écririez un fichier manifest dans le cadre d’une définition de carte, mais vous pouvez aussi écrire un fichier manifest autonome et l’utiliser avec une définition de carte existante.

Les fichiers manifest peuvent définir des dépendances vis-à-vis de bibliothèques de micropython-lib, ainsi que de fichiers Python du système de fichiers, et également d’autres fichiers manifest.

Écriture de fichiers manifest

Un fichier manifest est un fichier Python contenant une série d’appels de fonctions. Voir les fonctions disponibles définies ci-dessous.

Tous les chemins utilisés dans les fichiers manifest peuvent inclure les variables suivantes. Elles se résolvent toutes en chemins absolus.

  • $(MPY_DIR) – chemin vers le dépôt micropython.

  • $(MPY_LIB_DIR) – chemin vers le sous-module micropython-lib. Préférez utiliser require().

  • $(PORT_DIR) – chemin vers le port courant (par ex. ports/stm32)

  • $(BOARD_DIR) – chemin vers la carte courante (par ex. ports/stm32/boards/OPENMV4)

Les fichiers manifest personnalisés ne devraient pas se trouver dans le dépôt principal de MicroPython. Vous devriez les conserver dans le système de gestion de versions avec le reste de votre projet.

En général, un manifest utilisé pour compiler le micrologiciel devra inclure le manifest du port, lequel peut inclure des modules figés requis pour le fonctionnement de la carte. Si vous voulez simplement ajouter des modules supplémentaires à une carte existante, alors incluez le manifest de la carte (qui inclura à son tour le manifest du port).

Compilation avec un manifest personnalisé

Votre manifest peut être spécifié sur la ligne de commande make avec :

$ make BOARD=MYBOARD FROZEN_MANIFEST=/path/to/my/project/manifest.py

Cela s’applique à tous les ports, y compris ceux basés sur CMake (par ex. rp2), car le wrapper Makefile transmettra cette valeur à la compilation CMake.

Ajout d’un manifest à une définition de carte

Si vous disposez d’une définition de carte personnalisée, vous pouvez faire en sorte qu’elle inclue automatiquement votre manifest personnalisé. Sur les ports basés sur make (la plupart des ports), définissez la variable FROZEN_MANIFEST dans votre mpconfigboard.mk.

FROZEN_MANIFEST ?= $(BOARD_DIR)/manifest.py

Sur les ports basés sur CMake (par ex. rp2), utilisez plutôt mpconfigboard.cmake

set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py)

Fonctions de haut niveau

Ce sont les fonctions que vous utiliserez normalement. Elles ajoutent du code à l’ensemble qui est précompilé en bytecode et figé dans l’image du micrologiciel :

  • module et package figent vos sources locales personnelles — respectivement un fichier unique ou un répertoire de package entier.

  • require fige un package publié (et ses dépendances) de micropython-lib, par son nom.

  • include intègre un autre manifest afin que ses modules figés soient eux aussi ajoutés.

  • add_library et metadata sont des fonctions de support (enregistrement de chemins de recherche supplémentaires pour require, et déclaration des métadonnées de package).

Un manifest de micrologiciel typique commence par includer le manifest du port ou de la carte (afin que les modules dont la carte a besoin restent figés), puis ajoute ses propres lignes module/package/require.

Note : L’argument nommé opt peut être défini sur les différentes fonctions ; il contrôle le niveau d’optimisation utilisé par le compilateur croisé. Voir micropython.opt_level().

add_library(library, library_path, prepend=False)

Enregistre le chemin vers une library externe nommée.

Utilisez ceci lorsque vous voulez que require résolve les packages depuis un répertoire autre que micropython-lib — par exemple votre propre collection de pilotes, ou une copie de travail d’une bibliothèque tierce.

Le chemin library_path sera automatiquement recherché lors de l’utilisation de require. Par défaut, la bibliothèque ajoutée est placée à la fin de la liste des bibliothèques à parcourir. Passez True pour préfixer, c’est-à-dire l’ajouter au début de la liste.

De plus, la bibliothèque ajoutée peut être demandée explicitement en utilisant require("name", library="library").

package(package_path, files=None, base_path='.', opt=None)

Fige un package entier — un répertoire de fichiers .py (éventuellement avec des sous-packages) — afin qu’il puisse être importé avec import <package>. Utilisez plutôt module pour un fichier autonome unique.

Cela équivaut à copier le répertoire « package_path » sur l’appareil (sauf qu’il s’agit de code figé).

Dans le cas le plus simple, pour figer un package « foo » dans le répertoire courant :

package("foo")

inclura récursivement tous les fichiers .py de foo, qui seront figés sous la forme foo/**/*.py.

Si le package ne se trouve pas dans le même répertoire que le fichier manifest, utilisez base_path :

package("foo", base_path="path/to/libraries")

Vous pouvez utiliser les variables ci-dessus, comme $(PORT_DIR), dans base_path.

Pour restreindre à certains fichiers du package, utilisez files (note : les chemins doivent être relatifs au package) : package("foo", files=["bar/baz.py"]).

module(module_path, base_path='.', opt=None)

Fige un fichier .py autonome unique afin qu’il puisse être importé par son nom (module("foo.py") fait fonctionner import foo). Utilisez package pour un répertoire/package.

Si le fichier est dans le répertoire courant :

module("foo.py")

Sinon, utilisez base_path pour localiser le fichier :

module("foo.py", base_path="src/drivers")

Vous pouvez utiliser les variables ci-dessus, comme $(PORT_DIR), dans base_path.

require(name, library=None)

Requiert un package par son nom (et ses dépendances) depuis micropython-lib.

C’est ainsi que les extensions de la bibliothèque standard et les pilotes communautaires sont figés : le package nommé est récupéré depuis le sous-module micropython-lib et figé avec tout ce dont il dépend. Utilisez plutôt module ou package pour figer vos propres sources plutôt qu’un package publié.

Spécifiez éventuellement library (une chaîne) pour référencer un package d’une bibliothèque préalablement enregistrée avec add_library. Sinon, la liste des chemins de bibliothèques sera utilisée.

include(manifest_path)

Inclut un autre manifest. C’est ainsi que les manifests sont composés : un manifest de micrologiciel personnalisé devrait include le manifest du port (ou de la carte) afin que les modules dont la carte a besoin restent figés, puis ajouter ses propres entrées.

En général, un manifest utilisé pour compiler le micrologiciel devra inclure le manifest du port, lequel peut inclure des modules figés requis pour le fonctionnement de la carte.

L’argument manifest peut être une chaîne (nom de fichier) ou un itérable de chaînes.

Les chemins relatifs sont résolus par rapport au fichier manifest courant.

Si le chemin pointe vers un répertoire, alors il inclut implicitement le fichier manifest.py situé dans ce répertoire.

Vous pouvez utiliser les variables ci-dessus, comme $(PORT_DIR), dans manifest_path.

metadata(description=None, version=None, license=None, author=None)

Définit les métadonnées de ce fichier manifest. Ceci est utile pour les manifests des packages micropython-lib.

Ces champs sont consommés lorsqu’un package est publié sur / installé depuis micropython-lib via mip ; ils ne sont pas nécessaires dans un manifest de micrologiciel de carte.

Fonctions de bas niveau

Ces fonctions sont documentées par souci d’exhaustivité, mais à l’exception de freeze_as_str, toutes les fonctionnalités sont accessibles via les fonctions de haut niveau.

Les fonctions freeze* ne diffèrent que par la manière dont le code est stocké :

  • freeze_as_mpy / freeze_mpy stockent du bytecode précompilé (.mpy) en mémoire flash. Le code s’exécute directement depuis la mémoire flash, utilise un minimum de RAM et s’importe rapidement. C’est ce qu’utilisent module, package et require en interne.

  • freeze_as_str fige à la place les sources Python, qui sont compilées en bytecode au moment de l’importation (utilisant de la RAM et nécessitant le compilateur embarqué). C’est la seule capacité non exposée par les fonctions de haut niveau, ce qui explique l’exception mentionnée ci-dessus.

freeze(path, script=None, opt=0)

La primitive sous-jacente sur laquelle s’appuient les fonctions de haut niveau ; préférez ces dernières. Fige l’entrée spécifiée par path, en déterminant automatiquement son type. Un script .py sera d’abord compilé en .mpy puis figé, et un fichier .mpy sera figé directement.

path doit être un répertoire, qui est le répertoire de base à partir duquel commencer la recherche de fichiers. Lors de l’importation des modules figés résultants, le nom du module commence après path, c’est-à-dire que path est exclu du nom du module.

Si path est relatif, il est résolu par rapport au manifest.py courant.

Si script vaut None, tous les fichiers de path seront figés.

Si script est un itérable, alors freeze() est appelée sur tous les éléments de l’itérable (avec les mêmes path et opt transmis).

Si script est une chaîne, alors elle spécifie le fichier ou le répertoire à figer, et peut inclure des répertoires supplémentaires avant le fichier ou le dernier répertoire. Le fichier ou le répertoire sera recherché dans path. Si script est un répertoire, alors tous les fichiers de ce répertoire seront figés.

opt est le niveau d’optimisation à transmettre à mpy-cross lors de la compilation de .py en .mpy. Ces niveaux sont décrits dans micropython.opt_level().

freeze_as_str(path)

Fige le path donné et tous les scripts .py qu’il contient sous forme de chaîne, qui sera compilée lors de l’importation. N’utilisez ceci que lorsque le code figé doit rester sous forme de sources Python ; cela coûte de la RAM au moment de l’importation par rapport aux variantes .mpy.

freeze_as_mpy(path, script=None, opt=0)

Fige l’entrée en compilant d’abord les scripts .py en fichiers .mpy, puis en figeant les fichiers .mpy résultants. C’est ce que font module et package en coulisses. Voir freeze() pour plus de détails sur les arguments.

freeze_mpy(path, script=None, opt=0)

Fige l’entrée, qui doit être des fichiers .mpy figés directement (sans étape de compilation). Voir freeze() pour plus de détails sur les arguments.

Exemples

Pour figer un fichier unique du répertoire courant qui sera disponible via import mydriver, utilisez :

module("mydriver.py")

Pour figer un répertoire de fichiers dans un sous-répertoire « mydriver » du répertoire courant qui sera disponible via import mydriver, utilisez :

package("mydriver")

Pour figer la bibliothèque « hmac » de micropython-lib, utilisez :

require("hmac")

Un exemple plus complet d’un fichier manifest.py personnalisé (pour une carte ayant son propre manifest par défaut) est :

# Include the board's default manifest.
include("$(BOARD_DIR)/manifest.py")
# Add a custom driver
module("mydriver.py")
# Add aiorepl from micropython-lib
require("aiorepl")

La carte peut alors être compilée avec

$ cd ports/stm32
$ make BOARD=MYBOARD FROZEN_MANIFEST=~/src/myproject/manifest.py

Notez que la plupart des cartes n’ont pas leur propre manifest.py ; elles utilisent plutôt directement celui du port, auquel cas votre manifest devrait simplement faire include("$(PORT_DIR)/boards/manifest.py") à la place.