Files
paulhorn db41258d16 feat: PDA als libpda + pda nach P1204R0
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>
2026-08-30 16:39:35 +02:00

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")