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:

bitbake/bin/bitbake
1#!/usr/bin/env python3

Nella repository, i moduli sono definiti all’interno della cartella lib:

bitbake
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.conf
  • bblayers.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.

bitbake file
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 " ":

bitbake file
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:

bitbake file
1VAR_A = "apple \
2abstract \
3account \
4"
5
6VAR_B = "bee \
7    barbecue \
8    bow \
9    "

viene interpretato come:

when displayed
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:

bitbake file
1VAR_C = "${VAR_A}"
2VAR_C = "$VAR_A"

viene intepretato come:

when displayed
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:

don't do
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:

bitbake/lib/bb/data_smart.py
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:

bitbake file
1VAR = "OK"
2VAR:fault = "NONONO"

Quindi solitamente avremo:

when displayed
VAR = "OK"

ma se in qualcuno dei nostri file di configurazione (.conf) o in qualche ricetta compare:

bitbake file
1OVERRIDES = "fault"

allora avremo:

when displayed
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).

bitbake file
 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:

  • arm32
  • arm64
  • intel
  • 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:

meta/conf/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:

bitbake file
 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:

bitbake file
 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_fetch

Fun 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.

bitbake file
VAR = "apple bee coral"
VAR:remove = "bee"

allora avremo:

when displayed
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:

bitbake file
1VAR[a] = "abc"
2VAR[b] = "123"
3VAR[a] += "456"
when displayed
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:

bitbake file
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:

terminal
bitbake -e

A 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:

terminal
bitbake -e > vars.txt

Il perchè di questi comandi è meglio che lo vediamo più avanti.

Quindi, se in local.conf scriviamo delle istruzioni come:

local.conf
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:

vars.txt
#
# $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è.