db41258d16
Ersetzt das Geruest (counter/notebook) durch ein echtes Beispiel und teilt es in zwei eigenstaendige Projekte, wie P1204R0 es fuer Bibliothek plus Programm verlangt. libpda: textfile (C, Datei-I/O und Feld-Escaping), calculator (Recursive-Descent-Parser mit std::expected), contact, editor, explorer. Exportiert pda::pda ueber install(EXPORT) und ist per find_package(pda) benutzbar. pda: Shell ohne Ein-/Ausgabe (execute() gibt Text zurueck, deshalb ohne Terminal testbar), REPL und Stapelbetrieb in main.cpp. examples/consumer und tools/check-install.sh pruefen die Export-Kette Ende-zu-Ende: installieren, dann ein fremdes Projekt dagegen bauen. docs/CMAKE.md erklaert das Target-Modell, PUBLIC/PRIVATE/INTERFACE, Generator-Ausdruecke, install/export und Symbolsichtbarkeit an diesem Projekt. 71 Tests gruen unter Homebrew-clang 22, Apple clang und GCC 16, statisch und shared, mit und ohne ASan/UBSan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
277 lines
12 KiB
CMake
277 lines
12 KiB
CMake
# ---------------------------------------------------------------------------
|
|
# 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
|
|
# <repo>/libpda, damit <libpda/contact.hpp> auf
|
|
# <repo>/libpda/libpda/contact.hpp zeigt.
|
|
#
|
|
# INSTALL_INTERFACE nachdem installiert wurde. Der Pfad ist relativ zum
|
|
# Installationspraefix, also <prefix>/include, und
|
|
# <libpda/contact.hpp> zeigt auf
|
|
# <prefix>/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
|
|
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
|
|
"$<INSTALL_INTERFACE:include>"
|
|
)
|
|
|
|
# Auch das generierte Export-Header-Verzeichnis muss in beiden Welten stimmen.
|
|
target_include_directories(pda PUBLIC
|
|
"$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/generated>"
|
|
"$<INSTALL_INTERFACE:include>"
|
|
)
|
|
|
|
# Die Warnungen gelten beim Bauen DIESER Bibliothek. Wer sie linkt, soll
|
|
# unsere Warnungsliste nicht erben -- fremde Projekte haben ihre eigene.
|
|
#
|
|
# Warum $<BUILD_INTERFACE:...> 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 $<LINK_ONLY:...>
|
|
# 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 "$<BUILD_INTERFACE:pda_warnings>")
|
|
|
|
# Der C++-Standard als PUBLIC-Anforderung: wer libpda benutzt, braucht
|
|
# zwingend C++23, weil contact.hpp <expected> 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 <libpda/pda_export.h> 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: <modul>.test.c -> Executable <modul>.test -> CTest
|
|
# "unity.<modul>". 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")
|