From b145f796dca0bd0ef9c83546b053d4a5f176eacc Mon Sep 17 00:00:00 2001 From: Andrea Lacava Date: Fri, 28 Aug 2026 11:37:28 -0400 Subject: [PATCH 1/2] fix(build): stop exporting LATREC_DEFAULT_DIR through libe3.pc libe3.pc carried LATREC_DEFAULT_DIR as a quoted string macro, and pkg-config's Cflags tokenizer strips unescaped quotes. A consumer therefore received the bare path and the preprocessor could not lex it: error: expected expression before '/' token The macro only appears when the library is configured with LIBE3_ENABLE_LATREC=ON and LIBE3_BUILD_TESTS=ON -- the latter being the option's own default -- so anyone who did not think to pass -DLIBE3_BUILD_TESTS=OFF got a libe3.pc that broke their build. No quoting fixes this. Escaping as \" does survive pkg-config and works for CMake consumers reading *_CFLAGS_OTHER, but the same string then reaches a plain `cc $(pkg-config --cflags libe3)` as an unterminated literal ("missing terminating \" character"). There is no form that is safe for both consumption styles, so a string-valued macro must not be exported at all. It never needed to be. latrec_open_in() applies the fallback from src/core/latrec.c, which is libe3's own translation unit, so every caller of latrec_tls_open_as() inherits the compiled-in default without seeing the macro; nothing outside the library references it, and latrec.h already has an #ifndef fallback to /tmp/latrec for anyone who does. Making it PRIVATE keeps the build-tree default working for libe3's own tests and examples -- which is all it was ever for -- while removing it from the exported interface. Exporting it was wrong on a second count anyway: it is a build-tree path, meaningless to a consumer once the library is installed. LIBE3_ENABLE_LATREC itself stays PUBLIC. It is a bare flag with no quotes to mangle, and a consumer must see it or its own latrec_* calls resolve to the no-op stubs while linking a library that has the ring registry compiled in. Also guards the .pc generator against the whole class: it already fails loudly on a generator expression, and now does the same for a quoted value, so a future PUBLIC string macro cannot reintroduce this silently. Verified with LIBE3_ENABLE_LATREC=ON and LIBE3_BUILD_TESTS=ON, the configuration that used to break: the generated Cflags is `-I${includedir} -DLIBE3_ENABLE_JSON -DLIBE3_ENABLE_ASN1 -DLIBE3_HAS_ZMQ=1 -DLIBE3_ENABLE_LATREC` with no quotes left to strip, libe3's own flags.make still carries the private LATREC_DEFAULT_DIR, and ctest is 23/23. Closes #71 Assisted-by: Claude Opus 5 (1M context) --- cmake/libe3Install.cmake | 12 ++++++++++++ cmake/libe3Targets.cmake | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/cmake/libe3Install.cmake b/cmake/libe3Install.cmake index 4982b1f..afbc873 100644 --- a/cmake/libe3Install.cmake +++ b/cmake/libe3Install.cmake @@ -24,6 +24,18 @@ if(LIBE3_PUBLIC_DEFS) "libe3: PUBLIC compile definition '${_def}' is a generator " "expression and cannot be exported through libe3.pc") endif() + if(_def MATCHES "\"") + # pkg-config's Cflags tokenizer strips unescaped quotes, so a string + # macro exported this way reaches the compiler as a bare token and + # fails to lex. Escaping as \\" survives pkg-config and works for + # CMake consumers, but then breaks `cc $(pkg-config --cflags libe3)` + # with an unterminated string -- no single form is safe for both + # consumption styles. Keep string-valued macros PRIVATE and give the + # header an #ifndef fallback instead (see LATREC_DEFAULT_DIR). + message(FATAL_ERROR + "libe3: PUBLIC compile definition '${_def}' has a quoted value " + "and cannot be exported through libe3.pc -- make it PRIVATE") + endif() string(APPEND LIBE3_PC_CFLAGS " -D${_def}") endforeach() endif() diff --git a/cmake/libe3Targets.cmake b/cmake/libe3Targets.cmake index 65df668..fc76750 100644 --- a/cmake/libe3Targets.cmake +++ b/cmake/libe3Targets.cmake @@ -82,8 +82,17 @@ if(LIBE3_ENABLE_LATREC) # registry compiled in, or vice versa. target_compile_definitions(libe3 PUBLIC LIBE3_ENABLE_LATREC) if(LATREC_DEFAULT_DIR) + # PRIVATE, unlike the flag above. latrec_open_in() applies this fallback + # from src/core/latrec.c -- libe3's own translation unit -- so every + # caller of latrec_tls_open_as() inherits it without needing the macro + # itself. Exporting it would put a quoted string macro in libe3.pc, + # where pkg-config's tokenizer strips the quotes and the bare path then + # fails to lex ("expected expression before '/'"). Escaping as \\" keeps + # CMake consumers working but breaks `cc $(pkg-config --cflags libe3)` + # with an unterminated string, so there is no form that is safe for + # both. It is also a build-tree path, meaningless once installed. target_compile_definitions(libe3 - PUBLIC LATREC_DEFAULT_DIR=\"${LATREC_DEFAULT_DIR}\") + PRIVATE LATREC_DEFAULT_DIR=\"${LATREC_DEFAULT_DIR}\") endif() endif() @@ -152,8 +161,9 @@ endif() if(LIBE3_ENABLE_LATREC) target_compile_definitions(libe3_shared PUBLIC LIBE3_ENABLE_LATREC) if(LATREC_DEFAULT_DIR) + # PRIVATE for the same reason as the static target above. target_compile_definitions(libe3_shared - PUBLIC LATREC_DEFAULT_DIR=\"${LATREC_DEFAULT_DIR}\") + PRIVATE LATREC_DEFAULT_DIR=\"${LATREC_DEFAULT_DIR}\") endif() endif() From dad8089f2fe1ca6f895b80efff42e10fb10bfa89 Mon Sep 17 00:00:00 2001 From: Andrea Lacava Date: Fri, 28 Aug 2026 11:41:46 -0400 Subject: [PATCH 2/2] fix(build): make libe3.pc relocatable instead of baking the configure prefix libe3.pc.in wrote `prefix=@CMAKE_INSTALL_PREFIX@`, a configure-time value, so the installed file described where the build expected to go rather than where it ended up. `cmake --install --prefix ` moves every file but cannot rewrite the .pc, and neither can a DESTDIR-staged packaging step: the result hands a consumer -I, -L and `smdir` under the original prefix, which may hold no libe3 at all. pkg-config reports success while doing it, so the failure surfaces later as a missing header from a directory nobody asked for. That is not hypothetical. Installing this tree to a scratch prefix produced a .pc still claiming prefix=/usr/local, with `pkg-config --variable=smdir libe3` naming a service-model directory that did not exist, while the real one sat under the install prefix. pkg-config expands ${pcfiledir} to the directory holding the .pc, so the prefix is derived from that and the file describes wherever it actually is. The CMake package config has always been relocatable through @PACKAGE_INIT@; this only makes the pkg-config path agree with it. GNUInstallDirs permits any component directory to be absolute. Those are not under the prefix and have nothing relative to derive, so they keep their absolute value, and an absolute libdir also leaves the prefix with no relative form -- that is where the .pc lands -- so it falls back to the absolute prefix. The component loop also reads CMAKE_INSTALL_DATAROOTDIR rather than CMAKE_INSTALL_DATADIR, whose cache entry is empty until GNUInstallDirs derives it, which previously emitted `datarootdir=${prefix}/share` only by way of the derived variable already being set. Verified by installing to a non-default prefix and consuming from there: --cflags/--libs/--variable=smdir all resolve under that prefix, a program compiles and links through the shell-split `cc $(pkg-config ...)` path, the reported smdir really contains sm_simple, a DESTDIR-staged install is relocatable too, and ctest is 23/23. Bumps VERSION to 0.1.3, covering this and the LATREC_DEFAULT_DIR export fix in the preceding commit. Assisted-by: Claude Opus 5 (1M context) --- VERSION | 2 +- cmake/libe3.pc.in | 8 ++++---- cmake/libe3Install.cmake | 44 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/VERSION b/VERSION index 8294c18..b1e80bb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.2 \ No newline at end of file +0.1.3 diff --git a/cmake/libe3.pc.in b/cmake/libe3.pc.in index cd36b94..b37b934 100644 --- a/cmake/libe3.pc.in +++ b/cmake/libe3.pc.in @@ -1,8 +1,8 @@ -prefix=@CMAKE_INSTALL_PREFIX@ +prefix=@LIBE3_PC_PREFIX@ exec_prefix=${prefix} -libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@ -includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ -datarootdir=${prefix}/@CMAKE_INSTALL_DATADIR@ +libdir=@LIBE3_PC_LIBDIR@ +includedir=@LIBE3_PC_INCLUDEDIR@ +datarootdir=@LIBE3_PC_DATAROOTDIR@ # Root of the installed service-model definitions; each SM lives in its own # subdir, e.g. ${smdir}/sm_simple/e3sm_simple.asn (ASN.1) and .../e3sm_simple.proto # (Protobuf) — both are installed. Query with: pkg-config --variable=smdir libe3 diff --git a/cmake/libe3Install.cmake b/cmake/libe3Install.cmake index afbc873..9b033df 100644 --- a/cmake/libe3Install.cmake +++ b/cmake/libe3Install.cmake @@ -40,6 +40,50 @@ if(LIBE3_PUBLIC_DEFS) endforeach() endif() +# Make libe3.pc relocatable. CMAKE_INSTALL_PREFIX is a configure-time value, so +# baking it in makes the file describe where the build *expected* to be installed +# rather than where it ended up: `cmake --install --prefix `, and any +# packaging step that stages into a different root, moves every file but cannot +# rewrite the .pc. The result points a consumer's -I/-L and `smdir` at a prefix +# that may contain no libe3 at all -- and pkg-config reports success while doing +# it, so the failure surfaces later as a missing header. pkg-config expands +# ${pcfiledir} to the directory holding the .pc, so deriving the prefix from that +# describes wherever the file actually is. The CMake package config is already +# relocatable through @PACKAGE_INIT@; this makes the pkg-config path agree. +# +# GNUInstallDirs allows any component directory to be absolute, in which case it +# is not under the prefix and there is nothing relative to derive -- those keep +# their absolute value, and an absolute libdir also leaves the prefix itself with +# no relative form, since that is where the .pc lands. +if(IS_ABSOLUTE "${CMAKE_INSTALL_LIBDIR}") + set(LIBE3_PC_PREFIX "${CMAKE_INSTALL_PREFIX}") +else() + file(RELATIVE_PATH _libe3_pc_to_prefix + "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/pkgconfig" + "${CMAKE_INSTALL_PREFIX}") + string(REGEX REPLACE "/+$" "" _libe3_pc_to_prefix "${_libe3_pc_to_prefix}") + if(_libe3_pc_to_prefix STREQUAL "") + set(LIBE3_PC_PREFIX "\${pcfiledir}") + else() + set(LIBE3_PC_PREFIX "\${pcfiledir}/${_libe3_pc_to_prefix}") + endif() +endif() + +# CMAKE_INSTALL_DATADIR is empty in the cache until GNUInstallDirs derives it +# from DATAROOTDIR, so read the derived value rather than the cache entry. +foreach(_pair "LIBDIR:${CMAKE_INSTALL_LIBDIR}" + "INCLUDEDIR:${CMAKE_INSTALL_INCLUDEDIR}" + "DATAROOTDIR:${CMAKE_INSTALL_DATAROOTDIR}") + string(REPLACE ":" ";" _pair "${_pair}") + list(GET _pair 0 _name) + list(GET _pair 1 _dir) + if(IS_ABSOLUTE "${_dir}") + set(LIBE3_PC_${_name} "${_dir}") + else() + set(LIBE3_PC_${_name} "\${prefix}/${_dir}") + endif() +endforeach() + configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake/libe3.pc.in" "${CMAKE_CURRENT_BINARY_DIR}/libe3.pc"