Yocto Cooker (2/3)

Publié par cpb
Jan 20 2022

Dans le premier article de cette série nous avons vu comment utiliser cooker pour produire une image avec Yocto Project en ne renseignant qu’un seul fichier : le menu.

Dans cet article nous allons voir comment compléter ce menu pour produire plusieurs builds en une seule commande, certains d’entre-eux pouvant partager divers éléments de configuration.

Pour mémoire le fichier que nous avions utilisé était le suivant : menu-001.json

$ cat menu-001.json

{
  "sources" : [
    { "url": "git://git.yoctoproject.org/poky", "branch": "dunfell", "rev": "yocto-3.1.13" } 
  ],

  "layers" : [
    "poky/meta",
    "poky/meta-poky",
    "poky/meta-yocto-bsp"
  ],

  "builds" : {

    "qemuarm": {

      "target" : "core-image-base",

      "local.conf": [
        "MACHINE = 'qemuarm' ",
        "IMAGE_FEATURES += 'empty-root-password' "
      ]
    }
  }
}

Ajout d’un build

Commençons par ajouter un autre build dans notre menu. Par exemple la production d’une image pour une autre cible émulée par Qemu : qemux86. Pour cela nous ajoutons un deuxième couple clé/valeur dans l’attribut builds du menu.

Voici le nouveau menu :

{
  "sources" : [
    { "url": "git://git.yoctoproject.org/poky", "branch": "dunfell", "rev": "yocto-3.1.13" }
  ],

  "layers" : [
    "poky/meta",
    "poky/meta-poky",
    "poky/meta-yocto-bsp"
  ],

  "builds" : {

    "qemuarm": {

      "target" : "core-image-base",

      "local.conf": [
        "MACHINE = 'qemuarm' ",
        "IMAGE_FEATURES += 'empty-root-password' "
      ]
    },

    "qemux86": {

      "target" : "core-image-base",

      "local.conf": [
        "MACHINE = 'qemux86' ",
        "IMAGE_FEATURES += 'empty-root-password' "
      ]
    }
  }
}

À présent, si nous lançons cooker ainsi :

$ cooker  cook  menu-002.json

il va compiler successivement les deux images pour les deux machines voulues. Sauf si nous précisons explicitement quelle image produire en indiquant le nom du build sur la ligne de commande, par exemple :

$ cooker  cook  menu-002.json  qemux86

Avant de produire véritablement l’image, nous pouvons encore améliorer et enrichir un peu notre menu.

Attribut local.conf global

Nous observons que la ligne

" IMAGE_FEATURES += 'empty-root-password' "

est commune à nos deux builds. Nous pouvons la factoriser en l’extrayant de leurs propriétés local.conf et en la plaçant dans un attribut local.conf global à tout le menu. Bien sûr il faut s’assurer auparavant que cette ligne soit souhaitable pour tous les builds envisagés. Voici le menu modifié.

{
  "sources" : [
    { "url": "git://git.yoctoproject.org/poky", "branch": "dunfell", "rev": "yocto-3.1.13" }
  ],

  "layers" : [
    "poky/meta",
    "poky/meta-poky",
    "poky/meta-yocto-bsp"
  ],

  "local.conf": [
    "IMAGE_FEATURES += 'empty-root-password' "
  ],

  "builds" : {

    "qemuarm": {

      "target" : "core-image-base",

      "local.conf": [
        "MACHINE = 'qemuarm' "
      ]
    },

    "qemux86": {

      "target" : "core-image-base",

      "local.conf": [
        "MACHINE = 'qemux86' "
      ]
    }
  }
}

Dans les projets de mes clients, j’utilise surtout l’attribut local.conf au niveau du menu pour définir des variables de configuration qui sont employées ensuite dans des recettes pour le code métier. Une autre variable que j’indique souvent ici est "OE_TERMINAL = 'screen' " pour faciliter la configuration du kernel lorsque je me connecte à distance sur la machine de compilation.

Pour l’instant nous n’utilisons que le dépôt Poky comme source, avec ses trois layers incontournables. Mais nous pouvons en ajouter d’autres.

Layer supplémentaire

Supposons que dans l’image pour qemux86 nous souhaitions installer l’utilitaire nano (éditeur de texte). Pour cela nous devons inclure un layer supplémentaire (meta-oe) présent dans le dépôt meta-openembedded.

Cela implique plusieurs choses pour notre menu. Tout d’abord nous devons ajouter le dépôt meta-openembedded à la liste des sources à télécharger. Contrairement à Poky, meta-openembedded ne gère pas les tags, aussi devrons-nous indiquer un numéro de commit pour garantir la reproductibilité de la compilation.

  "sources" : [
    { "url": "git://git.yoctoproject.org/poky", "branch": "dunfell", "rev": "yocto-3.1.13" },
    { "url": "git://git.openembedded.org/meta-openembedded", "branch": "dunfell", "rev": "ab9fca48" }
  ],

Ensuite, il faut indiquer que le layer meta-oe est nécessaire pour le build qemux86. Mais uniquement pour lui. Pas pour qemuarm.

Pour cela, nous allons ajouter un attribut layers à l’objet build concerné. Cet attribut indiquera les layers supplémentaires à prendre en compte en plus de ceux de la propriété layers du menu.

  "qemux86": {

    "target" : "core-image-base",

    "layers" : [
      "meta-openembedded/meta-oe"
    ],
    [...]
  }

Enfin, il faut indiquer que l’on veut inclure la recette nano lors de la génération de l’image pour qemux86, ce que nous pouvons faire directement dans son attribut local.conf. Dans un projet réel on définirait plutôt une recette d’image spécifique dans un layer personnalisé

Voici donc le contenu du menu-002.json complet.

{
  "sources" : [
    { "url": "git://git.yoctoproject.org/poky", "branch": "dunfell", "rev": "yocto-3.1.13" },
    { "url": "git://git.openembedded.org/meta-openembedded", "branch": "dunfell", "rev": "ab9fca48" }
  ],

  "layers" : [
    "poky/meta",
    "poky/meta-poky",
    "poky/meta-yocto-bsp"
  ],

  "local.conf": [
    "IMAGE_FEATURES += 'empty-root-password' "
  ],

  "builds" : {

    "qemuarm": {

      "target" : "core-image-base",

      "local.conf": [
        "MACHINE = 'qemuarm' "
      ]
    },

    "qemux86": {

      "target" : "core-image-base",

      "layers" : [
        "meta-openembedded/meta-oe"
      ],

      "local.conf": [
        "MACHINE = 'qemux86' ",
        "IMAGE_INSTALL:append = ' nano' "
      ]
    }
  }
}

Note : l’ajout de la recette nano dans la variable IMAGE_INSTALL se fait avec la notation :append. Cette extension remplace, depuis la mi-2021 la précédente notation _append.

Héritage

Nous avons donc deux builds distincts dans le même menu, chacun pour une cible spécifique. L’un deux utilise un layer particulier. Bien sûr, dans l’exemple ci-dessus le layer en question est l’ultra-courant meta-oe, mais il pourrait s’agir d’un support spécifique pour une plateforme matérielle ou un ensemble de recettes pour le code applicatif métier.

Approfondissons encore cet exemple avec deux nouveaux builds. L’un pour Raspberry Pi 3 et l’autre pour Raspberry Pi 4.

Il nous faut d’abord indiquer le dépôt spécifique à télécharger pour l’architecture Raspberry Pi.

  "sources" : [
    { "url": "git://git.yoctoproject.org/poky", "branch": "dunfell", "rev": "yocto-3.1.13" },
    { "url": "git://git.openembedded.org/meta-openembedded", "branch": "dunfell", "rev": "ab9fca48" },
    { "url": "git://git.yoctoproject.org/meta-raspberrypi", "branch": "dunfell", "rev": "934064a0" }
  ],

Puis nous créons les deux builds nécessaires. Ils devront inclure ce nouveau layer. Et dans l’attribut local.conf, nous ajoutons quelques lignes de configuration matérielle propre à ces cartes.

  "builds" : {
     [...]

    "rpi3" : {
      "target": "core-image-base",
      "layers" : [
        "meta-raspberrypi
      ],
      "local.conf" : [
        "MACHINE = 'raspberrypi3'  ",
        "ENABLE_UART = '1'         ",
        "ENABLE_SPI_BUS =  '1'     ",
        "ENABLE_I2C =  '1'         ",
        "HDMI_FORCE_HOTPLUG = '1'  ",
        "CONFIG_HDMI_BOOST = '1'   "
      ],
    },

    "rpi4" : {
      "target": "core-image-base",
      "layers" : [
        "meta-raspberrypi
      ],
      "local.conf" : [
        "MACHINE = 'raspberrypi4'  ",
        "ENABLE_UART = '1'         ",
        "ENABLE_SPI_BUS =  '1'     ",
        "ENABLE_I2C =  '1'         ",
        "HDMI_FORCE_HOTPLUG = '1'  ",
        "CONFIG_HDMI_BOOST = '1'   "
      ],
    }
  }

J’ai peut-être un peu forcé le trait, mais je suppose que vous comprenez le problème : ces deux builds qui ne diffèrent que par leurs noms et les variables MACHINES sont obligés de répéter une grosse poignée de lignes identiques.

Contrairement à la ligne IMAGE_FEATURES += 'empty-root-password' que nous avons précédemment inscrite dans l’attribut local.conf global à tout le menu, les lignes de configuration ci-dessus ne concernent que les deux builds pour Raspberry Pi 3 et 4, pas les autres builds. Il n’est donc pas possible de rajouter ces éléments dans la propriété local.conf du menu.

Pour simplifier le menu et le rendre plus maintenable en évitant les répétitions, il est possible d’utiliser un mécanisme d’héritage.

Nous pouvons définir un objet dans l’ensemble des builds qui ne sera pas compilé par lui-meme mais dont les attributs seront ajoutés aux builds qui demandent à en hériter.

Pour rendre un build non compilable, deux possibilités s’offrent à nous :

  • ne pas lui définir d’attribut target ;
  • lui donner un nom qui commence par un point. On dit qu’il agit alors d’un template.

C’est la seconde option que j’ai choisie ici. Le build générique .raspberrypi ne sera pas compilé, mais ses attributs seront hérités par les deux autres builds.

Pour indiquer qu’un build hérite d’un autre, il suffit de lui ajouter un attribut inherit indiquant le template de base.

Voici donc le fichier menu-003.json définitif. Je n’ai pas reproduit les builds qemux86 etqemuarm` déjà présents précédemment.

$ cat menu-003.json

{
  "sources" : [
    { "url": "git://git.yoctoproject.org/poky", "branch": "dunfell", "rev": "yocto-3.1.13" },
    { "url": "git://git.openembedded.org/meta-openembedded", "branch": "dunfell", "rev": "ab9fca48" },
    { "url": "git://git.yoctoproject.org/meta-raspberrypi", "branch": "dunfell", "rev": "934064a0" }
  ],

  "layers" : [
    "poky/meta",
    "poky/meta-poky",
    "poky/meta-yocto-bsp"
  ],

  "local.conf": [
    "IMAGE_FEATURES += 'empty-root-password' "
  ],

  "builds" : {

    "qemuarm": {
      [...]
    },

    "qemux86": {
      [...]
    },

    ".raspberrypi" : {

      "target": "core-image-base",

      "layers" : [
        "meta-raspberrypi"
      ],

      "local.conf" : [
        "ENABLE_UART = '1'         ",
         "ENABLE_SPI_BUS =  '1'     ",
         "ENABLE_I2C =  '1'         ",
         "HDMI_FORCE_HOTPLUG = '1'  ",
         "CONFIG_HDMI_BOOST = '1'   "
      ]
    },

    "rpi3" : {
      "inherit": [ ".raspberrypi" ],
      "local.conf": [
        "MACHINE = 'raspberrypi3'  "
      ]
    },

    "rpi4" : {
      "inherit": [ ".raspberrypi" ],
      "local.conf": [
        "MACHINE = 'raspberrypi4'  "
      ]
    }
  }
}

La structure du menu est ainsi assez élégante, tout ce qui concerne le template de base est regroupé en un seul point, et des builds dérivés peuvent en hériter pour compléter les paramètres.

Héritage multiple

Comme vous l’avez peut-être remarqué, l’attribut inherit d’un objet build est un ensemble. Il peut donc contenir le nom de plusieurs templates de base.

Cette notion d’héritage peut s’appliquer, comme ci-dessus au support matériel, mais également aux options du code métier.

Supposons par exemple, que notre code applicatif soit ajouté par une recette nommée custom-app et que cette dernière puisse être compilée au choix en versions « allégée » (lite), « normal » (base) et « complète » (ff pour full featured).

On pourra donc définir des templates de base spécifiques comme :

  ".lite" : {
    "local.conf": [
      "PACKAGECONFIG:append:pn-custom-app = ' lite' "
    ]
  },

  ".normal" : {
    "local.conf": [
      "PACKAGECONFIG:append:pn-custom-app = ' normal' "
    ]
  },

  ".ff" : {
    "local.conf": [
      "PACKAGECONFIG:append:pn-custom-app = ' ff' "
    ]
  }

Il sera alors possible de combiner des builds héritant de plusieurs templates, par exemple on pourrait définir :

  "rpi3-lt" : {
    "inherit": [ ".raspberrypi", ".lite" ],
    "local.conf": [ "MACHINE = 'raspberrypi3'  " ]
  },

  "rpi3" : {
    "inherit": [ ".raspberrypi", ".normal" ],
    "local.conf": [ "MACHINE = 'raspberrypi3'  " ]
  },

  "rpi3-ff" : {
    "inherit": [ ".raspberrypi", ".ff" ],
    "local.conf": [ "MACHINE = 'raspberrypi3'  " ]
  },

  "rpi4-lt" : {
    "inherit": [ ".raspberrypi", ".lt" ],
    "local.conf": [ "MACHINE = 'raspberrypi4'  " ]
  },

  "rpi4" : {
    "inherit": [ ".raspberrypi", ".normal" ],
    "local.conf": [ "MACHINE = 'raspberrypi4'  " ]
  },

  "rpi4-ff" : {
    "inherit": [ ".raspberrypi", ".ff" ],
    "local.conf": [ "MACHINE = 'raspberrypi4'  " ]
  },

À titre d’exemple, j’ai utilisé cette approche pour un projet client où il fallait gérer plusieurs plateformes matérielles et plusieurs tailles d’écran (ce qui jouait sur les fichiers bitmaps à utiliser pendant le boot).

Bien sûr on évitera de trop multiplier le nombre de builds à produire, pour limiter la complexité du travail à la production et à la maintenance.

Commentaires

L’enchaînement des différents builds peut être parfois un peu compliqué à suivre.

On peut regretter que le format JSON n’accepte pas de commentaires permettant d’expliquer un choix de package, la nécessité d’un layer ou une option de configuration d’un package.

Pour pallier ce problème, nous avons choisi d’ajouter un attribut facultatif notes que l’on peut ajouter au niveau du menu global ou au niveau d’un objet build.

À titre d’exemple nous pouvons observer des extraits d’un fichier-menu fourni en exemple avec Yocto Cooker (répertoire sample-menus)/

{
  "notes" : [
    "This is a standard menu for Raspberry Pi 3.",
    "Please refer to the README file for instruction on how to build the image"
  ],

  "sources" : [
    { "url": "git://git.yoctoproject.org/poky", "branch": "zeus", "rev": "yocto-3.0.1" },

[...]

  "builds" : {

    "pi3": {

      "notes" : [
        "The default `core-image-base` image is used for bitbake."
      ],
      "target" : "core-image-base",

[...]

Conclusion

Nous avons vu que le fichier-menu utilisé par Yocto Cooker reste assez simple et lisible, même s’il offre des possibilités plutôt avancées, comme l’héritage multiple.

Dans l’article précédent nous n’avons utilisé que les actions cooker cook et cooker shell. Il existe plusieurs autres actions possibles pour générer les images ou s’assurer du fonctionnement de cooker. Nous les examinerons dans
le prochain article.

Suivant :

Précédent :

URL de trackback pour cette page