Recipe II

Proseguiamo il discorso che avevamo iniziato sui recipe in un articolo precedente.

Per capire come funziona un recipe dobbiamo intanto trovare dei nomi precisi per riferirci a delle cose precise. Usiamo quindi i nomi che bitbake utilizza per le variabili collegate ad un determinato recipe.

Per quanto riguarda il file .bb o .bbappend che si trova dentro il layer abbiamo:

  • FILE percorso assoluto al file .bb
  • FILE_DIRNAME percorso assoluto alla cartella contenente il .bb
  • FILE_LAYERNAME nome del relativo contenitore di recipe
  • PN nome
  • PV versione
  • BBINCLUDED percorsi dei .bb* utilizzati

Per quanto riguarda il processo di build del recipe, all’interno di build/tmp abbiamo:

  • WORKDIR dove avviene costruito il recipe
  • SRC_URI da dove arrivano i sorgenti per il recipe
  • S dove vengono messi i sorgenti
  • B dove avviene la compilazione
  • D dove viene creato il fake-root-filesystem

Seguendo l’esempio di qui sopra abbiamo:

FILE = "/layers/meta-mylayer/recipes-example/myapp/myapp_2.1.bb"
FILE_DIRNAME = "/layers/meta-mylayer/recipes-example/myapp"
FILE_LAYERNAME = "example"
PN = "myapp"
PV = "2.1"
BBINCLUDED = "......."

WORKDIR = "/build/tmp/work/devboard-2/myapp/2.1"
SRC_URI = "https://git.example.com"
S = "/build/tmp/work/devboard-2/myapp/2.1/src"
B = "/build/tmp/work/devboard-2/myapp/2.1/build"
D = "/build/tmp/work/devboard-2/myapp/2.1/image"

Un recipe non è altro che una lista di istruzioni da eseguire per ottenere un risultato a partire da una serie di ingredienti. SRC_URI è la variabile principale che ci indica quali sono questi ingredienti. Adesso vediamo come aggiungere ingredienti alla nostra ricetta.

aggiungere ingredienti

Prendiamo la seguente FILE_DIRNAME come esempio:

FILE_DIRNAME directory
myapp
 ├── files
 │    ├── special_constants.h
 │    ├── doc_special_constants.md
 │    └── dark-theme.patch
 └── myapp_2.1.bb

Avente come recipe:

myapp_2.1.bb
0SRC_URI = "https://git.example.com"
1SRCREV = "${AUTOREV}"
2
3S = "${WORKDIR}/git"
4B = "${WORKDIR}/build"
5
6do_compile(){
7    # compile commands
8}

SRC_URI ha un comportamento un po’ particolare, perchè a volte estrae i file dentro S, altre volte li estrae in WORKDIR. Dipende se stiamo definendo una “cartella/archivio” (come tar, git) o se stiamo definendo dei file singoli. I contenuti delle cartelle vengono estratti o clonati in una posizioine temporanea e poi spostati dentro S, mentre i file singoli vengono copiati direttamente dentro WORKDIR.

Per i file locali SRC_URI cerca in alcune sottocartelle predefinite di FILE_DIRNAME, come ad esempio files. Poi vedremo come poter utilizzare altri nomi.

Quindi se dopo il fetch e unpack la nostra WORKDIR appare così:

layer directory
WORKDIR
 ├── git
 │    ├── code.c
 │    ├── code.h
 │    └── Cmakelists.txt
 ├── build
 ├── image
 ├── packages
 └── temp

e vogliamo aggiungere il file special_constants.h senza dover modificare il progetto git originale possiamo modificare SRC_URI:

myapp_2.1.bb
 0SRC_URI = "https://git.example.com"
 1SRCREV = "${AUTOREV}"
 2
 3S = "${WORKDIR}/git"
 4B = "${WORKDIR}/build"
 5
 6# change original git without forking 
 7SRC_URI += "file://special_constants.h;subdir=git"
 8
 9do_compile(){
10    # compile commands
11}

dove la costruzione di SRC_URI rispetta la sintassi specifica

SRC_URI = "source1;option1;option2;option3 source2;option1 ..."

e l’opzione subdir=git informa bitbake di posizionare il file nella subdirectory git della WORKDIR. Altre opzioni di SRC_URI sono visibile nella documentazione ufficiale.

La nostra WORKDIR appare ora così:

recipe workdir
WORKDIR
 ├── git
 │    ├── code.c
 │    ├── code.h
 │    ├── special_constants.h 
 │    └── Cmakelists.txt
 ├── build
 ├── image
 ├── packages
 └── temp

modificare ingredienti

Spesso capita di dover applicare delle modifiche ad alcuni file di S e per farlo possiamo usare vari modi:

  • sostituire tali file (sconsigliato)
  • applicare una patch alla repository
  • creare una funzione apposita per effettuare un sed sui file

sostituzione completa

Se vogliamo rimpiazzare totalmente il file code.c con un omonimo, ma avente contenuti diversi, possiamo riscrivere il nostro recipe come:

myapp_2.1.bb
 1SRC_URI = "https://git.example.com"
 2SRCREV = "${AUTOREV}"
 3
 4S = "${WORKDIR}/git"
 5B = "${WORKDIR}/build"
 6
 7# replace file 
 8SRC_URI += "file://code.c;subdir=git"
 9
10do_compile(){
11    # compile commands
12}

patch

La creazione della patch la vedremo a breve, ma intanto facciamo conto di averne una che a partire dalla repository originale effettua delle modifiche su code.c:

myapp_2.1.bb
 1SRC_URI = "https://git.example.com"
 2SRCREV = "${AUTOREV}"
 3
 4S = "${WORKDIR}/git"
 5B = "${WORKDIR}/build"
 6
 7# apply patch to entire repository
 8SRC_URI += "file://dark-theme.patch;subdir=git"
 9
10do_compile(){
11    # compile commands
12}

Quando in SRC_URI compare un file con estensione .patch o .diff, bitbake prova in automatico ad applicarla. Non è necessario specificare la subdir corretta. L’opzione apply=no evita questo automatismo.

La nostra WORKDIR appare identica a quella iniziale, ma con la presenza del file dark-theme.patch e con i contenuti di code.c modificati dalla patch:

recipe workdir
WORKDIR
 ├── git
 │    ├── code.c
 │    ├── code.h
 │    ├── dark-theme.patch
 │    └── Cmakelists.txt
 ├── build
 ├── image
 ├── packages
 └── temp

sostituzione contenuti (sed)

Il comando sed (stream editor) fornisce diverse modalità per modificare un file o un qualsiasi input testuale. Dato un determinato pattern permette di modificare le singole occorrenze oppure l’intera riga in cui si verifica l’occorrenza. Per una spiegazione completa del comando guardare la documentazione, qui faremo solo una breve panoramica delle opzioni più utili perchè ne esistono veramente molte. Per modificare un file si utilizzano gli argomenti:

  • -i indica in-place
  • -e indica expression e viene seguito dal comando.

Il comando definisce l’operazione da effettuare e il modo in cui effettuarla. La sintassi generale prevede di indicare:

  • l’operazione
  • il pattern da cercare
  • quante volte eseguire l’operazione sui match trovati

L’operazione è indicata da una singola lettera e il primo carattere che la segue è considerato in automatico come il separatore dei vari campi (delimiter). Come separatore possiamo quindi usare |, ,, @ o altri caratteri a seconda di quale ci torna più comodo. L’ultimo campo indica dove effettuare l’operazione e può essere:

  • g per indicare global cioè su tutte le occorrenze
  • numero per indicare su quale occorrenza (1 indica la prima)
  • numero seguito da g per indicare tutte le occorrenze fino a quella definita

Le lettere associate alle operazioni sono:

  • s per substitute
  • a per append
  • i per insert
  • d per delete
  • c per change (sostituisce l’intera riga)
  • p per print

Quindi per sostituire tutte le occorrenze di un pattern in un file possiamo usare il comando:

sed -i -e 's|pattern|replacement|g' "file"

E se dato un file code.c vogliamo sostituire tutte le occorrenze di LED_PIN con OUTPUT_PIN possiamo utilizzare un recipe del tipo:

myapp_2.1.bb
 1SRC_URI = "https://git.example.com"
 2SRCREV = "${AUTOREV}"
 3
 4S = "${WORKDIR}/git"
 5B = "${WORKDIR}/build"
 6
 7do_compile(){
 8    # compile commands
 9}
10
11
12# define as bitbake variables
13SOURCE_FILE="${S}/code.c"
14PATTERN="LED_PIN"
15REPLACEMENT="OUTPUT_PIN"
16
17do_configure:append () {
18    echo "DYNAMIC CONFIG VALUES"
19   
20    # Check if the pattern exists after patching
21    grep -F "${PATTERN}" "${SOURCE_FILE}"
22    if [ $? -ne 0 ]; then
23        bbfatal "Pattern ${PATTERN} not found in ${SOURCE_FILE}"
24    fi
25
26    # Perform the substitution using bitbake variables
27    sed -i -e 's|${PATTERN}|${REPLACEMENT}|g' "${SOURCE_FILE}"
28}

rimozione ingredienti

Possiamo effettuarla tramite una patch come nell’ esempio precedente, oppure aggiungere del codice per rimuovere un file dopo che bitbake ha estratto la repository. Siccome l’estrazione avviene nel task do_unpack, aggiungiamo la nostra logica alla fine di questo task. Se vogliamo eliminare il file code.c possiamo riscrivere il nostro recipe come:

myapp_2.1.bb
 1SRC_URI = "https://git.example.com"
 2SRCREV = "${AUTOREV}"
 3
 4S = "${WORKDIR}/git"
 5B = "${WORKDIR}/build"
 6
 7do_compile(){
 8    # compile commands
 9}
10
11
12# define as bitbake variable
13RM_FILE="${S}/code.c"
14
15do_unpack:append() {
16    rm -f "${RM_FILE}"
17}

quality of life

Abbiamo visto che, per file locali, SRC_URI cerca nella sottocartella files. In realtà le sottocartelle di default sono:

  • ${FILE_DIRNAME}/${PF}
  • ${FILE_DIRNAME}/${P}
  • ${FILE_DIRNAME}/${PN}
  • ${FILE_DIRNAME}/${BP}
  • ${FILE_DIRNAME}/${BPN}
  • ${FILE_DIRNAME}/files
  • ${FILE_DIRNAME}

Per utilizzare dei nomi diversi da quelli di default dobbiamo aggiungere la nostra sottocartella al path di ricerca usato da bitbake. Per farlo è sufficiente utilizzare la variabile FILESEXTRAPATHS. Se vogliamo utilizzare una cartella di nome additional possiamo riscrivere il nostro recipe come:

myapp_2.1.bb
 1SRC_URI = "https://git.example.com"
 2SRCREV = "${AUTOREV}"
 3
 4S = "${WORKDIR}/git"
 5B = "${WORKDIR}/build"
 6
 7FILESEXTRAPATHS:prepend := "${THISDIR}/additional"
 8SRC_URI += "file://special_constants.h;subdir=git"
 9
10do_compile(){
11    # compile commands
12}

Così possiamo avere una struttura della FILE_DIRNAME del tipo:

FILE_DIRNAME directory
myapp
 ├── additional
 │    ├── special_constants.h
 │    ├── doc_special_constants.md
 │    └── dark-theme.patch
 └── myapp_2.1.bb

E questo meccanismo torna comodo soprattuto se utilizzato in un .bbappend invece che nel .bb, in quanto THISDIR rappresenta la cartella attuale in cui bitbake sta effettuando il parsing, mentre FILE_DIRNAME è sempre associata alla cartella in cui risiede il file .bb. Inoltre, il motivo per cui utilizziamo un := è per valutare immediatamente il valore di THISDIR. Il motivo per cui utilizziamo prepend è per portare questo path davanti a tutti gli altri già definiti. La variabile FILESEXTRAPATHS non è globale, ma privata per ogni recipe.

Considerando di lavorare su un layer differente rispetto a quello in cui myapp_2.1.bb è definito possiamo quindi avere:

layer 1: FILE_DIRNAME directory
myapp
 └── myapp_2.1.bb
layer 2: recipe directory
myapp
 ├── additional
 │    ├── special_constants.h
 │    └── doc_special_constants.md
 └── myapp_%.bbappend
myapp_%.bbappend
1FILESEXTRAPATHS:prepend := "${THISDIR}/additional"
2SRC_URI += "file://special_constants.h;subdir=git"

sviluppo (devshell)

In Yocto esiste uno strumento potentissimo che ci permette di “entrare nell’ambiente di un recipe” e avere a disposizione solo i comandi e le variabili che sono effettivamente utilizzabili dal recipe. Questo diventa molto utile per fare debugging e capire come implementare il nostro recipe. Il comando è devshell (development shell) e in Yocto non è implementato come un comando a sè stante, ma come un task associato ad ogni recipe. Se vogliamo entrare in una devshell del recipe busybox ci basta usare:

terminal
bitbake busybox -c devshell

A questo punto bitbake avvierà una bash shell all’interno della directory S e importerà tutte le variabili di ambiente necessarie. La shell verrà aperta in un terminale definito dalla variabile OE_TERMINAL. Il terminale può essere:

  • gnome
  • xfce
  • screen
  • …

Questo determina l’applicazione grafica che si occupa di disegnare il terminale, mentre la shell sarà sempre bash. Se stiamo usando Yocto all’interno di un ambiente non grafico, come ssh o un container, allora l’opzione consigliata è:

terminal
export OE_TERMINAL=screen

E’ importante sapere che devshell è un task che avviene dopo do_patch, quindi nel ambiente vedremo applicate tutte le patch definite all’interno del recipe.

Per altri dettagli visitare la documentazione.

creare patch

Utilizzando la devshell su un recipe possiamo entrare nella cartella S dopo che vi sono state applicate le patch definite nel recipe. Questo ci permette di effettuare qualsiasi tipo di modifica sulla repository e poi salvare le modifiche in un commit. Il commit verrà poi usato per creare la patch.

Quindi, per creare il commit:

terminal
git add .
git commit -m "message"

E’ importante sapere che per avere una patch che sia riconosciuta da Yocto, questa deve contenere un preciso metadata tag dentro il messaggio. Il tag è chiamato Upstream-Status e indica la relazione tra la patch e upstream source, con l’obiettivo di rendere più agevole il lavoro dei manutentori dei layer Yocto. E’ sufficiente che il messaggio contenga una stringa del tipo

Upstream-Status: status [reason]

Dove status può essere:

  • Accepted
  • Backport
  • Inappropriate
  • Denied
  • Pending
  • Submitted

Mentre reason può essere una stringa qualsiasi.

Una spiegazione più dettagliata è disponibile nella documentazione.

Se sbagliamo a inserire un tag corretto, bitbake genererà un errore durante l’applicazione della patch, come descritto nella documentazione errori QA.

Gli errori QA (Quality Assurance) sono generati dalla classe insane, come spiegato nella documentazione. Ne parleremo più avanti, intanto la soluzione rapida è disabilitare la generazione errori QA sul recipe in questione con:

myapp_%.bbappend
1INSANE_SKIP:${PN} = "patch-status "

Tornando alla generazione della patch, per creare il vero e proprio file .patch:

terminal
git format-patch HEAD^

questo definisce una patch che replica le modifiche introdotte dall’ ultimo commit (HEAD^). Il file .patch è disponibile in S e dovremo occuparci di copiarlo al di fuori di questa e dentro la cartella del recipe.