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:
Dario Lipicar
2026-08-12 10:54:58 -03:00
committed by GitHub
co-authored by Cursor Claude Opus 5
parent 56aa8bb45c
commit 503587797d
11 changed files with 61392 additions and 2902 deletions
+41 -18
View File
@@ -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
+101
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+44 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+12
View File
@@ -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;
+94
View File
@@ -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()
+21 -2
View File
@@ -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