Bitbake: funzioni

Oddio siamo ancora qua. Io te l’ho detto che Yocto è tosto, non mi credevi?

funzioni

Abbiamo visto che bitbake si basa sui seguenti file:

  • local.conf
  • bblayers.conf
  • .conf
  • .bb
  • .bbappend
  • .bbclass
  • .inc

Bene, le funzioni possono essere definite in tutti questi tipi di file, esclusi quelli con estensione .conf.

Esistono diversi tipi di funzioni, in base alla sintassi che vogliamo utilizzare e a come vengono riconosciute da bitbake:

  • shell
  • python
  • bitbake-style python

shell

Sono eseguite dalla shell di sistema, quindi /bin/sh, e non è detto che questa sia bash, quindi per favorire il riuso di recipe si evita di utilizzare proprietà disponibili sono in bash. Nei sistemi Ubuntu infatti, /bin/sh è un link simbolico a /bin/dash, perchè preferita per la sua velocità. La sintassi a cui affidarsi è quindi quella definita dello standard POSIX.

Un esempio di definizione di funzione shell:

bitbake file
some_function () {
    echo "Hello World"
}

Bitbake utilizza la shell di sistema, però non utilizza tutti i comandi presenti nel host, in quanto questo romperebbe il principio di isolamento del build-system, infatti nel file bitbake.conf del layer meta di Yocto troviamo una lista dei comandi permessi:

meta/conf/bitbake.conf
508##################################################################
509# Build utility info.
510##################################################################
511
512# Directory with symlinks to host tools used by build
513HOSTTOOLS_DIR = "${TMPDIR}/hosttools"
514
515# Tools needed to run builds with OE-Core
516HOSTTOOLS += " \
517    [ ar as awk basename bash bunzip2 bzip2 cat chgrp chmod chown chrpath cmp comm cp cpio \
518    cpp cut date dd diff diffstat dirname du echo egrep env expand expr false \
519    fgrep file find flock g++ gawk gcc getconf getopt git grep gunzip gzip \
520    head hostname iconv id install ld ldd ln ls lz4c make md5sum mkdir mkfifo mknod \
521    mktemp mv nm objcopy objdump od patch perl pr printf pwd \
522    python3 pzstd ranlib readelf readlink realpath rm rmdir rpcgen sed seq sh \
523    sha1sum sha224sum sha256sum sha384sum sha512sum \
524    sleep sort split stat strings strip tail tar tee test touch tr true truncate uname \
525    uniq unzstd wc wget which xargs zstd \
526"

python

Nonostante bitbake sia interamente scritto in python, ci sono delle accortezze da utilizzare per poter definire delle nuove funzioni python e integrarle nel resto del sistema.

La sintassi per definire una funzione di questo tipo è la classica sintassi python:

bitbake file
def some_function ():
    print("Hello World")

Spesso vedremo definizioni in cui è presente un argomento d:

bitbake file
def some_function (d):
    print(d.getVar("X"))

L’oggetto d è il datastore di bitbake. Questo contiene tutte le informazioni che bitbake ottiene dal suo parsing, quindi anche tutte le informazioni sulle variabili. Non si accede direttamente a questo oggetto, ma si utilizzano le funzioni API disponibili, come:

  • d.getVar("X", expand)
  • d.setVar("X", "value")
  • d.renameVar("X", "Y")
  • d.getVarFlags("X")

Importante notare che d non è visibile come variabile globale e deve quindi essere passato esplicitamente alla funzione in questione, poi vedremo come.

Una lista completa dei metodi di d è disponibile nella documentazione ufficiale.

Inoltre, nell’interprete python viene installato il modulo bb specifico per interagire con bitbake e che contiene funzioni utili come:

  • bb.fatal()
  • bb.note()

Quando scriviamo funzioni python possiamo importare moduli aggiuntivi, ma già di default sono importati i moduli:

  • bb
  • os

Possiamo quindi definire funzioni python:

def some_function(d):
    for i in ["binutils", "gas", "ld"]:
        builddir = os.path.join(d.getVar("B"), i)
        if not os.path.isdir(builddir):
            os.makedirs(builddir)
            bb.log("created folder {builddir}")
        with open(os.path.join(builddir, "newfile"), "w") as f:
            f.write("test")
            bb.log("created file {f}")

Possiamo esplorare il modulo bb partendo dal file di inizializzazione del modulo __init__.py in cui vediamo la definizione di funzioni come:

bitbake/lib/bb/__init__.py
178# Messaging convenience functions
179def plain(*args):
180    mainlogger.plain(''.join(args))
181
182def debug(lvl, *args):
183    if isinstance(lvl, str):
184        mainlogger.warning("Passed invalid debug level '%s' to bb.debug", lvl)
185        args = (lvl,) + args
186        lvl = 1
187    mainlogger.bbdebug(lvl, ''.join(args))
188
189def note(*args):
190    mainlogger.info(''.join(args))

python inline

All’interno di una normale espansione di bitbake è possibile utilizzare il carattere @ per informare bitbake che l’espansione deve essere eseguita con sintassi python:

${@    }

L’effetto finale è che l’espansione assumerà il valore di return del codice python. Quindi qui dentro possiamo utilizzare tutti i comandi che useremmo in una funzione python, incluse le funzioni python definite in precedenza nel file:

bitbake file
def get_bootloader(d):
    board = d.getVar("BOARD")
    match board:
        case "board1":
            bootloader = "generic-bootloader"
        case "board2":
            bootloader = "uboot-32"
        case _:
            bootloader = "generic-bootloader"
    return bootloader

BL = "${@get_bootloader(d)}"

O anche operazioni più complesse:

bitbake file
TODAY = "${@import time; time.strftime('%Y%m%d', time.gmtime())}"
when displayed
TODAY = "20251117"

In questo modo riusciamo quindi a chiamare del puro codice python all’interno di un file bitbake.

bitbake-style python

Le funzioni definite con codice python puro hanno lo svantaggio di necessitare della sintassi ${@ } per poter essere riconosciute ed eseguite da bitbake. Esiste una soluzione “ibrida” che permette di usare la sintassi python allo stesso modo, ma permettendo alla funzione di essere integrata meglio nel resto del codice bitbake. Questa tipologia di funzioni si chiama bitbake-style python e si definiscono allo stesso modo delle funzioni shell, ma antecedendo il nome della funzione con python:

bitbake file
python some_function () {
    d.setVar("TEXT", "Hello World")
    print d.getVar("TEXT")
}

Un grande vantaggio è che in queste funzioni il datastore d è visibile come variabile globale e non serve quindi passarlo esplicitamente alla funzione.

Queste funzioni figurano come funzioni shell e possono essere chiamate da altre funzioni shell in modo semplice:

bitbake file
python some_function () {
    d.setVar("VAR", "Hello World")
}

do_compile (){
    some_function
    echo ${VAR}
    # do other stuff
    # ...
}

python anonymous

Finora abbiamo visto funzioni che necessitano di essere chiamate per essere eseguite. In bitbake esistono anche funzioni che vengono eseguite in automatico alla fine del parsing, nell’ordine in cui sono state trovate. Vengono chiamate anonymous perchè appunto sono come delle funzioni bitbake-style python, ma senza un nome:

bitbake file
python () {
    if d.getVar('SOMEVAR') == 'value':
        d.setVar('ANOTHERVAR', 'value2')
}

Un altro modo per definirle è dandogli il nome __anonymous. Una cosa importante da sapere è che queste vengono eseguite dopo gli operatori differiti :append,:prepend e :remove e sono quindi in grado di resettare completamente una variabile:

bitbake file
VAR = "base"
VAR:append = " cock"

python () {
    d.setVar("VAR", "new value")
}

Che risulta in:

when displayed
VAR = "new value"

chiamate di funzioni

In base alla sintassi e a come vengono riconosciute da bitbake cambia il modo in cui possiamo chiamare una funzione da dentro un’altra. Se la sintassi della funzione è shell, allora possiamo chiamare funzioni shell o bitbake-style python con una semplice chiamata, mentre per funzioni pure python dobbiamo usare la sintassi python inline (${@ }).

Se la sintassi della funzione è python, allora possiamo chiamare funzioni shell o bitbake-style python con la funzione bb.build.exec_func(), mentre per funzioni pure python basta una semplice chiamata.

operatori differiti

Sulle funzioni shell o bitbake-style python è anche possibile utilizzare gli operatori differiti :append e :prepend per andare a modificare la funzione originale:

bitbake file
 1python do_foo:prepend() {
 2    bb.plain("first")
 3}
 4
 5python do_foo() {
 6    bb.plain("second")
 7}
 8
 9python do_foo:append() {
10    bb.plain("third")
11}

tasks

Solo le funzioni shell o bitbake-style python possono essere promosse a task con l’istruzione addtask. Se vengono promosse a task allora in WORKDIR/temp verranno generati dei relativi file run e log. Ricordiamo che ogni funzione che viene promossa a task è bene che abbia un nome che inizia con do_.

Le funzioni pure python sono quindi eseguite solo durante il parsing, non durante la build vera e propria. In sostanza rendono più dinamico il file bitbake.

flags

Come per le variabili, anche per i task è possibile dichiarare delle proprietà aggiuntive tramite i flags. Alcuni di questi sono già definiti da bitbake:

[cleandirs] indica le cartelle vuote che devono essere create prima che il task venga eseguito. Se tali cartelle esistono già, allora vengono eliminate e ricreate.

[dirs] indica le cartelle che devono essere create prima che il task venga eseguito. Se tali cartelle esistono già, allora vengono queste non vengono toccate. L’ultima cartella specificata viene utilizzata come working directory del task.

[prefuncs]indica le funzioni da chiamare prima di eseguire il task.

[postfuncs] indica le funzioni da chiamare alla fine del task.

Per una lista completa, consultare la documentazione ufficiale.

dipendenze

Per controllare le dipendenze tra i vari task, ricordiamo che l’istruzione addtask permette di definire anche quali task eseguire prima o dopo un determinato task:

addtask <taskname> before <task1 task2 ...> after <task3 task4 ...>

vederlo coi propri occhi

In questo caso non è possibile utilizzare semplicemente il file local.conf, perchè le funzioni non sono definibili nei file .conf, quindi dobbiamo utilizzare un recipe (.bb). Se non vogliamo crearne uno da zero, ci basta modificare uno di quelli presenti nel layer meta, ad esempio u-boot_2024.01.bb:

meta/recipes-bsp/u-boot/u-boot_2024.01.bb
1require u-boot-common.inc
2require u-boot.inc
3
4DEPENDS += "bc-native dtc-native python3-pyelftools-native"
5
6myfunc () {
7    echo "hellooooo"
8}

Siccome questa non è una funzione python anonymous, per essere eseguita deve essere chiamata da qualcuno. Visto che è una shell function possiamo promuoverla a task, ricordandoci anche di cambiarle il nome con do_ all’inizio:

meta/recipes-bsp/u-boot/u-boot_2024.01.bb
 1require u-boot-common.inc
 2require u-boot.inc
 3
 4DEPENDS += "bc-native dtc-native python3-pyelftools-native"
 5
 6do_myfunc () {
 7    echo "hellooooo"
 8}
 9
10addtask myfunc

Adesso possiamo chiamare direttamente il suo task:

terminal
bitbake u-boot -c myfunc

ed essendo un task possiamo vederne i relativi file run e log in build/tmp/work/*/u-boot*/*/temp/ (uso gli asterischi perchè il path completo dipende dalla variabile MACHINE e dalla versione del recipe).