Bitbake: variabili
Adesso è arrivato il momento di parlare del cuore di tutto il sistema Yocto, cioè bitbake.
Lo abbiamo già brevemente introdotto
e citato, ma per completezza alcune
cose le ripeteremo anche qua.
Bitbake è il build system su cui
si basa Yocto ed è molto simile
al classico make,
però con delle aggiunte fondamentali
che lo rendono molto comodo
per creare delle intere distribuzioni
Linux, quindi progetti molto grandi
e che richiedono un alto grado
di flessibilità.
Partiamo dalle basi,
innanzitutto bitbake è
uno script python che per
comodità di sviluppo è suddiviso
in molteplici moduli.
L’intero strumento è definito nella
repository bitbake
e l’entrypoint è definito dal
file bitbake/bin/bitbake.
Nella prima riga di questo vediamo
proprio che è definito come
script python:
1#!/usr/bin/env python3Nella repository,
i moduli sono definiti
all’interno della cartella lib:
bitbake
├── bin
│ ├── bitbake
│ └── ...
├── contrib
├── doc
└── lib
├── bb
│ ├── data.py
│ ├── event.py
│ └── ...
│
└── bblayers
├── common.py
└── ...Come il comando make
genera un output di build
a partire da un Makefile,
così bitbake
genera un output di build
a partire dai seguenti file:
local.confbblayers.conf.conf.bb.bbappend.bbclass.inc
Dove local.conf e bblayers.conf
sono definiti all’interno della cartella
conf di configurazione del progetto,
mentre gli altri tipi di file possiamo trovarli
all’interno dei vari layer.
Un aspetto importante di bitbake
è la sua capacità di “sovrapporre”
delle logiche definite in posti diversi
e con responsabilità differenti.
Nel senso che è possibile
utilizzare del codice fornito
da qualcun’altro,
ma applicarci sopra le nostre modifiche
in modo del tutto non-distruttivo,
quindi senza andare a cambiare il codice
originale.
Questo per evitare situazioni in
cui uno dei pezzi di codice che abbiamo usato
venga aggiornato dal autore originale
o un altro manutentore
e noi ci ritroviamo nella situazione
di dover ricreare le nostre modifiche
alla nuova codebase.
Si può gestire con git merge, sì,
ma con bitbake abbiamo molti altri strumenti
e li scegliamo in base alla quantità
di modifiche che vogliamo apportare
all’originale.
Daje, cominciamo.
variabili
Vediamo innanzitutto cosa possiamo aspettarci all’ interno dei vari file su cui si basa bitbake, poi vedremo anche il comando vero e proprio e le sue opzioni.
1VARIABLE = "value"In bitbake tutte le variabili sono delle stringhe,
quindi è necessario utilizzare le
doppie virgolette "
oppure le singole virgolette ',
altrimenti bitbake darà un errore di sintassi.
Non esistono differenze nell’interpretazione di
una stringa racchiusa tra doppie virgolette,
rispetto ad una racchiusa tra singole virgolette.
Inoltre, i whitespace definiti all’interno della stringa
rimangono,
sia che siano all’inizio,
sia che siano alla fine.
Una stringa può assumere il valore nullo con "",
oppure il valore “vuoto” con un singolo whitespace " ":
1VARIABLE = " value"
2VARIABLE = "value "
3VARIABLE = ""
4VARIABLE = " "Solitamente i nomi delle variabili sono
maiuscoli, ma solo per convenzione,
per distinguerli meglio dai nomi di funzioni.
I nomi che iniziano con un underscore _
non sono interpretati, perchè utilizzati
per variabili interne di bitbake.
Gli underscore possono però
essere utilizzati
all’interno del nome della variabile.
Per migliorare la leggibilità
è possibile assegnare una variabile
in modalità multi-line utilizzando
il carattere \, che spezza la riga.
Il parser di bitbake si occuperà di
unire i pezzi prima e dopo di tale carattere,
conservando tutti gli spazi:
1VAR_A = "apple \
2abstract \
3account \
4"
5
6VAR_B = "bee \
7 barbecue \
8 bow \
9 "viene interpretato come:
VAR_A = "apple abstract account "
VAR_B = "bee barbecue bow "Per richiamare il valore di una variabile
all’interno di una stringa si
utilizza la notazione ${},
quindi utilizzando ${VAR_A}
all’ interno di una stringa "${VAR_A}".
Questo segue il classico meccanismo
di variable expansion della bash Linux,
con la differenza che la semplice
notazione $ non è permessa
(il nome della variabile viene interpretato
come una stringa).
Quindi:
1VAR_C = "${VAR_A}"
2VAR_C = "$VAR_A"viene intepretato come:
VAR_C = "apple abstract account "
VAR_C = "$VAR_A"Non è consentito assegnare una self reference,
quindi il valore di una variabile non può
contenere una espansione della variabile stessa:
1VAR = "base"
2VAR = "${VAR} brick"Per aggiungere e rimuovere valori da una variabile senza riassegnarla completamente è necessario utilizzare gli operatori che vedremo di seguito. Oppure fare magheggi strani, sempre una opzione valida.
espansione differita
Se l’assegnazione è fatta con l’operatore =,
la variabile a sinistra non assume immediatamente
il valore delle variabili utilizzate a destra,
ma ne mantiene solo un riferimento.
Eventuali successive modifiche alle variabili a destra,
comporteranno modifiche anche alla variabile di sinistra.
L’espansione viene effettivamente valutata solo
quando la variabile è utilizzata.
E’ possibile anche utilizzare variabili a destra non esistenti
al momento dell’assegnazione.
Se la variabile di sinistra contiene riferimenti a
variabili di destra non ancora definite, questi
verranno intepretati come stringa letterale nel
caso la variabile di sinistra venga utilizzata.

espansione immediata
Se invece l’assegnazione è fatta con l’operatore :=,
le variabili a destra vengono immediatamente valutate ed espanse
e la variabile a sinistra ne assume il valore.
La variabile a sinistra sarà quindi slegata da eventuali modifiche
alle variabili di destra.
Se al momento dell’ assegnazione la variabile di destra non esiste,
verrà valutata come nulla.

append e prepend immediati
L’operatore di append += aggiunge in coda alla stringa
inserendo uno spazio aggiuntivo tra il valore originale
e quello aggiunto.
L’operatore di append .= fa la medesima
operazione, ma senza inserire uno spazio aggiuntivo.
Gli operatori prepend =+ =. sono analoghi,
ma aggiungono in testa alla stringa, quindi
precedono tutto il contenuto originale.
Tutti questi operatori hanno effetto immediato,
quindi se nella string di destra compaiono variabili
da espandere, questo viene fatto subito,
perdendo quindi il riferimento
(in modo simile a :=).
meccanismi particolari
definiti in:
1"""
2BitBake Smart Dictionary Implementation
3
4Functions for interacting with the data structure used by the
5BitBake build tools.
6
7"""overrides
Bitbake dispone di un meccanismo di
conditional metadata, ovvero
di assegnare ad una variabile
molteplici valori anche diversi
e di utilizzarne solo uno
a seconda che una condizione sia
verificata o meno.
Le “condizioni” sono delle stringhe
che possono comparire o meno
all’ interno della variabile
OVERRIDES.
Per specificare un valore condizionato
su una variabile si utilizza
il carattere : seguito dal nome
della condizione
e poi una normale assegnazione.
Ad esempio, possiamo utilizzare
come condizione la presenza o meno
della stringa fault
all’interno della variabile
OVERRIDES e cambiare di conseguenza
il valore di VAR:
1VAR = "OK"
2VAR:fault = "NONONO"Quindi solitamente avremo:
VAR = "OK"ma se in qualcuno dei nostri file
di configurazione (.conf) o in
qualche ricetta compare:
1OVERRIDES = "fault"allora avremo:
VAR = "NONONO"Possiamo anche definire molteplici override,
che possono essere attivi in modo distinto
o anche contemporaneo.
Nel caso una variabile abbia molteplici
override attivi,
vince quello che compare per ultimo
nella variabile OVERRIDES:

Per la definizione di un override
molto lungo
è consigliato non utilizzare
i caratteri : _,
in quanti usati come separatore
(_ nelle vecchie versioni Yocto)
e nemmeno gli operatori += =+
(aggiungono automaticamente dei whitespace).
1OVERRIDES = "fault:ok"
2
3# avoid
4OVERRIDES += "abc"
5OVERRIDES += "abc:def"
6OVERRIDES .= "very:long:override"
7OVERRIDES .= "very_long_override"
8
9# prefer these
10# append
11OVERRIDES .= ":abc"
12OVERRIDES .= ":def:ghi"
13OVERRIDES .= ":very-long-override"
14# prepend
15OVERRIDES =. "abc:"
16OVERRIDES =. "def:ghi:"
17OVERRIDES =. "very-long-override:"Un utilizzo comodo e comune degli override
è di utilizzare il valore
di variabili comuni come ARCH come condizione,
quindi aggiungendola in OVERRIDES:
OVERRIDES .= ":${ARCH}"Possiamo così creare una variabile VAR
condizionata su alcune architetture specifiche.
Mettiamo caso che i valori che può assumere
ARCH siano:
arm32arm64intel- o altro
Ad esempio possiamo utilizzare VAR
per decidere quale tipologia di bootloader utilizzare:

Il valore di VAR senza override
assume quindi il significato di “default”,
cioè il valore utilizzato se non esistono
condizioni particolari.
La lista degli ovverides definiti
di default da Yocto è visibile nel solito
file bitbake.conf:
802# Overrides are processed left to right, so the ones that are named later take precedence.
803# You generally want them to go from least to most specific. This means that:
804# A variable '<foo>:arm' overrides a variable '<foo>' when ${TARGET_ARCH} is arm.
805# A variable '<foo>:qemuarm' overrides '<foo>' and overrides '<foo>:arm' when ${MACHINE} is 'qemuarm'.
806# If you use combination ie '<foo>:qemuarm:arm', then '<foo>:qemuarm:arm' will override
807# '<foo>:qemuarm' and then '<foo>' will be overriden with that value from '<foo>:qemuarm'.
808# And finally '<foo>:forcevariable' overrides any standard variable, with the highest priority.
809# This works for functions as well, they are really just variables.
810#
811OVERRIDES = "${TARGET_OS}:${TRANSLATED_TARGET_ARCH}:pn-${PN}:layer-${FILE_LAYERNAME}:${MACHINEOVERRIDES}:${DISTROOVERRIDES}:${CLASSOVERRIDE}${LIBCOVERRIDE}:forcevariable"
812FILE_LAYERNAME ??= "config"
813LIBCOVERRIDE ?= ""
814CLASSOVERRIDE ?= "class-target"
815DISTROOVERRIDES ?= "${@d.getVar('DISTRO') or ''}"
816MACHINEOVERRIDES ?= "${MACHINE}"
817
818FILESOVERRIDES = "${TRANSLATED_TARGET_ARCH}:${MACHINEOVERRIDES}:${DISTROOVERRIDES}"Per il modo in cui sono valutati
(da sinistra verso destra),
è sensato fare un append a
OVERRIDES in modo che le condizioni
più specifiche siano le ultime:
1OVERRIDES .= "${BOARD}:rev-${REV}"
2
3# default
4VAR = "generic-bootloader"
5
6VAR:board1 = "generic-bootloader"
7
8# switched to uboot
9VAR:board2 = "uboot-32"
10VAR:board2:rev-v2 = "uboot-32"
11
12# hw change, 64bit cpu
13VAR:board2:rev-v3 = "uboot-64"
14
15# hw changes, back to 32bit cpu
16VAR:board2:rev-v4 = "uboot-32"Potremmo vedere altri esempi, ma la possibilità sono veramente troppe, quindi li vedremo direttamente in azione nelle sezioni successive.
Per farla breve,
il meccanismo di overrides
permette di evitare
di dover utilizzare delle funzioni
contenti degli if
o altri costrutti più complicati
solo per assegnare valori diversi a delle variabili,
con anche il problema di capire quando chiamare
tali funzioni:
1# default
2VAR = "generic-bootloader"
3
4python determine-bootloader() {
5 match d.getVar("BOARD"):
6 case "board1":
7 d.setVar("VAR", "generic-bootloader")
8
9 case "board2":
10 match d.getVar("REV"):
11 case "v2":
12 d.setVar("VAR", "uboot-32")
13 case "v3":
14 d.setVar("VAR", "uboot-64")
15 case "v4":
16 d.setVar("VAR", "uboot-32")
17 case _:
18 d.setVar("VAR", "uboot-32")
19
20 case _:
21 d.setVar("VAR", "generic-bootloader")
22}
23
24addtask determine-bootloader before do_fetchFun fact: in vecchie versioni di Yocto
(fino a Honister 3.4)
per definire un override
si usava il carattere _ invece che :,
quindi era un fottuto casino per chi
non sapeva di questo meccanismo capire
quando stava effettivamente venendo utilizzato.
Magical names do magical stuff.
Quindi in molti recipe pensati per vecchie
versioni di Yocto, state attenti a questo dettaglio.
append e prepend differiti
Gli operatori :append e :prepend
sfruttando la sintassi override
(sono considerati come caso particolare)
e hanno effetto dopo
tutti gli altri operatori.
In pratica avvisano bitbake di fare
una modifica alla variabile in questione,
ma solo nell’ ultimo momento possibile,
cioè quello della valutazione.
Questo permette di effettuare un append anche se la variabile viene successivamente riassegnata:

Questo significa che se
in uno dei molteplici possibili
posti nel nostro codice
viene utilizzato un :append
su una variabile,
l’unico modo per togliere
questo effetto è quello di utilizzare
l’operatore :remove.
Gli operatori :append, :prepend e :remove
vengono infatti valutati in questo esatto ordine.
Gli operatori :append e :prepend
non inseriscono automaticamente alcun whitespace,
quindi queste aggiunte le dobbiamo gestire noi manualmente.
Inoltre è possibile combinare questi operatori con i classici override per applicare delle modifiche solo se in presenza di una condizione:

La variabile VAR:special
viene quindi trattata come
una variabile separata
e se non le viene assegnato
esplicitamente un valore,
questa risulterà nulla
e poi vi verranno applicati
gli operatori definiti.
Successivamente le verranno applicati
anche gli operatori definiti su VAR.
Non è necessario che l’operatore si
trovi sempre come ultimo override,
anzi, bitbake ammette
che si possa utilizzare
un operatore sulla variabile (VAR)
e poi uno o più override:

In questo caso il valore
viene aggiunto alla lista degli
append di VAR
invece che di VAR:special,
perciò il valore originale
di VAR rimane.
Se precedentemente erano stati
definiti altri operatori su VAR,
quest’ultimo operatore andrà ad
aggiungersi dopo gli altri.
Questo meccanismo può diventare
rapidamente molto
imprevedibile e complesso da capire,
per questo cercheremo di utilizzarlo
con parsimonia.
In sostanza, tra i due esempi
cambia se l’append condizionato viene fatto
solo su una variante di VAR (cioè VAR:special)
oppure sul valore di default di VAR
(riversandosi quindi anche su tutte le sue varianti).
Un breve schema:

Ricordando che ciò che è definito più in alto
ha la precedenza su ciò che gli sta sotto.
Quindi gli append fatti da qualcuno in alto,
vengono valutati prima degli append inferiori,
quindi tale valore “in alto” nella stringa finale
comparirà prima di uno “in basso”.
Viceversa per i prepend.
Lo so, è un bordello, mi dispiace.
E per più possono esserci molteplici
override definiti
e la precedenza la ha chi è definito
“per primo” nella variabile OVERRIDES.
Basta, passiamo ad altro.
remove
L’operatore :remove è l’ultimo ad essere applicato
quindi il suo effetto è letteralmente definitivo.
In sostanza rimuove la stringa dalla variabile,
ma solo se questa compare effettivamente.
Non serve utilizzare i caratteri whitespace
perchè questo operatore spezza il valore della variabile
ad ogni whitespace e rimuove il pezzo che contiene
la stringa.
VAR = "apple bee coral"
VAR:remove = "bee"allora avremo:
VAR = "apple coral"Ci ritroveremo con dei whitespace in eccesso, ma onestamente non ce ne frega un cazzo.
flags
Bitbake implementa anche un meccanismo
per aggiungere delle proprietà ad attributi
alla variabili, tramite le var flags.
In sostanza si comportano come delle nuove variabili, ma bitbake le considera in relazione alla variabile vera e propria. Possiamo usare tutti gli operatori per modificarne il valore, tranne quelli basati su override.
Ad esempio, definendo dei
generici flag a e b:
1VAR[a] = "abc"
2VAR[b] = "123"
3VAR[a] += "456"VAR[a] = "abc 564"
VAR[b] = "123"E se utilizziamo un flag non definito, il suo valore sarà nullo.
Uno di quelli più utilizzati è doc,
che permette di documentare una variabile:
1CACHE[doc] = "The directory holding the cache of the metadata."vederlo coi propri occhi
Dopo tutti questi discorsi,
potrei avervi semplicemente detto un mare di cazzate
e dubitarne è giusto.
Per provare a vedere come bitbake effettua
il parsing ci basta scrivere le nostre istruzioni
in un file comodo come local.conf
ed eseguire il comando:
bitbake -eA schermo comparirà una valanga di output con tutte le variabili e funzioni bitbake, quindi per qualcosa di più comodo è meglio reindirizzare l’output del comando su un file:
bitbake -e > vars.txtIl perchè di questi comandi è meglio che lo vediamo più avanti.
Quindi, se in local.conf
scriviamo delle istruzioni come:
VAR = "base"
VAR := "now"
VAR += "add"
VAR =+ "pre"
VAR .= " another"
VAR =. "before "
VAR:append = " more"
VAR:prepend = "well "
VAR:remove = "today"
VAR:fault = "ohno"
VAR:fault:append = " nope"nel file vars.txt vedremo:
#
# $VAR [11 operations]
# set /workdir/build/conf/local.conf:269
# "base"
# immediate /workdir/build/conf/local.conf:270
# "now"
# append /workdir/build/conf/local.conf:271
# "add"
# prepend /workdir/build/conf/local.conf:272
# "pre"
# postdot /workdir/build/conf/local.conf:273
# " another"
# predot /workdir/build/conf/local.conf:274
# "before "
# :append /workdir/build/conf/local.conf:275
# " more"
# :prepend /workdir/build/conf/local.conf:276
# "well "
# :remove /workdir/build/conf/local.conf:277
# "today"
# override[fault]:set /workdir/build/conf/local.conf:278
# "ohno"
# override[fault]::append /workdir/build/conf/local.conf:279
# " nope"
# pre-expansion value:
# "well before pre now add another more"
VAR="well before pre now add another more"
#
# $VAR:fault [2 operations]
# set /workdir/build/conf/local.conf:278
# "ohno"
# :append /workdir/build/conf/local.conf:279
# " nope"
# pre-expansion value:
# "ohno nope"
VAR:fault="ohno nope"in cui vediamo tutte le operazioni che vengono effettuate sulla variabile. Gli operatori compaiono con i nomi utilizzati internamente da bitbake:
=set:=immediate+=append=+prepend.=postdot=.predot:append:append:prepend:prepend:remove:remove
e gli override con la dicitura:
override[<name>]:<operation>Concludiamo questa parte sulle variabili con un link alla sintassi completa nella documentazione ufficiale di bitbake.
Adesso, caffè.