Recipe
In Yocto, un recipe definisce l’intero processo di contorno di un qualsiasi applicativo o funzionalità, ovvero TUTTE le operazioni necessarie per poterla fare funzionare correttamente. Queste comprendono:
- dove trovare il codice sorgente (tar file, repository git pubblica o privata)
- quali modifiche è necessario applicare a questo (eventuali patch, configurazioni specifiche,…)
- come configurare il compilatore o l’interprete del codice (eventuali
#define, variabili di ambiente,…) - come compilare il codice (se necessario)
- quali dipendenze sono richieste (tool aggiuntivi, librerie, moduli, configurazioni del kernel,…)
- dove installare i risultati della compilazione nel file-system del target
- quali altre operazioni eseguire (installazione di file di configurazione, creazione di link simbolici,…)
Il recipe inoltre deve fornire informazioni anche sulla licenza del applicativo in questione (GPL, software proprietario,…) e sulla sua versione. Tutto questo rende molto comodo il tracciare quali software saranno presenti sul nostro sistema finale.
Per fare tutto ciò non esistono delle regole rigide, ma piuttosto delle linee guida implementabili utilizzando gli strumenti forniti.
preliminari
Per cominciare, iniziamo a capire quali sono le variabili che ci troveremo ad utilizzare praticamente sempre quando lavoriamo con i recipe, ovvero:
PNPVWORKDIR
PN indica il Package Name, cioè il
modo in cui bitbake può capire a quale
recipe ci riferiamo.
Si chiama “Package Name” invece che “Recipe Name”
perchè generalmente un singolo recipe dovrebbe
produrre un singolo package.
Vedremo però che spesso questo non è il caso,
quindi questo nome è da intendersi come riferito
al package principale che viene prodotto.
PV indica la Package Version, quindi
la versione specifica del software o funzionalità
che vogliamo realizzare.
Questo ci tornerà comodo perchè in un singolo
sistema possiamo generare molteplici versioni
diverse di un package ed è importante che
queste non vadano ad interferire l’una con l’altra.
WORKDIR indica la Working Directory,
ovvero il path assoluto nel nostro sistema
che identifica la cartella in cui
bitbake andrà a generare tutti i file temporanei
durante la costruzione di un recipe.
Queste variabili sono importanti
perchè le utilizzeremo spesso, ma
non è necessario che le definiamo noi,
in quanto bitbake gli assegna già
dei valori di default.
Queste assegnazioni le possiamo trovare all’interno
del layer meta:
238##################################################################
239# Package default variables.
240##################################################################
241
242PN = "${@bb.parse.vars_from_file(d.getVar('FILE', False),d)[0] or 'defaultpkgname'}"
243PV = "${@bb.parse.vars_from_file(d.getVar('FILE', False),d)[1] or '1.0'}"
244PR = "r0"
245PE = ""
246PF = "${PN}-${EXTENDPE}${PV}-${PR}"
247EXTENDPE = "${@['','${PE}_'][int(d.getVar('PE') or 0) > 0]}"
248P = "${PN}-${PV}"403STAMPS_DIR ?= "${TMPDIR}/stamps"
404STAMP = "${STAMPS_DIR}/${MULTIMACH_TARGET_SYS}/${PN}/${PV}"
405STAMPCLEAN = "${STAMPS_DIR}/${MULTIMACH_TARGET_SYS}/${PN}/*-*"
406BASE_WORKDIR ?= "${TMPDIR}/work"
407WORKDIR = "${BASE_WORKDIR}/${MULTIMACH_TARGET_SYS}/${PN}/${PV}"
408T = "${WORKDIR}/temp"
409D = "${WORKDIR}/image"
410S = "${WORKDIR}/${BP}"
411B = "${S}"L’assegnazione di PN e PV viene fatta tramite
un meccanismo di bitbake che legge il nome del file
.bb e spezza il nome ad ogni carattere _ (underscore).
Il primo pezzo viene assegnato a PN,
il secondo a PV.
Questa diventa quindi una convenzione
necessaria per la denominazione dei file .bb,
con l’aggiunta del fatto che il nome
deve essere completamente lower-case.
Alcuni esempi:
PN PV
myapp.bb >> myapp 1.0
bash_1.7.0.bb >> bash 1.7.0
bash_ubuntu-flavor-1.7.0.bb >> bash ubuntu-flavor-1.7.0Questo e altri meccanismi di bitbake
li possiamo trovare nel layer meta
in meta/bitbake/lib/
e ne parleremo meglio in un articolo dedicato.
L’assegnazione di WORKDIR invece la vediamo
nel file bitbake.conf che abbiamo già visto
qua sopra:
BASE_WORKDIR ?= "${TMPDIR}/work"
WORKDIR = "${BASE_WORKDIR}/${MULTIMACH_TARGET_SYS}/${PN}/${PV}"Dove:
TMPDIRindica la cartella per i file generati dalla buildMULTIMACH_TARGET_SYSidentifica la scheda target
La variabile TMPDIR ha un valore di default
che corrisponde a build/tmp, ma è tranquillamente
ridefinibile nel caso volessimo cambiarne la posizione.
La variabile MULTIMACH_TARGET_SYS è una
variabile molto specifica, ma in generale
è assegnata dalla variabile MACHINE,
che utilizzeremo nei file di progetto
local.conf per definire la scheda
per cui vogliamo realizzare il software.
Quindi, dopo tutta questa trafila, abbiamo capito
che il nome stesso del recipe determina
il valore di PN e PV
e a cascata anche la posizione della sua WORKDIR.
E visto che una immagine vale più di mille parole,
ecco un esempio:

Prima di continuare, notiamo che
una unica build potrebbe costruire software
per molteplici schede completamente diverse.
Per questo nella definizione di
WORKDIR compare anche il termine relativo
alla scheda, perchè di default
bitbake si assicura che il medesimo recipe,
costruito però per due schede diverse,
sia associato a due WORKDIR diverse,
per evitare che una delle due vada a
“sporcare” l’altra.
Inoltre se volessimo ridefinire
la posizione di WORKDIR
lo possiamo fare con una
nuova assegnazione di questa variabile,
ma questo lo vedremo in seguito
perchè prima bisogna capire la
sintassi di bitbake.
Al momento ci serve parlare di altro.
task pipeline
La classica task pipeline è
fetch / unpack / configure / compile / install / packageè una sequenza abbastanza generica di step che si adatta bene a molti applicativi, e a cui in ogni momento possiamo introdurre nuovi step o rimuovere quelli non necessari.
L’obiettivo del task fetch
è di procurare in un qualche modo
i sorgenti dell’applicativo
e di salvarli nella cartella downloads,
comune a tutti i recipe della build.
Solitamente i sorgenti vengono
salvati qui in formato .tar.
L’obiettivo del task unpack
è quindi di estrarre i sorgenti
in una cartella specifica per il recipe
in questione, quindi
in una sottocartella di WORKDIR.
L’obiettivo del task configure
è di applicare eventuali
modifiche ai sorgenti,
come configurazioni
o opzioni di compilazione.
L’obiettivo del task compile
è di effettuare la
compilazione dell’applicativo.
Se questo non richiede compilazione,
come per un semplice script python,
si omettere questo task.
L’obiettivo del task install
è di copiare determinati file
in una cartella temporanea
che rappresenta la struttura
di un tipico rootfs.
Questi file sono quindi quelli
che è necessario che siano poi
presenti nel rootfs
della scheda target.
In questa fase devono essere gestiti
anche i permessi Linux di tali file.
L’obiettivo del task package
è di creare uno o più packages
a partire dai contenuti
della cartella temporanea
di installazione.
Oltre al main package,
che spesso contiene il file binario
del nostro applicativo,
dei package tipici sono:
-devsolitamente installa i file necessari (come i file.he altre librerie) per lo sviluppo di altri applicativi basati sul nostro applicativo-docsolitamente installa i file di documentazione in una cartella standard per poter utilizzare il comandomansul nostro applicativo
Generalmente bitbake si aspetta di trovare
all’interno del recipe delle funzioni come:
0do_fetch(){
1 # download .tar file
2}
3
4do_unpack(){
5 # extract .tar file
6}
7
8do_configure(){
9 # edit header files
10}
11
12do_compile(){
13 # call gcc
14}
15
16do_install(){
17 # copy build output to
18 # sysroot folder
19}
20
21do_package(){
22 # create .deb package
23}Queste possono essere funzioni bash o python e
per convenzione, devono iniziare con do_,
per distinguerle da altre funzioni che
non sono associate a dei task di bitbake.
Per promuovere una funzione ad un
task bitbake si deve utilizzare
l’istruzione addtask.
25addtask fetch
26addtask unpack
27addtask configure
28addtask compile
29addtask install
30addtask packageSe il nome del task non inizia per do_,
questo viene implicitamente aggiunto.
Inoltre, se bitbake viene invocato
senza specificare il nome del task da eseguire,
esegue di default il task do_build.
Se il task non esiste, allora bitbake
non fa nulla.
Siccome Yocto poi andrà ad invocare proprio il task build per iniziare la costruzione del recipe, è necessario fare in modo che i task che abbiamo appena definito siano in relazione con il task build. Questo è possibile specificando le dipendenze tra task nell’istruzione addtask:
25addtask fetch after build
26addtask unpack after fetch
27addtask configure after unpack
28addtask compile after configure
29addtask install after compile
30addtask package after installIn questo modo possiamo creare la nostra pipeline, ovvero l’esecuzione di un task dopo l’altro. In generale l’istruzione addtask ha la seguente sintassi:
addtask <taskname> before <task1 task2 ...> after <task3 task4 ...>Se l’ordine non è ben specificato, bitbake assume che i task possano essere eseguiti in parallelo.
Per maggiori dettagli sul meccanismo, documentazione bitbake.
fetch
Utilizzando il layer meta,
questo processo di definizione dei task
viene molto semplificato,
infatti questi task comuni vengono
già definiti ed al loro interno
vengono utilizzate delle variabili
di cui poi noi potremo cambiare
il contenuto.
Ad esempio, la variabile SRC_URI
permette di evitare di implementare il task do_fetch
e richiede semplicemente un url:
0SRC_URI = "git://github.com/author/repo"Questa è una delle variabili più utilizzate, comode e configurabili, infatti permette di definire se il fetch viene fatto da:
- git repository
git:// - webserver
http:// - file locale
file://
La variabile può contenere molteplici url,
purchè questi siano separati da uno spazio e in aggiunta, per ogni url,
accetta anche altri argomenti
che vengono passati tramite
stringhe separate da ;.
Quello che ho appena scritto
potrebbe non avere senso,
forse perchè non so spiegare
o forse perchè è semplicemente
un metodo molto contorto
di usare le variabili.
Quindi vediamo degli esempi
e poi passiamo più in generale
alla sintassi che bitbake
usa per le variabili,
sennò qua facciamo solo
che bordello.
0SRC_URI = "git://author/repo file://file1.patch file://file2.patch;apply=no"Il valore assegnato a SRC_URI
può quindi diventare anche molto lungo
e per questo si utilizza spesso
il carattere \ per spezzare la linea
(tecnica presente in molti linguaggi di programmazione):
0SRC_URI = " \
1 git://author/repo \
2 file://file1.patch \
3 file://file2.patch;apply=no \
4 "Ultima cosa, se in SRC_URI utilizziamo file locali,
questi vengono cercati nelle cartelle definite
nella variabile di “path” utilizzata da bitbake,
ovvero FILESPATH.
Per aggiungere altri percorsi utilizzeremo
però la variabile FILESEXTRAPATHS invece
che modificare direttamente FILESPATH,
ma questo lo vedremo in seguito.
Ultima ultima cosa, giuro,
è bene definire anche il valore della variabile
S che definisce la sottocartella di
WORKDIR in cui vengono clonate
le repository git,
infatti di queste non viene
scaricato direttamente un tar
ma vengono prima clonate in una
sottocartella di WORKDIR,
poi compresse in un .tar
e poi copiate nella cartella downloads.
Se non la definiamo, questa assume il default:
S = "${WORKDIR}/${BP}"Ma è buona prassi non affidarsi ai valori di default
e rendere le cose il più esplicite possibile.
Solitamente si assegna lla sottocartella
il valore git oppure il nome della repository:
S = "${WORKDIR}/git"Inoltre per maggiori dettagli possiamo sempre consultare il glossario delle variabili Yocto.
sintassi variabili bitbake
Tutte le variabili bitbake sono
sostanzialmente delle variabili di tipo stringa.
Una unica, anche molto lunga, stringa.
Sia che vogliamo rappresentare un singolo elemento,
sia che vogliamo rappresentare una lista di elementi.
Bitbake tratta tutte le variabili come
un insieme di valori separati da caratteri whitespace .
Questo meccanismo è molto comune nei comandi
da terminale linux, dove ogni (singolo o multiplo) spazio
separa i vari argomenti.
Nel seguente esempio di funzione,
vediamo come iterare su una variabile bitbake:
0SRC_URI = "git://author/repo1 git://author/repo2"
1
2example_print(){
3 for ITEM in ${SRC_URI}; do
4 echo "url ${ITEM}"
5 done
6}E per sfruttare al meglio questo meccanismo, bitbake fornisce una sintassi speciale per poter aggiungere valori all’inizio o alla fine di una stringa:

La differenza tra gli operatori
+= .= :append
è molto intricata e non è
questo il momento di parlarne,
intanto ci basta sapere che esistono.
unpack
È sufficiente definire il valore di UNPACKDIR,
che di default vale
WORKDIR/sources-unpack.
Fino alla versione 5.0 styhead di Yocto,
i sorgenti venivano di default estratti
in S, ma poi si è deciso di introdurre
UNPACKDIR per fornire più possibilità
all’utente.
Solitamente in ogni recipe
si assegna il medesimo valore
sia a S che a UNPACKDIR:
S = "${WORKDIR}/git"
UNPACKDIR = "${S}"configure
Non è strettamente necesario,
ma solitamente in questa fase
si utilizzano vari comandi
per applicare delle modifiche
a dei file di testo
(come .h, Makefile)
oppure per definire delle
variabili di ambiente che
saranno necessarie
alla compilazione dell’applicativo.
Questo processo è altamente dipendente
dal tool che utilizziamo per
compilare il codice,
quindi vedremo degli esempi
quanto discuteremo di
compilazione utilizzando
diversi tool come
make, cmake e altro.
Questa fase può quindi essere omessa, oppure definita nulla nel seguente modo:
0do_configure() {
1 :
2}sfruttando il fatto che il comando
: nella bash di Linux non fa letteralmente nulla.
compile
Di solito per compilare un qualsiasi applicativo si usano comandi specifici a seconda dell’ambiente in cui il codice è stato sviluppato, come ad esempio:
gccmakecmakeqmakemeson
e troppi altri.
Visto che questi sono necessari
per la compilazione del recipe,
ma non sono necessari per il funzionamento
dell’applicativo finale,
è necessario dichiarare una dipendenza
del nostro recipe con il recipe
che crea questi strumenti.
Solitamente tali recipe sono di tipo native,
nel senso che costruiscono degli applicativi
che verranno utilizzati solo dal
nostro pc host e non dalla nostra scheda target.
Parlaremo più in dettaglio di questa differenza
più avanti, per il momento ci basta sapere
che questo tipo di dipendenza di dichiare utilizzando
la variabile DEPENDS.
Ad esempio, il compilatore gcc è costruito
nel recipe glibc che si occupa di costruire
una determinata versione della GNU C standard library:
DEPENDS = "glibc"Siccome la C standard library è essenziale
per praticamente qualsiasi tipo di applicativo,
non è necessario dichiare esplicitamente questa dipendenza
perchè lo fa già Yocto in automatico.
L’unica accortezza è di
non invocare direttamente gcc
nel nostro recipe, ma la variabile CC,
in quanto questa fa riferimento al
binario di gcc creato da Yocto
e non al gcc di sistema.
Questo per rendere la build
determinata e riproducibile,
cioè sapere esattamente con quale
versione del gcc i nostri applicativi sono stati
compilati
per permettere a chiunque di poterli ricreare
esattamente identici (byte by byte).
Ricordiamo che Yocto come primi passi
si occupa di creare la toolchain
da “zero”, quindi solo
dai sorgenti e dalla nostra configurazione,
anche per slegarsi dai tool del sistema host.
Detto questo, un esempio della fase di compilazione di un applicativo basato su codice c potrebbe essere:
0do_compile () {
1 ${CC} myapp.c -o myapp
2}Tutti i comandi eseguiti in questa fase
hanno come working directory B,
ovvero la sottocartella di WORKDIR
dedicata alla fase di build
vera e propria di un recipe
che di default è la stessa cartella
in cui sono presenti i sorgenti:
406BASE_WORKDIR ?= "${TMPDIR}/work"
407WORKDIR = "${BASE_WORKDIR}/${MULTIMACH_TARGET_SYS}/${PN}/${PV}"
408T = "${WORKDIR}/temp"
409D = "${WORKDIR}/image"
410S = "${WORKDIR}/${BP}"
411B = "${S}"Le definizioni delle variabili associate
agli altri comandi classici
della C standard library
sono definite sempre nel solito file bitbake.conf:
557TOOLCHAIN_OPTIONS = " --sysroot=${STAGING_DIR_TARGET}"
558
559export CC = "${CCACHE}${HOST_PREFIX}gcc ${HOST_CC_ARCH}${TOOLCHAIN_OPTIONS}"
560export CXX = "${CCACHE}${HOST_PREFIX}g++ ${HOST_CC_ARCH}${TOOLCHAIN_OPTIONS}"
561export FC = "${HOST_PREFIX}gfortran ${HOST_CC_ARCH}${TOOLCHAIN_OPTIONS}"
562export CPP = "${HOST_PREFIX}gcc -E${TOOLCHAIN_OPTIONS} ${HOST_CC_ARCH}"
563export LD = "${HOST_PREFIX}ld${TOOLCHAIN_OPTIONS} ${HOST_LD_ARCH}"
564export CCLD = "${CC}"
565export AR = "${HOST_PREFIX}gcc-ar"
566export AS = "${HOST_PREFIX}as ${HOST_AS_ARCH}"
567export RANLIB = "${HOST_PREFIX}gcc-ranlib"
568export STRIP = "${HOST_PREFIX}strip"
569export OBJCOPY = "${HOST_PREFIX}objcopy"
570export OBJDUMP = "${HOST_PREFIX}objdump"
571export STRINGS = "${HOST_PREFIX}strings"
572export NM = "${HOST_PREFIX}gcc-nm"
573export READELF = "${HOST_PREFIX}readelf"
574PYTHON = "${@sys.executable}"Per compilare applicativi più complessi
con molteplici file .c, .h
e chissà cos’altro, vedremo poi
come utilizzare
make, meson, cmake e altri
all’interno di Yocto.
Intanto continuiamo con la nostra pipeline.
install

È necessario implementare manualmente
il processo di creare le varie cartelle
del tipo usr bin all’interno
della cartella definita dalla variabile
D, ovvero la cartella temporanea del sysroot.
Generalmente si fa grande uso del comando
bash install, che permette di
creare cartelle,
copiare file in altre cartelle (seguendo i link simbolici)
e assegnare i permessi Linux tutto con
un unico comando.
Nonostante il rootfs di Linux
ha una struttura ormai ben definita
e non soggetta a stravolgimenti,
si preferisce comunque utilizzare delle variabili
standard per riferirsi ai path comuni
del tipo usr bin e altri.
Le definizioni di queste sono visibili
nel file:
17# Path prefixes
18export base_prefix = ""
19export prefix = "/usr"
20export exec_prefix = "${prefix}"29# Architecture independent paths
30export sysconfdir = "${base_prefix}/etc"
31export servicedir = "${base_prefix}/srv"
32export sharedstatedir = "${base_prefix}/com"
33export localstatedir = "${base_prefix}/var"
34runtimedir = "${base_prefix}/run"
35export datadir = "${prefix}/share"44# Architecture dependent paths
45export bindir = "${exec_prefix}/bin"
46export sbindir = "${exec_prefix}/sbin"
47export libdir = "${exec_prefix}/${baselib}"
48export libexecdir = "${exec_prefix}/libexec"
49export includedir = "${exec_prefix}/include"Quindi una possibile definizione del task install per il nostro recipe può essere:
0do_install () {
1 install -d ${D}${bindir}
2 install -m 644 ${B}/output.bin ${D}${bindir}/output.bin
3}package

È sufficiente indicare quali file
all’interno di D (holding area)
andranno a comporre il package.
Si utilizza la variabile FILES
per fare proprio questo:
FILES:${PN} += "${bindir}/output.bin"Il fatto che compaia FILES:${PN}
invece che semplicemente FILES
è dovuto ad un’altra particolare
sintassi di bitbake,
ovvero l’utilizzo degli override.
Questo è un meccanismo complesso che affronteremo
nella sezione dedicata alla sintassi bitbake,
ma per il momento ci basta sapere
che il layer meta di default
assume che un recipe possa generare
molteplici package,
quindi è necessario che la variabile
FILES possa assumere valori differenti
per ogni package.
Gli override permettono proprio
di utilizzare un valore differente di
FILES a seconda del package che
vuole utilizzare tale variabile,
gestendo quindi lo scope
di un determinato package.
La lista dei package definiti di default
è disponibile nel medesimo file bitbake.conf
con la variabile PACKAGES:
326PACKAGE_BEFORE_PN ?= ""
327PACKAGES = "${PN}-src ${PN}-dbg ${PN}-staticdev ${PN}-dev ${PN}-doc ${PN}-locale ${PACKAGE_BEFORE_PN} ${PN}"
328PACKAGES_DYNAMIC = "^${PN}-locale-.*"
329FILES = ""I package che non hanno un variabile
FILES definita non vengono generati,
per questo è importante utilizzare tale
variabile in modo scoped invece
che “globale”,
altrimenti tutti i package di default
vengono generati a partire
dai medesimi file.
Con la variabile ALLOW_EMPTY
possiamo forzare la generazione
di un package anche nel caso questo sia vuoto:
355ALLOW_EMPTY:${PN}-dev = "1"Troubleshooting
Ogni task è associato ad un file run,
che rappresenta lo script finale che
verrà eseguito,
ed un file log,
che contiene l’output a terminale
che siamo abituati a vedere quando
eseguiamo un comando bash.
Questi si trovano nella cartella temp
all’interno della workdir del recipe in questione:
${WORKDIR}
├── ${S}
├── ${B}
├── ${D}
└── temp
├── log.do_fetch
├── log.do_unpack
├── log.do_compile
├── log.do_install
├── ...
├── run.do_fetch
├── run.do_unpack
├── run.do_compile
├── run.do_installE sono estremamente utili
soprattutto perchè vengono
generati di default senza
doverci preoccupare di usare
debug statements
o altri meccanismi particolari.
Conclusioni
Bene, questo conclude la nostra prima visione di come funzionano i recipe, ma non gioire troppo presto perchè in realtà c’è ancora molta roba e ci dovremo ripassare sopra tra un po’. Semplicemente non volevo farti impazzire subito, ma piuttosto step-by-step. Un abbraccio.