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
CONAN
verwaltet 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
CONAN
die fertigen Dateien herunter und stellt sie dem Linker automatisch zur Verfügung. - Oder
CONAN
besteht 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 …