CONAN der Erbauer

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:

graph LR subgraph "CONAN Build Setup" conan[[conanfile.py]] cmake[[CMakeLists.txt]] end boost([boost/1.71.0]) zlib([zlib/1.2.11]) bzip([bzip2/1.0.8]) main[[my_program.cpp
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.

graph LR COF[conanfile.py
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.

graph LR COINST[CONAN INSTALL
/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.

graph LR COCREATE["CONAN CREATE
/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 …