Linux Embedded

Le blog des technologies libres et embarquées

Yocto : comprendre BitBake

La documentation de Yocto est abondante. De nombreux tutoriaux vous expliqueront comment construire une image, écrire une recette ou ajouter le support d’une nouvelle carte. Cette documentation couvre la plupart des aspects de Yocto et fournit des méthodes pour résoudre la plupart des problèmes, mais très peu de documents tentent de répondre à cette question : “Yocto, comment ça marche?”. Cet article va aborder le fonctionnement de Yocto en étudiant sa brique la plus fondamentale : BitBake.

Cet article suppose une connaissance préalable de Yocto. Vous devez être familier avec les concepts de layers, de recette et de distro pour comprendre les détails de la mécanique interne de BitBake.

Introduction à BitBake.

BitBake peut être décrit comme le make des distributions. Comme make, il sert à ordonner un certain nombre de tâches, vérifier s’il est nécessaire de les exécuter puis exécuter ces tâches dans le bon ordre. Mais là où make a été conçu pour transformer du code source en binaire, BitBake a été pensé pour récupérer du code source puis lancer d’autres outils pour faire la compilation. BitBake n’est qu’une brique de base sur laquelle construire un système de génération complet. Yocto et OpenEmbedded utilisent BitBake pour fournir un environnement complet, même si les éléments de base fournis par BitBake sont déjà très puissants.

Nous allons aborder les choses selon les angles suivants :

  • Un langage de configuration particulièrement riche et expressif
  • Une bibliothèque de fetch permettant de récupérer le code source des applications de façon très variée
  • La possibilité de contrôler de façon précise ce qui cause et ce qui ne cause pas de ré-exécution de tâches
  • La possibilité de conserver plusieurs versions des résultats d’une tâche en fonction de ses paramètres et donc de réduire les builds
  • Une compréhension inhérente aux problèmes de compilation croisée :
    • Compilation pour l’hôte et pour la cible
    • Installation dans un Sysroot
    • Séparation des résultats de compilation en paquetages multiples ( -dev, -dbg, -doc)

 

Une exécution de BitBake dans les grandes lignes.

Pour exécuter ses tâches, BitBake utilise une base de données d’information “clé – valeur” qui contient tout ce dont BitBake a besoin de savoir pour exécuter les tâches. Cette base de données est appelée l’environnement. A ne pas confondre avec l’environnement shell qui n’a rien à voir.

Lorsque vous écrivez :

MACHINE  = "qemux86"

Dans votre local.conf, vous positionnez la clé MACHINE pour contenir la chaîne de caractères qemux86. La notion de valeur doit être comprise dans un sens très large. Les valeurs peuvent être des chaînes de caractères, des listes, des fonctions python ou des morceaux de script shell. Il faut aussi comprendre que TOUT ce que BitBake sait, il le sait grâce à l’environnement. La liste des tâches à exécuter pour compiler une recette est la variable do_compile interprétée comme une section de code python.

Enfin, il est important de comprendre que BitBake analyse l’environnement en plusieurs passes. La première passe va simplement charger les valeurs comme du texte brut et les passes suivantes analyseront le contenu, étendront les variables et utiliseront le tout. Nous détaillerons le langage BitBake plus bas dans cet article.

Supposons que nous lancions la commande :

bitbake wayland -c compile

Nous disons à BitBake d’exécuter la tâche compile d’une recette appelée wayland. Mais que va faire BitBake exactement ?

La lecture des méta-données globales.

Quels que soient les arguments donnés en ligne de commande, BitBake va tout d’abord charger une configuration globale en suivant un ordre précis.

Le fichier conf/bblayers.conf

BitBake va tout d’abord rechercher un fichier nommé conf/bblayers.conf dans le répertoire courant et le lire. Le fichier bblayers.conf peut positionner des variables dans l’environnement mais son rôle principal est d’en positionner une particulière, la variable BBLAYERS.

La variable BBLAYERS contient une liste de répertoires contenant des layers. Une fois cette variable positionnée, BitBake va l’utiliser pour rechercher d’autres fichiers à inclure.

Les fichiers conf/layer.conf

Une layer est un répertoire contenant un sous-répertoire conf/ contenant lui-même un fichier de configuration layer.conf. Tout ce que vous savez sur les layers n’est que la conséquence du contenu de ce fichier.

BitBake prend la liste des répertoires contenus dans la variable BBLAYERS et pour chacun d’entre eux il va:

  • positionner la variable LAYERDIR au chemin qu’il est en train de traiter
  • rechercher un fichier conf/layer.conf dans ce répertoire
  • lire ce fichier et intégrer son contenu dans l’environnement.

Cette étape permet à chaque layer d’ajouter ou de modifier des variables dans l’environnement comme elle le souhaite, tout en respectant le mécanisme de priorités de layers que le développeur peut modifier via la variable BBFILE_PRIORITY_layer de layer.conf. En pratique il y a quelques variables que toutes les layers modifient.

  • BBPATH est une variable contenant un path (une liste de répertoires séparés par “:”). Ce chemin sera utilisé par BitBake pour trouver les fichiers conf/*.conf à inclure (lorsqu’il rencontre le mot clé require ou include) ainsi que les fichiers classes/*.bbclass (mot clé inherit  dans une recette ou variable  INHERIT à la fin de l’analyse de la configuration)
  • BBFILES est une variable pointant sur l’ensemble des fichiers .bb  et .bbappend. Elle utilise les caractères de globbing (“*”) pour spécifier facilement un ensemble de fichiers, chaque layer peut ainsi pointer BitBake vers ses recettes en fonction de son organisation en sous-répertoires.

Le fichiers bitbake.conf

Nous avons fourni à BitBake tous les éléments dont il aura besoin pour trouver ses fichiers de configuration, recettes et classes. Nous pouvons maintenant configurer véritablement BitBake.

BitBake va maintenant rechercher (grâce à BBPATH) un fichier bitbake.conf et le charger. Ce fichier est le véritable commencement de Yocto (par opposition à BitBake) et définit les différentes politiques de ce projet :

  • Définition de l’environnement shell pour l’exécution des tâches (prefixdir, libdir, execdir etc.).
  • Définition de l’environnement BitBake commun à toutes les tâches et toutes les recettes (*_ARCH, PN, PV, S, WORKDIR). Notons qu’à ce stade, les variables sont positionnées mais ne sont pas étendues. Cela permet de préparer des variables telles que PN alors que le fichier de recette n’a pas encore été analysé et que la recette elle-même n’est pas encore connue.
  • Préparation des variables nécessaires à la compilation croisée (CC, LD, CXX).
  • Inclusion d’un grand nombre d’autres fichiers de configuration, en particulier conf/local.conf qui sert de point d’entrée à la personnalisation du build.

Nous n’étudierons pas en détail le contenu du fichier bitbake.conf car son contenu sort de ce que veut couvrir cet article. Sa lecture est néanmoins intéressante. Allez-y jeter un coup d’œil à l’occasion. Si vous utilisez Yocto, ce fichier se trouve dans meta/conf/bitbake.conf .

Le fichier base.bbclass et la variable INHERIT

Si vous avez lu en détail le paragraphe précédent, vous noterez que le fichier bitbake.conf ne fait que positionner des variables. Il ne définit pas de fonctions ni de tâches.

Un fichier de configuration n’est pas autorisé à définir des fonctions et tous les fichiers que nous avons vus jusqu’à maintenant étaient des fichiers de configuration. Les seuls fichiers pouvant définir des fonctions sont les fichiers de recettes (.bb et .bbappend) et les fichiers de classes (.bbclass). BitBake va automatiquement charger un fichier base.bbclass ainsi que toute classe définie dans la variable INHERIT. La variable INHERIT est utilisée pour permettre à l’utilisateur de charger des classes dans toutes les recettes (via le fichier conf/local.conf). Le fichier base.bbclass permet à Yocto de définir des fonctions qui seront accessibles pour toutes les recettes.

Dans le cas de Yocto, le fichier se situe dans meta/classes/base.bbclass et définit entre autres :

  • Des fonctions utilitaires tels que die
  • Un certain nombre de tâches essentielles (build, fetch, confgure, compile…) ainsi que leur ordre d’exécution
  • Le code python qui gère le système de compilation conditionnel via la variable PACKAGECONFIG

Tout comme avec bitbake.conf, nous n’étudierons pas en détail ce fichier mais sa lecture est très instructive.

Le chargement des recettes

Les fichiers .bb et .bbappend

Nous avons fini de charger la configuration de base, commune à toutes les itérations de BitBake. Il est temps de charger les recettes.

Bitbake dispose de la liste de toutes les recettes via la variable BBFILES. Pour chaque recette, BitBake va effectuer les actions suivantes :

  • Faire une copie de l’environnement global, copie qui sera utilisée uniquement par la recette en cours
  • Charger le fichier .bb de la recette
  • Charger tous les fichiers .bbappend de la recette, dans l’ordre donné par BBFILES

Ce sont les recettes qui complètent la configuration en donnant le code des tâches à exécuter (généralement en chargeant des fichiers .bbclass qui leur fournissent le code à exécuter).

 L’exécution des tâches

BitBake a maintenant fini son analyse. Il a trouvé la tâche finale à exécuter et il a toutes les données nécessaires. BitBake utilise un mécanisme de somme de contrôle sur l’ensemble de l’environnement pour décider si la tâche doit être exécutée ou s’il suffit de restaurer les résultats de l’exécution précédente. Si nous sommes dans le deuxième cas, BitBake utilisera la variante _setscene de la tâche à exécuter qui se chargera de remplacer la tâche véritable en désarchivant les résultats de la tâche.

La syntaxe des fichiers de configuration

Dans leur forme la plus simple, les fichiers de configuration BitBake assignent des valeurs à des variables. La syntaxe est pourtant plus riche que ce que l’on pourrait croire, aussi allons nous prendre le temps de détailler un peu.

Assigner une variable.

Les fichiers de configuration BitBake servent principalement à positionner des variables. Il y a plusieurs opérateurs qui servent à positionner une variable et il est important de bien les différencier :

  • VAR = "valeur" Positionne la variable VAR  à la valeur valeur immédiatement. Si la variable VAR  était déjà positionnée, l’ancienne valeur est perdue. Si valeur contient des références à d’autres variables, ces références sont étendues à la fin de la lecture des fichiers de configuration
  •  VAR ?= "valeur" Positionne la variable VAR à la valeur valeur immédiatement, si la variable n’existait pas. Si la variable avait déjà une valeur, l’ancienne valeur est conservée.Si valeur contient des références à d’autres variables, ces références sont étendues à la fin de la lecture des fichiers de configuration
  • VAR ??= "valeur" Positionne la valeur uniquement si la variable n’est pas définie à la fin de l’analyse. Si valeur contient des références à d’autres variables, ces références sont étendues à la fin de la lecture des fichiers de configuration.
  • VAR := "valeur" Positionne la valeur immédiatement et étend immédiatement les indirections dans valeur 
  • VAR += "valeur" Ajoute immédiatement un espace suivi de valeur après le contenu de VAR
  • VAR =+ "valeur" Ajoute immédiatement valeur suivi d’un espace avant le contenu de VAR
  • VAR .= "valeur" Ajoute immédiatement valeur après le contenu de VAR. Si vous souhaitez utiliser VAR comme une liste, vous devez gérer les espaces vous-même
  • VAR =. "valeur" Ajoute immédiatement valeur avant le contenu de VAR. Si vous souhaitez utiliser VAR comme une liste, vous devez gérer les espaces vous-même
  • VAR_append = "valeur" Note d’ajouter valeur à la fin de VAR une fois l’analyse terminée. Vous pouvez utiliser tous les opérateurs ci-dessus avec le suffix _append. Le contenu de VAR_append sera ajouté après le contenu de VAR à la fin de l’analyse et sans espaces. Vous pouvez utiliser _append plusieurs fois, les différentes utilisations ne s’écraseront pas.
  • VAR_prepend= "valeur" fonctionne de façon similaire à _append mais le contenu de VAR_append sera ajouté avant le contenu de VAR à la fin de l’analyse
  • VAR_remove= "valeur" enlève toutes les occurrences de valeur de la liste VAR. La valeur valeur doit être un mot (i.e entouré d’espaces)

Syntaxe des valeurs

Syntaxe de base

Sous sa forme la plus simple, une assignation Yocto va du guillemet suivant l’opérateur jusqu’au dernier caractère de la ligne qui doit être un guillemet. Il est possible de prolonger une valeur sur plusieurs lignes grâce au caractère “\”.

Notez que la valeur va jusqu’à la fin de la ligne et non pas jusqu’au guillemet suivant. Vous pouvez donc mettre des caractères guillemet dans vos valeurs sans échappement.

Indirection

Il est possible de référencer une variable dans la valeur d’une autre variable avec la syntaxe suivante:

VAR="${VAR2}"

L’expansion se fait une fois l’ensemble des valeurs lues, sauf si l’opérateur d’assignation était “:=”

Drapeaux de variables

Il est possible d’attacher des métadonnées à des variables en suffixant un drapeau au nom de la variable, comme dans l’exemple ci-dessous :

VAR[doc] = "La variable VAR ne sert à rien"

Vous pouvez utiliser n’importe quel mot comme drapeau, mais vous ne pouvez pas utiliser les suffixes de type _append sur une assignation de drapeau. Les autres opérateurs tels que += fonctionnent correctement.

Notez que BitBake utilise un certain nombre de drapeaux que vous pouvez positionner pour changer le comportement d’une tâche. Vous trouverez les détails ici. L’exemple ci-dessous :

do_configure[noexec] = "1"

attache une valeur 1 au flag noexec de la variable do_configure .

La variable do_configure contient le code à exécuter lors de la tâche configure de la recette en cours, ceci indique à BitBake qu’il n’est pas nécessaire d’exécuter la tâche configure pour cette recette.

Intégration de code python dans une variable

Lorsque la valeur que vous souhaitez affecter ne peut pas être exprimée simplement grâce à d’autres variables, il est possible d’appeler du code python directement dans une affectation de valeur. La syntaxe “${@…}” permet de remplacer le contenu du bloc par le résultat du code python correspondant. L’environnement BitBake est accessible via la variable d :

DATE = "${@time.strftime('%Y%m%d',time.gmtime())}"
SRC_URI = "ftp://ftp.info-zip.org/pub/infozip/src/zip${@d.getVar('PV',1).replace('.', '')}.tgz

Le premier exemple positionne la variable DATE à la date actuelle, le deuxième exemple utilise la variable PV pour construire une url de téléchargement, mais remplace le caractère “.” par une chaîne vide dans le contenu de la variable PV.

Syntaxe conditionnelle

Une fois la première passe d’analyse terminée, BitBake utilise un mécanisme de variables conditionnelles pour remplacer la valeur de certaines variables par d’autres variables. Tout cela s’articule autour de la variable OVERRIDES. Si l’on regarde le contenu de cette variable, elle aura typiquement le contenu suivant :

OVERRIDES="linux:x86-64:build-linux:pn-weston:qemux86:poky:class-target:forcevariable:libc-glibc"

Il s’agit donc d’une liste de mots clés (séparés par “:” ). BitBake prendra l’ensemble des variables de l’environnement, cherchera toutes celles se terminant par “_<suffix>” et utilisera toutes celles dont le suffix est contenu dans OVERRIDES pour remplacer la variable correspondante. Ainsi, si notre local.conf contient les lignes suivantes

SRC_URI_pn-weston="file://ma_copie.gz"
SRC_URI_pn-glibc="file://une_autre_copie.gz"

la valeur SRC_URI (venant du fichier weston.bb) sera remplacée par la valeur de SRC_URI_pn-weston. L’autre variable ne sera utilisée que si OVERRIDES contient pn-glibc, c’est à dire lorsque la recette à exécuter est la recette glibc .

Notons qu’il y a de nombreuses valeurs intéressantes pour pouvoir écraser de façon sélective les variables:

  • poky : la distribution utilisée
  • x86-64 : l’architecture cible
  • pn-weston : la recette en cours d’exécution
  • qemux86 : la machine cible

BitBake continuera à remplacer des suffixes jusqu’à ce qu’il n’en trouve plus à remplacer.

Ordre de l’analyse

Avec toutes ces syntaxes il est difficile de comprendre exactement dans quel ordre BitBake va traiter tous les cas. Résumons rapidement ; BitBake va :

  1. Lire les fichiers de configuration en positionnant les valeurs (=, +=, =+,  .=,=., ?=). Le contenu des variables n’est pas analysé, sauf dans le cas de l’opérateur :=
  2. Positionner les variables qui n’ont pas de valeur mais sur lesquels l’opérateur ??= a été utilisé
  3. Étendre les syntaxe de type ${..} qui ne l’ont pas encore été
  4. Chercher les variables ayant des suffixes connus (_append, _prepend, _remove, OVERRIDES) et les traiter
  5. Recommencer la dernière étape jusqu’à ce qu’il n’y ait plus de variables à traiter

Voyons ensemble quelques exemples :

A${B} = "X"
B = "2"
A2 = "Y"
  • la variable A${B} est positionnée à “X”
    { “A${B}” : “X” }
  • La variable B est positionnée à “2”
    { “A${B}” : “X” , “B” : “2” } 
  • La variable A2 est positionnée à “Y”
    { “A${B}” : “X” , “B” : “2” , “A2” : “Y” }
  • Les variables sont étendues. La nouvelle variable A2 remplace l’ancienne
    { “B” : “2” , “A2” : “X” }

Vous aviez trouvé le piège ? Essayons un autre exemple…

OVERRIDES = "foo"
A = "Z"
A_foo_append = "X"
  • Les variables sont chargées avec leurs valeurs
    { “OVERRIDES” : “foo” , “A” : “Z”  };  ajouter “X” à la fin de A_foo
  • Il n’y a pas de variables à étendre
  • BitBake fait une première passe sur les suffixes et constate qu’il faut ajouter X à la fin de la variable A_foo (variable qui n’existe pas)
    { “OVERRIDES” : “foo” , “A” : “Z” , “A_foo” : “X” }
  • BitBake fait une deuxième passe sur les suffixes et constate qu’il faut remplacer la variable  par la variable A_foo car foo est contenu dans OVERRIDES
    { “OVERRIDES” : “foo” , “A” : “X” }

Et maintenant, un exemple presque identique au précédent…

OVERRIDES = "foo"
A = "Z"
A_append_foo = "X"
  • Les variables sont chargées avec leurs valeurs
    { “OVERRIDES” : “foo” , “A” : “Z” , “A_append_foo” : “X” }
  • Il n’y a pas de variables à étendre
  • BitBake fait une première passe sur les suffixes et constate qu’il faut remplacer la variable A_append  par la variable A_append_foo car foo est contenu dans OVERRIDES
    { “OVERRIDES” : “foo”
     “A” : “Z”} ajouter “X” à la fin de A
  • BitBake fait une deuxième passe sur les suffixes et constate qu’il faut ajouter X à la fin de la variable A
    { “OVERRIDES” : “foo” , “A” : “ZX” }

Un exemple un peu plus complexe ?

OVERRIDES = "foo"
A = "Y"
A_foo_append = "Z"
A_foo_append += "X"
  • BitBake positionne les variables OVERRIDES, A et A_foo_append
    { “OVERRIDES” : “foo” , “A” : “Y”} ajouter “Z” à la fin de A_foo
  • BitBake ajoute ” X” à la fin de la variable A_foo_append (l’opérateur += insère le blanc)
    { “OVERRIDES” : “foo” , “A” : “Y”  } ajouter “Z X” à la fin de A_foo
  • L’expansion de variables se déroule comme pour le premier exemple
    { “OVERRIDES” : “foo” , “A” : “Z X” }

Allez… un dernier exemple

 A = "1"
 A_append = "2"
 A_append = "3"
 A += "4"
 A .= "5"
  • La variable A est positionnée à “1”
    { “A” : “1” }
  • La variable A_append est positionnée à “2”
    { “A” : “1”} ajouter “2” à la fin de A
  • La variable A_append est positionnée à “3”
    { “A” : “1”  } ajouter “2” à la fin de A, ajouter “3” à la fin de A
  • La chaîne ” 4″ est ajoutée à la fin de la variable A  (notez l’espace)
    { “A” : “1 4”  } ajouter “2” à la fin de A, ajouter “3” à la fin de A
  • La chaîne “5” est ajoutée à la fin de la variable A  (notez l’absence d’espace)
    { “A” : “1 45” } ajouter “2” à la fin de A, ajouter “3” à la fin de A
  • Les suffixes _append sont traités dans l’ordre. Leur contenu est ajouté à la fin de A
    { “A” : “1 4523” }

bitbake -e

Ce genre d’exercice est amusant sur le papier, mais toute personne un peu sensée est sans doute horrifiée à l’idée d’utiliser cela dans du code industriel. Sans outils d’analyse puissant ce genre de syntaxe couplée à une arborescence de configuration complexe et des layers interagissant les unes avec les autres est une source de bug inépuisable.

BitBake fournit un outil formidable pour comprendre le fonctionnement de son environnement : l’option -e.

bitbake -e <recette>

Cette ligne affichera tout l’environnement qui a été obtenu pour la recette recette sur la sortie standard. Il faudra donc généralement la rediriger vers un fichier ou vers une page.

Afficher tout l’environnement semble particulièrement utile, mais ce qui rend cette commande absolument indispensable, ce sont les commentaires que BitBake insert entre les valeurs… Le fichier commence par un entête indiquant tous les fichiers qui ont été inclus puis chaque variable est détaillée avec l’historique de toutes les modifications qui lui ont été appliquées. Ainsi le dernier exemple ci-dessus donne le texte suivant :

# $A [7 operations]
# set /home/jrosen/poky/build/conf/local.conf:242
# "1"
# _append /home/jrosen/poky/build/conf/local.conf:243
# "2"
# _append /home/jrosen/poky/build/conf/local.conf:244
# "3"
# append /home/jrosen/poky/build/conf/local.conf:245
# "4"
# postdot /home/jrosen/poky/build/conf/local.conf:246
# "5"
# set data_smart.py:434 [finalize]
# "1 452"
# set data_smart.py:434 [finalize]
# "1 4523"
# pre-expansion value:
# "1 4523"
A="1 4523"

C’est un outil indispensable à la compréhension des recettes Yocto, en particulier lorsque l’on développe des .bbappend.

Intégrer du code dans ses recettes

Jusqu’ici, nous n’avons fait que positionner des variables. Mais BitBake est principalement là pour exécuter des tâches. Pour exécuter des tâches il faut être capable de donner du code à exécuter  à BitBake. BitBake peut utiliser des fonctions écrites en shell ou en python. Voyons comment lui en fournir…

Définir des fonctions shell pour BitBake.

Lorsque l’on vient du monde make ou plus généralement lorsqu’on essaye d’installer des fichiers sur la cible, le plus simple est d’utiliser des fonctions écrites en shell. BitBake fournit une syntaxe pour faire cela :

some_function () {
 echo "Hello World"
 }

Nous venons de définir une fonction shell qui peut être appelée par n’importe quelle autre fonction shell. Nous pouvons également utiliser cette méthode pour définir une fonction qui sera utilisée par une tâche (en appelant notre fonction do_install par exemple). Notons que nous définissons la variable some_function et qu’il est possible d’utiliser les opérateurs de type _append ou OVERRIDES sur ces fonctions (en définissant une fonction do_install_append par exemple).

Attention toutefois, BitBake ne garantit pas quel shell sera utilisé pour exécuter ces fonctions, vous devez donc utiliser la syntaxe sh et non pas bash  ou autre variante (certaines tâches pouvant être exécutées sur la cible, il faut être compatible avec le shell busybox entre autre).

Définir des fonctions python pour BitBake

Le shell est vraiment pratique pour faire des choses simples mais ses limites sont rapidement atteintes et coder en shell peut très vite devenir frustrant. BitBake étant codé en python, il est naturel de pourvoir définir des tâches en python. BitBake fournit deux syntaxes pour faire cela ; l’une permet d’utiliser du python “standard” tandis que l’autre fournit un environnement un peu plus riche avec des spécificités BitBake.

def get_depends(d):
 if d.getVar('SOMECONDITION', True):
 return "dependencywithcond"
 else:
 return "dependency"

La syntaxe ci-dessus permet de définir une fonction python “normale”. Notons que les modules bb et os sont automatiquement importés. L’environnement BitBake n’est pas importé, il est donc nécessaire de le passer en paramètre. Utiliser des fonctions standards permet de passer des paramètres aux fonctions, mais ces fonctions ne peuvent pas être appelées directement par BitBake. Elles sont appelées depuis des fonctions python du second type :

python some_python_function () {
         d.setVar("TEXT", "Hello World")
         print d.getVar("TEXT", True)
}

La syntaxe ci-dessous permet de définir des fonctions qui seront exécutées directement par BitBake. L’environnement est directement accessible via la variable globale d. Ici aussi les modules bb  et os sont inclus automatiquement. Cette forme permet de faire des fonctions qui peuvent être utilisées pour des tâches.

Exécuter du code lors de la lecture d’un fichier de configuration

Jusqu’à maintenant nous avons défini des fonctions (python ou shell) qui peuvent être appelées soit sous forme de tâches, soit lors de l’expansion des variables (syntaxe ${@…}). Il est parfois nécessaire d’exécuter du code python pendant l’analyse des fichiers de configuration (c’est particulièrement utile si vous souhaitez faire des manipulations complexes sur l’environnement). BitBake permet de définir des fonctions anonymes qui seront exécutées lorsqu’elles seront rencontrées.

python () {
         d.setVar("TEXT", "Hello World")
         print d.getVar("TEXT", True)
     }

Cette fonction sera appelée à l’endroit où elle est définie, elle permet de modifier directement l’environnement via la variable d.

Hello World en BitBake

Maintenant que nous avons exploré le fonctionnement de BitBake, nous allons mettre en place une configuration minimale. Cet exercice n’a que peu d’intérêt pratique, mais c’est une bonne façon de réviser tout ce que nous avons appris…

Mise en place du BBPATH

La première chose dont BitBake a besoin est d’un chemin pour charger ses configurations. Nous allons nous contenter d’utiliser une variable d’environnement shell, c’est suffisant.

 $ BBPATH="/home/rosen/exemple_bitbake"
 $ export BBPATH

Création de bitbake.conf

Si nous lançons bitbake, il refusera de s’exécuter car il n’y a pas de fichier bitbake.conf. Nous allons en créer un.

Créez un répertoire conf :

$ mkdir /home/rosen/exemple_bitbake/conf

puis créez un fichier /home/rosen/exemple_bitbake/conf/bitbake.conf avec le contenu suivant :

TMPDIR = "${TOPDIR}/tmp"
CACHE = "${TMPDIR}/cache"
STAMP = "${TMPDIR}/stamps"
T = "${TMPDIR}/work"
B = "${TMPDIR}"

La plupart de ces définitions sont utilisées par bitbake pour la gestion des stamps, cache et autres setscene.

Création de base.bbclass

Maintenant, BitBake proteste qu’il n’arrive pas à trouver le fichier base.bbclass. Il est temps de le créer :

$ mkdir /home/rosen/bitbake_exemple/classes
$ echo "addtask build" > /home/rosen/bitbake_exemple/classes/base.bbclass

Comme la classe base.bbclass est automatiquement héritée par toutes les recettes, nous venons d’ajouter une tâche build à toutes nos recettes. Nous n’avons pas encore de recettes, mais ça viendra.

La tâche build est un peu particulière. C’est la tâche que BitBake appellera lorsqu’aucun argument -c ne lui précise quelle tâche appeler.

Ajout d’une layer et d’une recette.

BitBake peut s’exécuter. Il ne proteste plus. Bien sûr, il ne peut rien faire non-plus, donc il est temps d’enrichir tout cela avec un peu de fonctionnel. Nous allons créer une layer minimale (ce n’est pas obligatoire, nous pourrions positionner directement les variables dans bitbake.conf, mais c’est une bonne habitude à prendre).

$ mkdir -p /home/rosen/bitbake_exemple/mylayer/conf

Il nous faut ensuite créer le fichier /home/rosen/bitbake_exemple/mylayer/conf/layer.conf avec le contenu ci-dessous :

 BBPATH .= ":${LAYERDIR}"

 BBFILES += "${LAYERDIR}/*.bb"

 BBFILE_COLLECTIONS += "mylayer"
 BBFILE_PATTERN_mylayer := "^${LAYERDIR}/"

Vous pouvez en apprendre plus sur la notion de collection dans la documentation BitBake, mais vous observerez que nous modifions les variables BBFILES et BBPATH pour pouvoir ajouter des recettes, classes, et fichiers de configuration. Ajoutons une recette dans le fichier /home/rosen/bitbake_exemple/mylayer/hello.bb :

DESCRIPTION = "Prints Hello World"
PN = 'printhello'
PV = '1'
do_build[nostamp] = '1'
python do_build() {
   bb.plain("********************");
   bb.plain("*                  *");
   bb.plain("*  Hello, World!   *");
   bb.plain("*                  *");
   bb.plain("********************");
}

Nous positionnons les variables minimales dont BitBake a besoin et ajoutons le code pour la fonction de build. bb.plain est une fonction qui affiche du texte brut sur la sortie standard. Nous signalons également qu’aucun stamp ne doit être généré pour la tâche build de la recette hello, c’est à dire que do_build doit toujours être exécutée, même si l’exécution précédente avait réussi.

BitBake ne trouvera pas notre layer magiquement, il faut lui indiquer où chercher :

echo "BBLAYERS ?= \"/home/rosen/bitbake_exemple/mylayer\" " > /home/rosen/bitbake_exemple/conf/bblayers.conf

et nous pouvons maintenant lancer BitBake :

$ bitbake printhello
 Parsing recipes: 100% |##################################################################################|
 Time: 00:00:00
 Parsing of 1 .bb files complete (0 cached, 1 parsed). 1 targets, 0 skipped, 0 masked, 0 errors.
 NOTE: Resolving any missing task queue dependencies
 NOTE: Preparing runqueue
 NOTE: Executing RunQueue Tasks
 ********************
 * *
 * Hello, World! *
 * *
 ********************
 NOTE: Tasks Summary: Attempted 1 tasks of which 0 didn't need to be rerun and all succeeded.

Conclusion

BitBake est un outil très puissant mais dont la conception n’est finalement pas si compliquée que ça lorsqu’on sait comment l’aborder. Comprendre le fonctionnement des variables BitBake et ce que sont véritablement une recette et une layer permet de se simplifier la vie lorsqu’on met au point une recette. Vous êtes maintenant capable de modifier une unique recette dans votre local.conf, de créer rapidement une mini-layer pour tester vos mises au point ou encore de chercher dans l’historique de l’environnement pourquoi une FEATURE est ou n’est pas présente dans votre build.

Je ne peux que vous recommander de lire de bout en bout le manuel utilisateur de BitBake pour découvrir toutes les options avancées de BitBake.

Bonne lecture à tous.