umpf - Git on a New Level
Moderne Softwareentwicklung ohne begleitende Versionsverwaltung wie Git ist heutzutage unvorstellbar - Änderungen am Quellcode sollen schließlich nachvollziehbar dokumentiert und beliebige Verssionsstände jederzeit einfach reproduziert werden können. Für Arbeiten an komplexeren Projekten wie etwa dem BSP ("Board Support Package") eines eingebetteten Systems mit mehreren Entwicklungssträngen skaliert ein bloßes Aufeinanderstapeln der einzelnen Änderungen jedoch nicht.
Häufig werden ausgehend von einem gemeinsamen Softwarestand parallel verschiedene Funktionalitäten entwickelt, müssen Back-Ports integriert und für Upstream gedachte Arbeiten separat verwaltet werden. Die dazu notwendige thematische Aufteilung bei der Entwicklung neuer Patches für das BSP geschieht daher in der Regel auf voneinander unabhängigen Zweigen in separaten Git Branches.
Kompliziert wird es mit diesem Modell allerdings bei der anschließenden Zusammenführung von mehr als zwei Branches, etwa, um das Gesamtprojekt testen zu können oder zur Veröffentlichung eines Releases. Das manuelle Mergen der einzelnen Zweige ist so aufwendig wie fehleranfällig und kollidiert mit den möglichst kurzen Testzyklen iterativer Entwicklungsmodelle.
Branches sinnvoll zusammenführen
Unter Anderem, um diesen letzten Schritt zu vereinfachen und aus einer Auswahl von Topic Branches reproduzierbar einen einzelnen Patch Stack generieren zu können, haben wir den Universal Magic Patch Functionator (umpf) als quelloffene Erweiterung für die lokale Git-Installation entwickelt.
Mit umpf lässt sich innerhalb der gewohnten Git Infrastruktur und über mehrere Git Branches ("Topic Branches") hinweg eine lineare Patch-Serie generieren, deren Abhängigkeiten dabei konserviert werden.
Diese Abfolge von Commits wird durch einen regulären Git Tag beschrieben, der von umpf um Referenzen zu den enthaltenen Topic Branches ergänzt wird. Ein solcher utag vereint somit mehrere Eigenschaften:
- Der Marker beinhaltet einen Datumsstempel und ist dadurch eindeutig zuordbar.
- Es kann ein linearer Patch Stack als Patch-Serie exportiert werden.
- Der Stand der eingebundenen Branches zum umpf-Zeitpunkt wird konserviert. Spätere auch tiefgreifende Änderungen eines Zweiges (z.B. Force-Pushes nach Bug-Fixes) haben dadurch keinen Einfluss auf den utag, weswegen der tatsächliche Entwicklungsstand zum Tagging-Zeitpunkt problemlos wiederhergestellt und nachvollzogen werden kann.
- Durch die Archivierung der enthaltenen Branches können deren eventuelle Abhängigkeiten untereinander mit normalen Git-Werkzeugen nachvollzogen werden.
- Durch Heuristik wird die Art des Projektes erkannt und passend ein Datumsstempel integriert - bspw. als EXTRAVERSION in einem Makefile. Dies ermöglicht später eine eindeutige Identifikation auch des bereits kompilierten und ausgelieferten Softwarestands.
Die Erstellung eines utags und seiner linearen Patch-Serie ist dabei relativ
unkompliziert und setzt neben umpf lediglich ein bestehendes Git Projekt
voraus. umpf selbst ist ein Bash Skript und bedient sich jener Werkzeuge, die
Git selbst zur Verfügung stellt. Deshalb können durch umpf generierte Tags und
Merges im Anschluss von jeder normalen Git Installation verarbeitet werden. Ein
utag wird folglich auch regulär mittels git push
dem bereits
bestehenden Repository hinzugefügt und kann analog dazu über git
checkout
wieder aufgerufen werden.
Um einen utag erzeugen zu können benötigt umpf drei Parameter, die sich zum
Beispiel in Form einer useries-Textdatei oder über Aufruf von umpf
init
übergeben lassen:
- base: Der Ausgangspunkt, auf dem die spätere Patchserie aufbauen soll. Die umpf-base ist meistens ein Upstream Commitish, etwa ein Release Tag (bei Linux z.B.: "v6.3").
- name: Der eigentliche Name des umpfs, der später auch Teil des utags wird. Es empfiehlt sich, diesen von der base abzuleiten, bspw. "6.3/release-name".
- topic: Die eigentlichen Git-Branches (Topic Branches) werden mit je
einem vorangestellten
# umpf-topic:
angeführt. Sie sollten selber auf dem base commit aufbauen, um Komplikationen zu vermeiden. Es ist daher ratsam, Branches mit einem anderen Ausgangspunkt als die umpf-base zuerst auf ebendiese zu re-basen.
Integration von Backports
Gerade bei Arbeiten mit Upstream-Projekten kommt es immer wieder vor, dass Bugfixes oder Funktionserweiterungen von dritter Seite eingepflegt werden müssen. Die Stärke von umpf ist hier, dass solche Änderungen in einem eigenen Branch gepflegt werden können und sie nicht weiter mit den Arbeiten am eigenen Projekt interferieren.
Zuverlässig funktioniert diese Branch-Integration bei umpf allerdings nur,
wenn der Ausgangscommit vergleichbar ist: Topic Branches für bspw. v5.15
und v5.16
können in der Regel problemlos durch umpf zusammen
verarbeitet werden. Ein weiterer Branch basierend etwa auf v6.2
wird
wegen der umfangreichen allgemeinen Unterschiede im Code allerdings zu
Komplikationen beim umpfen führen.
Um Bugfixes und sonstige Backports zu integrieren ist es daher empfehlenswert, diese mit ihrem eigenen Topic Branch einmalig auf die umpf-base zu re-basen.
Behandlung von Merge Konflikten
umpf kann selbstständig keine Merge-Konflikte auflösen. Die händische Konfliktlösung während des umpfens ist zwar möglich und bspw. bei der Arbeit mit Device-Trees manchmal unvermeidlich, sollte sich jedoch auf ein Minimum beschränken.
Die Lösungen zu den einzelnen Konflikten können allerdings mittels git-rerere gespeichert und im Wiederholungsfalle dadurch automatisiert aufgelöst werden. Die zu integrierenden Branche sollten trotzdem, falls nötig, im Vorfeld eines umpfs bereits entprechend umstrukuriert und aufgeräumt worden sein.
Lineare Serie generieren
Die so erstellte useries kann nun benutzt werden, um aus ihr einen utag abzuleiten und alle eingetragenen Entwicklungsstränge in einem gemeinsamen Release Tag vereinen zu lassen:
~/epic-project $ cat ./useries
# umpf-base: v6.3
# umpf-name: 6.3/special-customer-release
# umpf-topic: v6.3/topic/bugfix-branch
# umpf-topic: v6.3/topic/more-fixes
# umpf-end
~/epic-project $ umpf tag ./useries
umpf wird nun alle Branches in der Reihenfolge, wie sie in der useries eingetragen wurden, auf der angegebenen umpf-base stapeln. Dabei wird auch ein Autosquash durchgeführt, sodass Fixup-Commits aufgelöst werden und nicht in den finalen Patchstack gelangen.
Ein Beispiel, wie ein solcher utag in Git dargestellt wird, kann in der folgenden Abbildung betrachtet werden. Hierbei handelt es sich um einen so genannten qualifizierten umpf, da dieser alle notwendigen Informationen zur vollständigen Rekonstruktion beinhaltet:
- umpf-base: Der Ausgangscommit. Meist ist dies ein Upstream-Tag.
- Patch Stack: Commits der Topic Branches, gestapelt auf die umpf-base. Die Reihenfolge der Branches entspricht jener, wie sie umpf übergeben wurden.
- References: Der Zustand der integrierten Branches zum Zeitpunkt des umpf-ens. Dieser Zustand wird im Commit des utag über die HEADs der Zweige referenziert und dadurch konserviert, sodass selbst eine nachträgliche Änderung eines Branches keine Auswirkung hat, sollte der utag zu einem späteren Zeitpunkt erneut ausgechecked werden. Die Referenzierung bewirkt außerdem, dass die Git Garbage Collection die vermeintlich verwaiste Konstellation im Repository belässt. Die Reproduzierbarkeit des utag bleibt somit langfistig gegeben.
- utag: Ein regulärer Git Tag, in dem die integrierten Branches sowie die jeweils neusten Commit-IDs (HEADs) zum Zeitpunkt des umpf-ens hinterlegt sind.
- Release Tag: Ein weiterer regulärer Git Tag. Dieser wird für die Generierung einer Patch-Serie referenziert. Der Commit beinhaltet ggf. eine Modifikation des Projektcodes, um einen Datumsstempel zu integrieren.
Das abschließende Exportieren des Patchstacks erfolgt über den Befehl
umpf format-patch
. Der Export kann mit Parametern variiert werden: Ein
angehängtes -bb
erzeugt eine zu Yocto-Projekten kompatible Patch-Serie,
mit -p
kann der Zielpfad festgelegt, und mit -u
das
Überschreiben einer eventuell bereits vorhandenen Serie veranlasst werden.
Weitere Hinweise liefert umpf --help
.
umerge
Anstelle eines utags können wir auch ohne vorhandene useries durch den
Aufruf von umpf merge
schrittweise durch händisches einfügen der
einzelnen Topic Branches einen umerge aufbauen. Hierzu genügt es, mittels
git checkout
auf den Stand der erwünschten umpf-base zu wechseln um im
Anschluss mit umpf merge
gezielt die benötigten Branches nachzuladen:
~/epic-project $ git checkout v6.3
~/epic-project $ umpf merge v6.3/topic/bugfix-branch
umpf: merging 'v6.3/topic/bugfix-branch'...
[...]
~/epic-project $ umpf merge v6.3/topic/more-fixes
umpf: merging 'v6.3/topic/more-fixes'...
[...]
Diese Zusammenführung von mehreren Zweigen ähnelt einem Octopus Merge, welcher
bei Git für das gleichzeitige Mergen von mehr als zwei Branches genutzt
werden kann. Anders als bei Git bleibt nach einem umpf merge
jedoch
analog zum utag die Herkunft der hinzugekommenen Commits erhalten: Die
zugrundeliegenden Branches werden in ihrem Zustand zum Zeitpunkt des umerge
durch einen zusätzlichen Eintrag in der Commit-Nachricht des Merges
referenziert. Dadurch kann zu einem beliebigen späteren Zeitpunkt die Herkunft
der einzelnen Commits - also die ursprünglichen Namen ihrer Branches sowie deren
Historie - nachvollzogen werden.
Ein umerge kann auch direkt mit einer useries-Datei erzeugt werden. Hierzu
genügt der Aufruf von umpf build ./useries
.
Ein umerge bietet sich vorallem für die iterative Entwicklung an, bei der alle
Topic Branches eines Projekts benötigt werden, um bspw. hardwarenahe Entwicklung
am Kernel. Zusätzliche Änderungen am Quellcode können dann einfach auf den
umerge aufgesetzt und getestet werden. Im Anschluss lassen sich durch Aufruf
von umpf distribute
die neu erzeugten Patche interaktiv einem der
ursprünglichen Zweige zuordnen und werden von umpf dort direkt integriert.
umpf distribute
kann auch genutzt werden, wenn die Commits nicht auf
einem umerge, sondern stattdessen auf einem utag gestapelt wurden.
umerges können allerdings auch direkt in Projekten Verwendung finden, wenn Abhängigkeiten zwischen den einzelnen Topic Branches bestehen. Durch das Stacking Feature von umpf wird bei der späteren Verarbeitung der auf einem umerge aufbauende Zweig dann regulär in die linearisierte Abfolge der Commits integriert. Dabei ist zu beachten, dass der Eintrag des fraglichen Branches in der useries Datei nicht vor den Zweigen, von denen er abhängt, eingefügt werden darf; die Reihenfolge der Nennung ist hier ausschlaggebend.
Da der umerge den Zustand der Basiszweige für den abhängigen Branch konserviert, werden nachträgliche Arbeiten an diesen auch nicht auf bereits bestehende umerges übertragen. Dies kann beim anschließenden Genrerien eines neuen utags zu Konflikten führen, wenn dieser mit einem älteren umerge erzeugt werden soll. In einem solchen Fall dürfen sich nachträgliche Änderungen an den Basiszweigen nicht auf direkte Abhängigkeiten des darauf aufbauenden Branches auswirken.
Warum wir umpfen
Bei Pengutronix haben wir uns auf Embedded Linux, und damit auf die Arbeit am Linux Kernel und den assoziierten Projekten spezialisiert. Im Rahmen unserer "Mainline First"-Strategie bringen wir dabei die in unseren Projekten erarbeiteten Funktionalitäten so früh wie möglich in die offiziellen Upstream-Kanäle ein. Dies bringt zwar langfristig viele Vorteile mit sich - wie etwa Sicherstellung des hohen Qualitätsstandards unseres Codes durch Community Review - bedeutet auf der anderen Seite aber auch, dass die Verwaltung der zum Projekt gehörenden Patches nicht nur eine direkte Integration in unsere spezifischen BSPs ermöglichen, sondern eben auch den Anforderungen von Upstream genügen muss. Dies setzt eine saubere Trennung des jeweiligen Topic Branches vom restlichen Projekt voraus.
Der Anstoß zur Entwicklung von umpf kam letztendlich aus unserer Arbeit an der Treiberentwicklung für eingebettete Systeme: Moderne SoCs decken heutzutage ein unglaublich breites Spektrum an Implementierungen und Funktionalitäten ab; zugleich werden nach dem Baukasten-Prinzip aber oftmals baugleiche Komponenten (sog. "intellectual property cores" bzw. "IP-Cores") genutzt - auch herstellerübergreifend. Entsprechend haben wir deshalb bei uns spezifische Treiber-Branches aufgebaut, die sich ohne weitere Anpassungen über mehrere unserer Integrationsprojekte hinweg verwenden lassen.
Das direkte Arbeiten am Linux-Kernel mit seinen hunderten von Branches und fortwährender Upstream-Entwicklung ist natürlich ein Paradebeispiel dafür, wie umfangreich ein mit Git verwaltetes Projekt werden kann. Aber auch wesentlich kleinere Projekte profitieren spätestens ab dem zweiten eingeführten Branch von einer Automatisierungshilfe wie umpf.
Auch, wenn eine Erweiterung wie unser Universal Magic Patch Functionator Grenzen mit Blick auf die tatsächlich mögliche Komplexität eines Git Repositories aufweist, so befähigt umpf uns trotzdem seit Jahren erfolgreich dazu, bei einer Vielzahl von Projekten eine ganze Reihe verschiedener Patchstacks sauber zu verwalten und den generellen Versionierungsaufwand in dieser Beziehung überschaubar zu halten.
Wir haben uns nun entschlossen, dieses Tool unter MIT-Lizenz der Open Source Gemeinde zur Verfügung zu stellen - und wir freuen uns wie immer über eine rege Beteiligung!
Weiterführende Links
Komplexität beherrschen mit Open Source
Vor ein paar Tagen ist etwas spannendes passiert: Ich habe mein allererstes Embedded System wiedergesehen - eine nach nunmehr ca. 34 Jahren defekte Schrittmotorsteuerung für die Teleskope der Volkssternwarte Rothwesten, die ich in den Sommerferien in der 12. Klasse gebaut habe. Schaut man sich die Entwicklung von damals bis hin zu unseren aktuellen industriellen Embedded Systems an, wird schnell klar, warum sowas heute nur noch mit Open Source Software sinnvoll beherrschbar ist.
Pengutronix at the Linux Plumbers Conference
The Linux Plumbers Conference 2024 will take place in Vienna from 18. to 20.09.2024. Luckily this does not overlap with the ELCE. Pengutronix will attend the LPC with six colleagues - so watch out for our T-shirts and hoodies and and feel free to chat with us.
Pengutronix at FrOSCon 2024
Am 17. und 18. 08. 2024 ist es wieder soweit: Die FrOSCon findet an der Hochschule Bonn-Rhein-Sieg in Sankt Augustin statt - und Pengutronix ist wieder als Partner dabei.
Pulse Width Modulation (PWM) is easy, isn't it? - Turning it off and on again
Part of Uwe Kleine-König's work at Pengutronix is to review PWM (Pulse Width Modulation) drivers. In addition, he also sometimes refactors existing drivers and the Linux kernel PWM subsystem in general.
Yes we CAN... add new features
Have you ever experienced an otherwise fine product that is missing just the one feature you need for your application?
Pengutronix at Embedded World 2022
Welcome to our booth at the Embedded World 2022 in Nürnberg!
Die Pengutronix Kernel-Beiträge in 2021
2022 hat begonnen, und obwohl Corona unsere Workflows stark verändert hat, hat das Pengutronix Team auch in diesem Jahr wieder etliche Beiträge zum Linux-Kernel geleistet. Das letzte Kernel-Release in 2020 war 5.10, das letzte in 2021 war 5.15 - schauen wir also, was sich dazwischen bei uns getan hat.
Pengutronix at FOSDEM 2021
"FOSDEM is a free event for software developers to meet, share ideas and collaborate. Every year, thousands of developers of free and open source software from all over the world gather at the event in Brussels. In 2021, they will gather online." -- FOSDEM
Optimising tig's bash completion by a factor of 1000
Has this ever happened to you?
15 Years of i.MX in Mainline Linux
Today it has been 15 years since we mainlined support for Freescale/NXP's i.MX architecture in the Linux kernel! That was one small step for [a] man, one giant leap for (industrial Linux users') mankind :-) Here is some background about why it happened and what you might want to learn from history for your next embedded Linux project.