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.confbblayers.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:
shellpythonbitbake-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:
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:
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:
def some_function ():
print("Hello World")Spesso vedremo definizioni in cui
è presente un argomento d:
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:
bbos
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:
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:
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:
TODAY = "${@import time; time.strftime('%Y%m%d', time.gmtime())}"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:
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:
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:
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:
VAR = "base"
VAR:append = " cock"
python () {
d.setVar("VAR", "new value")
}Che risulta in:
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:
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:
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:
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 myfuncAdesso possiamo chiamare direttamente il suo task:
bitbake u-boot -c myfunced 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).