feat(shared-runtime): import the runtime instead of providing it (#182)

* feat(shared-runtime): import the runtime instead of providing it

PR-4 of the shared-runtime migration. liblogos_core stops being the single
provider of types it does not own, and becomes a consumer of the libraries that
do: logos-protocol#65 and logos-plugin-qt#22.

WHAT GOES AWAY. The whole if(WIN32) block that absorbed liblogos_protocol.a and
liblogos_qt_host.a with --whole-archive and re-published their symbols through a
generated .def, plus cmake/gen-shared-exports.sh itself. That scheme existed
because PE exports nothing it is not told to export and the definitions lived in
archives that every other image also linked; now they live in shared libraries
that export their own tables, so there is nothing for this repo to re-publish.

WHAT REPLACES IT is one line: logos_sdk carries logos_qt_host_shared rather than
the static archive. logos_qt_host_shared PUBLIC-links logos_protocol_shared, so
the protocol half arrives transitively and correctly layered.

THE INVARIANT IS UNCHANGED. The runtime must still exist exactly once per
process; it is now enforced by there being one shared library per type rather
than by one image absorbing everything. Measured, aarch64-darwin:

    liblogos_core.dylib       defines 0   (was 32 TokenManager symbols)
                              imports 8
    liblogos_protocol.dylib   defines 78  (TokenManager, LogosAPIClient, ...)
    liblogos_qt_host.dylib    defines 23  (LogosAPI)

OUT-OF-PROCESS CONSUMERS ARE DELIBERATELY UNAFFECTED. Module plugins and ui_qml
backends keep linking the STATIC archive: each runs in its own process where its
own copy is the CORRECT per-process singleton, and a .lgx records an empty nix
closure so it could not carry a shared library anyway.

TWO DEPLOYMENT FAILURES THIS ALSO FIXES, both of which built green.

nix/lib.nix now STAGES liblogos_protocol and liblogos_qt_host beside
liblogos_core, and asserts it did. liblogos_core records
@rpath/liblogos_qt_host.dylib with @loader_path as its only rpath, so the loader
looks for them in that directory and nowhere else. Before this:

    nix build .#default        OK
    logos_host --help          exit 0
    dlopen liblogos_core.dylib Library not loaded: @rpath/liblogos_qt_host.dylib

A help-text smoke test never touches the library, so nothing in the build or in
a boot check would have caught it. Hence the assertion rather than trust in the
copy loop.

CMakeLists.txt adds both to CMAKE_BUILD_RPATH, mirroring what
LOGOS_PACKAGE_MANAGER_ROOT already does. Without it logos_core_tests aborted at
dyld time, before main(), while .#default had already succeeded.

VERIFIED, aarch64-darwin: .#default OK, dlopen OK, checks.tests PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(shared-runtime): install RPATH too, or the installed test binary cannot load

BUILD_RPATH covers binaries run from the build tree; the test derivation runs the
INSTALLED one, which uses INSTALL_RPATH. Only Linux said so -- on macOS the
installed test binary resolved the libraries anyway and checks.tests passed,
while the same commit on Linux died before main() with

    error while loading shared libraries: liblogos_qt_host.so

A macOS-green run is not evidence for this class of failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(tests): put the shared runtime on the hand-set Linux RPATH

nix/tests.nix applies its RPATH with `patchelf --set-rpath`, which REPLACES
whatever CMake wrote. So CMAKE_BUILD_RPATH and CMAKE_INSTALL_RPATH have no
effect on the installed test binaries on Linux, and that list is the entire
search path: anything absent from it is absent at runtime.

Measured: adding both libraries to CMAKE_*_RPATH changed nothing and the suite
still died before main() with

    error while loading shared libraries: liblogos_qt_host.so

while the same commit passed on macOS, which does not go through this code path
at all. Two platforms, two independent rpath mechanisms, and only one of them
was wired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dario Lipicar
2026-08-21 09:51:26 -03:00
committed by GitHub
co-authored by Claude Opus 5
parent 3893c833ec
commit b2a9a0ba9d
6 changed files with 130 additions and 226 deletions
+33
View File
@@ -31,6 +31,39 @@ if(DEFINED LOGOS_PACKAGE_MANAGER_ROOT)
list(APPEND CMAKE_BUILD_RPATH "${LOGOS_PACKAGE_MANAGER_ROOT}/lib")
endif()
# The shared C++ runtime, which logos_core now IMPORTS rather than absorbing.
# Both libraries carry an @rpath install name, so a consumer that does not know
# where they live cannot load at all -- and the failure is at dyld time, before
# main(), with no hint from the build.
#
# Measured before this existed: logos_core_tests aborted with
# dyld: Library not loaded: @rpath/liblogos_qt_host.dylib
# while `nix build .#default` had already succeeded. The library builds and
# installs whether or not anything can load it.
#
# The INSTALLED liblogos_core is covered separately by INSTALL_RPATH $ORIGIN in
# src/CMakeLists.txt plus staging both libraries beside it (nix/lib.nix); this
# entry is what makes the BUILD tree -- tests especially -- resolve them.
# BUILD_RPATH covers binaries run from the build tree; INSTALL_RPATH covers the
# ones that get installed and run from $out -- which is what the test derivation
# actually executes. Both are needed, and only Linux says so: on macOS the
# installed test binary resolved the libraries anyway and the suite passed, while
# the same commit on Linux died with
# error while loading shared libraries: liblogos_qt_host.so
# A macOS-green test run is not evidence here.
#
# logos_core itself is unaffected by the INSTALL_RPATH lines: it sets its own
# INSTALL_RPATH ($ORIGIN / @loader_path in src/CMakeLists.txt), which overrides
# the directory-level default, and nix/lib.nix stages both libraries beside it.
if(DEFINED LOGOS_PROTOCOL_ROOT)
list(APPEND CMAKE_BUILD_RPATH "${LOGOS_PROTOCOL_ROOT}/lib")
list(APPEND CMAKE_INSTALL_RPATH "${LOGOS_PROTOCOL_ROOT}/lib")
endif()
if(DEFINED LOGOS_QT_HOST_ROOT)
list(APPEND CMAKE_BUILD_RPATH "${LOGOS_QT_HOST_ROOT}/lib")
list(APPEND CMAKE_INSTALL_RPATH "${LOGOS_QT_HOST_ROOT}/lib")
endif()
# Build src first to ensure logos_core is built before modules
add_subdirectory(src)
-123
View File
@@ -1,123 +0,0 @@
#!/bin/sh
# Generate the PE module-definition file that makes liblogos_core.dll the single
# provider of the shared C++ runtime (TokenManager, LogosAPI, LogosAPIClient and
# the LogosResult stream operators).
#
# WHY a generated .def rather than __declspec(dllexport) in the headers: the
# definitions live in liblogos_protocol.a / liblogos_qt_host.a (the Qt half was
# liblogos_qt_sdk.a until the host runtime moved into logos-qt-host; same
# objects, new home), which are also
# linked by logos_host.exe, ui-host.exe, every module plugin and every native
# platform. Annotating them for export would mean a second, Windows-only,
# export-annotated build of both archives, kept in sync forever, to solve a
# Windows-only problem. Exporting at liblogos_core's link instead leaves those
# archives compiled byte-identically to before; the consumer side is a pure
# opt-in (-DLOGOS_SHARED_USE_DLL, see logos-protocol/cpp/logos_shared_api.h).
#
# WHY the whole archive and not a curated class list: ld chooses archive members
# by object file, for reasons that have nothing to do with our symbols. Measured
# on this build, main_ui referenced std::string's move constructor and ld
# satisfied it out of the Qt archive's logos_api.cpp.obj — which then dragged
# LogosAPI, LogosAPIClient and TokenManager in behind it. The consumers
# therefore link an EMPTY archive (logos-basecamp/cmake/LogosSharedFromDll.cmake)
# and take everything from the DLL, which only works if the DLL really does
# provide everything. Hence --whole-archive at the link and "*" here: a partial
# export set turns into an undefined reference in a downstream repo, far from
# the cause.
#
# WHY COMDAT symbols are filtered out: they come from inline functions and
# templates in headers, so every consumer TU emits its own copy regardless of
# what the DLL exports. Exporting them turns the import library into a STRONG
# definition that then collides with the consumer's own copy. They are also
# precisely the symbols an export cannot deduplicate — a function-local static
# inside an inline function stays per-image on PE no matter what. Do not
# "simplify" this filter away.
#
# Usage: gen-shared-exports.sh <nm> <out.def> <archive> <obj,obj,...|*> [...]
set -eu
NM="$1"; shift
OUT="$1"; shift
TMP="${OUT}.tmp"
: > "$TMP"
while [ "$#" -gt 0 ]; do
archive="$1"; members="$2"; shift 2
[ -f "$archive" ] || { echo "gen-shared-exports: missing archive $archive" >&2; exit 1; }
"$NM" -A --defined-only "$archive" | awk -v members="$members" -v arch="$archive" '
BEGIN {
if (members == "*") { all = 1 }
else { n = split(members, a, ","); for (i = 1; i <= n; i++) want[a[i]] = 1 }
}
{
# nm -A on an archive prints "<archive>:<member>:<addr> <type> <name>".
split($1, p, ":")
mem = p[2]; typ = $2; name = $3
# COMDAT sections are named ".text$<mangled>" / ".rdata$<mangled>" /
# etc. Record the mangled tail so the symbol itself can be dropped.
if (name ~ /^\.[a-z]+\$/) {
tail = name; sub(/^\.[a-z]+\$/, "", tail)
comdat[mem SUBSEP tail] = 1
next
}
if (!all && !(mem in want)) next
# T=text D=data R=rodata B=bss, all uppercase == external linkage.
# Weak (V/W) is COMDAT by another name; lowercase is file-local and
# cannot be exported at all (that includes the function-local statics
# themselves — we export the ACCESSOR so callers reach ours).
if (typ != "T" && typ != "D" && typ != "R" && typ != "B") next
# Itanium-mangled C++ (^_Z), PLUS the logos-protocol C ABI (^lp_).
#
# The ^_Z test alone keeps toolchain bookkeeping such as
# qt_version_tag_6_11_used — which every image legitimately defines —
# out of the export table. But `lp_*` is `extern "C"`, so it is
# UNMANGLED and the same test silently dropped the entire C ABI.
#
# That was invisible until B5: before it, only C++ callers reached the
# shared runtime. B5 re-emits every Qt-typed dependency wrapper as a
# VENEER over the lp path, so a consumer that compiles such a wrapper —
# logos-basecamp compiles package_manager_api.cpp into its own exe —
# now calls lp_invoke / lp_client_create / lp_token_save directly. With
# them absent from the .def the exe cannot import them, and the link
# fails with plain `undefined reference to 'lp_invoke'`.
#
# Exporting is the correct fix rather than letting the consumer link
# liblogos_protocol.a itself: lp_token_save and friends operate on the
# TokenManager singleton, so a static copy in the exe would reinstate
# exactly the split-brain token store this whole .def scheme exists to
# prevent. Module plugins are separate processes and keep their own
# per-image copy by design (see logos_module_grant_host_services).
#
# The prefix is deliberately tight: `lp_` only, not "anything unmangled".
if (name !~ /^_Z/ && name !~ /^lp_/) next
seenmem[mem] = 1
k++; recmem[k] = mem; rectyp[k] = typ; recnam[k] = name
}
END {
if (!all) for (m in want) if (!(m in seenmem)) {
printf("gen-shared-exports: %s has no member %s\n", arch, m) > "/dev/stderr"
bad = 1
}
if (bad) exit 1
for (i = 1; i <= k; i++) {
if ((recmem[i] SUBSEP recnam[i]) in comdat) continue
print recnam[i] (rectyp[i] == "T" ? "" : " DATA")
}
}' >> "$TMP"
done
# DATA matters: a data export reached without the DATA keyword hands the
# consumer the CONTENTS of the slot instead of its address, which corrupts at
# runtime rather than at link. The keyword is derived from nm's type letter
# above, never written by hand.
{
echo "EXPORTS"
sort -u "$TMP"
} > "$OUT"
rm -f "$TMP"
count=$(grep -c . "$OUT" || true)
echo "gen-shared-exports: wrote $OUT ($((count - 1)) symbols)"
Generated
+6 -6
View File
@@ -2239,11 +2239,11 @@
]
},
"locked": {
"lastModified": 1787165449,
"narHash": "sha256-X/TaIJA+5o1VLfizu3NLPp/fhisIph8sXG0h0yHDlpo=",
"lastModified": 1787275742,
"narHash": "sha256-/5FwGSIV80CNdc52qYIb1lx1VXWaOZcnnU8zAbKlfSw=",
"owner": "logos-co",
"repo": "logos-plugin-qt",
"rev": "9b2c64e5a480245b5333e20183a4a3c572d543cc",
"rev": "1aa3e31c029062a8288e5ef4da1f25ca14a8a0dd",
"type": "github"
},
"original": {
@@ -2378,11 +2378,11 @@
]
},
"locked": {
"lastModified": 1787107309,
"narHash": "sha256-oNfr0T6OrD1D56rf1brXH+9hJoJvTaPpvUdy/d62SPs=",
"lastModified": 1787275122,
"narHash": "sha256-VfnbFZO7cF+pTuFlw+VIagB1JBbXYH+VjgDVF0f2UG4=",
"owner": "logos-co",
"repo": "logos-protocol",
"rev": "f4407ff4854bdaf486182547af5b55f4a0f55229",
"rev": "2e3344acccfba14cf2bd51019c62b7290dad10ca",
"type": "github"
},
"original": {
+42
View File
@@ -4,6 +4,9 @@
let
# Extract the package-manager root from CMake flags
logosPackageManagerRoot = common.env.LOGOS_PACKAGE_MANAGER_ROOT;
# The shared runtime liblogos_core now IMPORTS rather than defines.
logosProtocolRoot = common.env.LOGOS_PROTOCOL_ROOT;
logosQtHostRoot = common.env.LOGOS_QT_HOST_ROOT;
in
pkgs.runCommand "${common.pname}-lib-${common.version}"
{
@@ -46,6 +49,45 @@ pkgs.runCommand "${common.pname}-lib-${common.version}"
fi
done
# The shared C++ runtime, which liblogos_core now IMPORTS instead of
# absorbing. This is NOT optional packaging: liblogos_core records
# @rpath/liblogos_qt_host.dylib and @rpath/liblogos_protocol.dylib, and its
# only LC_RPATH is @loader_path -- so the loader looks for them BESIDE
# itself, i.e. in this directory, and nowhere else.
#
# Measured before this existed: the build succeeded, `logos_host --help`
# exited 0, and dlopen of the library failed outright with
# Library not loaded: @rpath/liblogos_qt_host.dylib
# A help-text smoke test does not touch the library, so nothing in the build
# or in a boot check would have caught it.
#
# Explicit `for` + `-f` rather than a glob array, for the reason spelled out
# in the Windows block below: a fully interpolated literal path survives into
# an array even under nullglob, so a guard over it passes vacuously.
for f in ${logosProtocolRoot}/lib/liblogos_protocol.so* \
${logosProtocolRoot}/lib/liblogos_protocol.dylib \
${logosProtocolRoot}/bin/liblogos_protocol.dll \
${logosQtHostRoot}/lib/liblogos_qt_host.so* \
${logosQtHostRoot}/lib/liblogos_qt_host.dylib \
${logosQtHostRoot}/bin/liblogos_qt_host.dll; do
if [ -f "$f" ]; then
cp -L "$f" $out/lib/
fi
done
# Assert it, rather than trusting the copy above. The failure mode is a
# library that builds and installs and cannot be loaded.
_shared_found=0
for f in $out/lib/liblogos_protocol.* $out/lib/liblogos_qt_host.*; do
[ -f "$f" ] && _shared_found=$((_shared_found + 1))
done
if [ "$_shared_found" -lt 2 ]; then
echo "ERROR: liblogos_core imports the shared runtime, but only $_shared_found" >&2
echo " of liblogos_protocol / liblogos_qt_host were staged into \$out/lib." >&2
echo " liblogos_core resolves them through @loader_path and will fail to load." >&2
exit 1
fi
${pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isWindows ''
# Windows only: libpackage_manager_lib's OWN dependency, liblgx.
#
+10 -2
View File
@@ -86,13 +86,21 @@ pkgs.stdenv.mkDerivation {
${pkgs.lib.optionalString pkgs.stdenv.isLinux ''
# Fix RPATH on Linux to avoid /build/ references and include all dependencies.
# spdlog links libfmt; both must be on RPATH because patchelf replaces the default search paths.
#
# THIS LIST IS THE WHOLE SEARCH PATH. `patchelf --set-rpath` REPLACES what
# CMake wrote, so CMAKE_BUILD_RPATH / CMAKE_INSTALL_RPATH have no effect on
# the installed binaries here -- anything missing from this string is
# missing at runtime, full stop. Measured: adding both to CMAKE_*_RPATH
# changed nothing and the suite still died with
# error while loading shared libraries: liblogos_qt_host.so
# while the same commit passed on macOS, which does not go through here.
# OpenSSL (libssl, libcrypto) is needed because the SDK's plain-C++ TLS
# transport links it transitively without this the wrapped binary
# dies with `libssl.so.3: cannot open shared object`.
_rpath="$out/lib:${pkgs.boost}/lib:${common.env.LOGOS_PACKAGE_MANAGER_ROOT}/lib:${pkgs.gtest}/lib:${pkgs.qt6.qtbase}/lib:${pkgs.qt6.qtremoteobjects}/lib:${pkgs.spdlog}/lib:${pkgs.fmt}/lib:${pkgs.openssl.out}/lib:${pkgs.stdenv.cc.cc.lib}/lib"
_rpath="$out/lib:${common.env.LOGOS_PROTOCOL_ROOT}/lib:${common.env.LOGOS_QT_HOST_ROOT}/lib:${pkgs.boost}/lib:${common.env.LOGOS_PACKAGE_MANAGER_ROOT}/lib:${pkgs.gtest}/lib:${pkgs.qt6.qtbase}/lib:${pkgs.qt6.qtremoteobjects}/lib:${pkgs.spdlog}/lib:${pkgs.fmt}/lib:${pkgs.openssl.out}/lib:${pkgs.stdenv.cc.cc.lib}/lib"
patchelf --set-rpath "$_rpath" $out/bin/logos_core_tests || true
# Fix RPATH on liblogos_core.so so it can find its transitive deps (e.g. libboost_process, spdlog, fmt, libssl)
_rpath_lib="$out/lib:${pkgs.boost}/lib:${common.env.LOGOS_PACKAGE_MANAGER_ROOT}/lib:${pkgs.qt6.qtbase}/lib:${pkgs.qt6.qtremoteobjects}/lib:${pkgs.spdlog}/lib:${pkgs.fmt}/lib:${pkgs.openssl.out}/lib:${pkgs.stdenv.cc.cc.lib}/lib"
_rpath_lib="$out/lib:${common.env.LOGOS_PROTOCOL_ROOT}/lib:${common.env.LOGOS_QT_HOST_ROOT}/lib:${pkgs.boost}/lib:${common.env.LOGOS_PACKAGE_MANAGER_ROOT}/lib:${pkgs.qt6.qtbase}/lib:${pkgs.qt6.qtremoteobjects}/lib:${pkgs.spdlog}/lib:${pkgs.fmt}/lib:${pkgs.openssl.out}/lib:${pkgs.stdenv.cc.cc.lib}/lib"
patchelf --set-rpath "$_rpath_lib" $out/lib/liblogos_core.so || true
''}
+39 -95
View File
@@ -40,7 +40,16 @@ if(EXISTS "${LOGOS_PROTOCOL_ROOT}/lib/cmake/logos-protocol"
PATHS "${LOGOS_QT_HOST_ROOT}/lib/cmake/logos-qt-host" NO_DEFAULT_PATH)
if(NOT TARGET logos_sdk)
add_library(logos_sdk INTERFACE IMPORTED)
target_link_libraries(logos_sdk INTERFACE logos-qt-host::logos_qt_host)
# The SHARED Qt host runtime, not the static archive. This is the line that
# replaces the whole-archive + generated-.def block further down: it makes
# liblogos_core a CONSUMER of the runtime rather than its provider, so
# TokenManager / LogosAPIClient / LogosAPI each have exactly one definition
# in the process because there is exactly one library defining them.
#
# logos_qt_host_shared itself PUBLIC-links logos_protocol_shared, so the
# protocol half arrives transitively and correctly layered -- it is asserted
# in logos-plugin-qt by checks.shared-runtime-layering.
target_link_libraries(logos_sdk INTERFACE logos-qt-host::logos_qt_host_shared)
endif()
else()
message(FATAL_ERROR "logos-protocol / logos-qt-host not found. Set "
@@ -207,106 +216,41 @@ target_compile_definitions(logos_core PRIVATE LOGOS_CORE_LIBRARY)
# Do not reinstate it; if the C API ever stops being exported, the cause is a
# missing LOGOS_CORE_EXPORT on a declaration, not a missing linker flag.
#
# The C API is not the whole story, though. Narrowing the export table to it was
# what made main_ui link again, but it also left every in-process image with its
# The C API is not the whole story, and the rest of it is no longer solved here.
#
# Narrowing the export table to the C API left every in-process image with its
# own statically linked copy of the shared C++ runtime -- and therefore its own
# TokenManager singleton, so a capability token saved by the host was invisible
# to the UI plugin and every cross-module call was refused. ELF and Mach-O
# interpose these symbols across the process and get one instance for free; PE
# does not. The block below closes that gap the only way PE allows: liblogos_core
# publishes the shared types EXPLICITLY, and the in-process consumers import them
# instead of re-linking them. Neither horn of the old dilemma -- export
# everything and collide, or export nothing and duplicate -- is taken.
# to the UI plugin and every cross-module call was refused. This file used to
# close that gap by absorbing liblogos_protocol.a and liblogos_qt_host.a whole
# (--whole-archive) and re-publishing their symbols through a generated .def, so
# that liblogos_core became the single provider for types it does not own.
#
# It is not the provider any more. logos-protocol and logos-qt-host now ship
# REAL SHARED LIBRARIES that own their own symbols -- liblogos_protocol exports
# its table from a generated .def of its own (logos-protocol#65), and
# liblogos_qt_host owns LogosAPI (logos-plugin-qt#22). liblogos_core links them
# like any other dependency and IMPORTS the runtime instead of defining it.
#
# So this whole block is gone, along with cmake/gen-shared-exports.sh. What
# replaces it is one line: logos_sdk carries logos_qt_host_shared rather than
# the static archive, near the top of this file.
#
# WHAT DID NOT CHANGE is the invariant. The runtime must still exist exactly
# once per process; it is now enforced by there being one shared library per
# type rather than by one image absorbing everything. Consumers still empty
# their static archives (logos-basecamp/cmake/LogosSharedFromDll.cmake) so ld
# cannot pull an archive member that would redefine an imported symbol, and
# logos-basecamp/nix/symbol-gate.nix still asserts the result.
#
# OUT-OF-PROCESS consumers are deliberately unaffected: module plugins and
# ui_qml backends keep linking the STATIC archive, because each runs in its own
# process where its own copy is the CORRECT per-process singleton, and because a
# .lgx records an empty nix closure and could not carry a shared library anyway.
#
# Full rationale, including why the consumer-side __declspec(dllimport) is the
# load-bearing half rather than the export, lives in
# logos-protocol/cpp/logos_shared_api.h.
if(WIN32)
# The two archives that make up the shared C++ runtime. The Qt half used to
# be logos-qt-sdk::logos_qt_sdk; it is logos-qt-host::logos_qt_host now --
# logos-qt-sdk handed the CODE to logos-qt-host, so it is logos-qt-host that
# owns liblogos_qt_host.a and $<TARGET_FILE:> only resolves on that target.
# find_package(logos-qt-host) at the top of this file imports it DIRECTLY,
# so the target is unconditionally here -- it is not inherited through
# logos-qt-sdk, and does not depend on logos-qt-sdk keeping a dependency on
# the host runtime.
#
# A missing target is FATAL rather than a skipped block, for the same reason
# a missing `nm` is fatal below: every failure mode of this block is silent.
# The old guard had the target tests in the `if`, so the day one of them was
# renamed, liblogos_core.dll would have been built with no export list and
# no diagnostic -- and the split-brain it exists to prevent (one
# TokenManager per image, tokens written into one and read from another)
# shows up much later as refused calls in an app, not as a build failure.
foreach(_logos_shared_target logos-protocol::logos_protocol logos-qt-host::logos_qt_host)
if(NOT TARGET ${_logos_shared_target})
message(FATAL_ERROR
"${_logos_shared_target} is not a target, so liblogos_core.dll cannot "
"be given the shared-runtime export list. Windows requires it: PE has no "
"symbol interposition, so without this every in-process image links its "
"own copy of TokenManager and cross-module calls are refused at runtime. "
"Check that find_package(logos-qt-host) resolved -- i.e. that "
"LOGOS_QT_HOST_ROOT points at a built logos-qt-host prefix.")
endif()
endforeach()
# Ask the imported targets where their archives actually are rather than
# rebuilding the path by hand, so a rename or a layout change fails at
# generate time instead of producing an empty export list.
set(_logos_protocol_archive "$<TARGET_FILE:logos-protocol::logos_protocol>")
set(_logos_qt_host_archive "$<TARGET_FILE:logos-qt-host::logos_qt_host>")
# --whole-archive, i.e. the whole of both libraries goes into the DLL even
# where liblogos itself never calls it.
#
# This is not gold-plating; it is what makes "single provider" true rather
# than approximate. The consumers link an EMPTY stand-in for these archives
# (logos-basecamp/cmake/LogosSharedFromDll.cmake) because ld would otherwise
# pull members for incidental reasons -- it was observed pulling
# logos_api.cpp.obj to satisfy std::string's move constructor -- and each
# pulled object collides with our export. Once the consumer has no archive to
# fall back on, anything the DLL failed to include becomes an undefined
# reference in a downstream repo. Linking normally would include only the
# objects liblogos happens to reference, which is a moving target.
target_link_options(logos_core PRIVATE
"-Wl,--whole-archive"
"${_logos_protocol_archive}"
"${_logos_qt_host_archive}"
"-Wl,--no-whole-archive")
# CMAKE_NM is normally set by the toolchain file; fall back to the
# cross-prefixed binary so a plain `cmake -DCMAKE_TOOLCHAIN_FILE=...` still
# generates. A missing nm must fail here, loudly, rather than silently
# emitting an empty .def and restoring the split-brain.
set(_logos_nm "${CMAKE_NM}")
if(NOT _logos_nm)
find_program(_logos_nm NAMES "${CMAKE_CXX_COMPILER_TARGET}-nm" x86_64-w64-mingw32-nm nm)
endif()
if(NOT _logos_nm)
message(FATAL_ERROR "nm not found; cannot generate the liblogos_core export definition")
endif()
set(_logos_shared_def "${CMAKE_CURRENT_BINARY_DIR}/logos_core_shared_exports.def")
add_custom_command(
OUTPUT "${_logos_shared_def}"
COMMAND ${CMAKE_COMMAND} -E env sh
"${CMAKE_CURRENT_SOURCE_DIR}/../cmake/gen-shared-exports.sh"
"${_logos_nm}" "${_logos_shared_def}"
"${_logos_protocol_archive}" "*"
"${_logos_qt_host_archive}" "*"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/../cmake/gen-shared-exports.sh"
"${_logos_protocol_archive}" "${_logos_qt_host_archive}"
COMMENT "Generating liblogos_core shared-runtime export definition"
VERBATIM
)
add_custom_target(logos_core_shared_exports DEPENDS "${_logos_shared_def}")
add_dependencies(logos_core logos_core_shared_exports)
# A .def passed to the link ADDS to the export table; the ~18
# __declspec(dllexport) logos_core_* C API entries survive alongside it.
target_link_options(logos_core PRIVATE "${_logos_shared_def}")
set_property(TARGET logos_core APPEND PROPERTY LINK_DEPENDS "${_logos_shared_def}")
endif()
# Portable build: selects portable LGX variants instead of dev variants
option(LOGOS_PORTABLE_BUILD "Build for portable variant selection" OFF)