Skip to content

fix(build): libe3.pc export and relocatability - #75

Merged
Thecave3 merged 2 commits into
mainfrom
71-libe3pc-exports-latrec_default_dir-as-a-quoted-macro-which-pkg-config-strips-breaking-the-build
Aug 28, 2026
Merged

fix(build): libe3.pc export and relocatability#75
Thecave3 merged 2 commits into
mainfrom
71-libe3pc-exports-latrec_default_dir-as-a-quoted-macro-which-pkg-config-strips-breaking-the-build

Conversation

@Thecave3

@Thecave3 Thecave3 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Two libe3.pc generation bugs. The first is #71; the second was found while verifying the fix for it, and has the same shape — a .pc confidently describing a location that holds no libe3.

VERSION 0.1.2 → 0.1.3, folded into the second commit.


1. LATREC_DEFAULT_DIR was exported as a quoted macro — closes #71

pkg-config's Cflags tokenizer strips unescaped quotes, so a consumer received the bare path and the preprocessor could not lex it:

error: expected expression before ‘/’ token

Only present when configured with LIBE3_ENABLE_LATREC=ON and LIBE3_BUILD_TESTS=ON — the latter being that 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 fix exists. The issue offered either fixing the quoting or dropping the path; the quoting route was tested and cannot work:

form in libe3.pc pkg-config --cflags emits verdict
-DD="/a/b" -DD=/a/b quotes stripped → fails to lex
-DD='/a/b' -DD=/a/b same
-DD=\"/a/b\" -DD=\"/a/b\" survives pkg-config…

…but that last form then reaches a plain cc $(pkg-config --cflags libe3) as an unterminated literal — missing terminating " character. No single form is safe for both consumption styles, so a string-valued macro should not be exported at all.

PRIVATE loses nothing. latrec_open_in() applies the fallback from src/core/latrec.c, libe3's own translation unit, so every caller of latrec_tls_open_as() inherits the compiled-in default without seeing the macro. Nothing in tests/, examples/ or benchmarks/ references it; the one example that opens a ring (examples/sm_simple/simple_service_model.hpp:209) goes through latrec_tls_open_as(); and latrec.h:461 already has an #ifndef fallback to /tmp/latrec for anyone calling the inline path directly. The build-tree default therefore keeps working for libe3's own tests and examples, which is all it was ever for. Exporting it was wrong on a second count regardless: it is a build-tree path, meaningless to a consumer once installed.

LIBE3_ENABLE_LATREC stays PUBLIC — 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.

The .pc generator already failed loudly on a generator expression; it now does the same for a quoted value, so a future PUBLIC string macro cannot reintroduce this silently.

2. libe3.pc baked the configure-time prefix

libe3.pc.in wrote prefix=@CMAKE_INSTALL_PREFIX@, so the installed file described where the build expected to go rather than where it ended up. cmake --install --prefix <other> moves every file but cannot rewrite the .pc, and neither can a DESTDIR-staged packaging step. The consumer gets -I, -L and smdir under the original prefix — and pkg-config reports success while doing it, so the failure surfaces later as a missing header from a directory nobody asked for.

Reproduced on this tree: installing 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.

The prefix is now derived from ${pcfiledir}, which pkg-config expands to the directory holding the .pc. 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 have nothing relative to derive and 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.

Verification

Built with LIBE3_ENABLE_LATREC=ON and LIBE3_BUILD_TESTS=ON — the configuration that used to break:

  • Exported Cflags: -I${includedir} -DLIBE3_ENABLE_JSON -DLIBE3_ENABLE_ASN1 -DLIBE3_HAS_ZMQ=1 -DLIBE3_ENABLE_LATREC — no quotes left to strip.
  • build/CMakeFiles/libe3.dir/flags.make still carries the private LATREC_DEFAULT_DIR, so the internal default is intact.
  • Installed to a non-default prefix and consumed from there: --cflags, --libs and --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.
  • ctest: 23/23 passed. scripts/check_commit_trailers.py: passes for both commits.

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)
… 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 <other>` 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)
@Thecave3
Thecave3 force-pushed the 71-libe3pc-exports-latrec_default_dir-as-a-quoted-macro-which-pkg-config-strips-breaking-the-build branch from 3dc1e4c to dad8089 Compare August 28, 2026 15:41
@Thecave3 Thecave3 changed the title fix(build): stop exporting LATREC_DEFAULT_DIR through libe3.pc fix(build): libe3.pc export and relocatability Aug 28, 2026
@Thecave3
Thecave3 marked this pull request as ready for review August 28, 2026 15:44
@github-actions

Copy link
Copy Markdown
Contributor

CI report — dad8089 — ✅ all checks passed

Workflow Result Time Run
Commit policy ✅ success 1m48s #100
E2E dApp Integration ✅ success 2m14s #112
E2E Topologies (multi-dApp / multi-RAN) ✅ success 2m43s #110
Full-loop Latency Benchmark ✅ success 1m16s #111
Unit Tests ✅ success 4m38s #138
latrec portability ⏭️ not triggered (paths filter)
MPMC Queue Benchmark ⏭️ not triggered (paths filter)
E2E Topologies (multi-dApp / multi-RAN)

zmq/ipc

  • 1 RAN - 1 dApp: indications=5
    • dapp peer=t11 ran=ran-solo sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
  • 1 RAN - 2 dApps: dApp#1 ind=5 sub=1, dApp#2 ind=6 sub=2, RAN saw 2 dApps
    • dapp1 peer=t12 ran=ran-shared sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=1 max=1 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
    • dapp2 peer=t12 ran=ran-shared sub=2 indications=6 seq=[0..5] dropped=0 (0%) age_ms(avg=0.833333 max=1 @seq=0) hist[<=1:6 2-5:0 6-10:0 >10:0]
  • 2 RANs - 1 dApp: from ran-a ind=5, from ran-b ind=5
    • dapp peer=t2a ran=ran-a sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0.2 max=1 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
    • dapp peer=t2b ran=ran-b sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]

zmq/tcp

  • 1 RAN - 1 dApp: indications=5
    • dapp peer=default ran=ran-solo sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
  • 1 RAN - 2 dApps: dApp#1 ind=5 sub=1, dApp#2 ind=6 sub=2, RAN saw 2 dApps
    • dapp1 peer=default ran=ran-shared sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=1 max=1 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
    • dapp2 peer=default ran=ran-shared sub=2 indications=6 seq=[0..5] dropped=0 (0%) age_ms(avg=1 max=1 @seq=0) hist[<=1:6 2-5:0 6-10:0 >10:0]
  • 2 RANs - 1 dApp: from ran-a ind=5, from ran-b ind=5
    • dapp peer=default ran=ran-a sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=1 max=1 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
    • dapp peer=off100 ran=ran-b sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=14.8 max=73 @seq=4) hist[<=1:4 2-5:0 6-10:0 >10:1] [note: indication queueing >1ms]
    • ⚠️ dapp indication queueing exceeded 1ms (report-only)
E2E dApp Integration

✅ posix/ipc

  • dApp exit: 0
  • Indications received: 7

✅ posix/tcp

  • dApp exit: 0
  • Indications received: 7

✅ zmq/ipc

  • dApp exit: 0
  • Indications received: 7

✅ zmq/tcp

  • dApp exit: 0
  • Indications received: 7
Full-loop Latency Benchmark

Full-loop latency

Full-loop latency benchmark (N=1102 after 50 warmup)

All values in microseconds (μs). Link: zmq, transport: ipc, encoding: ASN.1 APER.

# Description Tags mean p50 p99 max
1 Collect indication data RECORD_BEGIN to ENCODE_E3SM_BEGIN 0.12 0.10 0.35 0.60
2 Create & encode indication ENCODE_E3SM_BEGIN to ENCODE_E3SM_DONE 1.10 1.07 3.36 3.80
3 Encode E3AP (indication) EMIT_ENTER to ENQUEUE, then DEQUEUE to ENCODE_E3AP_DONE 2.61 2.90 3.80 7.50
4 Queuing (indication) ENQUEUE to DEQUEUE 5.32 5.51 15.90 83.69
5 Delivery (indication) ENCODE_E3AP_DONE to SEND_DONE 0.28 0.25 0.73 6.29
6 E3 wire (RAN -> dApp) SEND_DONE to RECV 53.68 54.39 67.03 452.77
7 Decode E3AP (indication) RECV to DECODE_E3AP_DONE 1.74 1.48 2.60 23.13
8 libe3 dispatch (indication) DECODE_E3AP_DONE to DELIVER_BEGIN 0.10 0.10 0.15 0.42
9 Decode indication DELIVER_BEGIN to DECODE_E3SM_DONE 0.64 0.57 0.91 1.34
10 Process data DECODE_E3SM_DONE to ENCODE_E3SM_BEGIN 0.04 0.04 0.05 0.15
11 Create & encode control ENCODE_E3SM_BEGIN to ENCODE_E3SM_DONE 0.36 0.35 0.55 0.75
12 Encode E3AP (control) EMIT_ENTER to ENQUEUE, then DEQUEUE to ENCODE_E3AP_DONE 3.84 3.83 5.34 6.78
13 Queuing (control) ENQUEUE to DEQUEUE 16.76 16.34 21.65 51.72
14 Delivery (control) ENCODE_E3AP_DONE to SEND_DONE 5.53 5.26 8.48 20.03
15 E3 wire (dApp -> RAN) SEND_DONE to RECV 54.31 54.59 63.37 650.26
16 Decode E3AP (control) RECV to DECODE_E3AP_DONE 2.93 2.85 6.25 15.65
17 libe3 dispatch (control) DECODE_E3AP_DONE to DECODE_E3SM_BEGIN 0.29 0.28 0.51 0.90
18 Decode & handle control DECODE_E3SM_BEGIN to DECODE_E3SM_DONE 0.45 0.41 0.70 0.95
Total Total round-trip 150.68 150.26 178.75 746.76

ubuntu-latest, Release build, ZMQ + IPC, ASN.1 APER.

These numbers are measured inside a GitHub Actions container and should be treated as an upper bound on E3AP's and the library's own latency, not a representative deployment measurement.

Ready to merge (fast-forward only)

A maintainer can land the reviewed commits with:

git fetch origin
git checkout main && git merge --ff-only dad8089f2fe1ca6f895b80efff42e10fb10bfa89 && git push origin main

Head: dad8089f2fe1ca6f895b80efff42e10fb10bfa89 (branch 71-libe3pc-exports-latrec_default_dir-as-a-quoted-macro-which-pkg-config-strips-breaking-the-build). If --ff-only fails as non-fast-forward, the branch must be rebased on the latest main.

One comment per PR, rewritten in place once every workflow for dad8089 finished.

@Thecave3
Thecave3 merged commit dad8089 into main Aug 28, 2026
24 checks passed
@Thecave3
Thecave3 deleted the 71-libe3pc-exports-latrec_default_dir-as-a-quoted-macro-which-pkg-config-strips-breaking-the-build branch August 28, 2026 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

libe3.pc exports LATREC_DEFAULT_DIR as a quoted macro, which pkg-config strips, breaking the build

1 participant