CONAN 2 Tests
« | 24 Nov 2024 | »Das Drama mit CONAN ist oft,
dass die Entwicklung in unterschiedlichen Kontexten läuft. Und ein
conan create führt Builds und Tests manchmal ganz anders aus, als ein
cmake build nach einem conan install.
Auf zur Analyse!
conan build und conan create laden alle Abhängigkeiten und fügen die
notewendigen Pfade in die PATH und LD_LIBRARY_PATH Umgebungsvariablen
ein, wenn Prozesse wie cmake gestartet werden.
Laufen dann Tests, erben die Test-Prozesse die Umgebung und alles läuft
problemlos.
Bei der Entwicklung nutzen wir aber häufig conan install und
conan source und danach starten wir cmake oder eine IDE
(wie VSCode) manuel, wo diese Umgebung fehlt.
Die Builds laufen dann, aber Tests können die notwendigen Bibliotheken
zur Laufzeit nicht finden und schlagen fehl.
Hinweis: Kleine Projekte haben das Problem nicht, denn wenn alles lokal gebaut wird, sind die korrekten Pfade des Build-Prozesses in den Binaries selbst gespeichert. Doch wenn per CI fertige Komponenten auf den eigenen Conan-Remote hochgeladen wurden und das eigene Projekt diese herunterlädt, dann passt das Setup nicht mehr zusammen und
PATHbzw.LD_LIBRARY_PATHwerden zwingend erforderlich.
CONAN virtual run environment
Die Standard Lösung lautet: Erzeuge ein Run-Environment und starte die
IDE oder den Build-Prozess aus diesem heraus.
Conan legt herfür conanrun Scripts an, die man einfach vor dem Build
oder der IDE starten muss:
Windows: CMake und Visual Code mit CONAN run environment
Linux: CMake und Visual Code mit CONAN run environment
CONAN und CMake generieren Environment und Launch Dateien
Das wiederholte manuelle Aufrufen von Scripts wird extrem mühsam, wenn man zwischen vielen Conan-Projekten hin und herwechseln muss.
Ich wollte daher eine Variante haben, in der jedes Projekt mit wenigen Zeilen in VSCode geladen werden kann, und zwar mit:
Um jetzt das notwendige Environment zu “konservieren”, damit man es beim
Debuggen benutzen kann, erzeuge ich in der Conan generate() Methode eine
.env Datei im Build-Verzeichnis.
Da Windows und Linux etwas unterschiedlich sind, fällt der Code etwas größer
aus und sieht etwa so aus:
1# conanfile.py 2 3class myProject_ConanFile(ConanFile): 4 ... 5 def create_env_file(self, env_file_path: str): 6 is_windows = self.settings.os == "Windows" 7 path_name = "PATH" 8 path_var = "%PATH%" if is_windows or "$PATH" 9 lib_name = "LIB" if is_windows or "LD_LIBRARY_PATH" 10 lib_var = "%LIB%" if is_windows or "$LD_LIBRARY_PATH" 11 12 env_bin_paths = [] 13 env_lib_paths = [] 14 15 ## add conan dependencies to PATH and LIBS array 16 for dep in self.dependencies.values(): 17 env_bin_paths.append(str(bin)) for bin in dep.cpp_info.bindirs 18 env_lib_paths.append(str(lib)) for lib in dep.cpp_info.libdirs 19 20 ## add current PATH entries 21 env_bin_paths.extend(os.getenv(path_name, "").split(os.pathsep)) 22 env_lib_paths.extend(os.getenv(lib_name, "").split(os.pathsep)) 23 24 ## remove empty entries 25 env_bin_paths = [e for e in env_bin_paths if e] 26 env_lib_paths = [e for e in env_lib_paths if e] 27 28 with open(env_file_path, "w") as f: 29 path_text = os.pathsep.join(env_bin_paths) 30 f.write("{}={}\n".format(path_name, path_text)) 31 lib_text = os.pathsep.join(env_lib_paths) 32 f.write("{}={}\n".format(lib_name, lib_text)) 33 34 def generate(self): 35 tc = CMakeToolchain(self) 36 tc.generate() 37 38 cmakedeps = CMakeDeps(self) 39 cmakedeps.generate() 40 41 if self.settings.build_type == "Debug": 42 env_file_path = os.path.join(self.build_folder, ".env") 43 self.create_env_file(env_file_path) 44 ...
Anschließend wird in den Launch-Jobs für VSCode die .env Datei
referenziert und somit startet jede Debug-Target-Start bzw. jeder
Debug-CTest-Start mit den Pfaden, die Conan in seinen Dependencies
bereits aufgelöst hat.
1// .vscode/launch.json 2{ 3 "version": "0.2.0", 4 "configurations": [ 5 { 6 "name": "(gdb) CMake Target", 7 "type": "cppdbg", 8 "MIMode": "gdb", 9 "request": "launch", 10 "program": "${command:cmake.launchTargetPath}", 11 "args": [], 12 "cwd": "${command:cmake.buildDirectory}", 13 "envFile": "${command:cmake.buildDirectory}/.env" 14 }, 15 { 16 "name": "(gdb) CTest Launch", 17 "type": "cppdbg", 18 "MIMode": "gdb", 19 "request": "launch", 20 "program": "${cmake.testProgram}", 21 "args": [ "${cmake.testArgs}" ], 22 "cwd": "${cmake.testWorkingDirectory}", 23 "envFile": "${command:cmake.buildDirectory}/.env" 24 }, 25 { 26 "name": "(msvc) CMake Target", 27 "type": "cppvsdbg", 28 "request": "launch", 29 "program": "${command:cmake.launchTargetPath}", 30 "args": [], 31 "cwd": "${command:cmake.buildDirectory}", 32 "envFile": "${command:cmake.buildDirectory}/.env" 33 }, 34 { 35 "name": "(msvc) CTest Launch", 36 "type": "cppvsdbg", 37 "request": "launch", 38 "program": "${cmake.testProgram}", 39 "args": [ "${cmake.testArgs}" ], 40 "cwd": "${cmake.testWorkingDirectory}", 41 "envFile": "${command:cmake.buildDirectory}/.env" 42 } 43 ] 44}
Wer CTest direkt (also ohne VS-Code Debugger) startet, hat das gleiche
Problem, doch hier kann man den Fix in CMake integrieren.
Die .env Datei wird gelesen und ihr Inhalt als Array dem CTest-Property
ENVIRONMENT zugeordnet.
1set(test_name "my_test") 2set(test_command "${PROJECT_NAME}") 3set(test_args) 4 5add_test(NAME ${test_name} COMMAND "${test_command}" ${test_args}) 6 7if(EXISTS "${CMAKE_BINARY_DIR}/.env") 8 file(READ "${CMAKE_BINARY_DIR}/.env" envfile_content) 9 10 # The Windows "path1;path2" format needs to be escaped 11 # to avoid conflicts with cmake's array ";" separation 12 string(REPLACE "\r" "" envfile_content "${envfile_content}") 13 string(REPLACE ";" "\\;" envfile_content "${envfile_content}") 14 string(REPLACE "\n" ";" envfile_entries "${envfile_content}") 15 16 set_property(TEST ${test_name} 17 PROPERTY ENVIRONMENT "${envfile_entries}") 18endif()
Eine andere Option wäre dann noch Conan-Variablen in CMake zu nutzen,
was ich aber nicht so gerne mache, schließlich soll CMake nicht von
Conan abhängen.
Trotzdem helfen folgende Zeilen manchmal sehr:
Fazit
Ein Problem, viele Möglichkeiten, und unnötig viel Komplexität.
Diese Beispiele sind das Ergebnis einer Monate-langen Herumspielerei.
Wir haben in der Firma 100+ Conan Projekte, die alle über eine gemeinsame
Conan- und CMake- Bibliothek zusammenhängen.
So kann man sicherstellen, dass alle Projekte auf alle Fälle vorbereitet sind. Im Einzelfall sind diese “Patches” oft nicht notwendig, doch wenn viele Ebenen aufeinandertreffen, gibt es immer wieder mal Pfad-Probleme, die so zumindest umgangen werden können.
Ein Problem ist vor allem, dass jeder Developer anders arbeitet.
Einer startet alles von der Konsole aus, einer nutzt Visual Studio und
andere nehmen VSCode. Die Platformen sind Windows, Liunx, 32-bit und 64-bit
für Debug und Release.
Es war echt mühsam immer wieder feststellen zu müssen, dass es genau in einer Variante hakte, während alle anderen liefen.
Aber das ist nunmal unser Job: Nämlich alles zu meistern.