CONAN der Erbauer
«« | 14 Sep 2019 | »»Manche denken beim Titel CONAN an den alten Kinofilm mit Arnold Schwarzenegger, andere erinnern sich an den Anime mit dem Detektiv-Jungen.
Und für einen meiner geschätzten Kollegen ist das Paket-Tool CONAN die Lösung aller Probleme in der C++ Entwicklung.
In der Theorie sieht es so aus:
- Wir schreiben einzelne C und C++ Dateien
- CMake fügt mehrere C/C++ Dateien zu einem generischen Projekt zusammen, das dann auf jeder Build-Umgebung übersetzt werden kann
- Und
CONANverwaltet alle Abhängigkeiten von Paketen zwischen Projekten und erstellt automatisch CMake Files um mehrere Projekte unter einen Hut zu bekommen.
Ob das in der Praxis auch immer so sein wird … das lerne ich gerade im Zuge meiner Arbeit.
Beispiel Szenario
Angenommen wir schreiben ein Programm, das Kompression einsetzt, z.B. durch die gute alte ZLIB.
Jetzt können wir die Quellcodes in ein Unterverzeichnis unseres Projektes
kopieren und einfach mitkompilieren.
Oder: wir nutzen CONAN und setzen eine Abhängigkeit auf ein CONAN-ZLIB-Paket.
Dann brauchen wir nämlich nur noch unsere eigenen Sourcen speichern und
ausliefern, und CONAN wird die fehlende ZLIB aus dem Internet (oder einem
lokalen Build-Artifactory) herunterladen.
CONAN stellt unterschiedliche Möglichkeiten seiner Nutzung zur Verfügung,
einige davon sind:
- Ein Paket kann alle Sourcen eines Moduls beinhalten und wird dann lokal bei bzw. vor der Nutzung neu kompiliert
- Ein Paket enthält Header und vorkompilierte Binärdateien. Damit lädt
CONANdie fertigen Dateien herunter und stellt sie dem Linker automatisch zur Verfügung. - Oder
CONANbesteht nur noch aus Binärdateien, die für die Auslieferung benötigt werden und wird so zu einem Setup/Deployment-Builder.
CONAN löst Abhängigkeiten rekursiv auf, dass heißt, wenn wir z.B. eine
Abhängigkeit zu boost deklarieren, definiert das boost-CONAN-Paket, dass
es von zlib und bzip2 abhängt. CONAN lädt oder baut dann alle drei
Abhängigkeiten und stellt sie uns für das eigene Projekt zur Verfügung:
my_component.cpp
my_utilities.cpp]] exe[[my_program.exe]] cache((CONAN Cache
boost, zlib, bzip2)) conan --requires--> boost boost --requires--> zlib boost --requires--> bzip conan --download
install--> cache cmake --include_directories
$CONAN_INCLUDE_DIRS--> cache cmake --target_link_libraries
$CONAN_LIBS--> cache cmake --compile--> main cache --link--> exe main --link--> exe
Implementierung
Ist CONAN auf dem System installiert, erweitert man CMAKE um ein paar
Zeilen und fügt die Datei conanfile.py hinzu.
Dabei handelt es sich um ein Python
Script, das eine spezielle Klasse implementiert, deren Methoden in den
einzelnen Ausführungsschritten das Verhalten von CONAN beeinflussen.
Am wichtigsten sind dabei die Abhängigkeiten, wo wir festlegen, welche
anderen Pakete für den Einsatz unseres Pakets benötigen.
Diese haben das Format:
name/version@remote/branch
Im Falle des offiziellen ZLIB-Pakets für CONAN lautet der Pfad:
zlib/1.2.11@conan/stable
Der remote Parameter ist mit der Installation automatisch auf die
Server von conan.io und bintray.com gesetzt. Will man Pakete von
anderen bzw. eigenen Servern nutzen, muss ein entsprechender Name
registriert und der CONAN-Konfiguration mitgeteilt werden.
Wird nun direkt oder indirekt ein Paket installiert, z.B. mit
conan install zlib/1.2.11@conan/stable
dann lädt CONAN alle benötigten Dateien herunter und speichert
sie in einem (zugegeben etwas merkwürdigen) Pfad im Home-Verzeichnis.
Wenn wir dann unser leicht modifiziertes CMAKE nutzen, um Projektdateien
zu erzeugen, setzt CONAN automatisch alle Pfade für CMAKE auf die
richtigen INCLUDE- und LIB- Verzeichnisse, womit wir unser Projekt
kompilieren können, ohne komplexe Such-Routinen in CMAKE implementieren
zu müssen.
Denn dass CMAKE alle seine Dateien findet, kann bei größeren Projekten
mit mehreren Abhängigkeiten durchaus herausfordernd werden. Und genau
diesen Job soll hier CONAN erledigen.
Ein wichtiges Detail ist, dass im Falle von Binärdateien CONAN für jede
Plattform und Build-Option ein eigenes Paket erstellen kann.
Für Windows gibt es in der Regel 4 unterschiedliche Pakete für ein Modul,
nämlich
- Debug Win32
- Release Win32
- Debug Win64
- Release Win64
Hierfür erstellt man je ein CONAN Profil pro Build-Konfiguration,
und CONAN leitet an CMAKE genau die Pfade weiter, die für die gewünschte
Build-Konfiguration notwendig ist. Und bei unterschiedlichen Compilern
bzw. auf Linux ist das ebenso.
Aus der Build-Konfiguration wird hierfür ein HASH erzeugt und dem
Paket zugeordnet. Die im Beispiel erwähnten 4 Windows Build-Varianten
würden also zu 4 separaten Hashes führen. Eine zlib.dll wäre also
4 mal gespeichert, doch über den Hash würde man nur genau die dll
bekommen, die man gerade braucht.
Leitet man z.B.: den X64-Release-Build des eigenen Programms ein, würde
über den Hash nur die X64-Release-zlib.dll ausgewählt und gelinkt werden.
Beispiel Abläufe
conanfile.py und CMakeLists.txt
conanfile.py definiert Abhängigkeiten, die von CONAN in den Cache
heruntergeladen werden.
Beim Build mit CMake verweist das angepasste CMakeLists.txt automatisch
auf Bibliothekenpfade im Cache. CMake’s find_package brauchen wir also nicht.
requires: zlib/1.2.11] --Download--> CC(CONAN Cache) CC --Build & Link
Dependencies--> CMF[CMakeLists.txt
target_link_libraries
my_app $CONAN_LIBS]
Einzelne Build-Schritte
Liegt den Sourcen ein conanfile.py bei, kann man in dem Verzeichnis die
Abhängigkeiten einbetten lassen, danach den Build anstarten und am Ende
die Resultate exportieren lassen. Die Umsetzung der Schritte wird durch
die Einstellungen und Methoden in conanfile.py definiert.
/source/path] -- Download
dependencies --> COBUILD COBUILD[CONAN BUILD
/source/path] -- Run CMake
Build --> COPACK COPACK[CONAN PACKAGE
/source/path] -- Extract Headers
and Binaries --> OUT[(Output
Files)]
Pakete erzeugen und verteilen
Ein fertiges CONAN Paket kann in einen konfigurierten Store hochgeladen
werden und von dort auf andere System weiterverteilt werden.
Dafür muss in conanfile.py eine entsprechende deploy Routine definiert
sein.
/source/path
name/version
@user/channel"] --> COCACHE COCACHE([CONAN
Cache]) --> COUPLOAD COUPLOAD["CONAN UPLOAD
name/version
@user/channel"] --> ARTIFACTORY[(Package
Artifactory
Server)] ARTIFACTORY --> CODOWN["CONAN INSTALL
name/version
@user/channel
--install-folder
/target/path"] --> CODEP[(Deployment
Output)]
Fazit
Nun, noch ist CONAN für mich ein Novum und die Umstellung eines größeren
Projektes (an der ich mitarbeiten darf) ist das teilweise recht aufwendig.
Tatsächlich liegt ein ordentlicher Teil des Migrationsaufwandes an jenen
Stellen, wo CMAKE Workarounds benutzt wurden um Features zu generieren,
die es ohne CONAN eben nicht gibt.
Solche Codes umzuschreiben und durch CONAN-Standards zu ersetzen erfordert
also dann doch etwas Programmieraufwand in Python und auch
Verzeichnisstrukturen oder Unit-Tests sind unter CONAN anders organisiert,
als bei einigen bestehenden Projekten.
Werde ich CONAN im GATE Projekt einsetzen?
Nein, vorerst nicht. Einerseits fehlt mir das Wissen, wie ein “ideales”
CONAN Projekt aussehen soll und außerdem sind externe Abhängigkeiten genau
das, was das GATE Projekt vermeiden will. Dennoch bleiben stets ein paar
Abhängigkeiten auch bei mir übrig (z.B. GTK unter Linux) und vielleicht
werden diese in Zukunft auch durch eine CONAN-Lösung aus dem Weg
geräumt werden.
Bis dahin gibt es aber noch viel für mich zu entdecken …
