Files
Dario LipicarandClaude Opus 5 8554ecd77b test(abi): assert the generated cdylib defines every declared module-impl export (#144)
* test(abi): assert the generated cdylib defines every declared module-impl export

logos-protocol DECLARES the module-impl C ABI; this generator emits the
definitions. Those are independent facts, and the gap has shipped twice —
grant_host_services at protocol 0.3, the teardown pair at 0.5 — each time
as an "undefined symbol" at dlopen, on Linux only, with the runtime still
reporting the module as LOADED.

checks.<system>.module-impl-abi generates in all four shapes the emitter
can be reached in — --from-header, --lidl, a zero-method module, and one
with records and events — and diffs what each DEFINES against the list
logos-protocol publishes (packages.<sys>.module-impl-abi, protocol#66).
Both the list and the protocol version come from that one output, so there
is no version arithmetic here and nothing hardcoded.

The interesting part is what makes this NOT vacuous, because the obvious
implementation is. The generated .cpp carries the version guards as TEXT,
so a plain grep counts all ten exports at every protocol version and the
check would pass forever. unifdef resolves them — but unifdef given an
expression it cannot evaluate exits 0 and leaves the text alone, which
looks exactly like "nothing was conditional". So:

  * no residual '#if/#else/#endif' may survive the resolution;
  * the same pipeline runs twice, at the real MINOR and at MINOR=0, and
    the MINOR=0 set must be a STRICT SUBSET of the real one.

That second assertion is the headline guard. It fires if unifdef no-opped,
if the version never reached it, or if the extraction were counting
mentions rather than definitions — without naming a single symbol or
version, so it does not go stale as the ABI grows.

Proven to fail. Commenting out the logos_module_string_free emission turns
it red naming that symbol, while `tests` and `generator-cli` both stay
GREEN — this is the only thing in the repo that notices a deleted export.
Moving the teardown guard from >= 5 to >= 6 turns it red naming both
teardown symbols, with the probe moving 10 -> 8, so the version resolution
is live rather than a name list.

CI runs it: ci.yml only ever built '.#tests' plus three binaries, so the
`checks` attrset was never evaluated at all — generator-cli has never run.

TODO in flake.nix: logos-protocol is rev-pinned to the protocol#66 branch.
Re-point at master once it merges; a rev pin never moves under
`nix flake update`, so leaving it would freeze the declared side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(deps): track logos-protocol master now that module-impl-abi has landed

logos-protocol#66 merged as 480f40f, so the temporary rev pin can go. A rev
pin never moves under `nix flake update`, so leaving it would freeze the
DECLARED side of this check at 0.5 and quietly stop catching anything a
later protocol adds — the exact silence the check exists to break.

Restores the master-tracking comment the pin displaced, with a line noting
that checks.module-impl-abi now also consumes
packages.<system>.module-impl-abi from this input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:17:17 -03:00

125 lines
5.3 KiB
Nix

{
description = "Logos C++ SDK";
inputs.logos-nix.url = "github:logos-co/logos-nix";
inputs.nixpkgs.follows = "logos-nix/nixpkgs";
# The protocol layer (transports, token exchange, lp_* C ABI). Follows our
# logos-nix so both repos resolve the identical nixpkgs/Qt pin — the QRO
# wire is Qt-version-sensitive.
#
# Master-tracking. This was rev-pinned to feat/per-client-token-store while
# logos_host_services.h's trust-root surface (lp_token_keys,
# lp_inform_module_token_to, lp_grant_host_services) lived only on that
# branch, with protocol master still at LOGOS_PROTOCOL_VERSION_MINOR 2 —
# the `tests` check could not compile against it. That branch has merged
# (logos-protocol#59): master is 0.4.0 and carries all three.
#
# checks.module-impl-abi additionally consumes
# packages.<system>.module-impl-abi from here (logos-protocol#66).
inputs.logos-protocol.url = "github:logos-co/logos-protocol";
inputs.logos-protocol.inputs.logos-nix.follows = "logos-nix";
# The canonical, language-neutral LIDL frontend (lexer/parser/AST/serializer/
# validator) the code generator links. Follows our logos-nix so it resolves
# the identical nixpkgs pin.
inputs.logos-lidl.url = "github:logos-co/logos-lidl";
inputs.logos-lidl.inputs.logos-nix.follows = "logos-nix";
outputs = { self, nixpkgs, logos-nix, logos-protocol, logos-lidl }:
let
systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
# Adds the "x86_64-windows" pseudo-system; a cross derivation's `system`
# is its BUILD platform, so it evaluates anywhere and realises on Linux.
forAllTargets = logos-nix.lib.forAllTargets;
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f {
pkgs = import nixpkgs { inherit system; };
});
in
{
packages = forAllTargets ({ pkgs, ... }:
let
# Common configuration
common = import ./nix/default.nix { inherit pkgs; };
src = ./.;
# Individual package components
bin = import ./nix/bin.nix { inherit pkgs common src logos-protocol; logos-lidl = logos-lidl.packages.${pkgs.system}.logos-lidl; };
lib = import ./nix/lib.nix { inherit pkgs common src logos-protocol; };
include = import ./nix/include.nix { inherit pkgs common src logos-protocol; };
tests = import ./nix/tests.nix { inherit pkgs common src logos-protocol; logos-lidl = logos-lidl.packages.${pkgs.system}.logos-lidl; };
# Combined SDK package. We re-declare propagatedBuildInputs on
# the join so downstream Nix derivations that depend on the
# combined `sdk` (rather than the nested `lib`) still inherit
# OpenSSL / Boost / nlohmann_json — symlinkJoin doesn't
# forward propagation from its `paths` attribute. Qt is
# excluded for the same setup-hook ordering reason as in
# `nix/lib.nix`; consumers must list qt6.qtbase +
# qt6.wrapQtAppsNoGuiHook themselves.
sdk = pkgs.symlinkJoin {
name = "logos-cpp-sdk";
paths = [ bin lib include ];
propagatedBuildInputs = common.propagatedBuildInputs;
};
in
{
# Individual outputs
logos-cpp-bin = bin;
logos-cpp-lib = lib;
logos-cpp-include = include;
inherit tests;
# Combined outputs (for backward compatibility)
logos-cpp-sdk = sdk;
cpp-generator = bin; # Alias for backward compatibility
# Default package
default = sdk;
}
);
checks = forAllSystems ({ pkgs }:
let
common = import ./nix/default.nix { inherit pkgs; };
src = ./.;
tests = import ./nix/tests.nix { inherit pkgs common src logos-protocol; logos-lidl = logos-lidl.packages.${pkgs.system}.logos-lidl; };
generator = import ./nix/bin.nix { inherit pkgs common src logos-protocol; logos-lidl = logos-lidl.packages.${pkgs.system}.logos-lidl; };
in
{
inherit tests;
# Runs the BINARY. The gtest suite links the generator's internals and
# never executes it, so a retired CLI flag can only be asserted here.
generator-cli = import ./nix/tests-generator-cli.nix {
inherit pkgs common generator;
};
# Diffs what the cdylib backend DEFINES against the module-impl C
# ABI logos-protocol DECLARES. Nothing else here can catch that gap:
# a module with a missing export links clean and only dies at
# dlopen(), on Linux. See nix/tests-module-impl-abi.nix.
module-impl-abi = import ./nix/tests-module-impl-abi.nix {
inherit pkgs common src generator;
module-impl-abi = logos-protocol.packages.${pkgs.system}.module-impl-abi;
};
}
);
devShells = forAllSystems ({ pkgs }: {
default = pkgs.mkShell {
nativeBuildInputs = [
pkgs.cmake
pkgs.ninja
pkgs.pkg-config
];
buildInputs = [
pkgs.qt6.qtbase
pkgs.qt6.qtremoteobjects
pkgs.gtest
pkgs.boost
pkgs.openssl
pkgs.nlohmann_json
];
};
});
};
}