mirror of
https://github.com/logos-co/logos-plugin-qt.git
synced 2026-08-27 08:51:07 +00:00
feat(qt-host): add logos_qt_host_shared, and assert its layering
PR-3 of the shared-runtime migration. Requires logos-protocol#65, which exports
logos_protocol_shared from the CMake package; without it this build hard-fails
by design rather than falling back.
WHY. LogosAPI (here) and TokenManager / LogosAPIClient / StoreRegistry (in
logos-protocol) must exist EXACTLY ONCE per process. Every image linking a
static archive gets its own copy of every function-local static inside it, so
the host writes a capability token into one store and another in-process image
reads an empty one -- with no build diagnostic. Linking ONE shared library is
what replaces the whole-archive + generated-.def scheme in which liblogos_core
absorbed both archives and re-exported them.
THE STATIC ARCHIVE STAYS, and not transitionally. Module plugins and ui_qml
backends run in their OWN processes, so their own copy is the CORRECT
per-process singleton; staying static also keeps a .lgx self-contained, since a
.lgx records an empty nix closure and a shared library would not travel with it.
In-process images link the shared target; out-of-process images link the archive.
THE ONE LINE THAT MATTERS is that logos_qt_host_shared PUBLIC-links
${LP_SHARED_TARGET}, not ${LP_TARGET}. Linking the static archive there would
embed a second TokenManager INSIDE liblogos_qt_host itself -- the same bug one
layer down, and invisible to the consumer-side symbol gate, which would treat
this library as a provider and pass. It is a one-word mistake that builds,
links, installs and loads, and surfaces only as refused calls at runtime in a
different repo. So this PR carries its own check:
checks.<system>.shared-runtime-layering
asserting liblogos_qt_host.{dylib,so} DEFINES LogosAPI, DEFINES NEITHER
TokenManager NOR LogosAPIClient, IMPORTS TokenManager, and has
liblogos_protocol on its link line. The positive assertion doubles as the
validity control: if nm or c++filt were broken it reports 0 for LogosAPI and
fails, rather than reporting a reassuring zero for the other two.
LogosAPI switches from LOGOS_SHARED_API to LOGOS_QT_HOST_API (logos-protocol#64)
because the two are not the same choice in one translation unit: building this
library, LogosAPI must be EXPORTED while TokenManager must be IMPORTED. On
Windows the shared target also compiles with LOGOS_SHARED_USE_DLL so the
protocol-owned types become dllimport; LOGOS_QT_HOST_BUILDING_SHARED is tested
FIRST in logos_shared_api.h, so LogosAPI still resolves to dllexport there.
ARCHIVE DESTINATION on the install is load-bearing on Windows and inert
elsewhere: a shared library's import library (.dll.a) is the ARCHIVE artifact
while the DLL is RUNTIME, so omitting it installs no import library and a
consumer gets an imported target whose IMPORTED_IMPLIB does not exist.
VERIFIED, aarch64-darwin:
liblogos_qt_host.dylib defines LogosAPI:: 23
defines TokenManager:: 0, LogosAPIClient:: 0
imports TokenManager:: 5, LogosAPIClient:: 1
links @rpath/liblogos_protocol.dylib
static archive symbol tables IDENTICAL to master (1494 lines), one
byte differing -- ar metadata, not content. The macro
switch is behaviour-preserving for every existing
consumer.
all 7 checks PASS
The layering check was confirmed discriminating rather than vacuous by running
its logic against liblogos_core.dylib, which defines TokenManager 32 times and
is correctly rejected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
55713b9c79
commit
6842e91063
@@ -27,11 +27,13 @@ if(EXISTS "${LOGOS_PROTOCOL_ROOT}/lib/cmake/logos-protocol")
|
||||
find_package(logos-protocol REQUIRED
|
||||
PATHS "${LOGOS_PROTOCOL_ROOT}/lib/cmake/logos-protocol" NO_DEFAULT_PATH)
|
||||
set(LP_TARGET logos-protocol::logos_protocol)
|
||||
set(LP_SHARED_TARGET logos-protocol::logos_protocol_shared)
|
||||
elseif(EXISTS "${LOGOS_PROTOCOL_ROOT}/cpp/CMakeLists.txt")
|
||||
# Source checkout → build it as a subproject (dev convenience).
|
||||
add_subdirectory("${LOGOS_PROTOCOL_ROOT}/cpp"
|
||||
"${CMAKE_BINARY_DIR}/logos-protocol-build")
|
||||
set(LP_TARGET logos_protocol)
|
||||
set(LP_SHARED_TARGET logos_protocol_shared)
|
||||
else()
|
||||
message(FATAL_ERROR "logos-protocol not found. Set LOGOS_PROTOCOL_ROOT to an "
|
||||
"installed logos-protocol prefix or a source checkout.")
|
||||
@@ -78,6 +80,86 @@ install(TARGETS logos_qt_host
|
||||
INCLUDES DESTINATION include
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# logos_qt_host_shared — the Qt host runtime as a SHARED library.
|
||||
#
|
||||
# WHY. The types in here (LogosAPI) and in logos-protocol (TokenManager,
|
||||
# LogosAPIClient, the per-identity StoreRegistry) must exist EXACTLY ONCE per
|
||||
# process. Every image that links a static archive gets its own copy of every
|
||||
# function-local static inside it, so the host writes a capability token into
|
||||
# one store and another in-process image reads an empty one -- with no build
|
||||
# diagnostic. Linking ONE shared library is what replaces the whole-archive +
|
||||
# generated-.def scheme in which liblogos_core absorbed both archives and
|
||||
# re-exported them.
|
||||
#
|
||||
# THE STATIC ARCHIVE STAYS, and not transitionally. Module plugins and ui_qml
|
||||
# backends run in their OWN processes, so their own copy is the CORRECT
|
||||
# per-process singleton; staying static also keeps a .lgx self-contained (a .lgx
|
||||
# records an empty nix closure, so a shared library would not travel with it).
|
||||
# In-process images link the shared one; out-of-process images link the archive.
|
||||
if(NOT TARGET ${LP_SHARED_TARGET})
|
||||
# Hard failure rather than "skip the shared target", because every failure
|
||||
# mode here is silent: a consumer that wanted the shared runtime would fall
|
||||
# back to the archive, link cleanly, and reintroduce the duplicate singleton
|
||||
# at runtime.
|
||||
message(FATAL_ERROR
|
||||
"${LP_SHARED_TARGET} is not a target, so logos_qt_host_shared cannot be "
|
||||
"built. It requires a logos-protocol that EXPORTS its shared library "
|
||||
"(logos-protocol#65). Check that LOGOS_PROTOCOL_ROOT points at a "
|
||||
"protocol build new enough to provide it.")
|
||||
endif()
|
||||
|
||||
add_library(logos_qt_host_shared SHARED ${QT_HOST_SOURCES})
|
||||
|
||||
# PUBLIC-links the SHARED protocol, NOT ${LP_TARGET}.
|
||||
#
|
||||
# This single line is the whole correctness of this target. Linking the static
|
||||
# archive here would embed a second copy of TokenManager INSIDE
|
||||
# liblogos_qt_host.{so,dylib,dll} -- the exact bug this migration removes, one
|
||||
# layer down, and invisible to a consumer-side symbol gate because the duplicate
|
||||
# would live in a library the gate treats as a provider.
|
||||
target_link_libraries(logos_qt_host_shared PUBLIC
|
||||
${LP_SHARED_TARGET}
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
Qt${QT_VERSION_MAJOR}::RemoteObjects
|
||||
)
|
||||
|
||||
# LogosAPI is EXPORTED from this library (LOGOS_QT_HOST_API), while TokenManager
|
||||
# and LogosAPIClient -- owned by logos-protocol -- must be IMPORTED from
|
||||
# liblogos_protocol. That is why they are two macros and not one: a single macro
|
||||
# cannot say "export" and "import" in the same translation unit.
|
||||
#
|
||||
# LOGOS_QT_HOST_BUILDING_SHARED is tested FIRST in logos_shared_api.h, so
|
||||
# LogosAPI still resolves to dllexport here even with LOGOS_SHARED_USE_DLL set.
|
||||
target_compile_definitions(logos_qt_host_shared PRIVATE LOGOS_QT_HOST_BUILDING_SHARED)
|
||||
if(WIN32)
|
||||
target_compile_definitions(logos_qt_host_shared PRIVATE LOGOS_SHARED_USE_DLL)
|
||||
endif()
|
||||
|
||||
target_include_directories(logos_qt_host_shared PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
)
|
||||
|
||||
# Same basename as the archive; the LIBRARY install lands it in $out/lib beside
|
||||
# liblogos_qt_host.a.
|
||||
set_target_properties(logos_qt_host_shared PROPERTIES
|
||||
OUTPUT_NAME logos_qt_host
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
|
||||
)
|
||||
|
||||
# ARCHIVE DESTINATION is load-bearing on Windows and inert elsewhere: a shared
|
||||
# library's IMPORT library (.dll.a) is the ARCHIVE artifact while the DLL is
|
||||
# RUNTIME, so omitting it installs no import library and a consumer gets an
|
||||
# imported target whose IMPORTED_IMPLIB does not exist. Invisible on ELF/Mach-O.
|
||||
install(TARGETS logos_qt_host_shared
|
||||
EXPORT logos-qt-hostTargets
|
||||
ARCHIVE DESTINATION lib
|
||||
LIBRARY DESTINATION lib
|
||||
RUNTIME DESTINATION bin
|
||||
INCLUDES DESTINATION include
|
||||
)
|
||||
|
||||
install(EXPORT logos-qt-hostTargets
|
||||
FILE logos-qt-hostTargets.cmake
|
||||
NAMESPACE logos-qt-host::
|
||||
|
||||
+14
-7
@@ -86,15 +86,22 @@ inline size_t qHash(const LogosAPIClientCacheKey& k, size_t seed = 0) noexcept
|
||||
*
|
||||
* This class initializes and keeps instances of the client provider and token manager.
|
||||
*
|
||||
* LOGOS_SHARED_API because this is the object handed across the DLL boundary:
|
||||
* the host constructs a LogosAPI inside liblogos_core and passes the pointer to
|
||||
* the UI plugin through PluginInterface::logosAPI. Its constructor caches
|
||||
* `&TokenManager::instance()`, so on PE a plugin that links its own copy of
|
||||
* logos_api.cpp.obj caches a DIFFERENT singleton than the one the host wrote
|
||||
* the token into. Importing instead of re-linking is what makes the two agree.
|
||||
* Marked because this is the object handed across the DLL boundary: the host
|
||||
* constructs a LogosAPI and passes the pointer to the UI plugin through
|
||||
* PluginInterface::logosAPI. Its constructor caches `&TokenManager::instance()`,
|
||||
* so an image that links its own copy of logos_api.cpp caches a DIFFERENT
|
||||
* singleton than the one the host wrote the token into. Importing instead of
|
||||
* re-linking is what makes the two agree.
|
||||
*
|
||||
* LOGOS_QT_HOST_API, not LOGOS_SHARED_API, because LogosAPI is owned by THIS
|
||||
* library while TokenManager and LogosAPIClient are owned by logos-protocol.
|
||||
* While building logos_qt_host_shared the first must be EXPORTED and the second
|
||||
* two IMPORTED, and one macro cannot say both in the same translation unit. Off
|
||||
* Windows the distinction is moot -- both resolve to default visibility -- which
|
||||
* is exactly why getting it wrong would go unnoticed until a Windows build.
|
||||
* See logos_shared_api.h in logos-protocol.
|
||||
*/
|
||||
class LOGOS_SHARED_API LogosAPI : public QObject
|
||||
class LOGOS_QT_HOST_API LogosAPI : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
|
||||
@@ -171,6 +171,18 @@
|
||||
# The Qt host runtime compiles and installs a usable CMake package.
|
||||
qt-host = self.packages.${system}.logos-qt-host;
|
||||
|
||||
# logos_qt_host_shared must OWN LogosAPI and BORROW everything
|
||||
# logos-protocol owns. The consumer-side symbol gate cannot see this:
|
||||
# a shared qt-host that linked the STATIC protocol archive would embed
|
||||
# its own TokenManager, and the gate would treat that library as a
|
||||
# provider and pass, with the duplicate one layer below anything it
|
||||
# inspects. One wrong word in target_link_libraries, and it builds,
|
||||
# links, installs and loads.
|
||||
shared-runtime-layering = import ./tests/test-shared-runtime-layering.nix {
|
||||
inherit pkgs;
|
||||
qtHost = self.packages.${system}.logos-qt-host;
|
||||
};
|
||||
|
||||
# Drive the glue generator over a real contract and assert on the
|
||||
# emitted C++.
|
||||
qt-host-generator = import ./tests/test-qt-host-generator.nix {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Asserts the LAYERING of logos_qt_host_shared: it must OWN LogosAPI and BORROW
|
||||
# everything logos-protocol owns.
|
||||
#
|
||||
# WHY THIS CHECK EXISTS SEPARATELY. The consumer-side symbol gate (logos-basecamp
|
||||
# and logos-logoscore-cli nix/symbol-gate.nix) asserts that no in-process
|
||||
# CONSUMER defines the runtime. It cannot see this failure: if
|
||||
# liblogos_qt_host.dylib linked the STATIC protocol archive it would embed its
|
||||
# own TokenManager, and the gate would treat that library as a provider and pass.
|
||||
# The duplicate would then be one layer below anything the gate inspects.
|
||||
#
|
||||
# The failure is a single-word mistake -- ${LP_TARGET} instead of
|
||||
# ${LP_SHARED_TARGET} in one target_link_libraries -- and it produces a library
|
||||
# that builds, links, installs and loads. It surfaces only as refused
|
||||
# cross-module calls at runtime, in a different repo.
|
||||
#
|
||||
# The positive assertion doubles as the validity control: LogosAPI must be
|
||||
# DEFINED here, so a broken nm or a missing c++filt fails the check rather than
|
||||
# reporting a reassuring zero.
|
||||
{ pkgs, qtHost }:
|
||||
|
||||
let
|
||||
isDarwin = pkgs.stdenv.isDarwin;
|
||||
definedCmd = if isDarwin then "nm -gU" else "nm -D --defined-only";
|
||||
undefCmd = if isDarwin then "nm -gu" else "nm -D --undefined-only";
|
||||
stripAddr = if isDarwin then "sed -E 's/^[0-9a-fA-F]+ [A-Za-z] //'" else "sed -E 's/^[0-9a-fA-F]* [A-Za-z] //'";
|
||||
stripUndef = if isDarwin then "sed -E 's/^ +U //'" else "sed -E 's/^ +U //'";
|
||||
in
|
||||
pkgs.runCommand "logos-qt-host-shared-runtime-layering" {
|
||||
nativeBuildInputs = [ pkgs.coreutils pkgs.gnugrep pkgs.gnused pkgs.stdenv.cc.bintools ];
|
||||
} ''
|
||||
set -uo pipefail
|
||||
FAIL=0
|
||||
note() { printf ' %-46s %s\n' "$1" "$2"; }
|
||||
bad() { FAIL=1; printf ' %-46s %s\n' "$1" "$2"; }
|
||||
|
||||
LIB=""
|
||||
for c in ${qtHost}/lib/liblogos_qt_host.dylib ${qtHost}/lib/liblogos_qt_host.so; do
|
||||
[ -e "$c" ] && LIB="$c" && break
|
||||
done
|
||||
[ -n "$LIB" ] || { echo "FATAL: no shared logos_qt_host under ${qtHost}/lib"; exit 1; }
|
||||
echo "library = $LIB"
|
||||
|
||||
defines() { ${definedCmd} "$LIB" 2>/dev/null | c++filt 2>/dev/null | ${stripAddr} | grep -cE "^$1::" || true; }
|
||||
imports() { ${undefCmd} "$LIB" 2>/dev/null | c++filt 2>/dev/null | ${stripUndef} | grep -cE "^$1::" || true; }
|
||||
|
||||
echo
|
||||
echo "== it OWNS LogosAPI (expect >0; also the validity control) =="
|
||||
n=$(defines LogosAPI)
|
||||
if [ "$n" -gt 0 ]; then note "LogosAPI:: defined" "$n OK"
|
||||
else bad "LogosAPI:: defined" "$n EXPECTED >0 (or nm/c++filt is broken)"; fi
|
||||
|
||||
echo
|
||||
echo "== it does NOT define what logos-protocol owns (expect 0) =="
|
||||
for sym in TokenManager LogosAPIClient; do
|
||||
n=$(defines "$sym")
|
||||
if [ "$n" -eq 0 ]; then note "$sym:: defined" "0 OK"
|
||||
else
|
||||
bad "$sym:: defined" "$n LAYERING VIOLATION"
|
||||
echo " logos_qt_host_shared linked the STATIC protocol archive."
|
||||
echo " Use \''${LP_SHARED_TARGET}, not \''${LP_TARGET}, in target_link_libraries."
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "== it BORROWS them instead (expect >0) =="
|
||||
n=$(imports TokenManager)
|
||||
if [ "$n" -gt 0 ]; then note "TokenManager:: imported" "$n OK"
|
||||
else bad "TokenManager:: imported" "$n EXPECTED >0"; fi
|
||||
|
||||
echo
|
||||
echo "== and links the shared protocol =="
|
||||
if ${if isDarwin then ''otool -L "$LIB" 2>/dev/null'' else ''objdump -p "$LIB" 2>/dev/null''} | grep -qi 'liblogos_protocol\.\(dylib\|so\)'; then
|
||||
note "liblogos_protocol on the link line" "OK"
|
||||
else
|
||||
bad "liblogos_protocol on the link line" "MISSING — it took the archive"
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$FAIL" -eq 0 ]; then echo "LAYERING: PASS"; mkdir -p $out; echo ok > $out/result
|
||||
else echo "LAYERING: FAIL"; exit 1; fi
|
||||
''
|
||||
Reference in New Issue
Block a user