Files
logos-basecamp/flake.nix
T
Dario Gabriel Lipicar 9b8cf6e47e feat(shell): ship main_ui as a plugin that links no logos runtime
Measured on the built artefact: main_ui.dylib DEFINES zero and IMPORTS zero of
TokenManager, StoreRegistry, LogosAPI, LogosAPIClient and logos_core_*, out of
3410 symbols read. It links Qt and nothing else from this workspace, which
nix/symbol-gate.nix enforces across the in-process image set.

Getting there needed the last non-Qt types off the boundary: the model
properties cross as QAbstractItemModel*, catalogInstallStageChanged carries an
int rather than InstallStage::Value, and the two prebuilt AppsFilterProxy
instances are declared in QML instead of owned by MainUIBackend. That last one
removes a real inversion -- PackageCoordinator called setRequiredPackages() on
a proxy the host held a pointer to; it now emits requiredPackagesResolved() and
QML binds to the republished property.

Window resolves the plugin, qobject_casts it to IShellView, checks
hostAbiVersion() against IShellHost_abi, and calls createShell(IShellHost*).
No error-label fallback widget: that degraded to something that looked like a
working app with an empty window.

Filter proxies read role constants off host-side models and InstallEnums is
used by nine host files, so app/interfaces/ gains the contract headers both
sides compile against -- the models inherit the role structs, leaving every
AppsModel::NameRole call site unchanged. The plugin's include path is
app/interfaces only, so including a host header does not compile.

Four things only running it finds:
  * Logos::DesignSystem may be linked by exactly ONE image -- both linked it
    and the app aborted with "Cannot add multiple registrations for
    Logos.Icons"; QML module registration is process-global
  * qmltyperegistrar emits no #include for a SOURCES header given as an
    absolute path outside the project
  * each qt_add_qml_module is its own target and inherits no include dirs
  * AUTOMOC pairs header<->cpp by same-basename-same-DIRECTORY, which the
    split breaks

tst_AppManagerView.qml grows five tests for the QML binding, checked with a
negative control: breaking one assertion fails qml-tests, so they run.
2026-08-22 16:42:13 -03:00

709 lines
40 KiB
Nix

{
description = "Logos Basecamp - Qt application with UI plugins";
inputs = {
logos-nix.url = "github:logos-co/logos-nix";
# Follow the same nixpkgs as logos-nix
nixpkgs.follows = "logos-nix/nixpkgs";
# Unpinned: feat/sdk-codegen-b3-d11 merged (logos-cpp-sdk#138), so the B3/B4
# SDK split and the generator entry points the module inputs below call are
# on master.
logos-cpp-sdk.url = "github:logos-co/logos-cpp-sdk";
logos-cpp-sdk.inputs.logos-protocol.follows = "logos-protocol";
# Unpinned: feat/per-client-token-store merged (logos-protocol#59), so
# TokenManager::forIdentity/isolateIdentity — which logos-qt-host calls — are
# on master, along with the host-services C ABI.
logos-protocol = {
url = "github:logos-co/logos-protocol";
inputs.logos-nix.follows = "logos-nix";
};
# The Qt HOST RUNTIME — LogosAPI, LogosAPIProvider, the LogosProviderBase
# macros and the legacy QMetaObject adapter — that LogosBasecamp links. It
# lived in logos-qt-sdk until the host split moved it here.
#
# NOTE: the runtime is no longer the only thing basecamp takes from
# logos-qt-sdk — see the logos-qt-sdk input below. Taking the runtime from
# BOTH would put two copies of the same 392 symbols on the link, which is
# why that input is headers-only and links nothing.
#
# Unpinned: feat/b4-qt-host-windows-target merged (logos-plugin-qt#19), so
# logos-qt-host is on master for every target. One master rev means one
# logos-qt-host in the closure, which is what the rev pin was protecting.
logos-plugin-qt = {
url = "github:logos-co/logos-plugin-qt";
inputs.logos-nix.follows = "logos-nix";
inputs.logos-protocol.follows = "logos-protocol";
inputs.logos-module.follows = "logos-module";
};
# logos-qt-sdk, HEADERS ONLY. It owns the Qt<->lp SEAM HEADERS
# (logos_qt_lp_bridge.h, logos_qt_wire.h) that are NOT part of the host
# runtime, and basecamp compiles a wrapper that opens with
# `#include "logos_qt_lp_bridge.h"`:
#
# nix/app.nix copies ${logosPackageManagerModule}/include/* into
# app/generated/, and since B5 the builder emits that Qt-typed dependency
# wrapper as a VENEER over the lp path. app/generated/package_manager_api.cpp
# therefore needs the seam header, and logos-qt-host does not ship it.
#
# This is the same reason logos-test-modules and logos-test-framework keep
# the input; only pure HOSTS (logos-standalone-app, logos-view-module-runtime)
# can drop it, because they load module plugins rather than compiling a
# module's dependency wrapper.
#
# Nothing links a logos-qt-sdk archive here — after B2a it is an INTERFACE
# library, and app/CMakeLists.txt consumes only its include directory.
#
# 8a06b870 is the tip of feat/sdk-codegen-b3-d11 and is the same rev
# logos-test-framework and the workspace pin; a different rev would put a
# second logos-qt-sdk in the closure.
# Unpinned: feat/sdk-codegen-b3-d11 merged (logos-qt-sdk#33). This pin was
# kept byte-identical to logos-test-framework's and the workspace's by hand,
# because NOTHING here makes logos-qt-sdk follow — and the convention had
# already drifted (logos-module-builder pinned aca2951, not 8a06b870), which
# is exactly the second-logos-qt-sdk-in-the-closure this guarded against.
# Tracking master puts every consumer on one rev structurally.
logos-qt-sdk = {
url = "github:logos-co/logos-qt-sdk";
inputs.logos-nix.follows = "logos-nix";
inputs.logos-protocol.follows = "logos-protocol";
inputs.logos-cpp-sdk.follows = "logos-cpp-sdk";
inputs.logos-plugin-qt.follows = "logos-plugin-qt";
};
# win_dll_search.h (included unconditionally by app/PluginLoader.cpp) arrives
# in logos-module's MODULE_LIB_HEADERS from fcaaf78 "fix(windows): resolve a
# plugin's private DLLs from its own directory". An older rev fails the build
# on EVERY platform, not just Windows — the header is installed unguarded.
logos-module.url = "github:logos-co/logos-module";
# The Qt-plugin module loader: the QtPluginFormatLoader that liblogos_core
# links AND the logos_host binary this app ships (nix/app.nix copies it out
# of ${logosLiblogos}/bin, which is just liblogos re-exporting this
# package's bin/ — see logos-liblogos/nix/bin.nix).
#
# Declared HERE, as a top-level input, purely so `logos-liblogos.inputs.
# default-module-loader` below has something to follow. liblogos's own lock
# pins e648735, the commit BEFORE the host-services grant, and that is the
# one hop of this chain basecamp was missing: 06134bfc is what teaches
# logos_host the `--host-services` flag, stamps it as the `hostServices`
# property on the LogosAPI object (right after loadModule()'s name check, so
# the identity the grant is bound to is already verified), and holds the
# host-side name-bound policy in QtPluginFormatLoader::buildArguments.
#
# Without it, capability_module at 07dba1f asks for `token_registry` and
# never receives it, so it fails CLOSED and every gated call becomes
# "ModuleProxy: rejecting unauthorized call". The module half of the chain
# is already in place: the plugin's generated glue reads that property and
# calls logos_module_grant_host_services -> lp_grant_host_services in its
# OWN image (verified with nm on the built plugin, not with strings).
#
# logos-protocol MUST follow, exactly as liblogos declares it for this same
# input: this loader's own lock pins protocol 03842db, which predates
# lp_grant_host_services entirely, and the emitted glue's
# `#if LOGOS_PROTOCOL_VERSION_MINOR >= 3` guard would compile away to
# nothing against it. c8bab12 is 0.4. logos-cpp-sdk / logos-qt-sdk are
# deliberately NOT followed: the loader's own lock already resolves them to
# e3744fb / c6be61d, which is exactly what liblogos's follows resolve to, so
# leaving them alone keeps this build byte-identical to liblogos's own
# except for the loader rev itself.
#
# Rev-pinned rather than tracking the branch: 06134bfc is the tip of
# feat/host-services-grant. Drop the rev once that merges, at which point
# liblogos's own lock can carry it and this input can go away.
logos-module-loader-qt = {
url = "github:logos-co/logos-module-loader-qt";
inputs.logos-protocol.follows = "logos-protocol";
};
# Rev-pinned: f2a15ef3 is the tip of fix/b4-align-protocol-with-qt-host —
# the liblogos aligned with the split host runtime and the per-client token
# store. liblogos_core.dll's export list (the other half of the Windows
# one-copy fix) lands there, not on master.
logos-liblogos = {
url = "github:logos-co/logos-liblogos";
inputs.default-module-loader.follows = "logos-module-loader-qt";
};
logos-package-manager.url = "github:logos-co/logos-package-manager";
logos-package-manager-module.url = "github:logos-co/logos-package-manager-module";
logos-package-downloader-module.url = "github:logos-co/logos-package-downloader-module";
# Rev-pinned: 07dba1f is the tip of feat/universal-capability, built against
# the new module-builder. Master's capability_module targets the pre-split
# runtime and would load a second host copy into this process.
#
# This was walked back to 0cb33fb (the legacy hand-written Qt plugin) for one
# reason: 07dba1f is a UNIVERSAL module that asks the host for the
# `token_registry` / `token_delivery` services and fails CLOSED when they do
# not arrive, and the loader pinned here could not grant them. That is fixed
# above, at logos-module-loader-qt — so the pin returns to the rev the
# comment always described.
logos-capability-module.url = "github:logos-co/logos-capability-module";
logos-package.url = "github:logos-co/logos-package";
# Rev-pinned: c932e1c is the tip of feat/universal-view-plugin — the
# generated view plugin. This UI is loaded in-process by the app, so it must
# come from the same generation as the host runtime above.
# Rev-pinned: f135195 is the tip of feat/universal-view-plugin — the
# generated view plugin. This UI is loaded in-process by the app, so it must
# come from the same generation as the host runtime above.
#
# f135195 is that branch WITH master merged in. The previous value, c932e1c,
# published packages for the four native systems only, so the cross build
# failed with "logos-module-builder: dependency 'package_manager' publishes
# no packages for x86_64-windows". Master fixed that (#68, a pure lock bump)
# but does not carry the view plugin, so tracking master here would trade a
# build failure for a runtime mismatch that still looks green. The merge
# gives both.
logos-package-manager-ui.url = "github:logos-co/logos-package-manager-ui";
logos-design-system.url = "github:logos-co/logos-design-system";
# Unpinned: logos-view-module-runtime#25 merged, so master carries the move
# onto the split host and no longer rev-pins logos-plugin-qt itself. That
# matters here beyond tidiness: a runtime naming a different plugin-qt puts a
# second logos-qt-host in the closure.
logos-view-module-runtime = {
url = "github:logos-co/logos-view-module-runtime";
inputs.nixpkgs.follows = "nixpkgs";
inputs.logos-cpp-sdk.follows = "logos-cpp-sdk";
};
nix-bundle-logos-module-install.url = "github:logos-co/nix-bundle-logos-module-install";
nix-bundle-dir.url = "github:logos-co/nix-bundle-dir";
logos-qt-mcp.url = "github:logos-co/logos-qt-mcp";
nix-bundle-appimage.url = "github:logos-co/nix-bundle-appimage";
nix-bundle-macos-app = {
url = "github:logos-co/nix-bundle-macos-app";
inputs.nixpkgs.follows = "nixpkgs";
inputs.nix-bundle-dir.follows = "nix-bundle-dir";
};
};
nixConfig = {
extra-substituters = [ "https://cache.nix.logos.co/public" ];
extra-trusted-public-keys = [ "public:l4HrXgL4nw246+LBh2SOJyhz64BoGegOYLheT/iIAPU=" ];
};
# logos-module-loader-qt is in the formals only because Nix passes EVERY
# declared input to this function and this set has no ellipsis. Nothing here
# uses it directly — it exists so logos-liblogos.inputs.default-module-loader
# has a top-level input to follow.
outputs = { self, nixpkgs, logos-nix, logos-cpp-sdk, logos-protocol, logos-plugin-qt, logos-qt-sdk, logos-module, logos-module-loader-qt, logos-liblogos, logos-package-manager, logos-package-manager-module, logos-package-downloader-module, logos-capability-module, logos-package, logos-package-manager-ui, logos-design-system, logos-view-module-runtime, logos-qt-mcp, nix-bundle-logos-module-install, nix-bundle-dir, nix-bundle-appimage, nix-bundle-macos-app }:
let
systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
# Build info (version + commit hashes) baked into the app binary so
# the Dashboard can render it. Commits come from the flake inputs'
# locked revs; self's rev is "dirty" when the checkout has uncommitted
# changes or is overridden via a path input.
revOf = input: input.rev or input.dirtyRev or "dirty";
buildInfo = {
# VERSION is only present on release branches. On master (pre-release
# CI builds) there is no VERSION file, so fall back to a
# "pre-release-{sha7}" string derived from self.rev — available on
# every clean CI checkout. Dirty local builds lack self.rev and get
# an empty string, which hides the badge (intentional for dev).
version = if builtins.pathExists ./VERSION
then nixpkgs.lib.removeSuffix "\n" (builtins.readFile ./VERSION)
else if (self ? rev) then "pre-release-${builtins.substring 0 7 self.rev}" else "";
commits = [
{ name = "logos-basecamp"; commit = revOf self; }
{ name = "logos-nix"; commit = revOf logos-nix; }
{ name = "logos-cpp-sdk"; commit = revOf logos-cpp-sdk; }
{ name = "logos-module"; commit = revOf logos-module; }
{ name = "logos-liblogos"; commit = revOf logos-liblogos; }
{ name = "logos-package-manager"; commit = revOf logos-package-manager; }
{ name = "logos-package-manager-module"; commit = revOf logos-package-manager-module; }
{ name = "logos-package-downloader-module"; commit = revOf logos-package-downloader-module; }
{ name = "logos-capability-module"; commit = revOf logos-capability-module; }
{ name = "logos-package"; commit = revOf logos-package; }
{ name = "logos-package-manager-ui"; commit = revOf logos-package-manager-ui; }
{ name = "logos-design-system"; commit = revOf logos-design-system; }
{ name = "logos-view-module-runtime"; commit = revOf logos-view-module-runtime; }
{ name = "logos-qt-mcp"; commit = revOf logos-qt-mcp; }
{ name = "nix-bundle-logos-module-install"; commit = revOf nix-bundle-logos-module-install; }
{ name = "nix-bundle-dir"; commit = revOf nix-bundle-dir; }
{ name = "nix-bundle-appimage"; commit = revOf nix-bundle-appimage; }
{ name = "nix-bundle-macos-app"; commit = revOf nix-bundle-macos-app; }
];
};
# The BUILD platform for a given target. Bundlers and code generators RUN
# during the build, so on the x86_64-windows cross target they must come
# from the build system -- taking them from packages.x86_64-windows would
# hand the builder a PE it cannot execute.
buildSystemFor = target:
if target == "x86_64-windows" then "x86_64-linux" else target;
# forAllSystems, plus the "x86_64-windows" pseudo-system. A cross
# derivation's `system` attr is its BUILD platform, so the Windows
# attributes evaluate anywhere and realise on x86_64-linux. Keying it as a
# system rather than a package-name suffix is what lets the 34
# `dep.packages.${system}.x` interpolations below stay untouched.
forAllSystems = f: logos-nix.lib.forAllTargets ({ system, pkgs }:
let buildSystem = buildSystemFor system; in f {
inherit system pkgs;
logosSdk = logos-cpp-sdk.packages.${system}.default;
# The SAME output carries BOTH the target headers/CMake package AND the
# logos-cpp-generator binary, so this is a SPLIT, not a swap: keep
# logosSdk for -DLOGOS_CPP_SDK_ROOT and use logosSdkBuild wherever the
# generator must RUN. Getting it backwards succeeds on native and, under
# cross, puts a PE on the builder's PATH -- the symptom is
# "logos-cpp-generator: command not found".
logosSdkBuild = logos-cpp-sdk.packages.${buildSystem}.default;
logosProtocolPkg = logos-protocol.packages.${system}.default;
logosQtHost = logos-plugin-qt.packages.${system}.logos-qt-host;
# HEADERS ONLY -- the Qt<->lp seam headers the generated dependency
# wrapper in app/generated/ includes. Nothing links this.
logosQtSdk = logos-qt-sdk.packages.${system}.default;
logosModule = logos-module.packages.${system}.default;
logosLiblogos = logos-liblogos.packages.${system}.default;
logosPackageManagerLibrary = logos-package-manager.packages.${system}.lib;
logosPackageManagerModule = logos-package-manager-module.packages.${system}.default;
logosPackageManagerModuleLib = logos-package-manager-module.packages.${system}.lib;
logosPackageDownloaderModule = logos-package-downloader-module.packages.${system}.default;
logosPackageDownloaderModuleLib = logos-package-downloader-module.packages.${system}.lib;
logosLiblogosPortable = logos-liblogos.packages.${system}.portable;
logosPackageManagerModuleLibPortable = logos-package-manager-module.packages.${system}.lib-portable;
logosCapabilityModule = logos-capability-module.packages.${system}.default;
logosPackageLib = logos-package.packages.${system}.lib;
# Headers-only output (include/ with logos/semver.hpp + semver/, no
# library). The app's AppsModel includes the shared semver comparator;
# it links nothing from lgx, so the headers output keeps liblgx out of
# the app entirely.
logosPackageHeaders = logos-package.packages.${system}.headers;
logosPackageManagerUI = logos-package-manager-ui.packages.${system}.default;
logosDesignSystem = logos-design-system.packages.${system}.default;
logosViewModuleRuntime = logos-view-module-runtime.packages.${system}.default;
# logos-qt-mcp is the QML inspector used by the UI test harness. It has
# no Windows target and is not needed to RUN the app -- nix/app.nix
# already takes `logosQtMcp ? null` and gates the inspector on it -- so
# Windows builds simply go without it. The inspector-dependent outputs
# (integration-test, shutdown-test, mcp-server) are correspondingly
# absent from the Windows package set; see the `packages` block.
logosQtMcp =
if system == "x86_64-windows" then null
else logos-qt-mcp.packages.${system}.default;
logosCppSdkSrc = logos-cpp-sdk.outPath;
logosLiblogosSrc = logos-liblogos.outPath;
logosPackageManagerModuleSrc = logos-package-manager-module.outPath;
logosCapabilityModuleSrc = logos-capability-module.outPath;
# Bundlers run ON the builder, so they are keyed by buildSystem, not by
# the target. nix-bundle-dir in particular is ELF/Mach-O only (its
# bundle.sh branches `file -b` -> Mach-O | ELF with no PE case), so on
# Windows it must not be invoked at all -- see nix/app.nix.
# Keyed by the TARGET, not buildSystem: the install bundler now does its
# own host/target split internally -- it takes lgpm from the build
# system (it runs there) and the .lgx bundler from the target (which
# decides the variant name and library extension). Keying the whole
# thing by buildSystem made it label a Windows package "linux-amd64"
# and look for a .so payload that was really a .dll.
installDev = nix-bundle-logos-module-install.bundlers.${system}.dev;
installPortable = nix-bundle-logos-module-install.bundlers.${system}.portable;
dirBundler = nix-bundle-dir.bundlers.${buildSystem}.qtApp;
});
in
{
packages = forAllSystems ({ pkgs, system, logosSdk, logosSdkBuild, logosProtocolPkg, logosQtHost, logosQtSdk, logosModule, logosLiblogos, logosLiblogosPortable, logosPackageManagerLibrary, logosPackageManagerModule, logosPackageManagerModuleLib, logosPackageManagerModuleLibPortable, logosPackageDownloaderModule, logosPackageDownloaderModuleLib, logosPackageLib, logosPackageHeaders, logosPackageManagerUI, logosCapabilityModule, logosDesignSystem, logosViewModuleRuntime, logosQtMcp, installDev, installPortable, dirBundler, ... }:
let
# Common configuration
common = import ./nix/default.nix {
inherit pkgs logosSdk logosProtocolPkg logosQtHost logosModule logosLiblogos;
};
src = ./.;
# Basecamp's own UI shell: a privilege-free plugin that links Qt and
# nothing else from this workspace, which nix/symbol-gate.nix enforces
# across the in-process image set. It builds from the SAME `src` as
# the app; its CMakeLists lives in src/ and can only see
# app/interfaces/, so it cannot include a host header even by accident.
mainUIPlugin = import ./nix/main-ui.nix {
inherit pkgs common src logosDesignSystem;
};
packageManagerUIPlugin = logosPackageManagerUI;
# Pre-installed modules/plugins (bundle + lgpm install in one step).
# Dev build: raw derivation (depends on /nix/store at runtime).
# Distributed build: portable self-contained bundle. Off Windows that
# portability comes from nix-bundle-dir; on Windows nix-bundle-lgx
# takes its own `mkWindowsPayload` path instead and does not call
# nix-bundle-dir at all. That is nix-bundle-lgx's business, not this
# flake's -- a MODULE must not carry the Qt/OpenSSL/runtime DLLs the
# host already ships in bin/, so the PE path needs a hostLibs strip
# that nix-bundle-dir does not have yet. Unrelated to binBundleDir
# below, which is the APP and therefore is the thing that ships them.
installedDev = map installDev [
logosPackageManagerModuleLib
logosPackageDownloaderModuleLib
logosCapabilityModule
packageManagerUIPlugin
];
installedDistributed = map installPortable [
logosPackageManagerModuleLibPortable
logosPackageDownloaderModuleLib
logosCapabilityModule
packageManagerUIPlugin
];
# App package (development build)
app = import ./nix/app.nix {
inherit pkgs common src logosModule logosLiblogos logosSdk logosProtocolPkg logosQtHost logosQtSdk logosDesignSystem logosViewModuleRuntime logosPackageManagerModule logosPackageDownloaderModule logosPackageHeaders buildInfo logosSdkBuild;
inherit logosQtMcp mainUIPlugin;
installedModules = installedDev;
};
# App package (distributed build for DMG/AppImage)
# Uses portable-compiled liblogos for portable variant selection
appDistributed = import ./nix/app.nix {
inherit pkgs common src logosModule logosSdk logosProtocolPkg logosQtHost logosQtSdk logosDesignSystem logosViewModuleRuntime logosPackageManagerModule logosPackageDownloaderModule logosPackageHeaders buildInfo logosSdkBuild;
inherit mainUIPlugin;
logosLiblogos = logosLiblogosPortable;
installedModules = installedDistributed;
portable = true;
enableInspector = false;
};
# Distributed build with inspector enabled (for macOS integration tests)
appDistributedWithInspector = import ./nix/app.nix {
inherit pkgs common src logosModule logosSdk logosProtocolPkg logosQtHost logosQtSdk logosDesignSystem logosViewModuleRuntime logosPackageManagerModule logosPackageDownloaderModule logosPackageHeaders buildInfo logosSdkBuild;
inherit logosQtMcp mainUIPlugin;
logosLiblogos = logosLiblogosPortable;
installedModules = installedDistributed;
portable = true;
enableInspector = true;
};
# macOS app for testing (distributed build with inspector enabled)
macosAppTest = if pkgs.stdenv.isDarwin then
nix-bundle-macos-app.lib.${system}.mkMacOSApp {
drv = appDistributedWithInspector;
name = "LogosBasecamp";
bundle = dirBundler appDistributedWithInspector;
icon = ./app/macos/logos.icns;
infoPlist = ./app/macos/Info.plist.in;
entitlements = ./app/macos/LogosBasecamp.entitlements;
}
else null;
macosApp = if pkgs.stdenv.isDarwin then
nix-bundle-macos-app.lib.${system}.mkMacOSApp {
drv = appDistributed;
name = "LogosBasecamp";
bundle = dirBundler appDistributed;
icon = ./app/macos/logos.icns;
infoPlist = ./app/macos/Info.plist.in;
entitlements = ./app/macos/LogosBasecamp.entitlements;
}
else null;
# Linux AppImage (only for Linux)
appImage = if pkgs.stdenv.isLinux then
import ./nix/appimage.nix {
inherit pkgs src;
app = appDistributed;
version = common.version;
}
else null;
# Self-contained directory bundle: appDistributed modules expect host Qt
# via @rpath; qtApp copies Qt frameworks into lib/ and rewrites the binary.
# (appDistributed alone is an intermediate used by AppImage / .app wrappers.)
withMainProgram = drv: drv.overrideAttrs (old: {
meta = (old.meta or {}) // {
mainProgram = "LogosBasecamp";
};
});
# dirBundler on EVERY platform, Windows included.
#
# The `winBundler = drv: drv` / `bundleFor` bypass that used to sit
# here was placed on an explicit condition: "the real fix is a PE
# branch in nix-bundle-dir's bundle.sh that skips relocation and keeps
# Qt staging; when that lands, DELETE bundleFor". It has landed, and
# this flake's root `nix-bundle-dir` input already resolves to it
# (f843b8ec, which is `main`), so the condition is met and the bypass
# is gone.
#
# Why it was never a working alternative: nix-bundle-dir does two
# separable jobs. (a) RELOCATION -- rewriting rpaths / install names
# so binaries stop pointing into /nix/store. (b) Qt STAGING -- the Qt
# plugin scan, the QML module scan and qt.conf generation; that half
# is FORMAT-AGNOSTIC and Windows needs it exactly as much as anywhere
# else, because Qt plugins and QML module DLLs are LoadLibrary'd and
# nothing in the import table reveals them.
#
# The bypass was argued for on the grounds that a PE needs none of
# (a). Only the rpath REWRITING half of that is true, and the
# measurement below is what corrects it: the un-bundled tree reaches
# a third of its bin/ through 12 SYMLINKS into /nix/store, which no
# amount of "PE imports are base names" makes portable. Phase 1's
# `cp -aL` is what dereferences them, and skipping the bundler
# skipped that as surely as it skipped (b).
#
# Measured, not assumed. Both trees were realised on x86_64-linux
# from ONE tree -- same appDistributed derivation, the only variable
# being whether dirBundler is applied -- and compared entry by entry:
#
# bypass(drv:drv) dirBundler
# entries 85 1740
# regular files 59 1657
# symlinks 12 0
# bytes 259 MB 479 MB
# *.dll in bin/ 33 88
# bin/qt.conf MISSING present
# lib/qt-6/…/platforms/qwindows.dll
# MISSING present
# lib/qt-6 (plugins + qml) MISSING 1533 files
# nix closure refs 12 1
#
# Those 12 symlinks are the part that matters most, and they are why
# "the bypass at least shipped the app" was never true. bin/Qt6Core
# .dll, Qt6Gui.dll, Qt6Widgets.dll, Qt6Network.dll, Qt6RemoteObjects
# .dll, libssl/libcrypto, libpng16, libzstd, libb2, pcre2 and
# double-conversion were SYMLINKS into /nix/store. Copy that tree to
# a Windows box -- the entire point of a portable bundle -- and every
# one of them dangles: 0xC0000135, no output, before main(). The
# bundled tree resolves all 12 into real files and has no symlink
# left. Its single remaining nix reference is an inert /nix string
# embedded in a PE's data, which imports nothing.
#
# 55 DLLs exist only in the bundled bin/: the Qt Quick / Controls /
# Labs set the fixpoint sweep pulls in once the QML modules are
# staged, plus libcurl and its TLS/HTTP2 chain mirrored beside the
# package_downloader module that imports them.
#
# The one thing the bundler does NOT carry over: README.txt and
# share/ (a .desktop file and a hicolor icon, 9 paths). That is not a
# Windows regression -- bundle.sh Phase 1 copies bin/, lib/ and
# extraDirs on EVERY platform, so the shipping Linux and macOS
# bundles have never had them either; the bypass "kept" them only by
# doing nothing at all. Both are dead weight off-store anyway:
# README.txt is a build-info file whose every line is a /nix/store
# path, and a .desktop file does nothing on Windows.
#
# Getting here also required three additions to the app's
# passthru.extraClosurePaths (see nix/app.nix) -- qtdeclarative,
# libjpeg.bin, sqlite.bin. Each was a build the bundler FAILED,
# naming the missing DLL and the plugin that imported it, rather than
# shipping a tree that dies before main(). That is the behaviour the
# bypass was hiding.
#
# NOT demonstrated, stated plainly:
#
# * None of this has been RUN on Windows, by this change or by CI,
# which has never executed a Windows binary. Every claim above is
# build-time and tree-shape only.
# * The two trees measured came from a harness that drops ONE entry
# from installedDistributed -- packageManagerUIPlugin -- because
# logos-package-manager-ui does not cross-compile at the rev this
# flake pins: its generated logos_sdk.h includes
# package_manager_api.h, which is not produced for the Windows
# target. That is pre-existing and independent of this change; the
# identical derivation fails when built straight from the pinned
# rev with no basecamp involved. Everything above concerns Qt
# staging and the DLL closure, which that one plugin does not
# participate in -- but the numbers are from a bundle missing it.
# * For the same reason `nix build .#packages.x86_64-windows.*`
# cannot succeed on this branch as pinned. A second, unrelated
# blocker sits in front of it at EVAL time: line ~119's
# logos-package-downloader-module has no x86_64-windows target, so
# the attribute cannot even be evaluated. Both blockers predate
# this change and are identical with the bypass in place.
binBundleDir = withMainProgram (dirBundler appDistributed);
binBundleDirInspector = withMainProgram (dirBundler appDistributedWithInspector);
in
{
# Individual outputs.
main-ui-plugin = mainUIPlugin;
package-manager-ui-plugin = packageManagerUIPlugin;
app = app;
# Self-contained flat directory (bin/ + lib/ with Qt).
# Run: nix run .#bin-bundle-dir
bin-bundle-dir = binBundleDir;
# Test-only twin of bin-bundle-dir WITH the QML inspector compiled in,
# so logos-qt-mcp can connect and drive the UI headlessly. Identical to
# the shipping bundle in every other respect.
#
# The inspector is a compile-time feature and is deliberately OFF in the
# shipping bin-bundle-dir / appimage / macos outputs — we do NOT ship
# the inspector in release builds. This output exists purely so the
# package-manager doc-test can install and exercise modules through
# the real bundled UI; it is not a release artifact.
# Build: nix build .#bin-bundle-dir-inspector
bin-bundle-dir-inspector = binBundleDirInspector;
# QML Inspector MCP server: nix build .#mcp-server -o result-mcp
mcp-server = logos-qt-mcp.packages.${system}.mcp-server;
# Full logos-qt-mcp package (includes test-framework, mcp-server, qt-plugin)
# Use: nix build .#logos-qt-mcp -o result-mcp
# Then: LOGOS_QT_MCP=./result-mcp node tests/ui-tests.mjs --ci ./result/bin/LogosBasecamp
logos-qt-mcp = logosQtMcp;
# Smoke test (also exposed as a package so it can be built standalone)
smoke-test = import ./nix/smoke-test.nix { inherit pkgs; appPkg = app; };
# One-runtime symbol gate. Asserts the logos C++ runtime (TokenManager,
# StoreRegistry, LogosAPI, LogosAPIClient) is DEFINED exactly once across
# the images that share one process — in liblogos_core, the single
# provider. A second definition is a second TokenManager and every
# cross-module call is refused at runtime with no build diagnostic.
# Build: nix build .#symbol-gate
symbol-gate = import ./nix/symbol-gate.nix { inherit pkgs; appPkg = app; };
# Negative control for the above. Plants a REAL duplicate runtime where
# an in-process consumer goes and asserts the gate REJECTS it. Ship both
# or neither: an absence assertion that has never been seen to fail is
# indistinguishable from a broken one.
# Build: nix build .#symbol-gate-negative
symbol-gate-negative = import ./nix/symbol-gate.nix {
inherit pkgs; appPkg = app; negativeControl = true;
};
# ui_qml sandbox-escape regression test (F-008). Focused C++ unit test:
# builds a real malicious QML plugin and asserts the production sandbox
# refuses to load it. Build: nix build .#sandbox-test
sandbox-test = import ./nix/sandbox-test.nix { inherit pkgs src; };
# Pure-model unit tests (AppsModel install-status logic, etc.). Same
# shape as sandbox-test — standalone QtTest project, no app launch,
# no IPC. Build: nix build .#unit-tests
unit-tests = import ./nix/unit-tests.nix {
inherit pkgs src logosPackageHeaders;
};
# QML component tests (Qt Quick Test)
qml-tests = import ./nix/qml-tests.nix { inherit pkgs src logosPackageHeaders; };
# Coverage report for the unit-test suite: same targets as
# .#unit-tests, compiled with --coverage and reported via gcovr.
# Report-only for now (failUnderLine = 0) — raise the threshold as
# the test plan phases land to make it a gate.
# Build: nix build .#coverage -L && open result/coverage.html
coverage = import ./nix/coverage.nix {
inherit pkgs src logosPackageHeaders;
failUnderLine = 0;
};
# Integration test (UI tests via Qt Inspector)
integration-test = import ./nix/integration-test.nix { inherit pkgs src logosQtMcp; appPkg = app; };
# Host-services grant guard. Asserts that a NON-"core" identity
# (ui-host running package_manager_ui) actually completes a
# capability-gated call chain — i.e. that capability_module really
# received its token_registry/token_delivery grant from the loader
# basecamp pins, rather than failing closed. See
# nix/host-services-test.nix and tests/host-services-assert.mjs.
# Build: nix build .#host-services-test
host-services-test = import ./nix/host-services-test.nix {
inherit pkgs src logosQtMcp; appPkg = app;
};
# Shutdown tests (SIGTERM, SIGINT, Ctrl+Q / ⌘Q). Spawns a fresh
# app per case and asserts orderly exit (code 0).
shutdown-test = import ./nix/shutdown-test.nix { inherit pkgs src logosQtMcp; appPkg = app; };
# Default package
default = app;
} // pkgs.lib.optionalAttrs pkgs.stdenv.isLinux {
bin-appimage = nix-bundle-appimage.lib.${system}.mkAppImage {
drv = appDistributed;
name = "logos-basecamp";
bundle = dirBundler appDistributed;
desktopFile = ./assets/logos-basecamp.desktop;
icon = ./app/icons/logos.png;
};
} // pkgs.lib.optionalAttrs pkgs.stdenv.isDarwin {
bin-macos-app = macosApp;
smoke-test-bundle = import ./nix/smoke-test.nix {
inherit pkgs;
appPkg = macosApp;
appBin = "${macosApp}/LogosBasecamp.app/Contents/MacOS/LogosBasecamp";
};
integration-test-bundle = import ./nix/integration-test.nix {
inherit pkgs src;
appPkg = macosAppTest;
inherit logosQtMcp;
appBin = "${macosAppTest}/LogosBasecamp.app/Contents/MacOS/LogosBasecamp";
};
host-services-test-bundle = import ./nix/host-services-test.nix {
inherit pkgs src;
appPkg = macosAppTest;
inherit logosQtMcp;
appBin = "${macosAppTest}/LogosBasecamp.app/Contents/MacOS/LogosBasecamp";
};
}
);
# nix run . → dev build (depends on /nix/store at runtime)
# nix run .#bin-bundle-dir → self-contained bundle (Qt frameworks in lib/)
apps = forAllSystems ({ system, ... }: {
default = {
type = "app";
program = "${self.packages.${system}.app}/bin/LogosBasecamp";
};
bin-bundle-dir = {
type = "app";
program = "${self.packages.${system}.bin-bundle-dir}/bin/LogosBasecamp";
};
});
checks = forAllSystems ({ pkgs, system, ... }: {
smoke-test = self.packages.${system}.smoke-test;
sandbox-test = self.packages.${system}.sandbox-test;
unit-tests = self.packages.${system}.unit-tests;
qml-tests = self.packages.${system}.qml-tests;
integration-test = self.packages.${system}.integration-test;
shutdown-test = self.packages.${system}.shutdown-test;
host-services-test = self.packages.${system}.host-services-test;
symbol-gate = self.packages.${system}.symbol-gate;
symbol-gate-negative = self.packages.${system}.symbol-gate-negative;
});
devShells = forAllSystems ({ pkgs, logosSdk, logosProtocolPkg, logosQtHost, logosModule, logosLiblogos, logosPackageManagerLibrary, logosPackageManagerModule, logosCapabilityModule, logosPackageLib, logosDesignSystem, logosCppSdkSrc, logosLiblogosSrc, logosPackageManagerModuleSrc, logosCapabilityModuleSrc, ... }: {
default = pkgs.mkShell {
nativeBuildInputs = [
pkgs.cmake
pkgs.ninja
pkgs.pkg-config
];
buildInputs = [
pkgs.qt6.qtbase
pkgs.qt6.qtremoteobjects
pkgs.zstd
pkgs.krb5
pkgs.abseil-cpp
];
shellHook = ''
# Nix package paths (pre-built for host system)
export LOGOS_CPP_SDK_ROOT="${logosSdk}"
export LOGOS_PROTOCOL_ROOT="${logosProtocolPkg}"
export LOGOS_QT_HOST_ROOT="${logosQtHost}"
export LOGOS_MODULE_ROOT="${logosModule}"
export LOGOS_LIBLOGOS_ROOT="${logosLiblogos}"
export LOGOS_PACKAGE_MANAGER_ROOT="${logosPackageManagerLibrary}"
export LOGOS_CAPABILITY_MODULE_ROOT="${logosCapabilityModule}"
export LGX_ROOT="${logosPackageLib}"
export LOGOS_DESIGN_SYSTEM_ROOT="${logosDesignSystem}"
# Source paths for iOS builds (from flake inputs)
export LOGOS_CPP_SDK_SRC="${logosCppSdkSrc}"
export LOGOS_LIBLOGOS_SRC="${logosLiblogosSrc}"
export LOGOS_PACKAGE_MANAGER_MODULE_SRC="${logosPackageManagerModuleSrc}"
export LOGOS_CAPABILITY_MODULE_SRC="${logosCapabilityModuleSrc}"
echo "Logos Basecamp development environment"
echo ""
echo "Nix packages (host builds):"
echo " LOGOS_CPP_SDK_ROOT: $LOGOS_CPP_SDK_ROOT"
echo " LOGOS_MODULE_ROOT: $LOGOS_MODULE_ROOT"
echo " LOGOS_LIBLOGOS_ROOT: $LOGOS_LIBLOGOS_ROOT"
echo " LOGOS_PACKAGE_MANAGER_ROOT: $LOGOS_PACKAGE_MANAGER_ROOT"
echo " LOGOS_CAPABILITY_MODULE_ROOT: $LOGOS_CAPABILITY_MODULE_ROOT"
echo " LGX_ROOT: $LGX_ROOT"
echo " LOGOS_DESIGN_SYSTEM_ROOT: $LOGOS_DESIGN_SYSTEM_ROOT"
echo ""
echo "Source paths (for iOS builds):"
echo " LOGOS_CPP_SDK_SRC: $LOGOS_CPP_SDK_SRC"
echo " LOGOS_LIBLOGOS_SRC: $LOGOS_LIBLOGOS_SRC"
echo " LOGOS_PACKAGE_MANAGER_MODULE_SRC: $LOGOS_PACKAGE_MANAGER_MODULE_SRC"
echo " LOGOS_CAPABILITY_MODULE_SRC: $LOGOS_CAPABILITY_MODULE_SRC"
'';
};
});
};
}