Files
Dario LipicarandClaude Opus 4.8 8d4236f945 feat: one semver implementation for the packaging stack (#30)
* feat: one semver implementation for the packaging stack

Version handling was duplicated across the packaging repos and the copies
disagreed with each other and with the spec. This makes logos-package the
single home for it.

Precedence is defined by SemVer 2.0.0, so it is delegated to a vendored
library (z4kn4fein/cpp-semver v0.4.0, MIT, single header, C++17):

  - numeric pre-release identifiers compare NUMERICALLY, so 1.0.0-rc.2 <
    1.0.0-rc.11. Every previous copy got this wrong -- the downloader
    compared the whole pre-release tag as one ASCII string, while lgpm and
    the package-manager UI dropped the tag entirely (atoi("0-rc1") == 0),
    so a pre-release compared EQUAL to its own release.
  - build metadata is ignored for precedence (spec 10).

Ranges (^ ~ x * || >= <= > < =) are NOT in the semver spec -- they are an
npm convention the manifests already use -- so that layer is written once,
in include/logos/semver.hpp, on top of the library. It adopts npm's
pre-release rule: a range never matches a pre-release unless the range
itself names one at the same major.minor.patch. Without it `^1.0.0` matches
`2.0.0-alpha`, i.e. an unreleased alpha of the next major satisfies a caret
range on 1.x and can be resolved as a dependency.

Exposed three ways so every consumer reaches the same code:

  - include/logos/semver.hpp, header-only and dependency-free (the
    package-manager UI is a QML plugin with no native link deps and must be
    able to include it without dragging in zlib/ICU/libsodium).
  - the lgx_semver_* C ABI.
  - `lgx semver compare|sort|satisfies|valid|valid-range`, for the catalog
    builder (logos-modules-release-tool's index.py), which is stdlib-only
    but already requires lgx -- so it can order versions without growing a
    second implementation in Python that could drift.

Manifest range validation now delegates here too; it was a separate regex
that accepted ranges the resolver could not actually evaluate.

Validation is strict but comparison is lenient (parse vs parse_lenient):
real manifests carry partial versions like "1.0", and treating those as
invalid at compare time would sort them below every real version and
silently reorder existing catalogs.

tests/test_semver.cpp covers the spec's own precedence chain verbatim, the
pre-release cases that were previously untested everywhere, and the npm
range rules.

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

* ci: print full nix build logs

Without -L a failing build is truncated to its last 25 lines, which hides
the first compiler error.

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

* fix: include <cstdint> before cpp-semver (Linux/libstdc++ build)

cpp-semver 0.4.0 uses uint64_t throughout but never includes <cstdint>
itself. libc++ drags it in transitively via <string>/<regex>, so this built
on macOS -- but libstdc++ stopped doing that in GCC 13, and on Linux the
header failed to parse with "'uint64_t' does not name a type". That
collapsed semver::version, which surfaced as a wall of bogus "has no member
named 'major'" errors pointing at our own header rather than the real cause.

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

* feat: add a headers-only package output

For consumers that need the shared semver implementation
(include/logos/semver.hpp) but must NOT link liblgx.

The package-manager UI is a Qt plugin, and the module builder copies every
*.so/*.dylib an external library ships into the plugin's output lib/.
ui-host then scans that directory and tries to load each file as a Qt
plugin: pointing it at the `lib` output put liblgx.dylib there, ui-host
failed with "is not a Qt plugin", and the entire UI never rendered.

Shipping no library at all is what makes `headers` safe -- there is nothing
to copy.

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

* fix: reject silently-widening ranges; install cpp-semver header via FetchContent

Addresses Copilot review on #30.

valid_range accepted '1.x.3', '1..2' and 'x.1', silently widening them to
'1.x' / '1' / '*' — a range claiming more than it means, which breaks the
syntax-validation contract. parse_partial now requires every component after
the first wildcard/empty one to also be a wildcard/empty; trailing wildcards
('1.2.x', '1.x') stay valid. Tests added.

The FetchContent path forced SEMVER_INSTALL off, so a downstream
'cmake --install' would ship logos/semver.hpp without the <semver/semver.hpp>
it includes. Install the fetched header alongside ours. (The find_package /
Nix path already vendors it.)

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

* fix: reject a non-SemVer package version at validate/verify time

Manifest::validate() only checked that 'version' was non-empty, never that it
was a valid version. So a package versioned '0.1.2.3' (four sections), 'v1.0.0'
or '1.0' passed 'lgx verify', got published, and only misbehaved later: it's
unparseable to the comparators, so it sorts BELOW every valid version (can
never be 'latest') and orders against other junk by raw byte comparison
(0.1.2.10 < 0.1.2.9).

validate() now requires a full SemVer 2.0.0 version via the shared
logos::semver::valid(), so 'lgx verify' fails loudly at build/publish time
instead. Confirmed end-to-end: a package with version 0.1.2.3 now fails with
"'version' is not a valid SemVer 2.0.0 version".

Every real module in the workspace already uses X.Y.Z, so nothing is broken.
This closes the gap the comparison layer only worked around: comparison stays
lenient (it must tolerate whatever is already in old catalogs), but validation
gates what new packages may ship.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:26:46 -03:00
..