Introduction
Yocto est un outil qui permet de faciliter la conception de distributions Linux personnalisées et embarquées. Il est construit comme une fédération de logiciels qui tendent tous vers le même but : fournir un environnement de développement système complet et modulaire. L'une des clés de cette modularité est un programme appelé Bitbake, qui fait l'objet par ailleurs d'un article spécifique sur ce même blog. C'est justement de cette modularité dont nous allons parler dans cet article. Nous allons présenter ici les différentes méthodes d'intégration d'un paquet au sein d'un environnement Yocto. Une fois l'environnement Yocto bien configuré, il sera possible de construire le paquet et de le cross-compiler pour qu'il soit intégré sur l'image de la machine cible (à condition bien sur que l'architecture soit supportée, et que les dépendances du paquet soient satisfaites).
Dans la suite de cet article, nous appelons paquet un ensemble de programmes qui fonctionnent ensemble pour fournir une fonctionnalité. Par exemple : le paquet Gstreamer fournit un ensemble de programmes permettant, entre autres, de lire des flux vidéo.
Gestion des paquets sous Yocto
Avant de discuter des différentes méthodes d'ajout de paquet, nous devons introduire la notion de couches fonctionnelles : les layers. Un projet Yocto est constitué d'un ensemble de couches fonctionnelles. Chaque couche fonctionnelle, chaque layer représente une fonctionnalité du futur système embarqué. Nous trouverons ainsi, par exemple, un layer qui gère les interfaces graphiques et qui apporte les fonctionnalités de framework comme Qt ou GTK+, tandis qu'un autre layer apportera les fonctionnalités pour gérer une cible matérielle spécifique.
Concrètement, chacune des couches fonctionnelles sera représentée par un dossier qui contient l'ensemble des paquets embarqués. Chacun de ces paquets sera de fait constitué d'une recette, et / ou de fragments, indiquant à Bitbake comment construire, installer et compiler le programme ou l'ensemble de programmes de notre paquet.
Ainsi, pour chacun des paquets que nous souhaitons intégrer, il sera nécessaire de savoir dans quel groupe de fonctionnalités l'intégrer et si il est nécessaire de créer un nouveau layer, etc.
Comment cela se passe-t-il ?
cd ~/poky bitbake-layers create-layer meta-newfunctionnality bitbake-layers add-layer meta-newfunctionnality
Depuis la racine du répertoire Yocto, nous ajoutons un nouveau layer avec les commandes fournies par Bitbake. La première commande permet de créer la structure du layer tandis que la seconde commande ajoute le nouveau layer dans le fichier de configuration adéquat pour qu'il soit pris en compte lors de la prochaine compilation. Dès que le layer existe, il est possible d'ajouter à l'intérieur de nouveaux paquets. Ce sont les différentes méthodes d'ajouts de paquets que nous allons voir dans les sections suivantes.
Utilisation d'une recette extérieure
Lorsqu'un projet nécessite l'intégration d'un nouveau paquet, la façon de faire la plus rapide est d'intégrer dans la couche fonctionnelle un fichier de recette standard qui contiendra les informations nécessaires à la construction du paquet et à son installation. Le site suivant recense un certain nombre des recettes les plus utilisées : https://layers.openembedded.org/layerindex/branch/master/recipes/
Il suffit alors de télécharger la recette souhaitée, et de placer le fichier au bon endroit dans la couche fonctionnelle. Il ne reste donc plus qu'à vérifier son contenu, éventuellement corriger des variables d'environnement pour satisfaire les exigences du projet en cours, puis de lancer le build avec Bitbake (cf article lié).
Rédaction d'une recette & gestion des outils de builds
Nous parlons du cas où le paquet désiré ne possède pas encore de recette standard. La méthode courante est de chercher une recette d'un paquet très proche fonctionnellement parlant, afin de l'adapter à notre projet en cours. Ce processus peut être long et fastidieux, et lorsqu'on parle d'un paquet suffisamment simple, il est plus pertinent et plus rapide de réécrire une recette à la main. Avant de vous lancer dans tel processus, lisez d'abord le chapitre suivant, qui décrit une méthode permettant d'automatiser en partie ce processus.
Voyons comment écrire une recette simple pour un nouveau paquet. Le cas courant est celui d'un paquet développé par une équipe de développeurs, qui souhaitent ensuite l'intégrer dans une distribution Yocto.
A l'intérieur du layer généré, il faut placer un dossier qui portera le même nom que notre recette, et à l'intérieur de celui-ci, le futur fichier de recette.
Le plus simple est d'utiliser une recette minimaliste et générique, du type de celle présentée ci dessous:
DESCRIPTION = "Recette basique pour paquet simple" LICENSE = "GPL" LIC_FILES_CHKSUM = "file://LICENSE;md5=XXXXXXXXXXXXXXXXXXXXXXXXXXXX" SRC_URI = "http://XXXXXXXXXXXXXXXXXXXXXXXXXXXX-Y.Z.tar.gz" do_install() { oe_runmake install DESTDIR=${D} } SRC_URI[md5sum] = "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
Nous voyons ici qu'une recette s'écrit en mettant à jour un certain nombre de variables, lesquelles seront lues et interprétées par Bitbake lors du processus de compilation. L'ensemble des variables disponibles, et la documentation, sont accessibles dans le manuel du projet Yocto en source de cet article.
Yocto permet également d'employer dans les recettes écrites un système d'héritage permettant de tenir compte du système de build utilisé dans la recette. Pour un paquet construit par CMake, nous aurions une recette de type :
DESCRIPTION = "Recette basique pour paquet simple construit avec CMake" LICENSE = "GPL" LIC_FILES_CHKSUM = "file://LICENSE;md5=XXXXXXXXXXXXXXXXXXXXXXXXXXXX" SRC_URI = "http://XXXXXXXXXXXXXXXXXXXXXXXXXXXX-Y.Z.tar.gz" inherit cmake SRC_URI[md5sum] = "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
Observez que nous n'avons plus besoin de définir une étape d'installation ou du compilation car ces étapes sont gérées par CMake. Le même type de mécanisme peut être utilisé pour d'autres outils tel que Autotools.
Devtools
Devtools est un ensemble d'outils intégré au projet Yocto permettant d'automatiser et d'accélérer la réalisation de la plupart des tâches simples. Ces outils ne remplacent en aucun cas la compétence sur la technologie, mais permettent simplement de gagner du temps quand la compréhension de l'outil est suffisante.
Les deux outils qui nous intéressent dans le cadre de l'intégration de paquet sous Yocto, sont les suivants:
-
devtool add
-
devtool finish
Le premier outil permet de générer automatiquement une recette minimaliste à partir des informations données en paramètre de la commande. Une utilisation classique de cette fonction se trouve dans le cas de l'intégration d'un paquet se trouvant sur un dépôt distant:
devtool add https://github.com/XXXXXXXXX
Lorsque cette commande est utilisée, elle génère dans l'environnement de travail une recette minimaliste construite à partir du dépôt distant. Il y a donc "lecture" du dépôt puis analyse, afin de tenter de deviner le type de la recette souhaitée.
Il convient de venir éditer le résultat et de le compléter avec la commande suivante:
devtool edit-recipe XXXXXXXXX
Lorsque l'édition est terminée et que l'on estime la recette prête, il faut essayer de la compiler avec la commande suivante:
devtool build XXXXXXXXX
Nous pouvons tester le paquet résultant en le déployant sur la cible:
devtool deploy-target XXXXXXXXX target
Cette partie du déploiement nécessite de connaître quelques métadonnées indispensables telles que le nom du serveur ssh de la cible, son adresse IP, les clés ssh éventuelles, etc.
Si le paquet se comporte comme attendu et que nos tests sont concluants, nous pouvons passer les modifications générées dans notre espace de travail directement au sein du projet avec la commande suivante:
devtool finish XXXXXXXXX layer
Les erreurs courantes
En intégration système, tout fonctionne rarement du premier coup. Il existe des groupes d'erreurs qui reviennent régulièrement dans les projets.
Parmi elles, nous trouvons en premier lieu les erreurs liées à l'étape do_fetch. Ces erreurs, sont liées à des coupures de réseau ou à l'instabilité d'une connexion. Il suffit souvent de relancer le processus lorsque la connexion a été stabilisée ou lorsque nous sommes branchés sur un câble ethernet.
Exemple :
ERROR: python3-goblinoid-0.1.0-r0 do_fetch: Fetcher failure: Fetch command export PSEUDO_DISABLED=1; unset _PYTHON_SYSCONFIGDATA_NAME; export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"; export PATH="/home/ebedev/Dev/board-yocto-main/build-board/tmp/sysroots-uninative/x86_64-linux/usr/bin:/home/ebedev/Dev/board-yocto-main/build-board/tmp/work/cortexa9t2hf-neon-dey-linux-gnueabi/python3-goblinoid/0.1.0-r0/recipe-sysroot-native/usr/bin/python3-native:/home/ebedev/Dev/board-yocto-main/sources/poky/scripts:/home/ebedev/Dev/board-yocto-main/build-board/tmp/work/cortexa9t2hf-neon-dey-linux-gnueabi/python3-goblinoid/0.1.0-r0/recipe-sysroot-native/usr/bin/arm-dey-linux-gnueabi:/home/ebedev/Dev/board-yocto-main/build-board/tmp/work/cortexa9t2hf-neon-dey-linux-gnueabi/python3-goblinoid/0.1.0-r0/recipe-sysroot/usr/bin/crossscripts:/home/ebedev/Dev/board-yocto-main/build-board/tmp/work/cortexa9t2hf-neon-dey-linux-gnueabi/python3-goblinoid/0.1.0-r0/recipe-sysroot-native/usr/sbin:/home/ebedev/Dev/board-yocto-main/build-board/tmp/work/cortexa9t2hf-neon-dey-linux-gnueabi/python3-goblinoid/0.1.0-r0/recipe-sysroot-native/usr/bin:/home/ebedev/Dev/board-yocto-main/build-board/tmp/work/cortexa9t2hf-neon-dey-linux-gnueabi/python3-goblinoid/0.1.0-r0/recipe-sysroot-native/sbin:/home/ebedev/Dev/board-yocto-main/build-board/tmp/work/cortexa9t2hf-neon-dey-linux-gnueabi/python3-goblinoid/0.1.0-r0/recipe-sysroot-native/bin:/home/ebedev/Dev/board-yocto-main/sources/poky/bitbake/bin:/home/ebedev/Dev/board-yocto-main/build-board/tmp/hosttools"; export HOME="/home/ebedev"; /usr/bin/env wget -t 2 -T 30 --passive-ftp --no-check-certificate -P /home/ebedev/__yocto-board-downloads 'https://files.pythonhosted.org/packages/86/03/238626336c383569372b85f68b035fb62b4812c8775f2d0c1d87995029f0/goblinoid-0.1.0.tar.gz' --progress=dot -v failed with exit code 4, output:
--2021-09-29 09:19:17-- https://files.pythonhosted.org/packages/86/03/238626336c383569372b85f68b035fb62b4812c8775f2d0c1d87995029f0/goblinoid-0.1.0.tar.gz
Resolving files.pythonhosted.org (files.pythonhosted.org)... failed: Temporary failure in name resolution.
wget: unable to resolve host address ‘files.pythonhosted.org’ERROR: python3-goblinoid-0.1.0-r0 do_fetch: Fetcher failure for URL: 'https://files.pythonhosted.org/packages/86/03/238626336c383569372b85f68b035fb62b4812c8775f2d0c1d87995029f0/goblinoid-0.1.0.tar.gz'. Unable to fetch URL from any source.
ERROR: Logfile of failure stored in: /home/ebedev/Dev/board-yocto-main/build-board/tmp/work/cortexa9t2hf-neon-dey-linux-gnueabi/python3-goblinoid/0.1.0-r0/temp/log.do_fetch.26912
ERROR: Task (/home/ebedev/Dev/board-yocto-main/sources/meta-projet/common/meta-projet/recipes-python/python3-goblinoid/python3-goblinoid_0.1.0.bb:do_fetch) failed with exit code '1'
Viennent ensuite les erreurs liées à des besoins de fichiers non satisfaits. Nous pouvons les subdiviser en deux grandes familles que sont les dépendances insatisfaites et les fichiers de configurations absents. La résolution de ces problèmes peut se faire de deux façons :
D'une part par la satisfaction de la demande, c'est à dire l'ajout du fichier ou du paquet demandé dans la distribution et la correction de la recette en cours d'écriture pour tenir compte de cette dépendance (qui se fera soit au niveau du SRC_URI, soit au niveau des variables RDEPENDS et DEPENDS).
Exemple :
ERROR: python3-shapely-1.7.1-r0 do_configure: Execution of '/home/ebedev/Dev/board-yocto-main/build-board/tmp/work/cortexa9t2hf-neon-dey-linux-gnueabi/python3-shapely/1.7.1-r0/temp/run.do_configure.42419' failed with exit code 1:
Failed `CDLL(libgeos_c.so.1)`
Failed `CDLL(libgeos_c.so)`
Traceback (most recent call last):
File "setup.py", line 85, in <module>
from shapely._buildcfg import geos_version_string, geos_version, \
File "/home/ebedev/Dev/board-yocto-main/build-board/tmp/work/cortexa9t2hf-neon-dey-linux-gnueabi/python3-shapely/1.7.1-r0/Shapely-1.7.1/shapely/_buildcfg.py", line 170, in <module>
fallbacks=['libgeos_c.so.1', 'libgeos_c.so'])
File "/home/ebedev/Dev/board-yocto-main/build-board/tmp/work/cortexa9t2hf-neon-dey-linux-gnueabi/python3-shapely/1.7.1-r0/Shapely-1.7.1/shapely/_buildcfg.py", line 164, in load_dll
libname, fallbacks or []))
OSError: Could not find library geos_c or load any of its variants ['libgeos_c.so.1', 'libgeos_c.so']
WARNING: exit code 1 from a shell command.ERROR: Logfile of failure stored in: /home/ebedev/Dev/board-yocto-main/build-board/tmp/work/cortexa9t2hf-neon-dey-linux-gnueabi/python3-shapely/1.7.1-r0/temp/log.do_configure.42419
Dans le cas de cet exemple, on voit qu'il manque le paquet geos, dont dépend cette librairie. La résolution passe par l'écriture d'une recette additionnelle pour geos (lui même dépendant de plusieurs autres paquets, il faudra "tirer" les dépendances une par une), puis inclure python3-geos dans la variable DEPENDS de la recette de shapely (cf plus bas, pour l'illustration de la mise à jours de DEPENDS).
D'autre part par l'édition des sources du paquet demandeur, à travers un patch, afin de désactiver cette demande. Nous pouvons, si on connaît suffisamment bien l'usage du paquet sur le système final, décider de nous passer de ces fonctions périphériques, et donc désactiver la demande de dépendance dans le code source. Le patch de fichiers sources peut également avoir lieu dans le cas de problème de chemins mal configurés dans les sources d'un paquet. On trouve un certain nombre de chemins dont la validité dépend directement du système hôte de développement, et qui doivent être mis à jours avant ou pendant l'intégration en fonction des contraintes du projet.
Certains types d'erreurs nécessitent une correction de la recette en cours d'écriture. Ce peut être le cas d'un hash md5 ou sha256 erroné, ou bien la nécessité d'utiliser un programme sur la machine hôte à l'étape de la compilation (do_compile). De ce fait, nous pouvons distinguer deux types de paquets : les paquets destinés à une cible embarquée sur la distribution en cours de construction et les paquets destinés à permettre la compilation du premier type de paquet (et donc provenant du système hôte). Ces derniers sont reconnaissables à l'extension "-native" en fin de nom de paquet.
Exemple :
ERROR: python3-pyrosm-0.6.0-r0 do_configure: Execution of '/home/ebedev/board-yocto-main/build-board/tmp/work/cortexa9t2hf-neon-dey-linux-gnueabi/python3-pyrosm/0.6.0-r0/temp/run.do_configure.2888342' failed with exit code 1:
Traceback (most recent call last):
File "setup.py", line 12, in <module>
from Cython.Build import cythonize
ModuleNotFoundError: No module named 'Cython'
WARNING: exit code 1 from a shell command.ERROR: Logfile of failure stored in: /home/ebedev/board-yocto-main/build-board/tmp/work/cortexa9t2hf-neon-dey-linux-gnueabi/python3-pyrosm/0.6.0-r0/temp/log.do_configure.2888342
Ici, la résolution est plus complexe. Le message d'erreur dit qu'il manque Cython, mais nous sommes à l'étape do_configure, le rootfs de la cible n'existe pas encore, et la compilation du paquet pyrosm qui pose problème n'a pas encore vraiment commencé. Cette erreur signale en fait la nécessité d'appeler le paquet Cython de l'hôte pour finir l'étape de configuration et pouvoir compiler le paquet. Ceci se fait en ajoutant le paquet python3-cython-native dans la variable DEPENDS de la recette de python3-pyrosm. Yocto téléchargera et compilera la version native de python3-cython et pourra l'utiliser pour l'étape de configuration de python3-pyrosm.
La recette du paquet pyrosm ressemblera alors à ceci :
SUMMARY = "A Python tool to parse OSM data from Protobuf format into GeoDataFrame."
HOMEPAGE = "https://pyrosm.readthedocs.io/"
AUTHOR = "Henrikki Tenkanen <henrikki.tenkanen@aalto.fi>"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"SRC_URI = "https://files.pythonhosted.org/packages/62/9f/3fc1d69584eeb8d71d88a9e9f7f93156cc937051e29f116f3af4b77307cf/pyrosm-0.6.0.tar.gz"
SRC_URI[md5sum] = "1f4dc4d2604f521c1e170ffe18f0942d"
SRC_URI[sha256sum] = "d7270a3cd4ff6ee9c1f7c309fb0a7df0e3b28a6a6ffd7978c94a9b3db162e62e"S = "${WORKDIR}/pyrosm-0.6.0"
RDEPENDS_${PN} = "python3"
DEPENDS = "python3 python3-cython python3-cython-native"inherit pypi setuptools3
Conclusions
Il existe une multitude de moyens pour ajouter un paquet dans une distribution Yocto. La bonne façon de faire dépend des contraintes du projet en bonne partie, mais également du paquet que l'on souhaite intégrer. Pour aller plus loin et comprendre en détail la manière dont Bitbake construit les images à partir des fichiers de configuration et des recettes de paquets, nous vous recommandons de lire les articles associés sur notre blog, et surtout de vous reporter à la documentation officielle.
Sources
https://www.yoctoproject.org/docs/latest/mega-manual/mega-manual.html