CONAN 2 Tests

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 PATH bzw. LD_LIBRARY_PATH werden 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

1cd myproject
2conan install .
3build\generators\conanrun.bat
4cmake --preset conan-debug
5cmake --build --preset conan-debug
6code .
7build\generators\deactivate_conanrun.bat

Linux: CMake und Visual Code mit CONAN run environment

1cd myproject
2conan install .
3source build/generators/conanrun.sh
4cmake --preset conan-debug
5cmake --build --preset conan-debug
6code .
7source build/generators/deactivate_conanrun.sh

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:

1cd myproject
2conan install .
3code .

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:

1foreach(item ${CONAN_RUNTIME_LIB_DIRS})
2  list(APPEND path_patches "PATH=path_list_append:${item}")
3endforeach()
4
5set_property(TEST ${test_name} 
6  PROPERTY ENVIRONMENT_MODIFICATION ${path_patches})

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.

📧 📋 🐘 | 🔗 🔔
 

Meine Dokus über:
 
Weitere externe Links zu:
Alle extern verlinkten Webseiten stehen nicht in Zusammenhang mit opengate.at.
Für deren Inhalt wird keine Haftung übernommen.



Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!