4 tecniche per organizzare un Board Support Package Yocto
Chi ha mai lavorato con Yocto avrà certamente avuto la necessità di integrare nel proprio progetto un buon numero di layer di terze parti, necessari per supportare il proprio hardware o integrare determinate feature. Ciascuno di questi layer viene spesso gestito come un singolo repository di codice, usando uno tra i tanti meccanismi di SCM.
Ciò ci pone di fronte ad un problema di organizzazione di progetto non banale: dovendo avere a che fare con un numero spesso considerevole di repository esterni, come possiamo rendere la nostra vita (presente e soprattutto futura) quanto più semplice possibile?
Come vedremo, la risposta a questa domanda è tutt’altro che scontata, specie quando iniziamo a scendere nel dettaglio di cosa si intende per “semplice”. La questione nasconde infatte molteplici sfaccettature, che spaziano da considerazioni prettamente logistiche, ad argomenti più profondi come la manutenibilità del progetto.
Presenteremo quindi quattro papabili approcci per affrontare questo problema, ciascuno con i suoi pro e contro, e soprattutto col proprio bagaglio di conseguenze sul futuro del progetto.
Poiché le soluzioni adottate sono fortemente legate sistema di SCM utilizzato, ed essendo git
lo standard di fatto in questo ambito, prenderemo quest’ultimo come nostro riferimento. Inoltre, gli approcci presentati non sono strettamente legati a Yocto, ma possono essere declinati per la gestione di qualsiasi progetto che abbia dipendenze esterne da tracciare, sebbene alcuni dei pro e contro messi in luce dipendano molto dal caso d’uso particolare di Yocto.
Problema? Quale problema?
La prima soluzione al problema è semplicemente quella di ignorare il problema.
Sebbene possa suonare come un controsenso, in termini di rapporto tempo investito/risultati, questo approccio è probabilmente quello che rende meglio nell’immediato, sebbene si trascini dietro tutta una serie di conseguenze da non sottovalutare.
L’approccio consiste essenzialmente nel trattare tutti i layer esterni importati nel nostro progetto come un ammasso di file non versionati, committandoli come tali nel nostro repository. Otterremo così una struttura piatta, in cui non esiste distinzione (a livello di SCM) tra i nostri layer e quelli di terze parti.
Di fatto ciò si traduce in:
my-yocto-project$ git clone --branch zeus git://git.openembedded.org/meta-openembedded
my-yocto-project$ cd meta-openembedded
meta-openembedded$ rm -rf .git
meta-openembedded$ git status
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
./
nothing added to commit but untracked files present (use "git add" to track)
my-yocto-project$ git add .
my-yocto-project$ git commit -m"Import meta-openembedded layer"
Eliminando la cartella .git
contenuta nel repo appena clonato, possiamo tranquillamente committare il contenuto di meta-openembedded
senza preoccupazione alcuna. Questo ci semplifica la vita sotto parecchi aspetti:
- non dobbiamo preoccuparci di cosa possa succedere nel caso in cui il repository hostato su
git.openembedded.org
venisse spostato, reso inaccessibile o addirittura eliminato, visto che una copia del suo contenuto è al sicuro nel nostro repository; - possiamo apportare qualsiasi modifica a
meta-openembedded
, e di questa modifica rimarrà traccia nella nostra storia; - se un domani volessimo aggiornarne il contenuto, possiamo semplicemente ripetere la stessa operazione cancellando prima l’intera directory.
Ovviamente se fosse tutto così facile, non staremmo qui a parlare degli altri tre metodi. Infatti i contro non sono pochi:
- eliminando la cartella
.git
, perdiamo di fatto qualsiasi informazione relativa alla storia dimeta-openembedded
; - a meno di non tracciare queste informazioni nel messaggio di commit, perdiamo anche l’informazione relativa a quale particolare ref del repository stiamo utilizzando,
- la dimensione del nostro repository (e potenzialmente i costi di banda e hosting) inizia a lievitare nel caso in cui abbiamo bisogno di parecchi layer,
- un eventuale aggiornamento del layer diventa non banale, in quanto richiede di fare un cherry-pick manuale di tutte le modifiche fatte al layer importato, se presenti.
In sintesi, questo approccio è estremamente semplice da applicare e mantenere, molto (forse troppo) versatile, ma anche molto fragile in termine di semplicità di sviluppi futuri.
git submodules
Passiamo dunque a quello che è forse l’approccio più diffuso, ossia l’utilizzo dei submodules.
Per chi non lo sapesse, un submodule in Git non è altro che un “puntatore” ad un preciso punto nella storia di un altro repository, ovunque esso sia hostato. Durante l’inizializzazione di un repository contenente submodules, questi devono essere “dereferenziati” con il contenuto del repository nel punto desiderato della sua storia.
Tutte queste informazioni vengono memorizzate in un singolo file di metadati, .gitmodules
, contenuto nella root del nostro repository. Il contenuto dei submodules stessi non “contaminerà” mai la storia del nostro repository.
L’uso dei submodules in Git è abbastanza immediato:
my-yocto-project$ git submodule add -b zeus git://git.openembedded.org/meta-openembedded
my-yocto-project$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: .gitmodules
new file: meta-openembedded
my-yocto-project$ git commit -m"Add meta-openembedded layer as a submodule"
In termini di pro e contro, questo approccio è pressoché il duale del precedente. Come punti a favore:
- viene mantenuta intatta la storia dei layer importati, permettendoci se necessario di verificare l’origine di un cambiamento o di un problema,
- rimane estremamente contenuta la dimensione del nostro repository, poiché vengono tracciati solo i puntatori ai layer utilizzati,
- l’aggiornamento di un singolo layer è molto semplice, ed è parte integrante del meccanismo dei submodules,
- è idiomatico rispetto alla filosofia di Yocto di non dover mai modificare i layer di terze parti.
Di contro:
- ogni nuovo clone deve andare a recuperare il contenuto di ogni submodule, con tutti i grattacapi che ne derivano nel caso in cui sia cambiato qualcosa nell’hosting,
- nel caso in cui sia davvero necessario modificare il sorgente dei layer, bisogna inventarsi un workaround, visto che nella stragrande maggioranza dei casi non avremo accesso in scrittura al repository in questione,
- in casi estremi, una riscrittura della storia del submodule da parte di chi lo hosta può comportare una rottura del nostro riferimento, e dunque un effort considerevole nel dover andare a ritroso nella storia per capire in che punto riagganciarsi per tornare avere un setup il più simile possibile.
Questo approccio è dunque valido nel caso in cui si reputa che il progetto sarà attivamente mantenuto per un tempo molto lungo, o sia comunque tenuto regolarmente sotto backup.
Nel caso di progetti molto brevi, o comunque relativamente statici nel tempo, gli altri approcci sono spesso preferibili.
repo
Nato in origine come parte del progetto Android, repo
è uno strumento che affianca Git nella gestione di progetti composti da un numero elevato di repository. Nel tempo, è stato adottato anche al di fuori dell’ambiente Android da vendor come NXP per la gestione di BSP basati su Yocto.
Il funzionamento di repo
è molto semplice, e si basa sull’utilizzo di un manifest in formato XML che descrive la struttura del progetto, i repository che lo compongono, dove recuparli, e di quale ref farne il checkout. Dispone inoltre di funzionalità che permettono di fare semplici operazioni di setup in fase di inizializzazione del progetto, come ad esempio copiare file o directory.
Questo è un esempio di manifest usato da NXP:
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<default sync-j="4" revision="thud"/>
<remote fetch="https://git.yoctoproject.org/git" name="yocto"/>
<remote fetch="https://github.com/Freescale" name="freescale"/>
<remote fetch="https://github.com/openembedded" name="oe"/>
<project remote="yocto" revision="thud" name="poky" path="sources/poky"/>
<project remote="yocto" revision="thud" name="meta-freescale" path="sources/meta-freescale"/>
<project remote="oe" revision="thud" name="meta-openembedded" path="sources/meta-openembedded"/>
<project remote="freescale" revision="thud" name="fsl-community-bsp-base" path="sources/base">
<linkfile dest="README" src="README"/>
<linkfile dest="setup-environment" src="setup-environment"/>
</project>
<project remote="freescale" revision="thud" name="meta-freescale-3rdparty" path="sources/meta-freescale-3rdparty"/>
<project remote="freescale" revision="thud" name="meta-freescale-distro" path="sources/meta-freescale-distro"/>
<project remote="freescale" revision="thud" name="Documentation" path="sources/Documentation"/>
</manifest>
Vengono qui definiti tre remote, che vengono poi utilizzati per effettuare il checkout di 7 repository alla revisione thud
(ie. Yocto 2.6), tutti posizionati sotto la directory sources/
. Sono presenti inoltre due direttive linkfile
per creare due symlink da file presenti in sources/base
alla root del progetto.
Usare repo
è molto semplice. L’installazione si riduce a:
~$ mkdir -p ~/.bin
~$ PATH="${HOME}/.bin:${PATH}"
~$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.bin/repo
~$ chmod a+rx ~/.bin/repo
Dopodiché, immaginando di avere un file manifest.xml
nella root del nostro repository, per inizializzare il nostro repository basta dare una serie di comandi una tantum:
my-yocto-project$ mkdir -p .repo/manifests
my-yocto-project$ cp manifest.xml .repo/manifests
my-yocto-project$ repo init -u . -m manifest.xml
my-yocto-project$ repo sync
Al termine di repo sync
, avremo la nostra struttura di progetto così come definita nel manifest.xml
.
In termini di vantaggi e svantaggi, la soluzione è molto simile all’utilizzo dei submodule, con la differenza che la gestione dei moduli è demandata a repo
, semplificando in parte la vita dello sviluppatore.
Come bonus, repo
implementa anche comandi classici di git
quali status
e diff
in modo che tengona conto della presenza di submodules nel progetto e applichino le relative operazioni anche al contenuto dei submodule stessi, piuttosto che fermarsi al confine con essi – come invece avviene per i corrispondenti comandi in git
.
Negli scenari adatti all’utilizzo di git submodule
, repo
risulta essere un’alternativa valida e semplice da usare.
git subtree
Arriviamo quindi all’ultimo approccio presentato, e che personalmente preferisco: git subtree
.
git subtree
permette di annidare un repository all’interno di un altro, in modo simile a come avviene usando i submodules, senza tuttavia aggiungere ulteriori metadati (ie. .gitmodules
) al repository. Viene bensì creata una copia locale del repository, similmente a quanto visto nel primo approccio.
Utilizzare git subtree
è semplice quanto lanciare questo singolo comando:
my-yocto-project$ git subtree add --prefix=meta-openembedded/ --squash git://git.openembedded.org/meta-openembedded zeus
Andiamo ad analizzare questa riga:
git subtree add
è il comando Git da usare per aggiungere un nuovo subtree- l’opzione
--prefix
è necessaria, e richiede di specificare la directory in cui verrà clonato il nuovo repository --squash
crea un singolo commit di squash, piuttosto che importare tutta la storia del repository aggiunto- gli ultimi due argomenti sono il remote da cui clonare, e la ref da clonare (in questo esempio, il branch
zeus
)
La storia risultante da questo tipo di merge è qualcosa del genere:
* 3b4d30004 - (HEAD -> master) Merge commit '5cbefde41f9f99a2c5101fabd8ab8388c99c3a57' as 'meta-openembedded'
|\
| * 5cbefde41 - Squashed 'meta-openembedded/' content from commit bb65c27a7
* 877cd2813 - Another awesome commit
* 18c08af2e - An awesome commit
dove il bb65c27a7
nel commit di squash è l’hash del commit corrispondente al tip del branch zeus
di meta-openembedded
al momento del fetch.
L’unico vincolo per l’utilizzo di git subtree
è che il prefix in cui il subtree verrà clonato sia inizialmente vuoto. Il contenuto del subtree così aggiunto è parte integrante del nostro repository, per cui un successivo clone
di quest’ultimo non richiede alcuna operazione aggiuntiva, o addirittura di sapere che subtree
è usato nel progetto.
git subtree
unisce dunque il meglio dei casi visti sopra:
- l’utilizzo negli scenari previsti da Yocto (ie. nessuna necessità di contribuire al subtree nella stragrande maggioranza dei casi) è semplice e immediato,
- non richiede che l’utilizzatore esegua step aggiuntivi per poter iniziare a lavorare sul repository,
- non aggiunge metadati, e il codice del subtree è sempre disponibile sul proprio repository,
- non comporta problemi nel caso in cui il remote originario del subtree non sia più accessibile,
- modifiche distruttive alla storia del subtree effettuate dai maintainer del subtree non hanno ripercussioni su di noi,
- se necessario, abbiamo la possibilità di integrare la storia del subtree con quella del nostro repository, omettendo l’opzione
--squash
, - è possibile modificare la propria copia locale del subtree e tracciare queste modifiche come parte del nostro repository, sebbene in seguito possano emergere conflitti nel momento in cui si decidesse di aggiornare il subtree.
Ovviamente questo approccio non è esente da svantaggi, in primis la complessità nell’effettuare operazioni di merge e/o rebase che coinvolgano branch interessati dalla presenza di subtree, ma si tratta di eventualità più uniche che rare, che sono fortemente contro-bilanciate dalla mole di vantaggi.
Conclusioni
Come visto, la scelta sulla gestione del progetto dipende molto dalle esigenze, ma a meno di non ricadere in un caso molto particolare che spinga necessariamente verso una particolare direzione, reputo che le caratteristiche di git subtree
si sposino benissimo con l’uso che ne richiede un progetto Yocto, motivo per cui è diventato velocemente il mio default nella gestione di questo tipo di progetti.
Per saperne di più su git subtree
, Atlassian ha pubblicato tempo fa un ottimo blog post a riguardo; in alternativa, man git-subtree
è un’ottima fonte di sapere!