mirror of
https://github.com/logos-co/logos-liblogos.git
synced 2026-08-27 12:51:10 +00:00
feat(windows): cross-compile liblogos, and make liblogos_core the single provider (#176)
* feat(windows): cross-compile logos-liblogos for x86_64-w64-mingw32
Adds the x86_64-windows pseudo-system to `packages` (checks and devShells stay
native). liblogos's own src/ needed NO portability work at all -- verified
exhaustively, not assumed: 17 files, 2304 lines, zero POSIX headers and zero
POSIX APIs. All process, plugin and socket work already lives in
logos-container-subprocess and logos-module-loader-qt, which were ported first.
Two real blockers, one of them silent:
* install(TARGETS logos_core ...) named only LIBRARY and ARCHIVE destinations.
A DLL is a RUNTIME artifact, so CMake SKIPPED IT WITHOUT COMMENT: the build
succeeded and shipped a lib/ containing liblogos_core.dll.a and no DLL at
all -- a link-only package that would have handed logosctl.exe an import
library with nothing behind it. Proven by reverting the fix: rc=0, no DLL.
lib.nix now refuses to produce an output with no loadable logos_core.
* The test suite is genuinely POSIX-only (spawn.h, sys/wait.h, mkdtemp, kill),
and CMake put it in the default `all` target, so it broke the cross build
before anything else could. Tests are now gated behind LOGOS_BUILD_TESTS,
with the gate wrapping the gtest FetchContent fallback too -- otherwise
dropping gtest sends CMake to the network inside the sandbox and it dies on
a misleading "downloading ... failed".
Also: package_manager_lib needed IMPORTED_IMPLIB (mingw links against the
import library, not the DLL), and Qt's host tools come in via
logosQtCrossCmakeFlags.
Verified: liblogos_core.dll is a PE32+ DLL whose export table carries the C
ABI (logos_core_init / _start / _load_module / _get_loaded_modules). The
output also ships logos_host_qt.exe with its 15 runtime DLLs and
capability_module_plugin.dll. Native unchanged: 181 tests, 174 passed, 7
skipped -- the same 7 ProcessManagerTest cases skipped before this change.
* fix(windows): carry the package-manager DLL closure, and type the manifest
lib.nix (Windows only): copy every *.dll / *.dll.a from the package-manager
root rather than just libpackage_manager_lib*, with a loud guard if liblgx.dll
is missing -- an absent runtime DLL otherwise shows up as an executable that
exits with no output at all.
modules.nix: emit "type": "core" in the generated manifest. Native-visible
but strictly additive.
* fix(windows): export only the C API from liblogos_core
liblogos_core.dll exported 13,252 symbols. Eighteen of them were the
logos_core_* C API; the rest were the entire internal C++ surface,
LogosAPI's included. Any consumer linking both liblogos_core and the
qt-sdk static library therefore failed with
multiple definition of `LogosAPI::LogosAPI(QString const&, QObject*)'
...liblogos_qt_sdk.a(logos_api.cpp.obj)
first defined here: ...liblogos_core.dll.a(...)
which is what blocked main_ui from linking.
Root cause: LOGOS_CORE_EXPORT expanded to
__attribute__((visibility("default"))), an ELF concept that mingw-gcc
accepts and silently ignores on PE. Nothing was explicitly exported, so
an earlier fix reached for -Wl,--export-all-symbols to get the C API out
-- correct as far as it went, and the reason the whole C++ surface
leaked with it.
Using __declspec(dllexport)/dllimport on Windows fixes it at the root and
does so twice over: GNU ld disables PE auto-export image-wide as soon as
any symbol is dllexported, so the internal surface stops leaking as a
side effect. Measured: logos_core_* exports unchanged at 18, mangled C++
exports 13,673 -> 452, LogosAPI no longer exported at all.
Two copies of LogosAPI (host and plugin) is the DESIGNED arrangement, not
a regression -- see logos_qt_lp_bridge.h, which notes a Qt plugin links
its own copy of the protocol library so TokenManager::instance() inside
it is deliberately not the host's, and syncTokens is the bridge. That
holds on Unix too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(windows): export the shared runtime from liblogos_core.dll
Makes liblogos_core the single provider of TokenManager, LogosAPI,
LogosAPIClient and the LogosResult stream operators, so the Basecamp
process has ONE of each instead of nine.
The export set comes from a GENERATED .def rather than dllexport in the
headers, because the definitions live in liblogos_protocol.a /
liblogos_qt_sdk.a -- archives 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 this link instead leaves those archives compiled byte-identically.
Two details in gen-shared-exports.sh that look like they could be
simplified and cannot:
* It exports the WHOLE archive, not a curated class list. ld picks
archive members by object file for reasons unrelated to our symbols
-- measured here, main_ui referenced std::string's move constructor
and ld satisfied it from logos_api.cpp.obj, dragging LogosAPI,
LogosAPIClient and TokenManager in behind it. Consumers link an empty
archive and take everything from the DLL, which only works if the DLL
really provides everything; a partial export set surfaces as an
undefined reference in a downstream repo, far from the cause.
* It filters COMDAT symbols out. Those come from inline functions and
templates in headers, so every consumer TU emits its own copy
regardless; exporting them makes the import library a strong
definition that collides with that copy.
Measured after: liblogos_core.dll goes from 18 exports to 376, of which
132 are the shared runtime; LogosBasecamp.exe drops 14.3 MB -> 1.08 MB
and main_ui.dll 23.2 MB -> 10.1 MB as the duplicated statics stop being
linked in. Native aarch64-darwin is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Merge origin/master into feat/windows-cross, and re-pin the L1-L7 inputs
master landed 56aa8bb (bump logos package manager); merged clean, no conflicts.
Then the whole chain moves to its merged revs: logos-nix (L1); logos-protocol,
logos-module, logos-container, process-stats (L2); logos-cpp-sdk,
logos-module-loader, default-container=logos-container-subprocess,
logos-package-manager (L3); logos-qt-sdk (L4);
default-module-loader=logos-module-loader-qt (L5); logos-capability-module (L7).
Verified packages.x86_64-windows.default evaluates to a .drv against the merged
set. The native aarch64-darwin build was NOT verified locally: this machine is
currently failing unrelated derivations two different ways -- clang killed with
signal 9 during CMake's trivial compiler probe, and macOS refusing a Nix-store
gtest dylib with "library load denied by system policy" during
gtest_discover_tests. The first was proven environmental rather than
input-related by building the IDENTICAL derivation hash standalone, where it
passed. CI is the authority for the native side.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
Claude Opus 5
parent
56aa8bb45c
commit
503587797d
+41
-18
@@ -34,35 +34,58 @@ endif()
|
||||
# Build src first to ensure logos_core is built before modules
|
||||
add_subdirectory(src)
|
||||
|
||||
# GoogleTest setup
|
||||
# Try to find GoogleTest via find_package first (for Nix and system installations)
|
||||
find_package(GTest QUIET)
|
||||
|
||||
if(NOT GTest_FOUND)
|
||||
# Fall back to FetchContent if GoogleTest is not found
|
||||
message(STATUS "GoogleTest not found via find_package, using FetchContent")
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(googletest
|
||||
URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip
|
||||
)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
else()
|
||||
message(STATUS "Using system GoogleTest")
|
||||
# Tests. Gated EXPLICITLY rather than left to find_package: the suite is
|
||||
# POSIX-only (tests/test_process_stats.cpp uses <spawn.h>, <sys/wait.h>,
|
||||
# posix_spawn/waitpid/kill and `environ`; tests/test_subprocess_manager.cpp
|
||||
# spawns /bin/sh), none of which mingw-w64 provides. The default build target
|
||||
# is `all`, so an ungated tests/ subdirectory would be compiled by every
|
||||
# cross build.
|
||||
#
|
||||
# The gate wraps the GTest lookup too: dropping only `add_subdirectory(tests)`
|
||||
# would still leave find_package(GTest) able to miss and fall through to the
|
||||
# FetchContent download, which dies in the nix sandbox with a misleading
|
||||
# "downloading ... failed".
|
||||
option(LOGOS_BUILD_TESTS "Build the logos_core test suite" ON)
|
||||
if(WIN32)
|
||||
set(LOGOS_BUILD_TESTS OFF)
|
||||
endif()
|
||||
|
||||
enable_testing()
|
||||
include(GoogleTest)
|
||||
if(LOGOS_BUILD_TESTS)
|
||||
# Try to find GoogleTest via find_package first (for Nix and system installations)
|
||||
find_package(GTest QUIET)
|
||||
|
||||
# Add tests subdirectory
|
||||
add_subdirectory(tests)
|
||||
if(NOT GTest_FOUND)
|
||||
# Fall back to FetchContent if GoogleTest is not found
|
||||
message(STATUS "GoogleTest not found via find_package, using FetchContent")
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(googletest
|
||||
URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip
|
||||
)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
else()
|
||||
message(STATUS "Using system GoogleTest")
|
||||
endif()
|
||||
|
||||
enable_testing()
|
||||
include(GoogleTest)
|
||||
|
||||
# Add tests subdirectory
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
# Install rules. The logos_host_qt binary (and its logos_host symlink) now live
|
||||
# in logos-module-loader-qt; liblogos installs only the core library here, and
|
||||
# bin.nix re-exports the host binary from that package so frontends are
|
||||
# unaffected.
|
||||
# RUNTIME DESTINATION matters on Windows: a DLL is a RUNTIME artifact, and with
|
||||
# only LIBRARY/ARCHIVE named CMake sends it to the DEFAULT runtime destination
|
||||
# (bin) while nix/lib.nix only ever copies ${build}/lib -- which would yield a
|
||||
# lib output holding the import library and no DLL, silently. Pointing RUNTIME
|
||||
# at lib keeps the output shape identical on all three platforms.
|
||||
install(TARGETS logos_core
|
||||
LIBRARY DESTINATION lib
|
||||
ARCHIVE DESTINATION lib
|
||||
RUNTIME DESTINATION lib
|
||||
)
|
||||
|
||||
# Install headers
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/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_sdk.a, 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 liblogos_qt_sdk.a'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++ only. Keeps toolchain bookkeeping such as
|
||||
# qt_version_tag_6_11_used — which every image legitimately defines —
|
||||
# out of the export table.
|
||||
if (name !~ /^_Z/) 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
+60960
-2850
File diff suppressed because it is too large
Load Diff
@@ -53,9 +53,45 @@
|
||||
logosPackageManager = logos-package-manager.packages.${system}.lib;
|
||||
logosPackageManagerPortable = logos-package-manager.packages.${system}.lib-portable;
|
||||
});
|
||||
|
||||
# Same as forAllSystems, plus the "x86_64-windows" pseudo-system. This
|
||||
# cannot just be logos-nix.lib.forAllTargets, because that only supplies
|
||||
# { system, pkgs } and this flake threads a dozen per-system dependencies
|
||||
# through.
|
||||
#
|
||||
# Every dependency below is a TARGET-side artifact (headers, archives, or
|
||||
# DLLs linked into logos_core, plus the host binary / plugin that are
|
||||
# merely re-exported). liblogos runs NO code generator at build time
|
||||
# (verified: no logos-cpp-generator / qt-generator anywhere in this repo),
|
||||
# so nothing here needs to come from the build platform's package set.
|
||||
#
|
||||
# Applied to `packages` ONLY: `checks` would have to execute PE test
|
||||
# binaries on the Linux builder, and a cross devShell offers no way to run
|
||||
# what it produces.
|
||||
windowsBuildSystem = "x86_64-linux";
|
||||
forAllTargets = f:
|
||||
nixpkgs.lib.genAttrs (systems ++ [ "x86_64-windows" ]) (system: f {
|
||||
inherit system;
|
||||
pkgs =
|
||||
if system == "x86_64-windows"
|
||||
then logos-nix.lib.mkWindowsPkgs { buildSystem = windowsBuildSystem; }
|
||||
else import nixpkgs { inherit system; };
|
||||
logosSdk = logos-cpp-sdk.packages.${system}.default;
|
||||
logosProtocolPkg = logos-protocol.packages.${system}.default;
|
||||
logosQtSdk = logos-qt-sdk.packages.${system}.default;
|
||||
capabilityModule = logos-capability-module.packages.${system}.default;
|
||||
logosModule = logos-module.packages.${system}.default;
|
||||
processStats = process-stats.packages.${system}.default;
|
||||
logosContainer = logos-container.packages.${system}.default;
|
||||
logosModuleLoader = logos-module-loader.packages.${system}.default;
|
||||
defaultContainer = default-container.packages.${system}.default;
|
||||
defaultModuleLoader = default-module-loader.packages.${system}.default;
|
||||
logosPackageManager = logos-package-manager.packages.${system}.lib;
|
||||
logosPackageManagerPortable = logos-package-manager.packages.${system}.lib-portable;
|
||||
});
|
||||
in
|
||||
{
|
||||
packages = forAllSystems ({ pkgs, system, logosSdk, logosProtocolPkg, logosQtSdk, capabilityModule, logosModule, processStats, logosContainer, logosModuleLoader, defaultContainer, defaultModuleLoader, logosPackageManager, logosPackageManagerPortable }:
|
||||
packages = forAllTargets ({ pkgs, system, logosSdk, logosProtocolPkg, logosQtSdk, capabilityModule, logosModule, processStats, logosContainer, logosModuleLoader, defaultContainer, defaultModuleLoader, logosPackageManager, logosPackageManagerPortable }:
|
||||
let
|
||||
# The built-in default container + format-loader implementations — the
|
||||
# single place the default is chosen. Each is just the package; it
|
||||
@@ -113,7 +149,6 @@
|
||||
logos-liblogos-bin = bin;
|
||||
logos-liblogos-lib = lib;
|
||||
logos-liblogos-include = include;
|
||||
logos-liblogos-tests = tests;
|
||||
logos-liblogos-modules = modules;
|
||||
|
||||
# Combined output
|
||||
@@ -125,6 +160,13 @@
|
||||
# Default package (dev)
|
||||
default = liblogos;
|
||||
}
|
||||
# The test suite is POSIX-only (posix_spawn/waitpid/kill, /bin/sh) and
|
||||
# CMake gates it off for a Windows host, so `ninja logos_core_tests`
|
||||
# would have no such target. Not exposing the output at all beats
|
||||
# shipping one that cannot be built.
|
||||
// pkgs.lib.optionalAttrs (!pkgs.stdenv.hostPlatform.isWindows) {
|
||||
logos-liblogos-tests = tests;
|
||||
}
|
||||
);
|
||||
|
||||
checks = forAllSystems ({ pkgs, system, ... }:
|
||||
|
||||
+4
-1
@@ -3,8 +3,11 @@
|
||||
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "${common.pname}-build";
|
||||
# qtbase's setup hook errors in qtPreHook unless a wrapper hook ran or this is
|
||||
# set; the wrapper hooks are absent on Windows and would skip a PE anyway.
|
||||
dontWrapQtApps = true;
|
||||
version = common.version;
|
||||
|
||||
|
||||
inherit src;
|
||||
inherit (common) nativeBuildInputs buildInputs cmakeFlags meta env;
|
||||
|
||||
|
||||
+13
-5
@@ -24,8 +24,12 @@
|
||||
pkgs.cmake
|
||||
pkgs.ninja
|
||||
pkgs.pkg-config
|
||||
pkgs.qt6.wrapQtAppsNoGuiHook
|
||||
];
|
||||
]
|
||||
# The Qt wrapper hooks are absent for a mingw host (they cannot even evaluate)
|
||||
# and would skip a PE anyway. Each derivation that carries qtbase in
|
||||
# buildInputs sets `dontWrapQtApps = true` to keep qtbase's setup hook from
|
||||
# erroring in qtPreHook; BOTH halves are required.
|
||||
++ pkgs.lib.optional (!pkgs.stdenv.hostPlatform.isWindows) pkgs.qt6.wrapQtAppsNoGuiHook;
|
||||
|
||||
# Common runtime dependencies. Boost, OpenSSL, and nlohmann_json
|
||||
# come in transitively via logosSdk's `propagatedBuildInputs`
|
||||
@@ -56,8 +60,12 @@
|
||||
logosPackageManager
|
||||
];
|
||||
|
||||
# Common CMake flags
|
||||
cmakeFlags = [
|
||||
# Common CMake flags. `logosQtCrossCmakeFlags` comes from logos-nix's Windows
|
||||
# overlay and points CMake at the BUILD-platform Qt tool packages (repc, moc,
|
||||
# qmltyperegistrar live in separate Qt6*Tools packages that must run on the
|
||||
# builder). It is undefined — hence empty — for native package sets, so no
|
||||
# isWindows guard is needed.
|
||||
cmakeFlags = (pkgs.logosQtCrossCmakeFlags or [ ]) ++ [
|
||||
"-GNinja"
|
||||
"-DLOGOS_CPP_SDK_ROOT=${logosSdk}"
|
||||
"-DLOGOS_PROTOCOL_ROOT=${logosProtocolPkg}"
|
||||
@@ -89,6 +97,6 @@
|
||||
# Metadata
|
||||
meta = with pkgs.lib; {
|
||||
description = "Logos liblogos core library";
|
||||
platforms = platforms.unix;
|
||||
platforms = platforms.unix ++ platforms.windows;
|
||||
};
|
||||
}
|
||||
|
||||
+63
@@ -16,10 +16,73 @@ pkgs.runCommand "${common.pname}-lib-${common.version}"
|
||||
cp -r ${build}/lib/* $out/lib/
|
||||
fi
|
||||
|
||||
# Fail loudly rather than shipping a lib output with no RUNTIME artifact.
|
||||
# Verified failure this guards against: drop `RUNTIME DESTINATION lib` from
|
||||
# install(TARGETS logos_core) and the Windows build still exits 0 while
|
||||
# liblogos_core.dll appears nowhere in the package -- only the
|
||||
# liblogos_core.dll.a import library does. Match the loadable image
|
||||
# explicitly; a `liblogos_core.*` glob is satisfied by the import library
|
||||
# and catches nothing.
|
||||
#
|
||||
# Tested explicitly with `for`/`-f` rather than a glob array: bash's
|
||||
# nullglob only drops patterns that CONTAIN a wildcard, so a literal
|
||||
# "$out/lib/liblogos_core.dll" survives into the array even when the file
|
||||
# does not exist and the check passes vacuously. (Observed: the first
|
||||
# version of this guard did exactly that and let the broken build through.)
|
||||
found=""
|
||||
for cand in $out/lib/liblogos_core.so $out/lib/liblogos_core.dylib $out/lib/liblogos_core.dll; do
|
||||
[ -f "$cand" ] && found="$cand"
|
||||
done
|
||||
if [ -z "$found" ]; then
|
||||
echo "Error: no loadable logos_core library in ${build}/lib" >&2
|
||||
ls -la ${build}/lib ${build}/bin >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Bundle package_manager_lib alongside logos_core (logos_core links against it)
|
||||
for f in ${logosPackageManagerRoot}/lib/libpackage_manager_lib*; do
|
||||
if [ -f "$f" ]; then
|
||||
cp -L "$f" $out/lib/
|
||||
fi
|
||||
done
|
||||
|
||||
${pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isWindows ''
|
||||
# Windows only: libpackage_manager_lib's OWN dependency, liblgx.
|
||||
#
|
||||
# An ELF or Mach-O consumer never needs this -- the copied
|
||||
# libpackage_manager_lib carries an RPATH/install-name pointing back at
|
||||
# the store path liblgx lives in, so the loader finds it there. PE has no
|
||||
# rpath: an import table carries the DLL BASE NAME and Windows resolves
|
||||
# it from the loading executable's directory. Whatever bundles this lib
|
||||
# output can only stage what is IN it, so a dependency left behind here
|
||||
# is a dependency that cannot be staged later.
|
||||
#
|
||||
# Measured, and the reason this exists: logosctl.exe with every other DLL
|
||||
# correctly beside it exited 53 with NO OUTPUT AT ALL -- the loader
|
||||
# failing on liblgx.dll before main() ran. Nothing in the build, the
|
||||
# package, or the run says which DLL is missing.
|
||||
#
|
||||
# Every DLL the package-manager output carries, not just liblgx: liblgx
|
||||
# has its own imports (libsodium, ICU, zlib), and package-manager stages
|
||||
# that whole closure for exactly this reason. Copying the set rather than
|
||||
# naming members keeps it transitive instead of a list that rots.
|
||||
#
|
||||
# Explicit `for` + `-f` rather than a nullglob array: nullglob only drops
|
||||
# patterns that CONTAIN a wildcard, so a fully interpolated literal path
|
||||
# survives into the array and a guard over it passes vacuously.
|
||||
lgx=0
|
||||
for f in ${logosPackageManagerRoot}/lib/*.dll ${logosPackageManagerRoot}/lib/*.dll.a; do
|
||||
[ -f "$f" ] || continue
|
||||
[ -e "$out/lib/$(basename "$f")" ] && continue
|
||||
cp -L "$f" $out/lib/
|
||||
case "$(basename "$f")" in liblgx.dll) lgx=$((lgx + 1));; esac
|
||||
done
|
||||
if [ "$lgx" -eq 0 ] && [ ! -f "$out/lib/liblgx.dll" ]; then
|
||||
echo "Error: liblgx.dll not found under ${logosPackageManagerRoot}/lib;" >&2
|
||||
echo " libpackage_manager_lib.dll imports it, so any .exe loading" >&2
|
||||
echo " liblogos_core.dll would fail with no diagnostic." >&2
|
||||
ls -la ${logosPackageManagerRoot}/lib >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
''}
|
||||
''
|
||||
|
||||
+39
-24
@@ -1,9 +1,25 @@
|
||||
# Bundles modules from external flake inputs into the logos_host_qt modules directory.
|
||||
# logos_host_qt expects: modules/<name>/manifest.json + <name>_plugin.{so,dylib}
|
||||
# logos_host_qt expects: modules/<name>/manifest.json + <name>_plugin.{so,dylib,dll}
|
||||
# When portableBuild is false (default/dev), manifest keys get a "-dev" suffix
|
||||
# to match the dev variant lookup in platformVariantsToTry().
|
||||
{ pkgs, common, capabilityModule, portableBuild ? false }:
|
||||
|
||||
let
|
||||
# The manifest keys must describe the TARGET, not the machine doing the
|
||||
# build. `uname` used to supply them, which is correct natively and wrong
|
||||
# under cross-compilation: a Windows build on the Linux builder emitted
|
||||
# "linux-x86_64-dev" keys, so PackageManagerLib::platformVariantsToTry()
|
||||
# (which returns "windows-x86_64-dev" on Windows) would find no entry and the
|
||||
# capability module would silently not load.
|
||||
hostPlatform = pkgs.stdenv.hostPlatform;
|
||||
platform =
|
||||
if hostPlatform.isDarwin then "darwin"
|
||||
else if hostPlatform.isWindows then "windows"
|
||||
else "linux";
|
||||
arch =
|
||||
if hostPlatform.isAarch64 then "aarch64" else "x86_64";
|
||||
suffix = if portableBuild then "" else "-dev";
|
||||
in
|
||||
pkgs.runCommand "${common.pname}-modules-${common.version}"
|
||||
{
|
||||
inherit (common) meta;
|
||||
@@ -11,14 +27,14 @@ pkgs.runCommand "${common.pname}-modules-${common.version}"
|
||||
''
|
||||
mkdir -p $out/modules/capability_module
|
||||
|
||||
# Copy the plugin library
|
||||
if [ -d ${capabilityModule}/lib ]; then
|
||||
for lib in ${capabilityModule}/lib/*.dylib ${capabilityModule}/lib/*.so; do
|
||||
if [ -f "$lib" ]; then
|
||||
cp "$lib" $out/modules/capability_module/
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# Copy the plugin library. Every extension is listed explicitly -- an
|
||||
# if/elif chain, or a Unix-only pair of globs, installs NOTHING on Windows
|
||||
# and still succeeds.
|
||||
shopt -s nullglob
|
||||
plugins=(${capabilityModule}/lib/*.dylib ${capabilityModule}/lib/*.so ${capabilityModule}/lib/*.dll)
|
||||
for lib in "''${plugins[@]}"; do
|
||||
cp "$lib" $out/modules/capability_module/
|
||||
done
|
||||
|
||||
# Determine the plugin filename that was copied
|
||||
pluginFile=""
|
||||
@@ -30,30 +46,29 @@ pkgs.runCommand "${common.pname}-modules-${common.version}"
|
||||
done
|
||||
|
||||
if [ -z "$pluginFile" ]; then
|
||||
echo "Error: No capability_module library found"
|
||||
echo "Error: No capability_module library found under ${capabilityModule}/lib" >&2
|
||||
ls -la ${capabilityModule}/lib >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine platform variant keys
|
||||
platform=""
|
||||
arch=""
|
||||
case "$(uname -s)" in
|
||||
Linux) platform="linux" ;;
|
||||
Darwin) platform="darwin" ;;
|
||||
esac
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch="x86_64" ;;
|
||||
aarch64|arm64) arch="aarch64" ;;
|
||||
esac
|
||||
|
||||
# Dev builds use "-dev" suffixed variant keys to match platformVariantsToTry()
|
||||
suffix="${if portableBuild then "" else "-dev"}"
|
||||
platform="${platform}"
|
||||
arch="${arch}"
|
||||
suffix="${suffix}"
|
||||
|
||||
# Create manifest.json for plugin discovery
|
||||
# "type" is REQUIRED, not decorative. PackageManagerLib::getInstalledModules
|
||||
# enumerates manifests with `types = {"core"}` and skips -- silently, with
|
||||
# no diagnostic -- every manifest whose type does not match. Without this
|
||||
# field the directory scans clean, the module never enters the registry,
|
||||
# and the only symptom is a later "Module not found in known modules:
|
||||
# capability_module" from whoever tried to load it. It matches the "core"
|
||||
# in logos-capability-module/metadata.json, which is what the lgx-bundled
|
||||
# manifest carries.
|
||||
cat > $out/modules/capability_module/manifest.json <<EOF
|
||||
{
|
||||
"name": "capability_module",
|
||||
"version": "1.0.0",
|
||||
"type": "core",
|
||||
"main": {
|
||||
"$platform-$arch$suffix": "$pluginFile",
|
||||
"$platform-amd64$suffix": "$pluginFile",
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
# Builds tests
|
||||
{ pkgs, common, build }:
|
||||
|
||||
# The suite is POSIX-only (posix_spawn/waitpid/kill, /bin/sh) and CMake turns
|
||||
# LOGOS_BUILD_TESTS off for a Windows host, so there would be no
|
||||
# `logos_core_tests` target to build. Refuse loudly instead: the configurePhase
|
||||
# below hand-rolls its `cmake` invocation and never expands $cmakeFlags, so a
|
||||
# Windows instantiation would silently drop -DCMAKE_SYSTEM_NAME=Windows and
|
||||
# every entry of logosQtCrossCmakeFlags -- i.e. configure as a NATIVE build and
|
||||
# link the wrong architecture, which is far worse than an error. flake.nix
|
||||
# already withholds this attribute on Windows; this makes that non-negotiable.
|
||||
if pkgs.stdenv.hostPlatform.isWindows then
|
||||
throw "logos-liblogos: the logos_core test suite is POSIX-only and cannot be cross-compiled for ${pkgs.stdenv.hostPlatform.system}"
|
||||
else
|
||||
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "${common.pname}-tests";
|
||||
version = common.version;
|
||||
|
||||
@@ -185,6 +185,91 @@ add_library(logos_core SHARED ${LOGOS_CORE_SOURCES})
|
||||
# Set the LOGOS_CORE_LIBRARY definition for the library
|
||||
target_compile_definitions(logos_core PRIVATE LOGOS_CORE_LIBRARY)
|
||||
|
||||
# PE export table: nothing to do here. LOGOS_CORE_EXPORT now expands to
|
||||
# __declspec(dllexport) on Windows (see logos_core/logos_core.h), which both
|
||||
# exports the C API and, because GNU ld disables auto-export image-wide once any
|
||||
# symbol is dllexported, stops the internal C++ surface leaking.
|
||||
#
|
||||
# This deliberately REPLACES an earlier `-Wl,--export-all-symbols`. That did
|
||||
# export the C API, but it also exported all 13,252 symbols in the image --
|
||||
# LogosAPI's among them -- so any consumer linking both liblogos_core and the
|
||||
# qt-sdk static library failed with "multiple definition of `LogosAPI::LogosAPI'".
|
||||
# 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
|
||||
# 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.
|
||||
#
|
||||
# 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 AND TARGET logos-protocol::logos_protocol AND TARGET logos-qt-sdk::logos_qt_sdk)
|
||||
# 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_sdk_archive "$<TARGET_FILE:logos-qt-sdk::logos_qt_sdk>")
|
||||
|
||||
# --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_sdk_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_sdk_archive}" "*"
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/../cmake/gen-shared-exports.sh"
|
||||
"${_logos_protocol_archive}" "${_logos_qt_sdk_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)
|
||||
if(LOGOS_PORTABLE_BUILD)
|
||||
@@ -213,6 +298,15 @@ if(DEFINED LOGOS_PACKAGE_MANAGER_ROOT AND EXISTS "${LOGOS_PACKAGE_MANAGER_ROOT}/
|
||||
IMPORTED_LOCATION "${LOGOS_PACKAGE_MANAGER_LIBRARY}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${LOGOS_PACKAGE_MANAGER_ROOT}/include"
|
||||
)
|
||||
# On Windows a SHARED IMPORTED target links through its IMPORT library, and
|
||||
# CMake hard-errors at generate time when IMPORTED_IMPLIB is unset. find_library
|
||||
# returns libpackage_manager_lib.dll.a there (CMAKE_FIND_LIBRARY_SUFFIXES for
|
||||
# MinGW is .dll.a;.a), which is exactly the import library to link.
|
||||
if(WIN32)
|
||||
set_target_properties(package_manager_lib PROPERTIES
|
||||
IMPORTED_IMPLIB "${LOGOS_PACKAGE_MANAGER_LIBRARY}"
|
||||
)
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "LOGOS_PACKAGE_MANAGER_ROOT not set or ${LOGOS_PACKAGE_MANAGER_ROOT}/lib does not exist")
|
||||
endif()
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
#ifndef LOGOS_CORE_H
|
||||
#define LOGOS_CORE_H
|
||||
|
||||
// Define export macro for the library
|
||||
#if defined(LOGOS_CORE_LIBRARY)
|
||||
// Define export macro for the library.
|
||||
//
|
||||
// Windows needs __declspec, not visibility attributes: mingw-gcc accepts
|
||||
// __attribute__((visibility)) and silently ignores it on PE, so this header
|
||||
// used to export nothing at all and the build compensated with
|
||||
// -Wl,--export-all-symbols. That worked, but it exported the ENTIRE image --
|
||||
// 13,252 symbols, of which only 18 are this C API -- including every internal
|
||||
// C++ symbol such as LogosAPI's. Any consumer that links liblogos_core AND the
|
||||
// qt-sdk static library then gets the same definition twice and the link fails
|
||||
// with "multiple definition of `LogosAPI::LogosAPI'".
|
||||
//
|
||||
// Annotating the C API explicitly fixes that at the root, and does so twice
|
||||
// over: GNU ld disables PE auto-export image-wide as soon as ANY symbol is
|
||||
// dllexported, so the internal C++ surface stops leaking as a side effect.
|
||||
#if defined(_WIN32)
|
||||
# if defined(LOGOS_CORE_LIBRARY)
|
||||
# define LOGOS_CORE_EXPORT __declspec(dllexport)
|
||||
# else
|
||||
# define LOGOS_CORE_EXPORT __declspec(dllimport)
|
||||
# endif
|
||||
#elif defined(LOGOS_CORE_LIBRARY)
|
||||
# define LOGOS_CORE_EXPORT __attribute__((visibility("default")))
|
||||
#else
|
||||
# define LOGOS_CORE_EXPORT
|
||||
|
||||
Reference in New Issue
Block a user