Skip to content

KodeXMachina/EVersion

Repository files navigation

EVersion logo

EVersion

Easy Version is a small C++20 library for exposing project, library, and dynamic-plugin version metadata without public VERSION_* macros.

EVersion provides four pieces:

Surface Purpose
eversion::Version A constexpr-friendly non-owning version view with weak precedence comparison and string formatting.
eversion::RuntimeVersion An owning runtime version value used by parsing and long-lived metadata.
eversion_declare() A CMake function that generates and installs an inline constexpr version header for a target.
<eversion/component_info.h> Stable C ABI component/plugin metadata types and validation APIs.
eversion_declare_plugin() A CMake function that generates a stable C ABI metadata entry point for a dynamic plugin.

Local Development Tools

The quality gate expects CMake, Ninja or another CMake generator, a C++20 compiler, clang-format, clang-tidy, and GoogleTest.

Ubuntu or Debian:

sudo apt-get update
sudo apt-get install -y cmake clang-format clang-tidy libgtest-dev ninja-build

macOS with Homebrew:

brew install cmake googletest llvm ninja
export PATH="$(brew --prefix llvm)/bin:$PATH"

Windows with Chocolatey and vcpkg:

choco install llvm ninja -y
vcpkg install gtest:x64-windows
$env:EVERSION_CMAKE_CONFIGURE_ARGS = "-G Ninja -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows"
python scripts/quality.py check

Install

The recommended usage is to install EVersion once, then consume it with find_package() from other projects:

cmake -S EVersion -B EVersion/build -DEVERSION_BUILD_TESTS=ON
cmake --build EVersion/build
cmake --install EVersion/build --prefix /path/to/eversion

Point consumer projects at that prefix with CMAKE_PREFIX_PATH:

cmake -S app -B app/build -DCMAKE_PREFIX_PATH=/path/to/eversion

EVersion also dogfoods itself: installed packages include <eversion/version_info.h> for EVersion's own version constants.

Generate Version Information for a Target

In a consumer CMake project:

find_package(EVersion CONFIG REQUIRED)

project(sample_core
    VERSION 1.0.0
    LANGUAGES CXX
)

add_library(sample_core ...)
target_link_libraries(sample_core PUBLIC eversion::eversion)

eversion_declare(sample_core
    NAMESPACE sample_core::version_info
    MAJOR ${PROJECT_VERSION_MAJOR}
    MINOR ${PROJECT_VERSION_MINOR}
    PATCH ${PROJECT_VERSION_PATCH}
    OUTPUT sample_core/version_info.h
)

Use the generated header from C++:

#include <sample_core/version_info.h>

static_assert(sample_core::version_info::kVersionMajor == 1);
static_assert(sample_core::version_info::kVersionString == "1.0.0");

constexpr eversion::Version current = sample_core::version_info::kVersion;

The generated header exposes these constants:

namespace sample_core::version_info {

inline constexpr std::uint32_t kVersionMajor;
inline constexpr std::uint32_t kVersionMinor;
inline constexpr std::uint32_t kVersionPatch;
inline constexpr std::string_view kVersionPrerelease;
inline constexpr std::string_view kVersionBuildMetadata;
inline constexpr std::string_view kVersionString;
inline constexpr ::eversion::Version kVersion;

}  // namespace sample_core::version_info

By default, eversion_declare() adds both build and install include paths and installs the generated header under include/<OUTPUT directory>. Use NO_INSTALL for purely internal generated headers. Use INSTALL_DESTINATION when a project intentionally installs public headers somewhere other than ${CMAKE_INSTALL_INCLUDEDIR}. OUTPUT must be a safe relative path, and each configure may use a generated NAMESPACE or OUTPUT only once.

Generate Version Metadata for a Dynamic Plugin

If a host application loads dynamic plugins at runtime, each plugin can export a small metadata entry point generated by EVersion. EVersion does not load plugins; it only defines the metadata structure and generates the exported function.

Plugin CMake example:

find_package(EVersion CONFIG REQUIRED)

add_library(sample_plugin MODULE ...)
target_link_libraries(sample_plugin PRIVATE eversion::eversion)

eversion_declare_plugin(sample_plugin
    ID sample.plugin
    NAME "Sample Plugin"
    NAMESPACE sample_plugin::version_info
    MAJOR 1
    MINOR 2
    PATCH 3
    BUILD_METADATA git.deadbeef
    OUTPUT sample_plugin/version_info.h
    NO_INSTALL
)

The plugin can still use its generated C++ version header internally:

#include <sample_plugin/version_info.h>

static_assert(sample_plugin::version_info::kVersionString ==
              "1.2.3+git.deadbeef");

The generated plugin source exports eversion_plugin_info. A host application can load the plugin, resolve that symbol, and validate the returned metadata:

#include <eversion/component_info.h>
#include <eversion/runtime_version.h>

#include <string>

void* symbol = LoadSymbol(plugin_handle, eversion::kPluginInfoSymbol.data());
auto info_function =
    reinterpret_cast<eversion::ComponentInfoFunction>(symbol);

const eversion::ComponentInfo* info = info_function();
if (eversion::IsValidComponentInfo(info)) {
  const eversion::Version plugin_version = eversion::ToVersion(info->version);
  const eversion::RuntimeVersion retained_version{
      .major = plugin_version.major,
      .minor = plugin_version.minor,
      .patch = plugin_version.patch,
      .prerelease = std::string{plugin_version.prerelease},
      .build_metadata = std::string{plugin_version.build_metadata},
  };
}

LoadSymbol() is intentionally left to the host application. For example, Linux/macOS hosts can use dlsym(), and Windows hosts can use GetProcAddress(). Plugin discovery, dynamic-library lifetime, and error handling are also host responsibilities. ToVersion(ComponentVersion) returns a non-owning view into the plugin-provided strings, so do not keep that Version after unloading the plugin. Include <eversion/runtime_version.h> and copy it into RuntimeVersion if the value must live longer than the plugin library.

Runtime Metadata ABI

Only ComponentInfo and ComponentVersion cross the plugin boundary. Those structures avoid std::string, std::string_view, virtual interfaces, exceptions, and RTTI, and contain only integers and const char* fields:

namespace eversion {

inline constexpr std::uint32_t kComponentInfoAbiVersion = 1U;
inline constexpr std::string_view kPluginInfoSymbol = "eversion_plugin_info";

struct ComponentVersion {
  std::uint32_t major;
  std::uint32_t minor;
  std::uint32_t patch;
  const char* prerelease;
  const char* build_metadata;
};

struct ComponentInfo {
  std::uint32_t abi_version;
  std::size_t struct_size;
  const char* id;
  const char* name;
  ComponentVersion version;
};

inline constexpr std::size_t kComponentInfoRequiredSize;

bool IsComponentInfoFieldAvailable(
    const ComponentInfo* info, std::size_t offset, std::size_t size);

using ComponentInfoFunction = const ComponentInfo* (*)() noexcept;

}  // namespace eversion

Host applications should call eversion::IsValidComponentInfo() before using a returned ComponentInfo. Within one kComponentInfoAbiVersion layout family, future fields may only be appended to the end of ComponentInfo. Hosts that read appended fields must first call IsComponentInfoFieldAvailable(). If a loaded plugin is older and does not provide a later field, the host must use a default value or treat that field as unavailable. Breaking layout changes must bump kComponentInfoAbiVersion.

Use Version Values

If a project does not need generated headers, it can include the value type directly:

#include <eversion/version.h>

constexpr eversion::Version current{1, 4, 0};
constexpr eversion::Version minimum{1, 2, 0};
static_assert(current >= minimum);

eversion::Version is a non-owning view: prerelease and build_metadata are std::string_view members. Use it for generated constants, string literals, or other storage that is known to outlive the view. Include <eversion/runtime_version.h> when parsed strings or metadata need owned storage:

#include <eversion/runtime_version.h>

#include <optional>

std::optional<eversion::RuntimeVersion> parsed =
    eversion::TryParseVersion("1.2.3-rc.1+git.deadbeef");
if (parsed.has_value()) {
  eversion::Version view = parsed->AsVersion();
}

TryParseVersion() accepts MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]. Core components must fit in std::uint32_t; prerelease and build metadata use the same dot-separated identifier grammar as eversion_declare().

Version comparison rules:

  • major, minor, and patch are compared numerically.
  • A version with a prerelease tag ranks below the same final version.
  • Numeric prerelease identifiers are compared numerically.
  • Numeric prerelease identifiers rank below non-numeric identifiers.
  • Build metadata is informational and does not affect precedence.

Therefore, 1.2.3+git.a and 1.2.3+git.b compare as weakly equivalent even though their string representations remain distinct.

Add Build Metadata

Pass build metadata such as a git commit or CI build id through BUILD_METADATA:

eversion_declare(sample_core
    NAMESPACE sample_core::version_info
    MAJOR 1
    MINOR 2
    PATCH 3
    BUILD_METADATA "${GIT_COMMIT_SHA}"
)

Plugins can use the same argument:

eversion_declare_plugin(sample_plugin
    ID sample.plugin
    NAME "Sample Plugin"
    NAMESPACE sample_plugin::version_info
    MAJOR 2
    MINOR 0
    PATCH 1
    BUILD_METADATA "${GIT_COMMIT_SHA}"
)

Vendored Source Builds

add_subdirectory(external/EVersion) is still supported for local development or vendored source builds:

add_subdirectory(external/EVersion)

add_library(sample_core ...)
target_link_libraries(sample_core PUBLIC eversion::eversion)
eversion_declare(sample_core ...)

The preferred reusable-library path remains installation plus find_package(EVersion CONFIG REQUIRED).

Build and Test EVersion

cmake -S . -B build -DEVERSION_BUILD_TESTS=ON
cmake --build build
ctest --test-dir build --output-on-failure

The tests cover:

  • eversion::Version comparison, stable-version checks, and string formatting.
  • eversion::RuntimeVersion and TryParseVersion() parsing boundaries.
  • Compile-time constants generated by eversion_declare().
  • Conversion from ComponentVersion to eversion::Version, including null string handling.
  • ABI validation, type boundaries, and field availability for ComponentInfo.
  • Dynamic plugin metadata generated by eversion_declare_plugin().
  • Installed-package consumption, including a producer that installs generated headers for a second consumer.

Development Scripts

Use the local quality gate before publishing changes:

python3 scripts/quality.py check

The script formats project-owned C++ files, verifies strict C++20 compile flags, runs CTest through the quality CMake preset, and runs clang-tidy on source translation units plus generated header smoke translation units. See scripts/README.md for detailed usage, file scope rules, and the EVERSION_CMAKE_CONFIGURE_ARGS override.

Useful build-variant checks:

EVERSION_CMAKE_CONFIGURE_ARGS="-G Ninja" python3 scripts/quality.py check
cmake --preset asan-ubsan
cmake --build --preset asan-ubsan
ctest --preset asan-ubsan

ThreadSanitizer is intentionally not part of the default quality gate because some containers block personality(ADDR_NO_RANDOMIZE), which TSan needs for its shadow-memory layout.

Design Choices

  • Public version information is exposed as inline constexpr C++ symbols.
  • No public version macros are generated.
  • CMake writes generated headers with configure_file(... @ONLY).
  • The plugin ABI uses C-style structs to avoid exposing C++ STL types, exceptions, or vtables across dynamic-library boundaries.
  • TryParseVersion() parses exact version strings, not version ranges.
  • EVersion does not implement version range expressions such as ^1.2 or ~1.2.3.
  • EVersion does not provide a plugin loading framework.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Generated from winterYANGWT/Cpp-Dev