# --------------------------------------------------------------------------- # libpda - die Bibliothek # # Eigenstaendiges Projekt (P1204R0). Laeuft in zwei Betriebsarten: # # 1. als Teil des Superprojekts: cmake --preset debug # 2. allein: cmake -S libpda -B build/nur-lib # # Betriebsart 2 ist der Grund fuer den PROJECT_IS_TOP_LEVEL-Block unten: was # sonst das Superprojekt bereitstellt, muss die Bibliothek sich dann selbst # holen. # --------------------------------------------------------------------------- cmake_minimum_required(VERSION 3.28) project(libpda VERSION 0.1.0 DESCRIPTION "PDA-Kernbibliothek: Kontakte, Rechner, Editor, Explorer" LANGUAGES C CXX) # PROJECT_IS_TOP_LEVEL (CMake >= 3.21) ist TRUE, wenn dieses project() das # oberste ist -- also genau in Betriebsart 2. if(PROJECT_IS_TOP_LEVEL) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake") include(ProjectDefaults) include(Warnings) include(Sanitizers) option(BUILD_SHARED_LIBS "Bibliothek als Shared Library bauen" OFF) option(PDA_BUILD_TESTS "Tests bauen" ON) if(PDA_BUILD_TESTS) enable_testing() # Die zweiargumentige Form von add_subdirectory: das Quellverzeichnis # liegt AUSSERHALB dieses Projekts, also muss man CMake sagen, wohin # die Build-Artefakte sollen. Ohne das zweite Argument bricht CMake ab. add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../third_party" third_party) endif() endif() # --------------------------------------------------------------------------- # Quellen. Kein file(GLOB): CMake merkt nicht, wenn eine Datei dazukommt -- # der Build bleibt gruen und die neue Datei ist einfach nicht dabei. # # *.test.c und *.test.cpp gehoeren NICHT hierher (P1204R0 Regel 7.1); die # werden weiter unten zu eigenen Executables. # --------------------------------------------------------------------------- set(LIBPDA_SOURCES libpda/textfile.c libpda/calculator.cpp libpda/contact.cpp libpda/editor.cpp libpda/explorer.cpp ) # Header hier aufzuzaehlen ist fuer den Compiler ueberfluessig -- er findet sie # ueber die #includes. Es hat zwei andere Zwecke: IDEs zeigen sie im # Projektbaum, und ein Blick in diese Liste sagt, was die Bibliothek ausmacht. set(LIBPDA_HEADERS libpda/textfile.h libpda/calculator.hpp libpda/contact.hpp libpda/editor.hpp libpda/explorer.hpp libpda/details/version.hpp ) # Targetname "pda", nicht "libpda": CMake stellt auf Unix selbst ein "lib" # voran, das Ergebnis heisst also libpda.a. Ein Target namens "libpda" ergaebe # liblibpda.a. add_library(pda ${LIBPDA_SOURCES} ${LIBPDA_HEADERS}) # Der Alias ist das, was Benutzer schreiben: target_link_libraries(x PRIVATE # pda::pda). Er kostet nichts und hat einen konkreten Nutzen -- ein Tippfehler # in einem Namen MIT Doppelpunkt ist ein sofortiger CMake-Fehler, ohne # Doppelpunkt haelt CMake ihn fuer den Namen einer Systembibliothek und # scheitert erst beim Linken. add_library(pda::pda ALIAS pda) # --------------------------------------------------------------------------- # Include-Pfade -- der wichtigste Block dieser Datei. # # PUBLIC heisst: gilt fuer diese Bibliothek UND fuer jeden, der sie linkt. # (PRIVATE = nur hier, INTERFACE = nur fuer die anderen.) # # Die beiden Generator-Ausdruecke unterscheiden zwei Welten: # # BUILD_INTERFACE waehrend hier gebaut wird. Der Include-Root ist # /libpda, damit auf # /libpda/libpda/contact.hpp zeigt. # # INSTALL_INTERFACE nachdem installiert wurde. Der Pfad ist relativ zum # Installationspraefix, also /include, und # zeigt auf # /include/libpda/contact.hpp. # # Ohne diese Trennung wuerde die installierte Bibliothek in ihrer # CMake-Konfigurationsdatei auf DEIN Build-Verzeichnis zeigen. Beim Benutzer # existiert das nicht -- der klassische "works on my machine"-Fehler beim # Verteilen einer Bibliothek. # --------------------------------------------------------------------------- target_include_directories(pda PUBLIC "$" "$" ) # Auch das generierte Export-Header-Verzeichnis muss in beiden Welten stimmen. target_include_directories(pda PUBLIC "$" "$" ) # Die Warnungen gelten beim Bauen DIESER Bibliothek. Wer sie linkt, soll # unsere Warnungsliste nicht erben -- fremde Projekte haben ihre eigene. # # Warum $ und nicht einfach PRIVATE: # # Bei einer STATISCHEN Bibliothek reicht PRIVATE nicht. Eine .a-Datei linkt # ihre Abhaengigkeiten nicht selbst -- das muss der tun, der sie benutzt. # CMake traegt PRIVATE-Abhaengigkeiten deshalb trotzdem als $ # ins INTERFACE_LINK_LIBRARIES ein. install(EXPORT) sieht dort pda_warnings # stehen, findet es in keinem Export-Set und bricht ab: # # install(EXPORT "pdaTargets" ...) includes target "pda" which requires # target "pda_warnings" that is not in any export set. # # Drei Wege heraus: pda_warnings mitexportieren (verseucht das Paket mit # unseren Flags), die Flags direkt per target_compile_options setzen (dann # bekommt third_party/ sie auch), oder -- richtig -- den Generator-Ausdruck # unten: er loest im Build-Baum zu "pda_warnings" auf und beim Installieren # zu nichts. target_link_libraries(pda PRIVATE "$") # Der C++-Standard als PUBLIC-Anforderung: wer libpda benutzt, braucht # zwingend C++23, weil contact.hpp im Interface hat. Ohne das # scheitert das Benutzerprojekt mit einem unverstaendlichen Header-Fehler # statt mit einer klaren Meldung. target_compile_features(pda PUBLIC cxx_std_23) # --------------------------------------------------------------------------- # Export-Header: erzeugt mit dem Makro PDA_EXPORT. # # Warum: bei einer Shared Library sind Symbole dank CXX_VISIBILITY_PRESET # hidden unsichtbar, unter Windows grundsaetzlich. Das Makro loest sich je # nach Plattform und Bauart in __declspec(dllexport), __attribute__((visibility # ("default"))) oder nichts auf. # # Wir generieren die Datei, statt sie von Hand zu schreiben: CMake kennt die # richtige Variante fuer jeden Compiler. # --------------------------------------------------------------------------- include(GenerateExportHeader) generate_export_header(pda BASE_NAME PDA EXPORT_FILE_NAME "${CMAKE_CURRENT_BINARY_DIR}/generated/libpda/pda_export.h") # Ohne diese Definition loest PDA_EXPORT im statischen Bau unter Windows zu # __declspec(dllimport) auf -- der Linker sucht dann eine DLL, die es nicht # gibt. generate_export_header sieht dafuer PDA_STATIC_DEFINE vor. # # PUBLIC, nicht PRIVATE: die Definition muss auch beim BENUTZER gelten, denn # er inkludiert denselben Header. if(NOT BUILD_SHARED_LIBS) target_compile_definitions(pda PUBLIC PDA_STATIC_DEFINE) endif() set_target_properties(pda PROPERTIES VERSION ${PROJECT_VERSION} # libpda.dylib.0.1.0 SOVERSION ${PROJECT_VERSION_MAJOR} # libpda.dylib.0 <- ABI-Stand ) # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- if(PDA_BUILD_TESTS) # Unit-Test in C: .test.c -> Executable .test -> CTest # "unity.". Der neotest-Adapter in Neovim erwartet genau diesen Namen. function(libpda_add_unity_test module) if(NOT TARGET unity) return() endif() set(target "${module}.test") add_executable(${target} "libpda/${module}.test.c") target_link_libraries(${target} PRIVATE pda unity pda_warnings) add_test(NAME "unity.${module}" COMMAND ${target}) endfunction() # Unit-Test in C++: doctest_discover_tests() ruft die Executable beim Build # mit --list-test-cases auf und legt EINEN CTest-Eintrag pro TEST_CASE an. # Das ist Pflicht, nicht Kosmetik: neotest-ctest kann Testnamen nur so auf # Quellpositionen abbilden. function(libpda_add_doctest_test module) if(NOT TARGET doctest) return() endif() set(target "${module}.test") add_executable(${target} "libpda/${module}.test.cpp") target_link_libraries(${target} PRIVATE pda doctest pda_warnings) doctest_discover_tests(${target} TEST_PREFIX "doctest.") endfunction() libpda_add_unity_test(textfile) libpda_add_doctest_test(calculator) libpda_add_doctest_test(contact) libpda_add_doctest_test(editor) libpda_add_doctest_test(explorer) add_subdirectory(tests) endif() # --------------------------------------------------------------------------- # Installation und Export # # Ab hier wird aus einem Build-Verzeichnis eine Bibliothek, die ANDERE # Projekte mit find_package(pda) benutzen koennen. Vier Schritte: # # 1. install(TARGETS ... EXPORT ...) Dateien kopieren, Target vormerken # 2. install(DIRECTORY ...) Header kopieren # 3. install(EXPORT ...) pdaTargets.cmake erzeugen # 4. configure_package_config_file() pdaConfig.cmake erzeugen # # find_package(pda) sucht nach pdaConfig.cmake, das laedt pdaTargets.cmake, # und darin steht das Target pda::pda mit allen Include-Pfaden und Flags. # --------------------------------------------------------------------------- include(GNUInstallDirs) # liefert CMAKE_INSTALL_LIBDIR usw. -- auf Fedora # ist das lib64, nicht lib. Nie selbst hinschreiben. include(CMakePackageConfigHelpers) install(TARGETS pda EXPORT pdaTargets RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") # Header. FILES_MATCHING mit PATTERN, weil sonst auch .cpp und .test.cpp # mitkopiert wuerden -- install(DIRECTORY) nimmt per Default ALLES. install(DIRECTORY libpda/ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/libpda" FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" PATTERN "private" EXCLUDE) # Der generierte Export-Header liegt im Build-, nicht im Quellbaum. install(FILES "${CMAKE_CURRENT_BINARY_DIR}/generated/libpda/pda_export.h" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/libpda") # NAMESPACE pda:: sorgt dafuer, dass das importierte Target genauso heisst wie # der Alias oben. Benutzer schreiben pda::pda -- egal ob sie die Bibliothek # installiert haben oder per add_subdirectory einbinden. install(EXPORT pdaTargets FILE pdaTargets.cmake NAMESPACE pda:: DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/pda") # Versionsdatei. SameMajorVersion heisst: find_package(pda 0.1) akzeptiert # 0.9, aber nicht 1.0 -- die uebliche Semver-Zusicherung. write_basic_package_version_file( "${CMAKE_CURRENT_BINARY_DIR}/pdaConfigVersion.cmake" VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion) # configure_package_config_file statt configure_file: es definiert zusaetzlich # PACKAGE_INIT, das die Pfade relativ zum tatsaechlichen Installationsort # aufloest. Damit funktioniert das Paket auch, wenn es jemand nach dem # Installieren verschiebt. configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/../cmake/pdaConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/pdaConfig.cmake" INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/pda") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pdaConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/pdaConfigVersion.cmake" DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/pda")