Dario Gabriel LipicarandClaude Opus 5 b7e2280738 feat(deps): evaluate a dependency's version range instead of only carrying it
resolveDependencies called build(dep.name) and discarded dep.version one
character from where it was needed, so a manifest declaring
{"name":"lib","version":"^2.0.0"} with lib 1.0.0 installed reported
"installed". The range travelled intact from metadata.json through the .lgx,
`lgx verify` and `lgpm install` onto disk, and was asked a question nowhere.

build() now takes the whole PackageDependency, because the range and the
signer live on the EDGE rather than on the package and so have to travel with
the recursion, and compares it with logos::semver::satisfies -- already
linked, already called at :114. This was a wiring gap, not a missing
capability; no new semver, no build-system change. A dependency installed at
a version its dependant refuses now reports DependencyStatus::VersionMismatch.

Precedence is deliberate: ABSENCE OUTRANKS MISMATCH. A range can only be
judged against a version we actually have, and "install it" is the remedy
either way, so a dependency that is both absent and constrained still reports
not_installed -- the stronger fact, and the one the user can act on. Naming
the weaker one would point at the wrong fix. Either way the declared range
rides along on the node, so a caller can say WHICH version to install.

An unparseable range is treated as unsatisfied rather than ignored: silently
dropping a typo'd range would fail open, and lgx verify already rejects the
syntax upstream, so a manifest reaching us with one bypassed that gate.

The signer is carried as data and compared by nobody -- who may sign a
dependency is a trust decision that does not belong to the scanner.

VersionMismatch is APPENDED to the enum, never inserted:
logos-package-manager-module compiles against this header and links
libpackage_manager_lib at run time, so existing enumerator values are ABI.
Both new node fields are omitted from the JSON when absent, so a tree of
bare-name dependencies -- every package in the workspace today -- serialises
byte-identically to before.

Tests 137 -> 148. Red on the base, asserted through dependencyStatusToString
so the probe compiles without the new enumerator:
  Expected: "version_mismatch"  Which is: "installed"

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 11:45:18 -03:00

logos-package-manager

C++ library and CLI for local Logos package management — installing .lgx packages, scanning installed modules, platform variant selection, and LGX extraction.

This repo handles local operations only. It does not fetch packages from the network — that is the responsibility of logos-package-downloader.

Library API

#include <package_manager_lib.h>

PackageManagerLib pm;

// Configure embedded directories (multiple, read-only at runtime)
pm.setEmbeddedModulesDirectory("/path/to/embedded/modules");       // clears + sets first
pm.addEmbeddedModulesDirectory("/path/to/more/embedded/modules");  // appends additional
pm.setEmbeddedUiPluginsDirectory("/path/to/embedded/plugins");
pm.addEmbeddedUiPluginsDirectory("/path/to/more/embedded/plugins");

// Configure user directories (single, writable, where new packages are installed)
pm.setUserModulesDirectory("/path/to/user/modules");
pm.setUserUiPluginsDirectory("/path/to/user/plugins");

// Install from a local .lgx file (auto-detects type, installs to user directory)
std::string errorMsg;
std::string installedPath = pm.installPluginFile("/path/to/module.lgx", errorMsg);
// skipIfNotNewer: skip installation if an equal-or-newer version is already installed
std::string path = pm.installPluginFile("/path/to/module.lgx", errorMsg, /*skipIfNotNewer=*/true);

// Scan installed packages (returns struct vectors)
// Each InstalledPackage carries all manifest.json fields plus installDir,
// mainFilePath, installType, and a nested Hashes struct. For ui_qml packages,
// `view` is the required QML entry point and `mainFilePath` is the optional
// backend plugin path.
std::vector<InstalledPackage> modules   = pm.getInstalledModules();     // type=core only
std::vector<InstalledPackage> uiPlugins = pm.getInstalledUiPlugins();   // type=ui,ui_qml only
std::vector<InstalledPackage> all       = pm.getInstalledPackages();    // all types

// Direct dependencies are already on InstalledPackage::dependencies; the
// library does not ship a parallel name-only API. Callers that want a flat
// transitive list build it over resolveDependencies / resolveDependents
// (see the `lgpm` CLI `deps` / `dependents` subcommands for a reference
// implementation — ~15 lines of BFS with a seen-set).
//
// A manifest `dependencies[]` entry is either a plain name or an object
// { name, version?, signer? } (LGX spec, "Dependency entries"). Both forms
// declare the same edge, so `dependencies` lists the NAME of every entry.
// The entries that also declared a semver range or a signer DID are repeated
// in `dependencyConstraints` with the constraint attached; the library records
// them but does not evaluate them (that is the resolver's job, in
// logos-package-downloader).
for (const PackageDependency& c : pkg.dependencyConstraints)
    std::cout << c.toString() << "\n";   // e.g. waku_module ^1.2.0 [signer=did:jwk:…]

// Dependency graph as a rich struct tree. Returns std::nullopt when the
// package is not installed. Children with status == NotInstalled are
// leaves; Cycle marks a back-edge during the walk.
std::optional<DependencyTreeNode> tree = pm.resolveDependencies("waku_module");

// Reverse-edge tree, rooted at the queried package. `children` are the
// direct reverse neighbours; their `children` are transitive ones.
// Returns std::nullopt when the package is not installed.
std::optional<DependentTreeNode> users = pm.resolveDependents("waku_module");

// Both tree types ship a BFS-with-dedup `flatten()` — handy for callers
// that only want "every unique descendant", no tree shape.
if (tree)  std::vector<DependencyTreeNode> allDeps   = tree->flatten();
if (users) std::vector<DependentTreeNode>  allUsers  = users->flatten();

// JSON serialization lives in a separate header (opt-in; the core lib
// header does not pull in nlohmann::json). nlohmann ADL `to_json` hooks
// are provided for every struct above.
#include <package_manager_json.h>
#include <nlohmann/json.hpp>
std::string modulesJson = nlohmann::json(modules).dump(2);

// Platform variant helpers
std::string variant = PackageManagerLib::currentPlatformVariant();     // e.g. "darwin-arm64"
std::vector<std::string> variants = PackageManagerLib::platformVariantsToTry(); // ordered fallback list

// Low-level LGX extraction
pm.extractLgxPackage("/path/to/module.lgx", "/output/dir", errorMsg);
pm.copyLibraryFromExtracted("/extracted/dir", "/target/dir",
                            /*isCoreModule=*/true, outModuleName, errorMsg);

// Signature verification policy
pm.setSignaturePolicy(SignaturePolicy::WARN);     // NONE, WARN (default), REQUIRE
pm.setKeyringDirectory("/path/to/trusted-keys");  // default: ~/.config/logos/trusted-keys/

// Standalone signature verification
SignatureVerificationResult sigInfo = pm.verifyPackageSignature("/path/to/module.lgx");
// sigInfo.is_signed, sigInfo.signature_valid, sigInfo.package_valid,
// sigInfo.signer_did, sigInfo.signer_name, sigInfo.signer_url, sigInfo.trusted_as, sigInfo.error

// Utilities
bool newer = PackageManagerLib::versionGreaterOrEqual("1.2.0", "1.1.0");
PackageManagerLib::copyDirectoryContents("/src", "/dst", errorMsg);

C API

A C-compatible API is available via lgpm.h for use from non-C++ consumers:

#include <lgpm.h>

lgpm_context_t ctx = lgpm_create();
lgpm_set_embedded_modules_dir(ctx, "/path/to/embedded/modules");
lgpm_add_embedded_modules_dir(ctx, "/path/to/more/embedded/modules");
lgpm_set_user_modules_dir(ctx, "/path/to/user/modules");

// Signature policy
lgpm_set_signature_policy(ctx, "require");  // "none", "warn", "require"
lgpm_set_keyring_path(ctx, "/path/to/trusted-keys");

char* result = lgpm_install_file(ctx, "/path/to/module.lgx", false, NULL, NULL);
lgpm_free_string(result);

// Flat name-only dependency queries (mirror liblogos' logos_core_get_module_*).
// Null-terminated array; caller frees with lgpm_free_string_array.
const char** deps       = lgpm_get_module_dependencies(ctx, "waku_module", false);
const char** allDeps    = lgpm_get_module_dependencies(ctx, "waku_module", true);
const char** dependents = lgpm_get_module_dependents(ctx, "waku_module", false);
const char** allUsers   = lgpm_get_module_dependents(ctx, "waku_module", true);
for (size_t i = 0; deps[i]; ++i) printf("%s\n", deps[i]);
lgpm_free_string_array(deps);
lgpm_free_string_array(allDeps);
lgpm_free_string_array(dependents);
lgpm_free_string_array(allUsers);

lgpm_free(ctx);

CLI (lgpm)

lgpm [options] <command> [arguments]

Commands:
  install --file <path>       Install from a local LGX file
  install --dir <path>        Install all LGX files in a directory
  list                        List installed packages
  info <package>              Show installed package details
  deps <package>              List packages that <package> depends on
  dependents <package>        List installed packages that depend on <package>

Options:
  --modules-dir <path>        Target directory for core modules
  --ui-plugins-dir <path>     Target directory for UI plugins
  --recursive, -r             For deps/dependents: walk the graph transitively
  --json                      Output in JSON format
  --allow-unsigned            Accept unsigned packages without warning
  --require-signatures        Reject unsigned packages and untrusted signers
  --keyring <path>            Override keyring directory
  -h, --help                  Show help
  -v, --version               Show version

Examples

# Install a local .lgx package
lgpm --modules-dir ./modules install --file ./waku_module.lgx

# Install all .lgx files in a directory
lgpm --modules-dir ./modules --ui-plugins-dir ./plugins install --dir ./packages/

# List installed modules
lgpm --modules-dir ./modules --ui-plugins-dir ./plugins list

# Show package info (JSON)
lgpm --modules-dir ./modules info waku_module --json

# What does waku_module depend on? (one name per line)
lgpm --modules-dir ./modules deps waku_module
lgpm --modules-dir ./modules deps waku_module --recursive

# What depends on waku_module?
lgpm --modules-dir ./modules --ui-plugins-dir ./plugins dependents waku_module
lgpm --modules-dir ./modules --ui-plugins-dir ./plugins dependents waku_module -r --json

How to Build

The package manager ships as a C++ library plus the lgpm CLI. There are two flavors: a dev build for local iteration and a portable build for distribution. They differ in which .lgx platform variant the CLI selects when installing packages — the dev build picks the -dev-suffixed variant (e.g. darwin-arm64-dev), the portable build picks the plain one (e.g. darwin-arm64). Installing a package built for the wrong flavor fails with a variant-mismatch error.

Dev Build

A standard Nix derivation whose dependencies live in /nix/store. It is the fastest way to iterate during development but is not portable — it only runs on the machine that built it. It matches the local/dev .lgx packages produced by nix-bundle-lgx (the default, non-portable bundler).

nix build                        # library + CLI (combined, dev)
nix build '.#lib'                # library only
nix build '.#cli'                # CLI only
./result/bin/lgpm --help

Portable Builds

Portable builds select the plain platform variant, matching portable .lgx packages — releases from logos-modules, downloads via logos-package-downloader, or bundles generated with nix bundle --bundler github:logos-co/nix-bundle-lgx#portable .... The cli-bundle-dir and cli-appimage outputs go further and are fully self-contained — no /nix/store references at runtime — for distribution.

Output Platform Format
cli-portable / lib-portable Linux, macOS Nix derivation (portable variant selection)
cli-bundle-dir Linux, macOS Self-contained flat directory with bin/ and lib/
cli-appimage Linux Single-file .AppImage executable
Portable Nix derivation (all platforms)
nix build '.#cli-portable'       # CLI only
nix build '.#lib-portable'       # library only
./result/bin/lgpm --help
Self-contained directory bundle (all platforms)
nix build '.#cli-bundle-dir'
./result/bin/lgpm --help
Linux AppImage (Linux only)
nix build '.#cli-appimage'
./result/lgpm.AppImage --help

Development Shell

nix develop

Note: In zsh, quote targets containing # to prevent glob expansion (e.g., '.#cli').

If you don't have flakes enabled globally:

nix build --extra-experimental-features 'nix-command flakes'

Testing

nix flake check                  # run all tests
nix build .#tests                # build and run tests

Dependencies

  • logos-package — LGX format library
  • nlohmann_json — JSON parsing

Executable doc-tests

The doctests/ directory holds executable docs powered by logos-doctest

Spec What it covers
lgpm-cli.test.yaml Builds lgpm, hand-crafts two .lgx packages, installs them, and exercises every subcommand (install, list, info, deps, dependents).
install-real-module.test.yaml Clones a real module (logos-accounts-module), builds its .lgx via the module's #lgx flake output, and installs it with lgpm.
./doctests/run.sh

Or invoke the doctest CLI directly on a single spec:

# Execute a spec end-to-end (builds lgpm, installs packages, asserts output)
nix run github:logos-co/logos-doctest -- run doctests/lgpm-cli.test.yaml --verbose

# Render the Markdown doc from a spec (into the gitignored outputs/ dir)
nix run github:logos-co/logos-doctest -- generate doctests/lgpm-cli.test.yaml -o doctests/outputs/lgpm-cli.md

Add --report report.html to the run to get a self-contained two-column HTML report — the rendered doc on the left, the exact commands that ran and their output on the right — handy for reviewing a run or publishing from CI:

nix run github:logos-co/logos-doctest -- run doctests/lgpm-cli.test.yaml --report report.html
S
Description
No description provided
Readme
3.6 MiB
Languages
C++ 82.1%
Shell 6.5%
Nix 5.5%
CMake 2.6%
C 2.6%
Other 0.7%