Files
logos-liblogos/nix/modules.nix
T
503587797d 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>
2026-08-12 10:54:58 -03:00

85 lines
3.2 KiB
Nix

# 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,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;
}
''
mkdir -p $out/modules/capability_module
# 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=""
for f in $out/modules/capability_module/*; do
if [ -f "$f" ]; then
pluginFile="$(basename "$f")"
break
fi
done
if [ -z "$pluginFile" ]; then
echo "Error: No capability_module library found under ${capabilityModule}/lib" >&2
ls -la ${capabilityModule}/lib >&2 || true
exit 1
fi
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",
"$platform-arm64$suffix": "$pluginFile",
"$platform-x86_64$suffix": "$pluginFile",
"$platform-aarch64$suffix": "$pluginFile"
}
}
EOF
echo "Modules directory contents:"
ls -laR $out/modules/
''