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. |
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-buildmacOS 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 checkThe 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/eversionPoint consumer projects at that prefix with CMAKE_PREFIX_PATH:
cmake -S app -B app/build -DCMAKE_PREFIX_PATH=/path/to/eversionEVersion also dogfoods itself: installed packages include
<eversion/version_info.h> for EVersion's own version constants.
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_infoBy 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.
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.
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 eversionHost 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.
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, andpatchare 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.
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}"
)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).
cmake -S . -B build -DEVERSION_BUILD_TESTS=ON
cmake --build build
ctest --test-dir build --output-on-failureThe tests cover:
eversion::Versioncomparison, stable-version checks, and string formatting.eversion::RuntimeVersionandTryParseVersion()parsing boundaries.- Compile-time constants generated by
eversion_declare(). - Conversion from
ComponentVersiontoeversion::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.
Use the local quality gate before publishing changes:
python3 scripts/quality.py checkThe 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-ubsanThreadSanitizer 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.
- Public version information is exposed as
inline constexprC++ 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.2or~1.2.3. - EVersion does not provide a plugin loading framework.
