From 23932429ac68eecdb8ca7698f35783ee5d89a04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Thu, 11 Jun 2026 18:39:28 +0200 Subject: [PATCH 1/6] build: commit SilKitVersionMacros.h and regenerate with tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marius Börschig --- .../include/silkit/capi/SilKitVersionMacros.h | 13 + SilKit/source/CMakeLists.txt | 45 +- SilKit/source/MakeVersionMacros.cmake.in | 47 -- SilKit/source/SilKit.rc | 2 +- SilKit/source/SilKitVersionImpl.cpp | 2 +- SilKit/source/util/CMakeLists.txt | 8 + SilKit/source/util/sil-kit-version-gen.cpp | 401 ++++++++++++++++++ SilKit/source/version_macros.hpp.in | 14 - Utilities/SilKitMonitor/CMakeLists.txt | 6 +- Utilities/SilKitMonitor/sil-kit-monitor.rc | 2 +- Utilities/SilKitRegistry/CMakeLists.txt | 6 +- Utilities/SilKitRegistry/SilKitRegistry.rc | 2 +- .../SilKitSystemController/CMakeLists.txt | 6 +- .../SilKitSystemController.rc | 2 +- 14 files changed, 443 insertions(+), 113 deletions(-) create mode 100644 SilKit/include/silkit/capi/SilKitVersionMacros.h delete mode 100644 SilKit/source/MakeVersionMacros.cmake.in create mode 100644 SilKit/source/util/sil-kit-version-gen.cpp delete mode 100644 SilKit/source/version_macros.hpp.in diff --git a/SilKit/include/silkit/capi/SilKitVersionMacros.h b/SilKit/include/silkit/capi/SilKitVersionMacros.h new file mode 100644 index 000000000..86c83aae3 --- /dev/null +++ b/SilKit/include/silkit/capi/SilKitVersionMacros.h @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2022 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once + +#define SILKIT_GIT_HASH "f3b467c2028695e487d0337868ddae43a5674fe7" +#define SILKIT_VERSION_MAJOR 5 +#define SILKIT_VERSION_MINOR 0 +#define SILKIT_VERSION_PATCH 6 +#define SILKIT_BUILD_NUMBER 0 +#define SILKIT_VERSION_STRING "5.0.6" +#define SILKIT_VERSION_SUFFIX "" diff --git a/SilKit/source/CMakeLists.txt b/SilKit/source/CMakeLists.txt index 930af61ad..466133f38 100644 --- a/SilKit/source/CMakeLists.txt +++ b/SilKit/source/CMakeLists.txt @@ -14,44 +14,20 @@ add_subdirectory(dashboard) find_package(Threads REQUIRED) -# Encode the current GIT and version infos into version_macros.hpp -# If this file is present in the current source directory we assume -# that this source-tree is a non-git/packaged source tree and re-use -# the file unmodified. -set(GIT_DIR "${PROJECT_SOURCE_DIR}/../.git") -set(GIT_HEAD_FILE "${GIT_DIR}/HEAD") -set(VERSION_MACROS_HPP ${CMAKE_CURRENT_BINARY_DIR}/version_macros.hpp) -if(EXISTS ${CMAKE_CURRENT_LIST_DIR}/version_macros.hpp) - message(STATUS "SIL Kit: using deployed version_macros.hpp") - set(VERSION_MACROS_HPP ${CMAKE_CURRENT_LIST_DIR}/version_macros.hpp) -elseif(EXISTS "${GIT_HEAD_FILE}") - configure_file( - "MakeVersionMacros.cmake.in" - ${CMAKE_CURRENT_BINARY_DIR}/MakeVersionMacros.cmake - @ONLY) - include(${CMAKE_CURRENT_BINARY_DIR}/MakeVersionMacros.cmake) - if(SILKIT_INSTALL_SOURCE) - install(FILES - ${CMAKE_CURRENT_BINARY_DIR}/version_macros.hpp - DESTINATION - ${INSTALL_SOURCE_DIR}/SilKit/source - COMPONENT source - ) - endif() -else() - message(STATUS "SIL Kit: Cannot determine hash of current git head! GIT_HEAD_HASH will be set to UNKNOWN") - set(GIT_HEAD_HASH "UNKNOWN") - configure_file( - version_macros.hpp.in - ${CMAKE_CURRENT_BINARY_DIR}/version_macros.hpp - @ONLY) +# SilKitVersionMacros.h is committed to source and updated by running sil-kit-version-gen. +# No generation at configure time — any build tool can use the file as a regular source file. +# To update: sil-kit-version-gen --major X --minor Y --patch Z [--source-dir .] +set(VERSION_MACROS_HPP "${CMAKE_CURRENT_LIST_DIR}/../include/silkit/capi/SilKitVersionMacros.h") +if(NOT EXISTS "${VERSION_MACROS_HPP}") + message(FATAL_ERROR + "SilKitVersionMacros.h not found. Run sil-kit-version-gen to create it:\n" + " sil-kit-version-gen --major ${SILKIT_VERSION_MAJOR} --minor ${SILKIT_VERSION_MINOR}" + " --patch ${SILKIT_VERSION_PATCH} ${VERSION_MACROS_HPP}") endif() -# now install the configured version_macros.hpp as a header into the `include/silkit/capi` directory. -# NB please keep the version_macros.hpp C11 compatible! + install(FILES ${VERSION_MACROS_HPP} DESTINATION ${INSTALL_INCLUDE_DIR}/silkit/capi - RENAME SilKitVersionMacros.h COMPONENT dev ) @@ -144,7 +120,6 @@ target_link_libraries(O_SilKit_CreateSilKitRegistryImpl add_library(O_SilKit_VersionImpl OBJECT SilKitVersionImpl.cpp SilKitVersionImpl.hpp - ${VERSION_MACROS_HPP} ) target_link_libraries(O_SilKit_VersionImpl diff --git a/SilKit/source/MakeVersionMacros.cmake.in b/SilKit/source/MakeVersionMacros.cmake.in deleted file mode 100644 index c384dac40..000000000 --- a/SilKit/source/MakeVersionMacros.cmake.in +++ /dev/null @@ -1,47 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Vector Informatik GmbH -# -# SPDX-License-Identifier: MIT - -set(gitHashFile "${CMAKE_CURRENT_BINARY_DIR}/git_hash_file") -set(gitHeadFile "${CMAKE_CURRENT_BINARY_DIR}/git_head_file") - -configure_file("@GIT_HEAD_FILE@" ${gitHeadFile} COPYONLY) - -file(READ ${gitHeadFile} GIT_HEAD LIMIT 512) -string(REGEX MATCH "^ref: (.*)\n" GIT_HEAD_REF ${GIT_HEAD}) - -set(linkedHeadFile "${GIT_DIR}/${CMAKE_MATCH_1}") -set(linkedRef "${CMAKE_MATCH_1}") -if(GIT_HEAD_REF) - if(NOT EXISTS "${linkedHeadFile}") - # the workspace might have a .git/packed-refs file instead of - # .git/refs/heads/master - message("-- SIL Kit GIT Version: using ${GIT_DIR}/packed-refs") - file(READ ${GIT_DIR}/packed-refs packedRefs LIMIT 4096) - string(REGEX MATCH "([abcdef0-9]+) ${linkedRef}" headHash ${packedRefs}) - if(NOT headHash) - message(FATAL_ERROR "MakeVersionMacros: cannot find linked git ref \"${linkedRef}\"") - endif() - file(WRITE ${gitHashFile} "${CMAKE_MATCH_1}\n") - else() - message("-- SIL Kit GIT Version: using ${linkedHeadFile}") - configure_file( - "${linkedHeadFile}" - ${gitHashFile} - COPYONLY) - endif() -else() - configure_file( - ${gitHeadFile} - ${gitHashFile} - COPYONLY) -endif() - - -file(READ ${gitHashFile} GIT_HEAD_HASH LIMIT 512) -string(STRIP "${GIT_HEAD_HASH}" GIT_HEAD_HASH) -message(STATUS "SIL Kit GIT Version: ${GIT_HEAD_HASH}") -configure_file( - version_macros.hpp.in - ${CMAKE_CURRENT_BINARY_DIR}/version_macros.hpp - @ONLY) diff --git a/SilKit/source/SilKit.rc b/SilKit/source/SilKit.rc index 6dadcc8df..0b57ab705 100644 --- a/SilKit/source/SilKit.rc +++ b/SilKit/source/SilKit.rc @@ -1,5 +1,5 @@ #include -#include "version_macros.hpp" +#include "silkit/capi/SilKitVersionMacros.h" #pragma code_page(65001) // UTF-8 for © symbol #define STRING_HELPER(x) #x diff --git a/SilKit/source/SilKitVersionImpl.cpp b/SilKit/source/SilKitVersionImpl.cpp index 36be9aaf1..22c24a784 100644 --- a/SilKit/source/SilKitVersionImpl.cpp +++ b/SilKit/source/SilKitVersionImpl.cpp @@ -4,7 +4,7 @@ #include "SilKitVersionImpl.hpp" -#include "version_macros.hpp" +#include "silkit/capi/SilKitVersionMacros.h" namespace SilKit { namespace Version { diff --git a/SilKit/source/util/CMakeLists.txt b/SilKit/source/util/CMakeLists.txt index d9fba7454..d851f4afd 100644 --- a/SilKit/source/util/CMakeLists.txt +++ b/SilKit/source/util/CMakeLists.txt @@ -85,4 +85,12 @@ add_silkit_test_to_executable(SilKitUnitTests ) +add_executable(SilKit_version_gen sil-kit-version-gen.cpp) +set_target_properties(SilKit_version_gen PROPERTIES + CXX_STANDARD 17 + OUTPUT_NAME "sil-kit-version-gen" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$" +) + + add_subdirectory(tests) diff --git a/SilKit/source/util/sil-kit-version-gen.cpp b/SilKit/source/util/sil-kit-version-gen.cpp new file mode 100644 index 000000000..c49553a98 --- /dev/null +++ b/SilKit/source/util/sil-kit-version-gen.cpp @@ -0,0 +1,401 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +// Standalone tool: generates version_macros.hpp from CLI arguments and the +// current git HEAD hash. Optionally rotates the changelog when the version +// number changes. Requires C++17 (std::filesystem). No external dependencies. +// +// Usage: sil-kit-version-gen [options] +// --major N Version major (required) +// --minor N Version minor (required) +// --patch N Version patch (required) +// --build N Build number (default: 0) +// --suffix S Version suffix (default: "") +// --git-dir PATH Path to .git directory (default: search upward from CWD) +// --git-hash HASH Override git hash; skips .git reading +// --source-dir PATH Source tree root for changelog files (default: CWD) +// --no-changelog Skip changelog rotation even if version changed +// --dry-run Print actions without writing any files +// Pass '-' as output-header to write to stdout. + +#include +#include +#include +#include +#include +#include + + +namespace fs = std::filesystem; + +// --------------------------------------------------------------------------- +// File helpers +// --------------------------------------------------------------------------- + +static std::string readFileFull(const fs::path& path) +{ + std::ifstream f(path); + if (!f.is_open()) + return ""; + std::ostringstream ss; + ss << f.rdbuf(); + return ss.str(); +} + +static bool writeFileFull(const fs::path& path, const std::string& content) +{ + std::ofstream f(path); + if (!f.is_open()) + { + std::cerr << "error: cannot write " << path << "\n"; + return false; + } + f << content; + return f.good(); +} + +static std::string trimStr(const std::string& s) +{ + size_t start = s.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) + return ""; + size_t end = s.find_last_not_of(" \t\r\n"); + return s.substr(start, end - start + 1); +} + +// --------------------------------------------------------------------------- +// Git hash resolution (mirrors the old MakeVersionMacros.cmake.in logic) +// --------------------------------------------------------------------------- + +static fs::path findGitDir() +{ + fs::path dir = fs::current_path(); + while (true) + { + fs::path candidate = dir / ".git"; + if (fs::exists(candidate / "HEAD")) + return candidate; + fs::path parent = dir.parent_path(); + if (parent == dir) + break; + dir = parent; + } + return {}; +} + +static std::string resolveGitHash(const fs::path& gitDir) +{ + std::string headContent = trimStr(readFileFull(gitDir / "HEAD")); + if (headContent.empty()) + return "UNKNOWN"; + + const std::string refPrefix = "ref: "; + if (headContent.compare(0, refPrefix.size(), refPrefix) == 0) + { + std::string ref = headContent.substr(refPrefix.size()); + fs::path refFile = gitDir / ref; + + if (fs::exists(refFile)) + return trimStr(readFileFull(refFile)); + + // Fall back to packed-refs + std::string packedRefs = readFileFull(gitDir / "packed-refs"); + if (!packedRefs.empty()) + { + std::istringstream stream(packedRefs); + std::string line; + while (std::getline(stream, line)) + { + if (line.empty() || line[0] == '#' || line[0] == '^') + continue; + size_t space = line.find(' '); + if (space != std::string::npos && trimStr(line.substr(space + 1)) == ref) + return line.substr(0, space); + } + } + return "UNKNOWN"; + } + + // Detached HEAD — content is the hash directly + if (headContent.size() >= 40) + return headContent.substr(0, 40); + return headContent; +} + +// --------------------------------------------------------------------------- +// Parse current version from an existing version_macros.hpp +// --------------------------------------------------------------------------- + +struct Version +{ + int major{-1}, minor{-1}, patch{-1}; + bool valid() const { return major >= 0 && minor >= 0 && patch >= 0; } +}; + +static int extractDefineInt(const std::string& content, const std::string& macroName) +{ + std::string search = "#define " + macroName + " "; + size_t pos = content.find(search); + if (pos == std::string::npos) + return -1; + pos += search.size(); + size_t end = content.find_first_of("\r\n", pos); + return std::atoi(trimStr(content.substr(pos, end - pos)).c_str()); +} + +static Version parseVersionFromHeader(const fs::path& path) +{ + Version v; + std::string content = readFileFull(path); + if (content.empty()) + return v; + v.major = extractDefineInt(content, "SILKIT_VERSION_MAJOR"); + v.minor = extractDefineInt(content, "SILKIT_VERSION_MINOR"); + v.patch = extractDefineInt(content, "SILKIT_VERSION_PATCH"); + return v; +} + +// --------------------------------------------------------------------------- +// Changelog rotation +// --------------------------------------------------------------------------- + +static bool rotateChangelog(const fs::path& sourceDir, const Version& oldVer, bool dryRun) +{ + fs::path versionsDir = sourceDir / "docs" / "changelog" / "versions"; + fs::path latestMd = versionsDir / "latest.md"; + fs::path templateMd = versionsDir / "template.md"; + + fs::path archiveMd = versionsDir / (std::to_string(oldVer.major) + "." + std::to_string(oldVer.minor) + "." + + std::to_string(oldVer.patch) + ".md"); + + if (!fs::exists(latestMd)) + { + std::cerr << "warning: changelog rotation skipped — " << latestMd << " not found\n"; + return true; + } + if (!fs::exists(templateMd)) + { + std::cerr << "warning: changelog rotation skipped — " << templateMd << " not found\n"; + return true; + } + if (fs::exists(archiveMd)) + { + std::cerr << "warning: changelog rotation skipped — " << archiveMd << " already exists\n"; + return true; + } + + if (dryRun) + { + std::cout << "[dry-run] copy " << latestMd << "\n" + << " -> " << archiveMd << "\n"; + std::cout << "[dry-run] copy " << templateMd << "\n" + << " -> " << latestMd << "\n"; + return true; + } + + std::error_code ec; + std::cout << "changelog: " << latestMd << " -> " << archiveMd << "\n"; + fs::copy_file(latestMd, archiveMd, ec); + if (ec) + { + std::cerr << "error: " << ec.message() << "\n"; + return false; + } + std::cout << "changelog: " << latestMd << " <- " << templateMd << "\n"; + fs::copy_file(templateMd, latestMd, fs::copy_options::overwrite_existing, ec); + if (ec) + { + std::cerr << "error: " << ec.message() << "\n"; + return false; + } + return true; +} + +// --------------------------------------------------------------------------- +// Header generation +// --------------------------------------------------------------------------- + +static void replaceAll(std::string& s, const std::string& from, const std::string& to) +{ + for (size_t pos = 0; (pos = s.find(from, pos)) != std::string::npos; pos += to.size()) + s.replace(pos, from.size(), to); +} + +static void substituteVar(std::string& s, const std::string& name, const std::string& value) +{ + replaceAll(s, "@" + name + "@", value); +} + +// clang-format off +static constexpr auto kHeaderTemplate = R"(// SPDX-FileCopyrightText: 2022 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once + +#define SILKIT_GIT_HASH "@GIT_HASH@" +#define SILKIT_VERSION_MAJOR @MAJOR@ +#define SILKIT_VERSION_MINOR @MINOR@ +#define SILKIT_VERSION_PATCH @PATCH@ +#define SILKIT_BUILD_NUMBER @BUILD@ +#define SILKIT_VERSION_STRING "@VERSION_STRING@" +#define SILKIT_VERSION_SUFFIX "@SUFFIX@" +)"; +// clang-format on + +static std::string buildHeader(const std::string& gitHash, int major, int minor, int patch, int build, + const std::string& suffix) +{ + const std::string versionStr = std::to_string(major) + "." + std::to_string(minor) + "." + + std::to_string(patch) + (suffix.empty() ? "" : "-" + suffix); + + std::string result{kHeaderTemplate}; + substituteVar(result, "GIT_HASH", gitHash); + substituteVar(result, "MAJOR", std::to_string(major)); + substituteVar(result, "MINOR", std::to_string(minor)); + substituteVar(result, "PATCH", std::to_string(patch)); + substituteVar(result, "BUILD", std::to_string(build)); + substituteVar(result, "VERSION_STRING", versionStr); + substituteVar(result, "SUFFIX", suffix); + return result; +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- + +static void usage(const char* prog) +{ + std::cerr << "Usage: " << prog << " [options] \n" + << "\n" + << " --major N Version major (required)\n" + << " --minor N Version minor (required)\n" + << " --patch N Version patch (required)\n" + << " --build N Build number (default: 0)\n" + << " --suffix S Version suffix (default: \"\")\n" + << " --git-dir PATH Path to .git directory\n" + << " --git-hash HASH Override git hash (skips .git reading)\n" + << " --source-dir PATH Source tree root for changelog files\n" + << " --no-changelog Skip changelog rotation\n" + << " --dry-run Print actions without writing any files\n"; +} + +int main(int argc, char* argv[]) +{ + int major = -1, minor = -1, patch = -1, build = 0; + std::string suffix; + fs::path gitDir; + std::string gitHashOverride; + fs::path sourceDir; + std::string outputPath; + bool noChangelog = false; + bool dryRun = false; + + for (int i = 1; i < argc; ++i) + { + std::string arg = argv[i]; + + auto requireNext = [&]() -> std::string { + if (i + 1 >= argc) + { + std::cerr << "error: " << arg << " requires an argument\n"; + std::exit(1); + } + return argv[++i]; + }; + + if (arg == "--major") + major = std::atoi(requireNext().c_str()); + else if (arg == "--minor") + minor = std::atoi(requireNext().c_str()); + else if (arg == "--patch") + patch = std::atoi(requireNext().c_str()); + else if (arg == "--build") + build = std::atoi(requireNext().c_str()); + else if (arg == "--suffix") + suffix = requireNext(); + else if (arg == "--git-dir") + gitDir = requireNext(); + else if (arg == "--git-hash") + gitHashOverride = requireNext(); + else if (arg == "--source-dir") + sourceDir = requireNext(); + else if (arg == "--no-changelog") + noChangelog = true; + else if (arg == "--dry-run" || arg == "-n") + dryRun = true; + else if (!arg.empty() && arg[0] != '-') + outputPath = arg; + else + { + std::cerr << "error: unknown option '" << arg << "'\n"; + usage(argv[0]); + return 1; + } + } + + if (major < 0 || minor < 0 || patch < 0) + { + std::cerr << "error: --major, --minor, --patch are required\n"; + usage(argv[0]); + return 1; + } + if (outputPath.empty()) + { + std::cerr << "error: output path is required\n"; + usage(argv[0]); + return 1; + } + + // Resolve git hash + std::string gitHash; + if (!gitHashOverride.empty()) + { + gitHash = gitHashOverride; + } + else + { + if (gitDir.empty()) + gitDir = findGitDir(); + gitHash = gitDir.empty() ? "UNKNOWN" : resolveGitHash(gitDir); + } + + // Changelog rotation — read existing header before overwriting + if (!noChangelog && outputPath != "-") + { + Version oldVer = parseVersionFromHeader(outputPath); + bool versionChanged = + oldVer.valid() && (oldVer.major != major || oldVer.minor != minor || oldVer.patch != patch); + if (versionChanged) + { + fs::path srcDir = sourceDir.empty() ? fs::current_path() : sourceDir; + if (!rotateChangelog(srcDir, oldVer, dryRun)) + return 1; + } + } + + // Generate and write the header + std::string content = buildHeader(gitHash, major, minor, patch, build, suffix); + + if (dryRun) + { + std::cout << "[dry-run] write " << outputPath << "\n" + << "---\n" + << content; + return 0; + } + + if (outputPath == "-") + { + std::cout << content; + } + else + { + if (!writeFileFull(outputPath, content)) + return 1; + std::cout << "wrote " << outputPath << " (git hash: " << gitHash << ")\n"; + } + + return 0; +} diff --git a/SilKit/source/version_macros.hpp.in b/SilKit/source/version_macros.hpp.in deleted file mode 100644 index 6529e06be..000000000 --- a/SilKit/source/version_macros.hpp.in +++ /dev/null @@ -1,14 +0,0 @@ - -// SPDX-FileCopyrightText: 2022 Vector Informatik GmbH -// -// SPDX-License-Identifier: MIT - -#pragma once - -#define SILKIT_GIT_HASH "@GIT_HEAD_HASH@" -#define SILKIT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ -#define SILKIT_VERSION_MINOR @PROJECT_VERSION_MINOR@ -#define SILKIT_VERSION_PATCH @PROJECT_VERSION_PATCH@ -#define SILKIT_BUILD_NUMBER @SILKIT_BUILD_NUMBER@ -#define SILKIT_VERSION_STRING "@PROJECT_VERSION@" -#define SILKIT_VERSION_SUFFIX "@SILKIT_VERSION_SUFFIX@" diff --git a/Utilities/SilKitMonitor/CMakeLists.txt b/Utilities/SilKitMonitor/CMakeLists.txt index 3639bfa9e..c4ea08297 100644 --- a/Utilities/SilKitMonitor/CMakeLists.txt +++ b/Utilities/SilKitMonitor/CMakeLists.txt @@ -29,12 +29,10 @@ target_link_libraries(sil-kit-monitor # Set versioning infos on exe if(MSVC) - get_target_property(SILKIT_BINARY_DIR SilKit BINARY_DIR) get_target_property(SILKIT_SOURCE_DIR SilKit SOURCE_DIR) - # Include the generated version_macros.hpp in SilKit/source + # Include SilKit/include so the RC compiler finds silkit/capi/SilKitVersionMacros.h target_include_directories(sil-kit-monitor - PRIVATE ${SILKIT_BINARY_DIR} - PRIVATE ${SILKIT_SOURCE_DIR} + PRIVATE ${SILKIT_SOURCE_DIR}/../include ) target_sources(sil-kit-monitor PRIVATE sil-kit-monitor.rc) endif() diff --git a/Utilities/SilKitMonitor/sil-kit-monitor.rc b/Utilities/SilKitMonitor/sil-kit-monitor.rc index 0c9d3e583..26dafb501 100644 --- a/Utilities/SilKitMonitor/sil-kit-monitor.rc +++ b/Utilities/SilKitMonitor/sil-kit-monitor.rc @@ -1,5 +1,5 @@ #include -#include "version_macros.hpp" +#include "silkit/capi/SilKitVersionMacros.h" #pragma code_page(65001) // UTF-8 for © symbol #define STRING_HELPER(x) #x diff --git a/Utilities/SilKitRegistry/CMakeLists.txt b/Utilities/SilKitRegistry/CMakeLists.txt index bb0f8f56b..9194b001b 100644 --- a/Utilities/SilKitRegistry/CMakeLists.txt +++ b/Utilities/SilKitRegistry/CMakeLists.txt @@ -46,12 +46,10 @@ target_link_libraries(sil-kit-registry # Set versioning infos on exe if(MSVC) - get_target_property(SILKIT_BINARY_DIR SilKit BINARY_DIR) get_target_property(SILKIT_SOURCE_DIR SilKit SOURCE_DIR) - # Include the generated version_macros.hpp in SilKit/source + # Include SilKit/include so the RC compiler finds silkit/capi/SilKitVersionMacros.h target_include_directories(sil-kit-registry - PRIVATE ${SILKIT_BINARY_DIR} - PRIVATE ${SILKIT_SOURCE_DIR} + PRIVATE ${SILKIT_SOURCE_DIR}/../include ) target_sources(sil-kit-registry PRIVATE SilKitRegistry.rc) endif() diff --git a/Utilities/SilKitRegistry/SilKitRegistry.rc b/Utilities/SilKitRegistry/SilKitRegistry.rc index d4e1c0226..b4e810376 100644 --- a/Utilities/SilKitRegistry/SilKitRegistry.rc +++ b/Utilities/SilKitRegistry/SilKitRegistry.rc @@ -1,5 +1,5 @@ #include -#include "version_macros.hpp" +#include "silkit/capi/SilKitVersionMacros.h" #pragma code_page(65001) // UTF-8 for © symbol #define STRING_HELPER(x) #x diff --git a/Utilities/SilKitSystemController/CMakeLists.txt b/Utilities/SilKitSystemController/CMakeLists.txt index 46d5968f3..d08a061b4 100644 --- a/Utilities/SilKitSystemController/CMakeLists.txt +++ b/Utilities/SilKitSystemController/CMakeLists.txt @@ -29,12 +29,10 @@ target_link_libraries(sil-kit-system-controller # Set versioning infos on exe if(MSVC) - get_target_property(SILKIT_BINARY_DIR SilKit BINARY_DIR) get_target_property(SILKIT_SOURCE_DIR SilKit SOURCE_DIR) - # Include the generated version_macros.hpp in SilKit/source + # Include SilKit/include so the RC compiler finds silkit/capi/SilKitVersionMacros.h target_include_directories(sil-kit-system-controller - PRIVATE ${SILKIT_BINARY_DIR} - PRIVATE ${SILKIT_SOURCE_DIR} + PRIVATE ${SILKIT_SOURCE_DIR}/../include ) target_sources(sil-kit-system-controller PRIVATE SilKitSystemController.rc) endif() diff --git a/Utilities/SilKitSystemController/SilKitSystemController.rc b/Utilities/SilKitSystemController/SilKitSystemController.rc index b34c5f3b1..dc29ac43e 100644 --- a/Utilities/SilKitSystemController/SilKitSystemController.rc +++ b/Utilities/SilKitSystemController/SilKitSystemController.rc @@ -1,5 +1,5 @@ #include -#include "version_macros.hpp" +#include "silkit/capi/SilKitVersionMacros.h" #pragma code_page(65001) // UTF-8 for © symbol #define STRING_HELPER(x) #x From f7895f13b8a7dc877a960163912474359281b732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Tue, 1 Sep 2026 17:14:43 +0200 Subject: [PATCH 2/6] fixup! build: commit SilKitVersionMacros.h and regenerate with tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit use silKitVersion.cmake to generate the C header Signed-off-by: Marius Börschig --- SilKit/ci/Jenkinsfile | 636 ----------------- SilKit/cmake/SilKitVersion.cmake | 11 +- .../include/silkit/capi/SilKitVersionMacros.h | 18 +- SilKit/source/CMakeLists.txt | 23 +- SilKit/source/util/CMakeLists.txt | 22 +- SilKit/source/util/GenerateVersion.cpp | 309 +++++++++ SilKit/source/util/GenerateVersion.hpp | 90 +++ .../source/util/sil-kit-generate-version.cpp | 645 ++++++++++++++++++ SilKit/source/util/sil-kit-version-gen.cpp | 401 ----------- SilKit/source/util/tests/CMakeLists.txt | 1 + .../util/tests/Test_Util_GenerateVersion.cpp | 345 ++++++++++ docs/CMakeLists.txt | 4 + docs/changelog/versions/latest.md | 6 + docs/development/build.rst | 5 +- docs/development/release.md | 206 ++++++ 15 files changed, 1666 insertions(+), 1056 deletions(-) delete mode 100755 SilKit/ci/Jenkinsfile create mode 100644 SilKit/source/util/GenerateVersion.cpp create mode 100644 SilKit/source/util/GenerateVersion.hpp create mode 100644 SilKit/source/util/sil-kit-generate-version.cpp delete mode 100644 SilKit/source/util/sil-kit-version-gen.cpp create mode 100644 SilKit/source/util/tests/Test_Util_GenerateVersion.cpp create mode 100644 docs/development/release.md diff --git a/SilKit/ci/Jenkinsfile b/SilKit/ci/Jenkinsfile deleted file mode 100755 index 83aa024fd..000000000 --- a/SilKit/ci/Jenkinsfile +++ /dev/null @@ -1,636 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Vector Informatik GmbH -// -// SPDX-License-Identifier: MIT - -// vim: set ft=groovy: - -//################# -// Global Variables -//################# - -// pipeline config -projectName="vib-main" -artifactName='SilKit' -keepNumBuilds=10 //number of log files on Jenkins, workspaces are always cleaned -publishToArtifactory=false -globalBuildDebugBin=false //debug binaries are only build when doing a release to customers -buildWarningsAsErrors=true // compiler warnings are treated as errors -// artifactory for archiving / packaging -artifactoryServer = Artifactory.server('IntegrationBusArtifactory') -artifactoryBuildInfo = Artifactory.newBuildInfo() - -def gitBranch="main-mirror" // overriden in parallel pipeline based on scm vars - -def buildConfigs = [ - // Windows builds: - [ - - Name: "VS2017-Win64", - NodeLabel: "ninja && buildtools2019", - Arch:"x64", - MsvcVersion:"14.1", - PublishArtifacts: true, - BuildDocs: true, - CmakePreset: "vs141-x64-release", - ] - ,[ - Name: "VS2017-Win32", - NodeLabel: "ninja && buildtools2019", - Arch:"x64_x86", - MsvcVersion:"14.1", - PublishArtifacts: true, - BuildDocs: true, - CmakePreset: "vs141-x86-release", - ] - - // Linux builds: - ,[ - Name: "Ubuntu-18.04-clang", - NodeLabel: "docker && linux", - DockerImage: "silkit-ubuntu", - DockerBuildArgs: "--build-arg UBUNTU_VERSION=18.04", - PublishArtifacts: false, - CmakePreset: "clang10-release", - ] - ,[ - Name: "Ubuntu-18.04-gcc", - NodeLabel: "docker && linux", - DockerImage: "silkit-ubuntu", - DockerBuildArgs: "--build-arg UBUNTU_VERSION=18.04", - PublishArtifacts: true, - BuildDocs: true, - CmakePreset: "gcc8-release", - TriggerAbiCheck: true, - ] - ,[ - Name: "Ubuntu-20.04-gcc-10", - NodeLabel: "docker && linux", - DockerImage: "silkit-ubuntu", - DockerBuildArgs: "--build-arg UBUNTU_VERSION=20.04", - PublishArtifacts: false, - CmakePreset: "gcc10-release", - ] - ,[ - Name: "Ubuntu-20.04-clang-12", - NodeLabel: "docker && linux", - DockerImage: "silkit-ubuntu", - DockerBuildArgs: "--build-arg UBUNTU_VERSION=20.04", - PublishArtifacts: false, - CmakePreset: "clang12-release", - ] - ,[ - Name: "Ubuntu-22.04-gcc-12", - NodeLabel: "docker && linux", - DockerImage: "silkit-ubuntu", - DockerBuildArgs: "--build-arg UBUNTU_VERSION=22.04", - PublishArtifacts: false, - CmakePreset: "gcc12-release", - ] - ,[ - Name: "Ubuntu-22.04-clang-14", - NodeLabel: "docker && linux", - DockerImage: "silkit-ubuntu", - DockerBuildArgs: "--build-arg UBUNTU_VERSION=22.04", - PublishArtifacts: false, - CmakePreset: "clang14-release", - checkLicenses: true, - ] - // Sanitizer builds: - ,[ - Name: "Ubuntu-22.04-clang-14-thread-sanitizer", - NodeLabel: "docker && linux", - DockerImage: "silkit-ubuntu", - DockerBuildArgs: "--build-arg UBUNTU_VERSION=22.04", - PublishArtifacts: false, - CmakeArgs: "-D SILKIT_ENABLE_THREADSAN=ON -D SILKIT_BUILD_DASHBOARD=OFF", - CmakePreset: "clang14-release", - TestDebug: true, - ] - ,[ - Name: "Ubuntu-22.04-clang-14-address-sanitizer", - NodeLabel: "docker && linux", - DockerImage: "silkit-ubuntu", - DockerBuildArgs: "--build-arg UBUNTU_VERSION=22.04", - PublishArtifacts: false, - CmakeArgs: "-D SILKIT_ENABLE_ASAN=ON -D SILKIT_BUILD_DASHBOARD=OFF", - CmakePreset: "clang14-release", - TestDebug: true, - ] - ,[ - Name: "Ubuntu-22.04-clang-14-undefined-behavior-sanitizer", - NodeLabel: "docker && linux", - DockerImage: "silkit-ubuntu", - DockerBuildArgs: "--build-arg UBUNTU_VERSION=22.04", - PublishArtifacts: false, - CmakeArgs: "-D SILKIT_ENABLE_UBSAN=ON -D SILKIT_BUILD_DASHBOARD=OFF", - CmakePreset: "clang14-release", - TestDebug: true, - ] -] - - -//########################################## -// Utilities -//########################################## -@NonCPS -def isFullBuildRequired(branchName) { - // Master and Pull Requests are always full builds - if(branchName == 'main-mirror' || branchName.startsWith("PR-")) { - return true; - } - // A user requests a fullbuild by adding a "-full" suffix - if(branchName.contains("-full")) { - return true; - } - return false -} - -@NonCPS -def isDockerNode(label) { - return label.toUpperCase().contains('DOCKER') -} - -@NonCPS -def isClangBuild(label) { - return label.toUpperCase().contains('CLANG') -} - -def setBuildRetention(bi) { - if(gitBranch == 'main-mirror' || gitBranch == 'origin/main-mirror') { - bi.retention(maxBuilds: 40, deleteBuildArtifacts: true, async: true) - } else { - bi.retention(maxBuilds: 2, maxDays: 50, deleteBuildArtifacts: true, async: true) - } -} - -//recursive submodule git checkout -def checkoutGit() { - return checkout([$class: 'GitSCM', - branches: scm.branches, - doGenerateSubmoduleConfigurations: false, - extensions: [ - [$class: 'SubmoduleOption', - disableSubmodules: false, - parentCredentials: false, - recursiveSubmodules: true, - reference: '', - trackingSubmodules: false]], - submoduleCfg: [], - userRemoteConfigs: scm.userRemoteConfigs - ]) -} -def shallowCheckoutGit() { - return checkout([$class: 'GitSCM', - branches: scm.branches, - doGenerateSubmoduleConfigurations: false, - userRemoteConfigs: scm.userRemoteConfigs - ]) -} - -def archiveToArtifactory(pattern) { - if (! publishToArtifactory ) { - print("INFO: skipping publishing to artifactory because of user request") - return - } - - configFileProvider( - [configFile(fileId: 'SILKIT_ARTIFACTORY_REPO', variable: 'SILKIT_ARTIFACTORY_REPO')]) { - def repo = readFile(file: SILKIT_ARTIFACTORY_REPO).trim() - print("Artifactory: archiving with regexp file spec pattern=${pattern}") - spec = """{ - "files": [{ - "pattern": "${pattern}", - "regexp" : "true", - "target" : "${repo}/${projectName}/${env.GIT_BRANCH}/${env.GIT_COMMIT}/", - "props" : "git_commit=${env.GIT_COMMIT};git_branch=${env.GIT_BRANCH};git_url=${env.GIT_URL}" - }]}""" - print("artifactory spec=${spec}") - def buildInfo = artifactoryServer.upload( - spec: spec, - failNoOp: true - ) - setBuildRetention(buildInfo) - artifactoryBuildInfo.append(buildInfo) - } -} - -def buildDocker(config) { - stage("Building Docker Image") { - def dockerImage = config["DockerImage"] - def dockerDir = "./SilKit/ci/docker" - def buildArgs = "--build-arg ARTIFACTORY=${artifactoryServer.url} " - - configFileProvider([ - configFile(fileId: 'VECTOR_DOCKER_REGISTRY', variable: 'VECTOR_DOCKER_REGISTRY'), - configFile(fileId: 'SILKIT_ARTIFACTORY_REPO', variable: 'SILKIT_ARTIFACTORY_REPO'), - configFile(fileId: 'PROXY_DOMAIN', variable: 'PROXY_DOMAIN'), - configFile(fileId: 'PYPI_MIRROR', variable: 'PYPI_MIRROR'), - ]) { - def registry = readFile(file: VECTOR_DOCKER_REGISTRY).trim() - buildArgs += " --build-arg REGISTRY=${registry} " - - def repo = readFile(file: SILKIT_ARTIFACTORY_REPO).trim() - buildArgs += " --build-arg SILKIT_ARTIFACTORY_REPO=${repo} " - - def proxyDomain = readFile(file: PROXY_DOMAIN).trim() - buildArgs += " --build-arg PROXY_DOMAIN=${proxyDomain} " - - def pypiMirror = readFile(file: PYPI_MIRROR).trim() - buildArgs += " --build-arg PYPI_MIRROR=${pypiMirror} " - - print("Building docker image ${dockerImage}") - if(config.containsKey("DockerBuildArgs")) { - buildArgs += config["DockerBuildArgs"] - } - // We assume the docker file (located in the docker subfolder) is named exactly like the docker image - buildArgs += " -f ${dockerDir}/${dockerImage} ${dockerDir}" - docker.build(dockerImage, buildArgs) - } - } -} -// runOnNode: a helper that runs the userStages closure on an appropriate node -// i.e., this allows running the same stages on docker and windows -def runOnNode(config, userStages) { - def buildName = config["Name"] - def wsDir = "workspace/${env.JOB_NAME}/" \ - + buildName.replaceAll(" ", "_") + "_${env.BUILD_NUMBER}" - return { - node(config["NodeLabel"]) { - ws(wsDir) { - try { - if(isDockerNode(config["NodeLabel"])) { - //the built image should be cached and shared anyway - shallowCheckoutGit() - print("Calling docker inside in ${pwd()}") - buildDocker(config) - docker.image(config["DockerImage"]).inside { - userStages() - } - } else { - print("Working in ${pwd()}") - userStages() - } - } finally { - cleanWs(cleanWhenNotBuilt: true, cleanWhenFailure: true, - cleanWhenSuccess: true, cleanWhenAborted: true, - deleteDirs: true, disableDeferredWipeout: true) - } - } //ws - } //node - } //return -} - -// Canonical way on CMake + Ninja + VS is to source the appropriate vcvarsall script -// before invoking cmake -G Ninja -def getVsBuildEnv(arch, version){ - def localEnv=[] - def toolDir="${tool 'BuildTools2019'}\\..\\..\\.." - // get vcvarsall.bat environment - def envstr = bat returnStdout: true, script: "\"${toolDir}\\VC\\Auxiliary\\Build\\vcvarsall.bat\" ${arch} -vcvars_ver=${version} &set" - def lines = envstr.split("\r\n") - for(int i = 0; i < lines.size(); i++) { - def line = lines[i] - if(line =~ /^\S+=.*/) { - localEnv += line - } - } - return localEnv -} - -def run(what) { - if(isUnix()) { - sh "${what}" - } else { - bat "${what}" - } -} -// quote percent sign for DOS batch, eg. uses in git log --format=%ct -def quoteBat(userStr) { - def res="@" //disable printing the command itself - for(int i = 0; i < userStr.size(); i++) { - def ch = userStr[i] - if( ch == '%' ) { - res += "%%" - } else{ - res += ch - } - } - return res -} -def runWithOutput(what) { - if(isUnix()) { - return sh(script: "${what}", returnStdout: true).trim() - } else { - return bat(script: "${quoteBat(what)}", returnStdout: true).trim() - } -} - -//########################################## -//the actual build process -//########################################## - -// instantiate a build on an appropriate node -def doBuild(Map config) { - return runOnNode(config, { - def buildName = config["Name"] - def buildPreset = config["CmakePreset"] - def debugPreset = buildPreset.replace("-release", "-debug") - def buildCmakeArgs = config.getOrDefault("CmakeArgs", "") - def buildDocs = config.getOrDefault("BuildDocs", false) - def publishArtifacts = config.getOrDefault("PublishArtifacts", false).toBoolean() - def buildTestDebug = config.getOrDefault("TestDebug", false) - def triggerAbiCheck = config.getOrDefault("TriggerAbiCheck", false) - def checkLicenses = config.getOrDefault("checkLicenses", false) - def pipenvExtraArgs = "" - def pythonExe = isUnix() ? "python" : "py -3.9" - - def buildEnv = [] - def scmVars = [:] - - stage("Git checkout and ENV setup (${buildName})") { - if(config.containsKey("MsvcVersion")) { - // visual studio toolset selection via vcvarsall env - buildEnv = buildEnv + getVsBuildEnv(config["Arch"], config["MsvcVersion"]) - buildEnv.add("MSBUILDDISABLENODEREUSE=1") //fixes spurious windows failures - } - - scmVars = checkoutGit() - - // For reproducible build set SOURCE_DATE_EPOCH to time of last commit - def commitTime = runWithOutput("git log --max-count=1 --format=%ct -r origin/main-mirror") - buildEnv.add("SOURCE_DATE_EPOCH=${commitTime}") - buildEnv.add("TZ=UTC") - buildEnv.add("LC_ALL=C.UTF-8") - buildEnv.add("LANG=C.UTF-8") - - print("DEBUG scmVars=${scmVars}, commitTime=${commitTime}, TZ=${env.TZ}, LC_ALL=${env.LC_ALL} buildEnv=${buildEnv}") - - // save branch name for publishing stage - gitBranch = scmVars.GIT_BRANCH - buildEnv.add("GIT_BRANCH=${scmVars.GIT_BRANCH}") - buildEnv.add("GIT_COMMIT=${scmVars.GIT_COMMIT}") - buildEnv.add("GIT_URL=${scmVars.GIT_URL}") - } - - if(checkLicenses) { - stage("License Check (${buildName})") { - run "sh ./SilKit/ci/check_licenses.sh" - } - } - - def warningsAsError = config.getOrDefault("WarningAsErrors", false).toBoolean() - if(warningsAsError) { - print("Enabling Warnings-as-Errors for the current build config") - buildCmakeArgs +=" -DSILKIT_WARNINGS_AS_ERRORS=ON " - } else { - print("Disabling Warnings-as-Errors for the current build config") - } - if(buildDocs) { - buildCmakeArgs +=" -DSILKIT_BUILD_DOCS=ON " - } - if(publishArtifacts || buildDocs) { - buildCmakeArgs +=" -DSILKIT_INSTALL_SOURCE=ON " - } - - if(config.containsKey("MsvcVersion")) { - def msvcVersion = config["MsvcVersion"] - //needed for binary download URL resolution - buildCmakeArgs += " -D MSVC_TOOLSET_VERSION=${msvcVersion} " - } - - withEnv(buildEnv) { - stage("${buildName}: cmake version") { - run("cmake --version") - } - stage("${buildName}: cmake configure for preset ${buildPreset}") { - if(buildDocs) { - // Install dependencies for documentation - configFileProvider([ - configFile(fileId: 'PYPI_MIRROR', variable: 'PYPI_MIRROR'), - ]) { - def pypiMirror = readFile(file: PYPI_MIRROR).trim() - print("Using pipenv to install docs dependencies") - if(!isUnix()) { - run("py -3.9 -m pip install pipenv==2022.9.8") - pipenvExtraArgs = "--python 3.9" - } - run("${pythonExe} -m pipenv ${pipenvExtraArgs} install --pypi-mirror ${pypiMirror} -r SilKit/ci/docker/docs_requirements.txt") - } - } - run("cmake --preset ${buildPreset} ${buildCmakeArgs}") - } - - stage("${buildName}: cmake build for preset ${buildPreset}") { - if(buildDocs) { - run("${pythonExe} -m pipenv ${pipenvExtraArgs} run cmake --build --preset ${buildPreset}") - } else { - run("cmake --build --preset ${buildPreset}") - } - } - - stage("${buildName}: ctest for preset ${buildPreset}") { - timeout(time: 3, unit: 'MINUTES') { - run("ctest --preset ${buildPreset} -R \"^Test\" --output-on-failure") - junit("_build/${buildPreset}/**/*gtestresults.xml") - } - timeout(time: 10, unit: 'MINUTES') { - run("ctest --preset ${buildPreset} -R \"^ITest\" --output-on-failure") - junit("_build/${buildPreset}/**/*gtestresults.xml") - } - } - - if(globalBuildDebugBin || buildTestDebug) { - stage("${buildName}: cmake build for preset ${buildPreset} @Debug") { - // in Debug builds we only need the library and symbols - def debugFlags = "" - debugFlags += " -D SILKIT_BUILD_DOCS=OFF " - debugFlags += " -D SILKIT_INSTALL_SOURCE=OFF " - debugFlags += " -D SILKIT_BUILD_UTILITIES=OFF " - debugFlags += " -D SILKIT_BUILD_DEMOS=OFF " - - run "cmake --preset ${debugPreset} ${debugFlags}" - run "cmake --build --preset ${debugPreset}" - run "cmake --build --preset ${debugPreset} --target package" - } - } - // run tests for sanitizer builds also in debug mode - if(buildTestDebug) { - stage("${buildName}: ctest for preset ${debugPreset}") { - timeout(time: 3, unit: 'MINUTES') { - run("ctest --preset ${debugPreset} -R \"^Test\" --output-on-failure") - junit("_build/${debugPreset}/**/*gtestresults.xml") - } - timeout(time: 10, unit: 'MINUTES') { - run("ctest --preset ${debugPreset} -R \"^ITest\" --output-on-failure") - junit("_build/${debugPreset}/**/*gtestresults.xml") - } - } - } - stage("${buildName}: packaging artifacts for preset ${buildPreset}") { - if(publishArtifacts) { - run "cmake --build --preset ${buildPreset} --target package" - if(globalBuildDebugBin) { - run "python ./SilKit/ci/package.py \"_build/${buildPreset}/${artifactName}-*-Release.zip\" \"_build/${debugPreset}/${artifactName}-*-Debug.zip\"" - archiveToArtifactory("${artifactName}-.*.zip") - } else { - archiveToArtifactory("_build/${buildPreset}/${artifactName}-.*.zip") - } - - // safe the debug symbols files on windows/Linux - archiveToArtifactory("_build/${debugPreset}/${artifactName}-.*-SYMBOLS.zip") - if(!isUnix()) { - archiveToArtifactory("_build/${buildPreset}/${artifactName}-.*-SYMBOLS.zip") - } - } else { - print("Package publishing disabled: config=${config}") - } - } - if (publishArtifacts && triggerAbiCheck) { - stage("${buildName}: Triggering ABI Check") { - def versionList = [] - - // look for the archive file created in the previous step - for (zipFile in findFiles(glob: "**.zip")) - { - def m = zipFile =~ /^.*SilKit-([0-9]+[.][0-9]+[.][0-9]+)-[^\/]*.zip$/ - if (!m) - { - continue - } - - versionList += m[0][1] - } - - def versions = versionList.toSet() - - if (versions.size() == 0) - { - echo "No version found for which the ABI check could be triggered!" - return - } - - if (versions.size() > 1) - { - error "Multiple versions found for which the ABI check would be triggered!" - } - - // take the first artifact we found and extract the version from it - def silKitVersion = versions.first() - echo "Triggering ABI-Check for SIL Kit version ${silKitVersion} (${env.GIT_BRANCH}, ${env.GIT_COMMIT})" - - build( - job: '/SilKit/sil-kit-dev-abi-check/main', - wait: false, - parameters: [ - string(name: 'TEST_BRANCH', value: env.GIT_BRANCH), - string(name: 'TEST_COMMIT', value: env.GIT_COMMIT), - string(name: 'TEST_VERSION', value: silKitVersion) - ] - ) - } - } - } - }) -} - -// node main entry. parallel invocation of all builds, packaging -node { - try { - properties([ - buildDiscarder(logRotator(numToKeepStr: "${keepNumBuilds}")), - parameters([ - booleanParam(name: "ForceFullBuild", defaultValue: false, - description: "Force a full build with artificat uploads") - ,booleanParam(name: "ForceArtifactUpload", defaultValue: false, - description: "Force uploading artifact uploads") - ,booleanParam(name: "UseThreadSanitizer", defaultValue: false, - description: "Build with thread sanitizer, pelase refer to the output logs of tests") - ,booleanParam(name: "UseAddressSanitizer", defaultValue: false, - description: "Build with thread sanitizer, pelase refer to the output logs of tests") - ]) - ]) - - def doFullBuild = isFullBuildRequired(env.BRANCH_NAME) - if(params.ForceFullBuild) { - print("Forcing a full build at user request!") - doFullBuild = true - } - - def configNames = [] - - if(params.UseAddressSanitizer) { - print("Building on Ubuntu with -f sanitizer=thread") - configNames = [ - "Ubuntu-22.04-clang-14-thread-sanitizer" - ] - } - else if (params.UseThreadSanitizer) { - print("Building on Ubuntu with -f sanitizer=address") - configNames = [ - "Ubuntu-22.04-clang-14-address-sanitizer" - ] - } - else if(doFullBuild) { - print("Doing a full build and publishing artifacts on branch ${env.BRANCH_NAME}") - configNames = [ - "VS2017-Win32", - "VS2017-Win64", - "Ubuntu-18.04-gcc", - "Ubuntu-22.04-clang-14", // most modern compiler, no artifacts - "Ubuntu-22.04-clang-14-thread-sanitizer", // TSan with most modern compiler, no artifacts - "Ubuntu-22.04-clang-14-address-sanitizer", // ASan with most modern compiler, no artifacts - "Ubuntu-22.04-clang-14-undefined-behavior-sanitizer", // UBSan with most modern compiler, no artifacts - ] - publishToArtifactory = true - globalBuildDebugBin = true - } else { - print("Doing a minimal build without publishing artifacts on branch ${env.BRANCH_NAME}") - configNames = [ - "VS2017-Win32", - "Ubuntu-22.04-clang-14-thread-sanitizer", // TSan with most modern compiler - "Ubuntu-22.04-clang-14-address-sanitizer", // ASan with most modern compiler - "Ubuntu-22.04-clang-14-undefined-behavior-sanitizer", // UBSan with most modern compiler - ] - } - def builds = [:] - configNames.each { - def configName = it - def buildConfig = buildConfigs.find { it.Name == configName} - if(params.ForceArtifactUpload) { - print("ForceArtifactUpload: PublishArtifacts is enabled for ${configName}") - buildConfig["PublishArtifacts"] = true - globalBuildDebugBin = true - } else { - if(!doFullBuild) { - print("PublishArtifacts is disabled for ${configName}") - buildConfig["PublishArtifacts"] = false - } - } - builds.put(it, doBuild(buildConfig)) - } - - if(params.ForceArtifactUpload) { - print("Forcing Artifact Upload") - publishToArtifactory = true - } - - parallel(builds) - - } - finally { - if(publishToArtifactory) { - stage("Publishing Build Info") { - print("INFO: publishing artifactory build info for branch ${gitBranch}") - setBuildRetention(artifactoryBuildInfo) - artifactoryServer.publishBuildInfo(artifactoryBuildInfo) - } - } else { - stage("Done (No Publishing)") { - print("INFO: not publishing artifacts") - } - } - cleanWs(cleanWhenNotBuilt: true, cleanWhenFailure: true, - cleanWhenSuccess: true, cleanWhenAborted: true, - deleteDirs: true, disableDeferredWipeout: true ) - } -} diff --git a/SilKit/cmake/SilKitVersion.cmake b/SilKit/cmake/SilKitVersion.cmake index 852d55beb..cc451c2ef 100644 --- a/SilKit/cmake/SilKitVersion.cmake +++ b/SilKit/cmake/SilKitVersion.cmake @@ -3,10 +3,13 @@ # SPDX-License-Identifier: MIT # SIL Kit Versioning: -# * Major and minor release number is configured here. The patch number should not be changed here; it will be set by -# the Jenkins workflow to the master branch's build number for packaging (cmake -SILKIT_BUILD_NUMBER). -# * Major and minor release number, as well as the sprint number are encoded into Version.hpp and compiled into the library, -# they will be accessible from public headers. +# * Major, minor and patch release number are configured here. This is the source of truth: the generated public header +# SilKit/include/silkit/capi/SilKitVersionMacros.h is produced from these numbers and compiled into the library, so +# they are accessible from public headers at runtime. +# * Do not edit the numbers below by hand. Run the sil-kit-generate-version tool, which keeps this file, the generated +# header and the changelog in sync. See docs/development/release.md. +# * SILKIT_BUILD_NUMBER is a build-time override (cmake -DSILKIT_BUILD_NUMBER=N), not source tree state. It defaults to +# 0 and is passed to the sources as a compile definition; the generated header only carries the fallback. macro(configure_silkit_version project_name) set(SILKIT_VERSION_MAJOR 5) set(SILKIT_VERSION_MINOR 0) diff --git a/SilKit/include/silkit/capi/SilKitVersionMacros.h b/SilKit/include/silkit/capi/SilKitVersionMacros.h index 86c83aae3..78a15a924 100644 --- a/SilKit/include/silkit/capi/SilKitVersionMacros.h +++ b/SilKit/include/silkit/capi/SilKitVersionMacros.h @@ -2,12 +2,22 @@ // // SPDX-License-Identifier: MIT +// GENERATED FILE - DO NOT EDIT BY HAND. +// Written by the sil-kit-generate-version tool from the version numbers in +// SilKit/cmake/SilKitVersion.cmake. See docs/development/release.md. + #pragma once -#define SILKIT_GIT_HASH "f3b467c2028695e487d0337868ddae43a5674fe7" +#define SILKIT_GIT_HASH "23932429ac68eecdb8ca7698f35783ee5d89a04b" #define SILKIT_VERSION_MAJOR 5 #define SILKIT_VERSION_MINOR 0 -#define SILKIT_VERSION_PATCH 6 -#define SILKIT_BUILD_NUMBER 0 -#define SILKIT_VERSION_STRING "5.0.6" +#define SILKIT_VERSION_PATCH 8 +#define SILKIT_VERSION_STRING "5.0.8" #define SILKIT_VERSION_SUFFIX "" + +// The build number is a property of the build, not of the source tree: pass +// -DSILKIT_BUILD_NUMBER=N to CMake to set it. This is the fallback for anyone +// compiling against the installed header without it. +#ifndef SILKIT_BUILD_NUMBER +#define SILKIT_BUILD_NUMBER 0 +#endif diff --git a/SilKit/source/CMakeLists.txt b/SilKit/source/CMakeLists.txt index 466133f38..136a76002 100644 --- a/SilKit/source/CMakeLists.txt +++ b/SilKit/source/CMakeLists.txt @@ -14,15 +14,17 @@ add_subdirectory(dashboard) find_package(Threads REQUIRED) -# SilKitVersionMacros.h is committed to source and updated by running sil-kit-version-gen. -# No generation at configure time — any build tool can use the file as a regular source file. -# To update: sil-kit-version-gen --major X --minor Y --patch Z [--source-dir .] +# SilKitVersionMacros.h is committed to source and regenerated by the +# sil-kit-generate-version tool. Nothing is generated at configure time, so any +# build tool can consume the file as a regular source file. +# See docs/development/release.md for the version bump procedure. set(VERSION_MACROS_HPP "${CMAKE_CURRENT_LIST_DIR}/../include/silkit/capi/SilKitVersionMacros.h") if(NOT EXISTS "${VERSION_MACROS_HPP}") message(FATAL_ERROR - "SilKitVersionMacros.h not found. Run sil-kit-version-gen to create it:\n" - " sil-kit-version-gen --major ${SILKIT_VERSION_MAJOR} --minor ${SILKIT_VERSION_MINOR}" - " --patch ${SILKIT_VERSION_PATCH} ${VERSION_MACROS_HPP}") + "SilKitVersionMacros.h not found. Build the sil-kit-generate-version target and run it" + " from the source tree root to create it:\n" + " sil-kit-generate-version\n" + "See docs/development/release.md.") endif() install(FILES @@ -45,6 +47,15 @@ target_include_directories(I_SilKit INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}" ) +# The build number is a property of a build, not of the source tree, so it is not +# stored in the generated SilKitVersionMacros.h. This definition carries it to +# both the C++ sources and the .rc resource scripts (CMake passes a target's +# compile definitions to the resource compiler); the header only provides the +# fallback of 0 for anyone compiling against the installed header without CMake. +target_compile_definitions(I_SilKit + INTERFACE SILKIT_BUILD_NUMBER=${SILKIT_BUILD_NUMBER} +) + if (MSVC) target_compile_definitions(I_SilKit INTERFACE _WIN32_WINNT=0x0601 diff --git a/SilKit/source/util/CMakeLists.txt b/SilKit/source/util/CMakeLists.txt index d851f4afd..e59940fb4 100644 --- a/SilKit/source/util/CMakeLists.txt +++ b/SilKit/source/util/CMakeLists.txt @@ -85,10 +85,24 @@ add_silkit_test_to_executable(SilKitUnitTests ) -add_executable(SilKit_version_gen sil-kit-version-gen.cpp) -set_target_properties(SilKit_version_gen PROPERTIES - CXX_STANDARD 17 - OUTPUT_NAME "sil-kit-version-gen" +# Maintainer tool for version bumps. Standalone on purpose: it must not depend +# on I_SilKit, since it generates a header the library itself is built from. +add_library(O_SilKit_Util_GenerateVersion OBJECT + GenerateVersion.hpp + GenerateVersion.cpp +) + +# Root-relative includes as everywhere else in SilKit/source, so consumers write +# #include "util/GenerateVersion.hpp". +target_include_directories(O_SilKit_Util_GenerateVersion + PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/.." +) + +add_executable(sil-kit-generate-version sil-kit-generate-version.cpp) +target_link_libraries(sil-kit-generate-version + PRIVATE O_SilKit_Util_GenerateVersion +) +set_target_properties(sil-kit-generate-version PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$" ) diff --git a/SilKit/source/util/GenerateVersion.cpp b/SilKit/source/util/GenerateVersion.cpp new file mode 100644 index 000000000..f04d3c66d --- /dev/null +++ b/SilKit/source/util/GenerateVersion.cpp @@ -0,0 +1,309 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include "GenerateVersion.hpp" + +#include +#include +#include +#include + +namespace SilKit { +namespace VersionGen { + +namespace { + +// The literal contents of SilKitVersionMacros.h. Keep this ASCII only: the file +// is consumed by the MSVC resource compiler as well as by C and C++. +constexpr auto kHeaderTemplate = R"(// SPDX-FileCopyrightText: 2022 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +// GENERATED FILE - DO NOT EDIT BY HAND. +// Written by the sil-kit-generate-version tool from the version numbers in +// SilKit/cmake/SilKitVersion.cmake. See docs/development/release.md. + +#pragma once + +#define SILKIT_GIT_HASH "@GIT_HASH@" +#define SILKIT_VERSION_MAJOR @MAJOR@ +#define SILKIT_VERSION_MINOR @MINOR@ +#define SILKIT_VERSION_PATCH @PATCH@ +#define SILKIT_VERSION_STRING "@VERSION_STRING@" +#define SILKIT_VERSION_SUFFIX "@SUFFIX@" + +// The build number is a property of the build, not of the source tree: pass +// -DSILKIT_BUILD_NUMBER=N to CMake to set it. This is the fallback for anyone +// compiling against the installed header without it. +#ifndef SILKIT_BUILD_NUMBER +#define SILKIT_BUILD_NUMBER 0 +#endif +)"; + +void ReplaceAll(std::string& s, const std::string& from, const std::string& to) +{ + for (size_t pos = 0; (pos = s.find(from, pos)) != std::string::npos; pos += to.size()) + { + s.replace(pos, from.size(), to); + } +} + +void SubstituteVar(std::string& s, const std::string& name, const std::string& value) +{ + ReplaceAll(s, "@" + name + "@", value); +} + +// set(SILKIT_VERSION_MAJOR 5), tolerating any whitespace CMake would accept. +std::regex CMakeIntSetter(const std::string& variable) +{ + return std::regex{"(set[ \t]*\\([ \t]*" + variable + "[ \t]+)([0-9]+)"}; +} + +// set(SILKIT_VERSION_SUFFIX "rc1") +std::regex CMakeStringSetter(const std::string& variable) +{ + return std::regex{"(set[ \t]*\\([ \t]*" + variable + "[ \t]+\")([^\"]*)(\")"}; +} + +int MatchCMakeInt(const std::string& content, const std::regex& re) +{ + std::smatch match; + if (!std::regex_search(content, match, re)) + { + return -1; + } + return std::atoi(match[2].str().c_str()); +} + +// #define SILKIT_VERSION_MAJOR 5 +int DefineInt(const std::string& content, const std::string& macroName) +{ + std::smatch match; + const std::regex re{"#[ \t]*define[ \t]+" + macroName + "[ \t]+(-?[0-9]+)"}; + if (!std::regex_search(content, match, re)) + { + return -1; + } + return std::atoi(match[1].str().c_str()); +} + +// #define SILKIT_VERSION_SUFFIX "rc1" +bool DefineString(const std::string& content, const std::string& macroName, std::string& out) +{ + std::smatch match; + const std::regex re{"#[ \t]*define[ \t]+" + macroName + "[ \t]+\"([^\"]*)\""}; + if (!std::regex_search(content, match, re)) + { + return false; + } + out = match[1].str(); + return true; +} + +// Replaces the text captured by the given group of the first match. Done by +// position rather than with a regex_replace format string, because "$1" glued +// in front of a digit would read as a reference to group 1x. +bool ReplaceCapture(std::string& content, const std::regex& re, int group, const std::string& value) +{ + std::smatch match; + if (!std::regex_search(content, match, re)) + { + return false; + } + content.replace(static_cast(match.position(group)), static_cast(match.length(group)), value); + return true; +} + +} // namespace + +std::string Version::ToShortString() const +{ + return std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(patch); +} + +std::string Version::ToString() const +{ + return ToShortString() + (suffix.empty() ? "" : "-" + suffix); +} + +Version ParseVersionFromCMake(const std::string& content) +{ + Version version; + version.major = MatchCMakeInt(content, CMakeIntSetter("SILKIT_VERSION_MAJOR")); + version.minor = MatchCMakeInt(content, CMakeIntSetter("SILKIT_VERSION_MINOR")); + version.patch = MatchCMakeInt(content, CMakeIntSetter("SILKIT_VERSION_PATCH")); + + std::smatch match; + if (std::regex_search(content, match, CMakeStringSetter("SILKIT_VERSION_SUFFIX"))) + { + version.suffix = match[2].str(); + } + return version; +} + +Version ParseVersionFromHeader(const std::string& content) +{ + Version version; + version.major = DefineInt(content, "SILKIT_VERSION_MAJOR"); + version.minor = DefineInt(content, "SILKIT_VERSION_MINOR"); + version.patch = DefineInt(content, "SILKIT_VERSION_PATCH"); + DefineString(content, "SILKIT_VERSION_SUFFIX", version.suffix); + return version; +} + +std::string ParseGitHashFromHeader(const std::string& content) +{ + std::string gitHash; + DefineString(content, "SILKIT_GIT_HASH", gitHash); + return gitHash; +} + +bool LooksLikeGeneratedHeader(const std::string& content) +{ + std::string gitHash; + return DefineInt(content, "SILKIT_VERSION_MAJOR") >= 0 && DefineString(content, "SILKIT_GIT_HASH", gitHash); +} + +bool PatchCMakeVersion(std::string& content, const Version& version, std::string& error) +{ + struct Setter + { + const char* variable; + int value; + }; + const Setter setters[] = { + {"SILKIT_VERSION_MAJOR", version.major}, + {"SILKIT_VERSION_MINOR", version.minor}, + {"SILKIT_VERSION_PATCH", version.patch}, + }; + + std::string result = content; + for (const auto& setter : setters) + { + if (!ReplaceCapture(result, CMakeIntSetter(setter.variable), 2, std::to_string(setter.value))) + { + error = std::string{"no 'set("} + setter.variable + " )' found"; + return false; + } + } + + if (!ReplaceCapture(result, CMakeStringSetter("SILKIT_VERSION_SUFFIX"), 2, version.suffix)) + { + error = "no 'set(SILKIT_VERSION_SUFFIX \"...\")' found"; + return false; + } + + content = result; + return true; +} + +std::string RenderHeader(const std::string& gitHash, const Version& version) +{ + std::string result{kHeaderTemplate}; + SubstituteVar(result, "GIT_HASH", gitHash); + SubstituteVar(result, "MAJOR", std::to_string(version.major)); + SubstituteVar(result, "MINOR", std::to_string(version.minor)); + SubstituteVar(result, "PATCH", std::to_string(version.patch)); + SubstituteVar(result, "VERSION_STRING", version.ToString()); + SubstituteVar(result, "SUFFIX", version.suffix); + return result; +} + +std::string FinalizeChangelogHeading(const std::string& content, const Version& version, const std::string& date) +{ + // Only the first '# [x.y.z] - ' heading is rewritten; a body that + // happens to quote another one is left alone. + const std::regex heading{"[ \t]*#[ \t]*\\[[^\\]\r\n]*\\][ \t]*-[ \t]*[^\r\n]*"}; + std::smatch match; + if (!std::regex_search(content, match, heading)) + { + return content; + } + + std::string result = content; + result.replace(static_cast(match.position(0)), static_cast(match.length(0)), + "# [" + version.ToString() + "] - " + date); + return result; +} + +std::string RenderChangelogStub(const Version& version) +{ + return "# [" + version.ToString() + "] - UNRELEASED\n\n> This changelog entry is still empty.\n"; +} + +bool InsertChangelogToctreeEntry(std::string& content, const Version& version, std::string& error) +{ + const std::string entry = "versions/" + version.ToShortString() + ".md"; + if (content.find(entry) != std::string::npos) + { + error = "'" + entry + "' is already listed"; + return false; + } + + const std::string anchor = "versions/latest.md"; + const size_t anchorPos = content.find(anchor); + if (anchorPos == std::string::npos) + { + error = "no '" + anchor + "' toctree entry to insert after"; + return false; + } + + // Reuse the anchor line's indentation and line ending verbatim. + const size_t lineStart = content.rfind('\n', anchorPos); + const std::string indent = content.substr(lineStart + 1, anchorPos - (lineStart + 1)); + + const size_t lineEnd = content.find('\n', anchorPos); + if (lineEnd == std::string::npos) + { + content += "\n" + indent + entry; + return true; + } + const bool crlf = lineEnd > 0 && content[lineEnd - 1] == '\r'; + const std::string eol = crlf ? "\r\n" : "\n"; + + content.insert(lineEnd + 1, indent + entry + eol); + return true; +} + +std::string TodayIsoDate() +{ + const std::time_t now = std::time(nullptr); + std::tm local{}; +#if defined(_WIN32) + localtime_s(&local, &now); +#else + localtime_r(&now, &local); +#endif + char buffer[16] = {}; + std::snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d", local.tm_year + 1900, local.tm_mon + 1, local.tm_mday); + return buffer; +} + +bool UsesCrlf(const std::string& content) +{ + return content.find("\r\n") != std::string::npos; +} + +std::string WithLineEndings(const std::string& content, bool crlf) +{ + std::string result; + result.reserve(content.size() + content.size() / 16); + for (size_t i = 0; i < content.size(); ++i) + { + const char character = content[i]; + if (character == '\r' && i + 1 < content.size() && content[i + 1] == '\n') + { + continue; // the '\n' below re-adds the requested ending + } + if (character == '\n' && crlf) + { + result += '\r'; + } + result += character; + } + return result; +} + +} // namespace VersionGen +} // namespace SilKit diff --git a/SilKit/source/util/GenerateVersion.hpp b/SilKit/source/util/GenerateVersion.hpp new file mode 100644 index 000000000..138cb3a78 --- /dev/null +++ b/SilKit/source/util/GenerateVersion.hpp @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +// Pure text transformations behind the sil-kit-generate-version tool. +// +// Everything here operates on in-memory strings so it can be unit tested +// without touching the source tree. All rendered output uses LF line endings +// and stays within ASCII. + +#pragma once + +#include + +namespace SilKit { +namespace VersionGen { + +struct Version +{ + int major{-1}; + int minor{-1}; + int patch{-1}; + std::string suffix; + + bool IsValid() const + { + return major >= 0 && minor >= 0 && patch >= 0; + } + + // "5.0.8", without the suffix + std::string ToShortString() const; + + // "5.0.8" or "5.0.8-rc1" + std::string ToString() const; + + bool SameNumbers(const Version& other) const + { + return major == other.major && minor == other.minor && patch == other.patch; + } +}; + +// Parses SILKIT_VERSION_{MAJOR,MINOR,PATCH,SUFFIX} out of the contents of +// SilKit/cmake/SilKitVersion.cmake. Returns an invalid Version if a number is +// missing. +Version ParseVersionFromCMake(const std::string& content); + +// Parses the same values out of a generated SilKitVersionMacros.h. +Version ParseVersionFromHeader(const std::string& content); + +// Extracts SILKIT_GIT_HASH from a generated header, or "" if absent. +std::string ParseGitHashFromHeader(const std::string& content); + +// True if the content is recognizably a generated version-macros header. Used +// to refuse overwriting hand-written headers such as silkit/capi/Version.h. +bool LooksLikeGeneratedHeader(const std::string& content); + +// Rewrites the set(SILKIT_VERSION_*) lines in SilKitVersion.cmake, leaving +// every other byte untouched. Returns false and fills 'error' if one of the +// expected lines is missing. +bool PatchCMakeVersion(std::string& content, const Version& version, std::string& error); + +// Renders the full SilKitVersionMacros.h. The build number is deliberately not +// a parameter: it is a build-time CMake definition, not source tree state. +std::string RenderHeader(const std::string& gitHash, const Version& version); + +// Rewrites the leading '# [x.y.z] - UNRELEASED' heading of a changelog entry to +// the given version and release date. The body is left untouched. If no such +// heading is found the content is returned unchanged. +std::string FinalizeChangelogHeading(const std::string& content, const Version& version, const std::string& date); + +// The placeholder written to latest.md right after a bump. +std::string RenderChangelogStub(const Version& version); + +// Inserts a 'versions/.md' line into the toctree of +// docs/changelog/overview.rst, directly after 'versions/latest.md'. Returns +// false and fills 'error' if the anchor is missing or the entry already exists. +bool InsertChangelogToctreeEntry(std::string& content, const Version& version, std::string& error); + +// Today's date as YYYY-MM-DD in local time. +std::string TodayIsoDate(); + +// True if the content uses CRLF line endings. The working tree may be either, +// depending on core.autocrlf, so generated files follow what is already there. +bool UsesCrlf(const std::string& content); + +// Rewrites CRLF/LF line endings to the requested kind. A lone CR is left alone. +std::string WithLineEndings(const std::string& content, bool crlf); + +} // namespace VersionGen +} // namespace SilKit diff --git a/SilKit/source/util/sil-kit-generate-version.cpp b/SilKit/source/util/sil-kit-generate-version.cpp new file mode 100644 index 000000000..2db6a3e1f --- /dev/null +++ b/SilKit/source/util/sil-kit-generate-version.cpp @@ -0,0 +1,645 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +// Standalone maintainer tool that performs a SIL Kit version bump: +// +// * rewrites the version numbers in SilKit/cmake/SilKitVersion.cmake +// * regenerates SilKit/include/silkit/capi/SilKitVersionMacros.h +// * archives docs/changelog/versions/latest.md as .md and lists +// it in docs/changelog/overview.rst +// +// Either all of that succeeds or nothing is written. Run without version +// arguments to only refresh the generated header (e.g. after a rebase). +// +// See docs/development/release.md for the full procedure. +// +// Requires C++17 (std::filesystem). No external dependencies. + +#include "GenerateVersion.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; +using SilKit::VersionGen::Version; + +namespace { + +// --------------------------------------------------------------------------- +// File helpers +// --------------------------------------------------------------------------- + +// Paths are printed through Show(): streaming an fs::path quotes it and escapes +// the backslashes, which is unreadable on Windows. Normalizing also keeps the +// separators consistent in paths assembled from "a/b/c" fragments. +std::string Show(const fs::path& path) +{ + return path.lexically_normal().make_preferred().string(); +} + +std::string ReadFileFull(const fs::path& path) +{ + std::ifstream file{path, std::ios::binary}; + if (!file.is_open()) + { + return ""; + } + std::ostringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); +} + +// Written in binary mode: the line endings are chosen explicitly by the caller +// to match the working tree rather than left to the platform's text mode. +bool WriteFileFull(const fs::path& path, const std::string& content) +{ + std::ofstream file{path, std::ios::binary | std::ios::trunc}; + if (!file.is_open()) + { + std::cerr << "error: cannot write " << Show(path) << "\n"; + return false; + } + file << content; + if (!file.good()) + { + std::cerr << "error: failed while writing " << Show(path) << "\n"; + return false; + } + return true; +} + +std::string TrimStr(const std::string& str) +{ + const size_t start = str.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) + { + return ""; + } + const size_t end = str.find_last_not_of(" \t\r\n"); + return str.substr(start, end - start + 1); +} + +// --------------------------------------------------------------------------- +// Source tree layout +// --------------------------------------------------------------------------- + +const char* const kCMakeVersionFile = "SilKit/cmake/SilKitVersion.cmake"; +const char* const kVersionMacrosHeader = "SilKit/include/silkit/capi/SilKitVersionMacros.h"; +const char* const kChangelogVersionsDir = "docs/changelog/versions"; +const char* const kChangelogOverview = "docs/changelog/overview.rst"; + +struct Layout +{ + fs::path sourceDir; + fs::path cmakeVersionFile; + fs::path versionsDir; + fs::path latestMd; + fs::path overviewRst; +}; + +Layout MakeLayout(const fs::path& sourceDir) +{ + Layout layout; + layout.sourceDir = sourceDir; + layout.cmakeVersionFile = sourceDir / kCMakeVersionFile; + layout.versionsDir = sourceDir / kChangelogVersionsDir; + layout.latestMd = layout.versionsDir / "latest.md"; + layout.overviewRst = sourceDir / kChangelogOverview; + return layout; +} + +// A directory is the source tree root if it holds SilKitVersion.cmake. +fs::path FindSourceDir() +{ + fs::path dir = fs::current_path(); + while (true) + { + if (fs::exists(dir / kCMakeVersionFile)) + { + return dir; + } + const fs::path parent = dir.parent_path(); + if (parent == dir) + { + break; + } + dir = parent; + } + return {}; +} + +// --------------------------------------------------------------------------- +// Git hash resolution (no git binary required) +// --------------------------------------------------------------------------- + +// In a linked worktree '.git' is a file containing 'gitdir: '. +fs::path ResolveGitDir(const fs::path& candidate) +{ + if (fs::is_directory(candidate)) + { + return candidate; + } + if (!fs::is_regular_file(candidate)) + { + return {}; + } + + const std::string content = TrimStr(ReadFileFull(candidate)); + const std::string prefix = "gitdir: "; + if (content.compare(0, prefix.size(), prefix) != 0) + { + return {}; + } + fs::path gitDir = TrimStr(content.substr(prefix.size())); + if (gitDir.is_relative()) + { + gitDir = candidate.parent_path() / gitDir; + } + return fs::is_directory(gitDir) ? gitDir : fs::path{}; +} + +std::string FindPackedRef(const fs::path& packedRefs, const std::string& ref) +{ + const std::string content = ReadFileFull(packedRefs); + if (content.empty()) + { + return ""; + } + std::istringstream stream{content}; + std::string line; + while (std::getline(stream, line)) + { + if (line.empty() || line[0] == '#' || line[0] == '^') + { + continue; + } + const size_t space = line.find(' '); + if (space != std::string::npos && TrimStr(line.substr(space + 1)) == ref) + { + return line.substr(0, space); + } + } + return ""; +} + +std::string ResolveGitHash(const fs::path& gitDir) +{ + const std::string head = TrimStr(ReadFileFull(gitDir / "HEAD")); + if (head.empty()) + { + return "UNKNOWN"; + } + + const std::string refPrefix = "ref: "; + if (head.compare(0, refPrefix.size(), refPrefix) != 0) + { + // Detached HEAD: the content is the hash itself. + return head.size() >= 40 ? head.substr(0, 40) : head; + } + + const std::string ref = TrimStr(head.substr(refPrefix.size())); + + // A linked worktree keeps HEAD locally but shares refs via 'commondir'. + std::vector searchDirs{gitDir}; + const std::string commonDir = TrimStr(ReadFileFull(gitDir / "commondir")); + if (!commonDir.empty()) + { + fs::path common = commonDir; + if (common.is_relative()) + { + common = gitDir / common; + } + searchDirs.push_back(fs::weakly_canonical(common)); + } + + for (const auto& dir : searchDirs) + { + if (fs::exists(dir / ref)) + { + return TrimStr(ReadFileFull(dir / ref)); + } + } + for (const auto& dir : searchDirs) + { + const std::string packed = FindPackedRef(dir / "packed-refs", ref); + if (!packed.empty()) + { + return packed; + } + } + return "UNKNOWN"; +} + +// --------------------------------------------------------------------------- +// Command line +// --------------------------------------------------------------------------- + +void PrintUsage(const char* prog) +{ + std::cout << "Usage: " << prog << " [options] [output-header|-]\n" + << "\n" + << "Performs a SIL Kit version bump: patches SilKitVersion.cmake, regenerates\n" + << "SilKitVersionMacros.h, and rotates the changelog. Without --major/--minor/\n" + << "--patch it only regenerates the header from the current version.\n" + << "\n" + << " --major N New version major (requires --minor and --patch)\n" + << " --minor N New version minor\n" + << " --patch N New version patch\n" + << " --suffix S Version suffix, e.g. rc1 (default: keep current)\n" + << " --date YYYY-MM-DD Release date for the archived changelog entry\n" + << " (default: today)\n" + << " --source-dir PATH Source tree root (default: search upward from CWD)\n" + << " --git-dir PATH Path to .git (default: /.git)\n" + << " --git-hash HASH Override the git hash; skips reading .git\n" + << " --no-changelog Do not rotate the changelog\n" + << " --force Overwrite an existing archived changelog entry\n" + << " --check Verify the header matches SilKitVersion.cmake and exit;\n" + << " writes nothing, non-zero exit on mismatch\n" + << " --dry-run, -n Print what would happen, write nothing\n" + << " --help, -h Show this help\n" + << "\n" + << "The output header defaults to /" << kVersionMacrosHeader << ".\n" + << "Pass '-' to write the header to stdout instead. An existing file that is not\n" + << "a generated version-macros header is never overwritten.\n" + << "\n" + << "The build number is not set here: it is a property of a build, not of the\n" + << "source tree. Pass -DSILKIT_BUILD_NUMBER=N to CMake; the generated header only\n" + << "provides the fallback of 0.\n" + << "\n" + << "See docs/development/release.md for the release procedure.\n"; +} + +struct Options +{ + int major{-1}; + int minor{-1}; + int patch{-1}; + bool haveSuffix{false}; + std::string suffix; + std::string date; + fs::path sourceDir; + fs::path gitDir; + bool haveGitDir{false}; + std::string gitHashOverride; + std::string outputPath; + bool haveOutputPath{false}; + bool noChangelog{false}; + bool force{false}; + bool check{false}; + bool dryRun{false}; +}; + +bool ParseOptions(int argc, char* argv[], Options& options) +{ + for (int i = 1; i < argc; ++i) + { + const std::string arg = argv[i]; + + auto requireNext = [&]() -> std::string { + if (i + 1 >= argc) + { + std::cerr << "error: " << arg << " requires an argument\n"; + std::exit(1); + } + return argv[++i]; + }; + + if (arg == "--help" || arg == "-h") + { + PrintUsage(argv[0]); + std::exit(0); + } + else if (arg == "--major") + { + options.major = std::atoi(requireNext().c_str()); + } + else if (arg == "--minor") + { + options.minor = std::atoi(requireNext().c_str()); + } + else if (arg == "--patch") + { + options.patch = std::atoi(requireNext().c_str()); + } + else if (arg == "--suffix") + { + options.suffix = requireNext(); + options.haveSuffix = true; + } + else if (arg == "--date") + { + options.date = requireNext(); + } + else if (arg == "--source-dir") + { + options.sourceDir = requireNext(); + } + else if (arg == "--git-dir") + { + options.gitDir = requireNext(); + options.haveGitDir = true; + } + else if (arg == "--git-hash") + { + options.gitHashOverride = requireNext(); + } + else if (arg == "--no-changelog") + { + options.noChangelog = true; + } + else if (arg == "--force") + { + options.force = true; + } + else if (arg == "--check") + { + options.check = true; + } + else if (arg == "--dry-run" || arg == "-n") + { + options.dryRun = true; + } + else if (arg == "-" || (!arg.empty() && arg[0] != '-')) + { + if (options.haveOutputPath) + { + std::cerr << "error: more than one output path given ('" << options.outputPath << "' and '" << arg + << "')\n"; + return false; + } + options.outputPath = arg; + options.haveOutputPath = true; + } + else + { + std::cerr << "error: unknown option '" << arg << "'\n"; + PrintUsage(argv[0]); + return false; + } + } + return true; +} + +// --------------------------------------------------------------------------- +// Planned file writes: everything is validated before anything is written +// --------------------------------------------------------------------------- + +struct PlannedWrite +{ + fs::path path; + std::string content; + std::string what; +}; + +bool CommitWrites(const std::vector& writes, bool useCrlf, bool dryRun) +{ + for (const auto& write : writes) + { + if (dryRun) + { + std::cout << "[dry-run] " << write.what << ": " << Show(write.path) << "\n"; + continue; + } + if (!WriteFileFull(write.path, SilKit::VersionGen::WithLineEndings(write.content, useCrlf))) + { + return false; + } + std::cout << write.what << ": " << Show(write.path) << "\n"; + } + return true; +} + +int RunCheck(const Layout& layout, const fs::path& headerPath, const Version& cmakeVersion) +{ + if (!fs::exists(headerPath)) + { + std::cerr << "error: " << Show(headerPath) << " does not exist\n"; + return 1; + } + + const std::string headerContent = ReadFileFull(headerPath); + const Version headerVersion = SilKit::VersionGen::ParseVersionFromHeader(headerContent); + if (!headerVersion.IsValid()) + { + std::cerr << "error: " << Show(headerPath) << " has no usable version macros\n"; + return 1; + } + if (!headerVersion.SameNumbers(cmakeVersion) || headerVersion.suffix != cmakeVersion.suffix) + { + std::cerr << "error: version drift\n" + << " " << Show(layout.cmakeVersionFile) << ": " << cmakeVersion.ToString() << "\n" + << " " << Show(headerPath) << ": " << headerVersion.ToString() << "\n" + << " run sil-kit-generate-version (without version arguments) to regenerate the header\n"; + return 1; + } + + std::cout << "ok: " << Show(headerPath) << " matches " << Show(layout.cmakeVersionFile) << " at " + << cmakeVersion.ToString() << "\n"; + return 0; +} + +} // namespace + +int main(int argc, char* argv[]) +{ + Options options; + if (!ParseOptions(argc, argv, options)) + { + return 1; + } + + // --- locate the source tree ------------------------------------------- + + fs::path sourceDir = options.sourceDir; + if (sourceDir.empty()) + { + sourceDir = FindSourceDir(); + } + if (sourceDir.empty() || !fs::exists(sourceDir / kCMakeVersionFile)) + { + std::cerr << "error: no SIL Kit source tree found (expected " << kCMakeVersionFile << ")\n" + << " run from inside the source tree or pass --source-dir PATH\n"; + return 1; + } + const Layout layout = MakeLayout(sourceDir); + + // --- the version currently recorded in the source tree ------------------ + + std::string cmakeContent = ReadFileFull(layout.cmakeVersionFile); + // Generated files follow the line endings the working tree already uses, + // which depends on git's core.autocrlf. + const bool useCrlf = SilKit::VersionGen::UsesCrlf(cmakeContent); + const Version currentVersion = SilKit::VersionGen::ParseVersionFromCMake(cmakeContent); + if (!currentVersion.IsValid()) + { + std::cerr << "error: cannot read the version from " << Show(layout.cmakeVersionFile) << "\n"; + return 1; + } + + // --- output header ------------------------------------------------------ + + const bool toStdout = options.outputPath == "-"; + const fs::path headerPath = + (options.haveOutputPath && !toStdout) ? fs::path{options.outputPath} : sourceDir / kVersionMacrosHeader; + + if (options.check) + { + return RunCheck(layout, headerPath, currentVersion); + } + + // --- the version being written ------------------------------------------ + + const int given = (options.major >= 0) + (options.minor >= 0) + (options.patch >= 0); + if (given != 0 && given != 3) + { + std::cerr << "error: --major, --minor and --patch must be given together\n"; + return 1; + } + + Version newVersion = currentVersion; + if (given == 3) + { + newVersion.major = options.major; + newVersion.minor = options.minor; + newVersion.patch = options.patch; + } + if (options.haveSuffix) + { + newVersion.suffix = options.suffix; + } + + const bool bumping = !newVersion.SameNumbers(currentVersion); + const bool cmakeChanged = bumping || newVersion.suffix != currentVersion.suffix; + + // --- guard the output header -------------------------------------------- + + if (!toStdout && fs::exists(headerPath)) + { + const std::string existing = ReadFileFull(headerPath); + if (!SilKit::VersionGen::LooksLikeGeneratedHeader(existing)) + { + std::cerr << "error: refusing to overwrite " << Show(headerPath) << "\n" + << " this is not a generated version-macros header (no SILKIT_VERSION_MAJOR /\n" + << " SILKIT_GIT_HASH defines). Did you mean " << kVersionMacrosHeader << "?\n"; + return 1; + } + + // Report drift instead of silently taking the header's word for it. + const Version headerVersion = SilKit::VersionGen::ParseVersionFromHeader(existing); + if (headerVersion.IsValid() && !headerVersion.SameNumbers(currentVersion)) + { + std::cout << "note: header was at " << headerVersion.ToString() << ", " << Show(layout.cmakeVersionFile) + << " says " << currentVersion.ToString() << "; using the latter\n"; + } + } + + // --- git hash ----------------------------------------------------------- + + std::string gitHash = options.gitHashOverride; + if (gitHash.empty()) + { + const fs::path gitDir = ResolveGitDir(options.haveGitDir ? options.gitDir : sourceDir / ".git"); + gitHash = gitDir.empty() ? "UNKNOWN" : ResolveGitHash(gitDir); + if (gitHash == "UNKNOWN") + { + std::cerr << "warning: could not determine the git hash; writing \"UNKNOWN\"\n"; + } + } + + // --- stdout mode: no side effects --------------------------------------- + + if (toStdout) + { + std::cout << SilKit::VersionGen::RenderHeader(gitHash, newVersion); + return 0; + } + + // --- plan every write, validating as we go ------------------------------ + + std::vector writes; + const bool rotating = bumping && !options.noChangelog; + + if (cmakeChanged) + { + std::string error; + if (!SilKit::VersionGen::PatchCMakeVersion(cmakeContent, newVersion, error)) + { + std::cerr << "error: cannot update " << Show(layout.cmakeVersionFile) << ": " << error << "\n"; + return 1; + } + writes.push_back({layout.cmakeVersionFile, cmakeContent, + "version " + currentVersion.ToString() + " -> " + newVersion.ToString()}); + } + + if (rotating) + { + const fs::path archiveMd = layout.versionsDir / (currentVersion.ToShortString() + ".md"); + + if (!fs::exists(layout.latestMd)) + { + std::cerr << "error: " << Show(layout.latestMd) << " not found; cannot rotate the changelog\n" + << " pass --no-changelog to bump the version anyway\n"; + return 1; + } + if (fs::exists(archiveMd) && !options.force) + { + std::cerr << "error: " << Show(archiveMd) << " already exists\n" + << " " << Show(layout.cmakeVersionFile) << " says the current version is " + << currentVersion.ToString() << ", but that entry is already archived.\n" + << " Check the version numbers, or pass --force to overwrite, or --no-changelog\n" + << " to leave the changelog alone.\n"; + return 1; + } + if (!fs::exists(layout.overviewRst)) + { + std::cerr << "error: " << Show(layout.overviewRst) << " not found\n"; + return 1; + } + + const std::string date = options.date.empty() ? SilKit::VersionGen::TodayIsoDate() : options.date; + const std::string latestContent = ReadFileFull(layout.latestMd); + + writes.push_back({archiveMd, SilKit::VersionGen::FinalizeChangelogHeading(latestContent, currentVersion, date), + "changelog: archive " + currentVersion.ToString() + " (" + date + ")"}); + writes.push_back({layout.latestMd, SilKit::VersionGen::RenderChangelogStub(newVersion), + "changelog: reset latest.md for " + newVersion.ToString()}); + + std::string overviewContent = ReadFileFull(layout.overviewRst); + std::string error; + if (SilKit::VersionGen::InsertChangelogToctreeEntry(overviewContent, currentVersion, error)) + { + writes.push_back({layout.overviewRst, overviewContent, + "changelog: list " + currentVersion.ToShortString() + ".md in the toctree"}); + } + else if (!options.force) + { + std::cerr << "error: cannot update " << Show(layout.overviewRst) << ": " << error << "\n"; + return 1; + } + else + { + std::cout << "note: " << Show(layout.overviewRst) << " left unchanged: " << error << "\n"; + } + } + + writes.push_back({headerPath, SilKit::VersionGen::RenderHeader(gitHash, newVersion), + "header " + newVersion.ToString() + " (git hash: " + gitHash + ")"}); + + if (!CommitWrites(writes, useCrlf, options.dryRun)) + { + return 1; + } + + if (!options.dryRun && bumping) + { + std::cout << "\nNext: fill in " << Show(layout.latestMd) << " and review 'git diff'.\n" + << "See docs/development/release.md.\n"; + } + return 0; +} diff --git a/SilKit/source/util/sil-kit-version-gen.cpp b/SilKit/source/util/sil-kit-version-gen.cpp deleted file mode 100644 index c49553a98..000000000 --- a/SilKit/source/util/sil-kit-version-gen.cpp +++ /dev/null @@ -1,401 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH -// -// SPDX-License-Identifier: MIT - -// Standalone tool: generates version_macros.hpp from CLI arguments and the -// current git HEAD hash. Optionally rotates the changelog when the version -// number changes. Requires C++17 (std::filesystem). No external dependencies. -// -// Usage: sil-kit-version-gen [options] -// --major N Version major (required) -// --minor N Version minor (required) -// --patch N Version patch (required) -// --build N Build number (default: 0) -// --suffix S Version suffix (default: "") -// --git-dir PATH Path to .git directory (default: search upward from CWD) -// --git-hash HASH Override git hash; skips .git reading -// --source-dir PATH Source tree root for changelog files (default: CWD) -// --no-changelog Skip changelog rotation even if version changed -// --dry-run Print actions without writing any files -// Pass '-' as output-header to write to stdout. - -#include -#include -#include -#include -#include -#include - - -namespace fs = std::filesystem; - -// --------------------------------------------------------------------------- -// File helpers -// --------------------------------------------------------------------------- - -static std::string readFileFull(const fs::path& path) -{ - std::ifstream f(path); - if (!f.is_open()) - return ""; - std::ostringstream ss; - ss << f.rdbuf(); - return ss.str(); -} - -static bool writeFileFull(const fs::path& path, const std::string& content) -{ - std::ofstream f(path); - if (!f.is_open()) - { - std::cerr << "error: cannot write " << path << "\n"; - return false; - } - f << content; - return f.good(); -} - -static std::string trimStr(const std::string& s) -{ - size_t start = s.find_first_not_of(" \t\r\n"); - if (start == std::string::npos) - return ""; - size_t end = s.find_last_not_of(" \t\r\n"); - return s.substr(start, end - start + 1); -} - -// --------------------------------------------------------------------------- -// Git hash resolution (mirrors the old MakeVersionMacros.cmake.in logic) -// --------------------------------------------------------------------------- - -static fs::path findGitDir() -{ - fs::path dir = fs::current_path(); - while (true) - { - fs::path candidate = dir / ".git"; - if (fs::exists(candidate / "HEAD")) - return candidate; - fs::path parent = dir.parent_path(); - if (parent == dir) - break; - dir = parent; - } - return {}; -} - -static std::string resolveGitHash(const fs::path& gitDir) -{ - std::string headContent = trimStr(readFileFull(gitDir / "HEAD")); - if (headContent.empty()) - return "UNKNOWN"; - - const std::string refPrefix = "ref: "; - if (headContent.compare(0, refPrefix.size(), refPrefix) == 0) - { - std::string ref = headContent.substr(refPrefix.size()); - fs::path refFile = gitDir / ref; - - if (fs::exists(refFile)) - return trimStr(readFileFull(refFile)); - - // Fall back to packed-refs - std::string packedRefs = readFileFull(gitDir / "packed-refs"); - if (!packedRefs.empty()) - { - std::istringstream stream(packedRefs); - std::string line; - while (std::getline(stream, line)) - { - if (line.empty() || line[0] == '#' || line[0] == '^') - continue; - size_t space = line.find(' '); - if (space != std::string::npos && trimStr(line.substr(space + 1)) == ref) - return line.substr(0, space); - } - } - return "UNKNOWN"; - } - - // Detached HEAD — content is the hash directly - if (headContent.size() >= 40) - return headContent.substr(0, 40); - return headContent; -} - -// --------------------------------------------------------------------------- -// Parse current version from an existing version_macros.hpp -// --------------------------------------------------------------------------- - -struct Version -{ - int major{-1}, minor{-1}, patch{-1}; - bool valid() const { return major >= 0 && minor >= 0 && patch >= 0; } -}; - -static int extractDefineInt(const std::string& content, const std::string& macroName) -{ - std::string search = "#define " + macroName + " "; - size_t pos = content.find(search); - if (pos == std::string::npos) - return -1; - pos += search.size(); - size_t end = content.find_first_of("\r\n", pos); - return std::atoi(trimStr(content.substr(pos, end - pos)).c_str()); -} - -static Version parseVersionFromHeader(const fs::path& path) -{ - Version v; - std::string content = readFileFull(path); - if (content.empty()) - return v; - v.major = extractDefineInt(content, "SILKIT_VERSION_MAJOR"); - v.minor = extractDefineInt(content, "SILKIT_VERSION_MINOR"); - v.patch = extractDefineInt(content, "SILKIT_VERSION_PATCH"); - return v; -} - -// --------------------------------------------------------------------------- -// Changelog rotation -// --------------------------------------------------------------------------- - -static bool rotateChangelog(const fs::path& sourceDir, const Version& oldVer, bool dryRun) -{ - fs::path versionsDir = sourceDir / "docs" / "changelog" / "versions"; - fs::path latestMd = versionsDir / "latest.md"; - fs::path templateMd = versionsDir / "template.md"; - - fs::path archiveMd = versionsDir / (std::to_string(oldVer.major) + "." + std::to_string(oldVer.minor) + "." - + std::to_string(oldVer.patch) + ".md"); - - if (!fs::exists(latestMd)) - { - std::cerr << "warning: changelog rotation skipped — " << latestMd << " not found\n"; - return true; - } - if (!fs::exists(templateMd)) - { - std::cerr << "warning: changelog rotation skipped — " << templateMd << " not found\n"; - return true; - } - if (fs::exists(archiveMd)) - { - std::cerr << "warning: changelog rotation skipped — " << archiveMd << " already exists\n"; - return true; - } - - if (dryRun) - { - std::cout << "[dry-run] copy " << latestMd << "\n" - << " -> " << archiveMd << "\n"; - std::cout << "[dry-run] copy " << templateMd << "\n" - << " -> " << latestMd << "\n"; - return true; - } - - std::error_code ec; - std::cout << "changelog: " << latestMd << " -> " << archiveMd << "\n"; - fs::copy_file(latestMd, archiveMd, ec); - if (ec) - { - std::cerr << "error: " << ec.message() << "\n"; - return false; - } - std::cout << "changelog: " << latestMd << " <- " << templateMd << "\n"; - fs::copy_file(templateMd, latestMd, fs::copy_options::overwrite_existing, ec); - if (ec) - { - std::cerr << "error: " << ec.message() << "\n"; - return false; - } - return true; -} - -// --------------------------------------------------------------------------- -// Header generation -// --------------------------------------------------------------------------- - -static void replaceAll(std::string& s, const std::string& from, const std::string& to) -{ - for (size_t pos = 0; (pos = s.find(from, pos)) != std::string::npos; pos += to.size()) - s.replace(pos, from.size(), to); -} - -static void substituteVar(std::string& s, const std::string& name, const std::string& value) -{ - replaceAll(s, "@" + name + "@", value); -} - -// clang-format off -static constexpr auto kHeaderTemplate = R"(// SPDX-FileCopyrightText: 2022 Vector Informatik GmbH -// -// SPDX-License-Identifier: MIT - -#pragma once - -#define SILKIT_GIT_HASH "@GIT_HASH@" -#define SILKIT_VERSION_MAJOR @MAJOR@ -#define SILKIT_VERSION_MINOR @MINOR@ -#define SILKIT_VERSION_PATCH @PATCH@ -#define SILKIT_BUILD_NUMBER @BUILD@ -#define SILKIT_VERSION_STRING "@VERSION_STRING@" -#define SILKIT_VERSION_SUFFIX "@SUFFIX@" -)"; -// clang-format on - -static std::string buildHeader(const std::string& gitHash, int major, int minor, int patch, int build, - const std::string& suffix) -{ - const std::string versionStr = std::to_string(major) + "." + std::to_string(minor) + "." - + std::to_string(patch) + (suffix.empty() ? "" : "-" + suffix); - - std::string result{kHeaderTemplate}; - substituteVar(result, "GIT_HASH", gitHash); - substituteVar(result, "MAJOR", std::to_string(major)); - substituteVar(result, "MINOR", std::to_string(minor)); - substituteVar(result, "PATCH", std::to_string(patch)); - substituteVar(result, "BUILD", std::to_string(build)); - substituteVar(result, "VERSION_STRING", versionStr); - substituteVar(result, "SUFFIX", suffix); - return result; -} - -// --------------------------------------------------------------------------- -// main -// --------------------------------------------------------------------------- - -static void usage(const char* prog) -{ - std::cerr << "Usage: " << prog << " [options] \n" - << "\n" - << " --major N Version major (required)\n" - << " --minor N Version minor (required)\n" - << " --patch N Version patch (required)\n" - << " --build N Build number (default: 0)\n" - << " --suffix S Version suffix (default: \"\")\n" - << " --git-dir PATH Path to .git directory\n" - << " --git-hash HASH Override git hash (skips .git reading)\n" - << " --source-dir PATH Source tree root for changelog files\n" - << " --no-changelog Skip changelog rotation\n" - << " --dry-run Print actions without writing any files\n"; -} - -int main(int argc, char* argv[]) -{ - int major = -1, minor = -1, patch = -1, build = 0; - std::string suffix; - fs::path gitDir; - std::string gitHashOverride; - fs::path sourceDir; - std::string outputPath; - bool noChangelog = false; - bool dryRun = false; - - for (int i = 1; i < argc; ++i) - { - std::string arg = argv[i]; - - auto requireNext = [&]() -> std::string { - if (i + 1 >= argc) - { - std::cerr << "error: " << arg << " requires an argument\n"; - std::exit(1); - } - return argv[++i]; - }; - - if (arg == "--major") - major = std::atoi(requireNext().c_str()); - else if (arg == "--minor") - minor = std::atoi(requireNext().c_str()); - else if (arg == "--patch") - patch = std::atoi(requireNext().c_str()); - else if (arg == "--build") - build = std::atoi(requireNext().c_str()); - else if (arg == "--suffix") - suffix = requireNext(); - else if (arg == "--git-dir") - gitDir = requireNext(); - else if (arg == "--git-hash") - gitHashOverride = requireNext(); - else if (arg == "--source-dir") - sourceDir = requireNext(); - else if (arg == "--no-changelog") - noChangelog = true; - else if (arg == "--dry-run" || arg == "-n") - dryRun = true; - else if (!arg.empty() && arg[0] != '-') - outputPath = arg; - else - { - std::cerr << "error: unknown option '" << arg << "'\n"; - usage(argv[0]); - return 1; - } - } - - if (major < 0 || minor < 0 || patch < 0) - { - std::cerr << "error: --major, --minor, --patch are required\n"; - usage(argv[0]); - return 1; - } - if (outputPath.empty()) - { - std::cerr << "error: output path is required\n"; - usage(argv[0]); - return 1; - } - - // Resolve git hash - std::string gitHash; - if (!gitHashOverride.empty()) - { - gitHash = gitHashOverride; - } - else - { - if (gitDir.empty()) - gitDir = findGitDir(); - gitHash = gitDir.empty() ? "UNKNOWN" : resolveGitHash(gitDir); - } - - // Changelog rotation — read existing header before overwriting - if (!noChangelog && outputPath != "-") - { - Version oldVer = parseVersionFromHeader(outputPath); - bool versionChanged = - oldVer.valid() && (oldVer.major != major || oldVer.minor != minor || oldVer.patch != patch); - if (versionChanged) - { - fs::path srcDir = sourceDir.empty() ? fs::current_path() : sourceDir; - if (!rotateChangelog(srcDir, oldVer, dryRun)) - return 1; - } - } - - // Generate and write the header - std::string content = buildHeader(gitHash, major, minor, patch, build, suffix); - - if (dryRun) - { - std::cout << "[dry-run] write " << outputPath << "\n" - << "---\n" - << content; - return 0; - } - - if (outputPath == "-") - { - std::cout << content; - } - else - { - if (!writeFileFull(outputPath, content)) - return 1; - std::cout << "wrote " << outputPath << " (git hash: " << gitHash << ")\n"; - } - - return 0; -} diff --git a/SilKit/source/util/tests/CMakeLists.txt b/SilKit/source/util/tests/CMakeLists.txt index a414b774f..c3dded289 100644 --- a/SilKit/source/util/tests/CMakeLists.txt +++ b/SilKit/source/util/tests/CMakeLists.txt @@ -10,3 +10,4 @@ add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_Timer.cpp LIBS I_SilK add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_Util_FileHelpers.cpp LIBS O_SilKit_Util_FileHelpers) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_Util_StringHelpers.cpp LIBS O_SilKit_Util_StringHelpers) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_Uri.cpp) +add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_Util_GenerateVersion.cpp LIBS O_SilKit_Util_GenerateVersion) diff --git a/SilKit/source/util/tests/Test_Util_GenerateVersion.cpp b/SilKit/source/util/tests/Test_Util_GenerateVersion.cpp new file mode 100644 index 000000000..7beecacdd --- /dev/null +++ b/SilKit/source/util/tests/Test_Util_GenerateVersion.cpp @@ -0,0 +1,345 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include "gtest/gtest.h" + +#include "util/GenerateVersion.hpp" + +namespace { + +using namespace SilKit::VersionGen; + +// A faithful excerpt of SilKit/cmake/SilKitVersion.cmake, including the +// surrounding lines that must survive a patch untouched. +const char* const kCMakeVersion = R"(# SPDX-License-Identifier: MIT + +macro(configure_silkit_version project_name) + set(SILKIT_VERSION_MAJOR 5) + set(SILKIT_VERSION_MINOR 0) + set(SILKIT_VERSION_PATCH 8) + set(SILKIT_BUILD_NUMBER 0 CACHE STRING "The build number") + set(SILKIT_VERSION_SUFFIX "") + + set(VERSION_STRING "${SILKIT_VERSION_MAJOR}.${SILKIT_VERSION_MINOR}.${SILKIT_VERSION_PATCH}") +endmacro() +)"; + +// The header as committed before the generated-file banner and the #ifndef +// build-number fallback were introduced. Kept verbatim: LooksLikeGeneratedHeader +// must still accept an older generated header. +const char* const kLegacyHeader = R"(#pragma once + +#define SILKIT_GIT_HASH "23932429ac68eecdb8ca7698f35783ee5d89a04b" +#define SILKIT_VERSION_MAJOR 5 +#define SILKIT_VERSION_MINOR 0 +#define SILKIT_VERSION_PATCH 6 +#define SILKIT_BUILD_NUMBER 42 +#define SILKIT_VERSION_STRING "5.0.6" +#define SILKIT_VERSION_SUFFIX "" +)"; + +// silkit/capi/Version.h, the hand-written public API header that must never be +// mistaken for a generated one. +const char* const kPublicApiHeader = R"(#pragma once +#include "silkit/capi/SilKitMacros.h" + +SILKIT_BEGIN_DECLS +SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Version_Major(uint32_t* outVersionMajor); +SILKIT_END_DECLS +)"; + +const char* const kOverviewRst = R"(Changelog +========= + +.. toctree:: + :maxdepth: 1 + :glob: + + versions/latest.md + versions/5.0.7.md + versions/4.rst +)"; + +TEST(Test_Util_GenerateVersion, ParseVersionFromCmake) +{ + const auto version = ParseVersionFromCMake(kCMakeVersion); + ASSERT_TRUE(version.IsValid()); + EXPECT_EQ(version.major, 5); + EXPECT_EQ(version.minor, 0); + EXPECT_EQ(version.patch, 8); + EXPECT_EQ(version.suffix, ""); + EXPECT_EQ(version.ToString(), "5.0.8"); +} + +TEST(Test_Util_GenerateVersion, ParseVersionFromCmakeIgnoresBuildNumber) +{ + // SILKIT_BUILD_NUMBER carries a CACHE clause and must not be mistaken for a + // version component. + const auto version = ParseVersionFromCMake(kCMakeVersion); + EXPECT_EQ(version.ToShortString(), "5.0.8"); +} + +TEST(Test_Util_GenerateVersion, ParseVersionFromCmakeWithoutVersionIsInvalid) +{ + EXPECT_FALSE(ParseVersionFromCMake("# nothing here\n").IsValid()); +} + +TEST(Test_Util_GenerateVersion, ParseVersionFromHeader) +{ + const auto version = ParseVersionFromHeader(kLegacyHeader); + ASSERT_TRUE(version.IsValid()); + EXPECT_EQ(version.ToString(), "5.0.6"); + EXPECT_EQ(ParseGitHashFromHeader(kLegacyHeader), "23932429ac68eecdb8ca7698f35783ee5d89a04b"); +} + +TEST(Test_Util_GenerateVersion, GeneratedHeaderIsRecognized) +{ + EXPECT_TRUE(LooksLikeGeneratedHeader(kLegacyHeader)); + + Version version; + version.major = 5; + version.minor = 0; + version.patch = 9; + EXPECT_TRUE(LooksLikeGeneratedHeader(RenderHeader("abc", version))); +} + +TEST(Test_Util_GenerateVersion, PublicApiHeaderIsNotMistakenForAGeneratedOne) +{ + // This is the guard that keeps a mistyped output path from clobbering + // silkit/capi/Version.h. + EXPECT_FALSE(LooksLikeGeneratedHeader(kPublicApiHeader)); + EXPECT_FALSE(LooksLikeGeneratedHeader("")); +} + +TEST(Test_Util_GenerateVersion, PatchCmakeVersionOnlyTouchesTheVersionLines) +{ + Version version; + version.major = 6; + version.minor = 1; + version.patch = 2; + version.suffix = "rc1"; + + std::string content{kCMakeVersion}; + std::string error; + ASSERT_TRUE(PatchCMakeVersion(content, version, error)) << error; + + EXPECT_NE(content.find("set(SILKIT_VERSION_MAJOR 6)"), std::string::npos); + EXPECT_NE(content.find("set(SILKIT_VERSION_MINOR 1)"), std::string::npos); + EXPECT_NE(content.find("set(SILKIT_VERSION_PATCH 2)"), std::string::npos); + EXPECT_NE(content.find("set(SILKIT_VERSION_SUFFIX \"rc1\")"), std::string::npos); + + // Everything else survives verbatim. + EXPECT_NE(content.find("set(SILKIT_BUILD_NUMBER 0 CACHE STRING \"The build number\")"), std::string::npos); + EXPECT_NE(content.find("macro(configure_silkit_version project_name)"), std::string::npos); + EXPECT_NE(content.find("${SILKIT_VERSION_MAJOR}.${SILKIT_VERSION_MINOR}"), std::string::npos); + + EXPECT_EQ(ParseVersionFromCMake(content).ToString(), "6.1.2-rc1"); +} + +TEST(Test_Util_GenerateVersion, PatchCmakeVersionReportsAMissingSetter) +{ + Version version; + version.major = 1; + version.minor = 2; + version.patch = 3; + + std::string content = "set(SILKIT_VERSION_MAJOR 5)\n"; + std::string error; + EXPECT_FALSE(PatchCMakeVersion(content, version, error)); + EXPECT_FALSE(error.empty()); +} + +TEST(Test_Util_GenerateVersion, RenderedHeaderIsAsciiLfAndRoundTrips) +{ + Version version; + version.major = 5; + version.minor = 0; + version.patch = 9; + version.suffix = "rc2"; + + const auto header = RenderHeader("deadbeef", version); + + for (const char character : header) + { + EXPECT_GE(static_cast(character), 0x09u) << "non-ASCII byte in the generated header"; + EXPECT_LT(static_cast(character), 0x80u) << "non-ASCII byte in the generated header"; + } + EXPECT_EQ(header.find('\r'), std::string::npos) << "the generated header must use LF line endings"; + + EXPECT_NE(header.find("#define SILKIT_VERSION_STRING \"5.0.9-rc2\""), std::string::npos); + EXPECT_NE(header.find("DO NOT EDIT"), std::string::npos); + + EXPECT_EQ(ParseVersionFromHeader(header).ToString(), "5.0.9-rc2"); + EXPECT_EQ(ParseGitHashFromHeader(header), "deadbeef"); +} + +TEST(Test_Util_GenerateVersion, BuildNumberIsOnlyAFallbackInTheHeader) +{ + // The build number is a property of a build, supplied by CMake as + // -DSILKIT_BUILD_NUMBER=N. The header must not pin it, or the definition + // could never take effect. + Version version; + version.major = 5; + version.minor = 0; + version.patch = 8; + + const auto header = RenderHeader("abc", version); + + EXPECT_NE(header.find("#ifndef SILKIT_BUILD_NUMBER"), std::string::npos); + EXPECT_NE(header.find("#define SILKIT_BUILD_NUMBER 0"), std::string::npos); + EXPECT_NE(header.find("#endif"), std::string::npos); + + // The guarded define is the only one, and it comes after the #ifndef. + EXPECT_LT(header.find("#ifndef SILKIT_BUILD_NUMBER"), header.find("#define SILKIT_BUILD_NUMBER")); + EXPECT_EQ(header.find("#define SILKIT_BUILD_NUMBER", header.find("#define SILKIT_BUILD_NUMBER") + 1), + std::string::npos); +} + +TEST(Test_Util_GenerateVersion, RenderedHeaderIsStable) +{ + Version version; + version.major = 5; + version.minor = 0; + version.patch = 8; + EXPECT_EQ(RenderHeader("abc", version), RenderHeader("abc", version)); +} + +TEST(Test_Util_GenerateVersion, FinalizeChangelogHeadingSetsVersionAndDate) +{ + const std::string entry = "# [5.0.8] - UNRELEASED\n\n## Added\n\n- Something\n"; + + Version version; + version.major = 5; + version.minor = 0; + version.patch = 8; + + const auto finalized = FinalizeChangelogHeading(entry, version, "2026-09-01"); + EXPECT_EQ(finalized, "# [5.0.8] - 2026-09-01\n\n## Added\n\n- Something\n"); +} + +TEST(Test_Util_GenerateVersion, FinalizeChangelogHeadingCorrectsAStaleVersion) +{ + // After a rebase latest.md may carry the wrong number; the archived file + // must name the version actually being released. + const std::string entry = "# [5.0.6] - UNRELEASED\n\n## Fixed\n"; + + Version version; + version.major = 5; + version.minor = 0; + version.patch = 8; + + EXPECT_EQ(FinalizeChangelogHeading(entry, version, "2026-09-01"), "# [5.0.8] - 2026-09-01\n\n## Fixed\n"); +} + +TEST(Test_Util_GenerateVersion, FinalizeChangelogHeadingLeavesAHeadinglessEntryAlone) +{ + const std::string entry = "no heading here\n"; + Version version; + version.major = 1; + version.minor = 0; + version.patch = 0; + EXPECT_EQ(FinalizeChangelogHeading(entry, version, "2026-09-01"), entry); +} + +TEST(Test_Util_GenerateVersion, ChangelogStubMatchesTheEstablishedFormat) +{ + Version version; + version.major = 5; + version.minor = 0; + version.patch = 9; + EXPECT_EQ(RenderChangelogStub(version), "# [5.0.9] - UNRELEASED\n\n> This changelog entry is still empty.\n"); +} + +TEST(Test_Util_GenerateVersion, InsertToctreeEntryAfterLatest) +{ + Version version; + version.major = 5; + version.minor = 0; + version.patch = 8; + + std::string content{kOverviewRst}; + std::string error; + ASSERT_TRUE(InsertChangelogToctreeEntry(content, version, error)) << error; + + const size_t latest = content.find("versions/latest.md"); + const size_t inserted = content.find("versions/5.0.8.md"); + const size_t next = content.find("versions/5.0.7.md"); + ASSERT_NE(inserted, std::string::npos); + EXPECT_LT(latest, inserted); + EXPECT_LT(inserted, next); + + // The toctree indentation is preserved. + EXPECT_NE(content.find("\n versions/5.0.8.md\n"), std::string::npos); +} + +TEST(Test_Util_GenerateVersion, InsertToctreeEntryRejectsADuplicate) +{ + Version version; + version.major = 5; + version.minor = 0; + version.patch = 7; + + std::string content{kOverviewRst}; + std::string error; + EXPECT_FALSE(InsertChangelogToctreeEntry(content, version, error)); + EXPECT_FALSE(error.empty()); + EXPECT_EQ(content, kOverviewRst); +} + +TEST(Test_Util_GenerateVersion, InsertToctreeEntryReportsAMissingAnchor) +{ + Version version; + version.major = 5; + version.minor = 0; + version.patch = 8; + + std::string content = "Changelog\n=========\n"; + std::string error; + EXPECT_FALSE(InsertChangelogToctreeEntry(content, version, error)); + EXPECT_FALSE(error.empty()); +} + +TEST(Test_Util_GenerateVersion, InsertToctreeEntryKeepsCrlfLineEndings) +{ + Version version; + version.major = 5; + version.minor = 0; + version.patch = 8; + + std::string content = ".. toctree::\r\n\r\n versions/latest.md\r\n versions/4.rst\r\n"; + std::string error; + ASSERT_TRUE(InsertChangelogToctreeEntry(content, version, error)) << error; + EXPECT_NE(content.find("\r\n versions/5.0.8.md\r\n"), std::string::npos); +} + +TEST(Test_Util_GenerateVersion, DetectLineEndings) +{ + EXPECT_TRUE(UsesCrlf("a\r\nb\r\n")); + EXPECT_FALSE(UsesCrlf("a\nb\n")); + EXPECT_FALSE(UsesCrlf("")); +} + +TEST(Test_Util_GenerateVersion, ApplyLineEndings) +{ + EXPECT_EQ(WithLineEndings("a\nb\n", true), "a\r\nb\r\n"); + EXPECT_EQ(WithLineEndings("a\r\nb\r\n", false), "a\nb\n"); + + // Idempotent, so applying the working tree's endings to content spliced out + // of an existing file cannot double them up. + EXPECT_EQ(WithLineEndings("a\r\nb\r\n", true), "a\r\nb\r\n"); + EXPECT_EQ(WithLineEndings("a\nb\n", false), "a\nb\n"); + + // A lone CR is not a line ending and is left alone. + EXPECT_EQ(WithLineEndings("a\rb\n", true), "a\rb\r\n"); +} + +TEST(Test_Util_GenerateVersion, TodayIsAnIsoDate) +{ + const auto today = TodayIsoDate(); + ASSERT_EQ(today.size(), 10u); + EXPECT_EQ(today[4], '-'); + EXPECT_EQ(today[7], '-'); +} + +} // anonymous namespace diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index 0de904ac2..07b551b52 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -42,6 +42,10 @@ set(SPHINX_INDEX_FILE ${SPHINX_BUILD}/index.html) # - Our doc files have been updated # - The Sphinx config has been updated file(GLOB_RECURSE SPHINX_SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.rst) +# Markdown pages (the changelog entries, development/release.md) are part of the +# Sphinx sources too and must retrigger the build when edited. +file(GLOB_RECURSE SPHINX_MD_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.md) +list(APPEND SPHINX_SOURCE_FILES ${SPHINX_MD_FILES}) file(GLOB_RECURSE SVG_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.svg) list(APPEND SPHINX_SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/_static/custom.css) diff --git a/docs/changelog/versions/latest.md b/docs/changelog/versions/latest.md index 2e63e8d26..2acbc7517 100644 --- a/docs/changelog/versions/latest.md +++ b/docs/changelog/versions/latest.md @@ -13,6 +13,12 @@ ## Changed +- `SilKitVersionMacros.h` is now committed to the source tree instead of being generated at CMake configure time. + The new `sil-kit-generate-version` maintainer tool regenerates it and performs a complete version bump + (`SilKitVersion.cmake`, the generated header and the changelog) in one step. See `docs/development/release.md`. +- `SILKIT_BUILD_NUMBER` is now purely a build-time setting (`cmake -DSILKIT_BUILD_NUMBER=N`) rather than a value + stored in the source tree. The generated header only carries an `#ifndef` fallback of `0`, so + `SilKit_Version_BuildNumber()` reports `0` unless a build sets it. - Changes to the SIL KIT MSI installer: - Default installation path changed from `\Vector SIL Kit ` to `\SIL Kit ` - Windows System Service Name changed from `VectorSilKitRegistry` to `SilKitRegistry` diff --git a/docs/development/build.rst b/docs/development/build.rst index b44aadb59..f52072fd7 100644 --- a/docs/development/build.rst +++ b/docs/development/build.rst @@ -69,9 +69,12 @@ Refer to :doc:`rst-help` for guidelines on formatting the documentation. ~~~~~~~~~~~~~ SIL Kit uses CPack to generate the release distributions in ZIP form. It can be packaged using the *package* target:: - + cmake --build . --target package +Refer to :doc:`release` for bumping the version number and rotating the +changelog before a release. + The generated package adheres to the following template ``SilKit----.zip``. Its contents are as follows: diff --git a/docs/development/release.md b/docs/development/release.md new file mode 100644 index 000000000..3a8329597 --- /dev/null +++ b/docs/development/release.md @@ -0,0 +1,206 @@ +--- +orphan: true +--- + +# !!! Releasing SIL Kit: Version Bumps and the Changelog + +This page is for SIL Kit maintainers. It describes how the version number is +stored in the source tree and how to change it. + +**Everything on this page is done by one tool: `sil-kit-generate-version`.** +Do not edit the version by hand: several files have to agree, and the tool is +what keeps them in sync. + +## Where the version lives + +| File | Who writes it | +| --- | --- | +| `SilKit/cmake/SilKitVersion.cmake` | **Source of truth.** Patched by the tool. Everything in the build reads the version from here. | +| `SilKit/include/silkit/capi/SilKitVersionMacros.h` | **Generated. Never edit by hand.** Committed to the repository, and compiled into the library and into the Windows resources of the utilities. | +| `docs/changelog/versions/latest.md` | Hand-written during development, reset by the tool on a bump. | +| `docs/changelog/versions/.md` | Created by the tool when it archives `latest.md`. | +| `docs/changelog/overview.rst` | The toctree line for the archived entry is added by the tool. | + +The build number is deliberately absent from that list: it is set at configure +time, not stored in the source tree. See the "Build identity" section below. + +```{warning} +`silkit/capi/Version.h` and `silkit/SilKitVersion.hpp` are **hand-written public +API** headers that declare the version *query functions*. They contain no version +numbers and are never touched by a version bump. Only +`silkit/capi/SilKitVersionMacros.h` is generated. + +The tool refuses to overwrite any file that is not recognizably a generated +version-macros header, so a mistyped output path fails instead of destroying an +API header. +``` + +`docs/changelog/versions/template.md` is authoring guidance for writing a +changelog entry. It is excluded from the documentation build and is not copied +anywhere by the tool. + +## Build the tool + +``` +cmake --build --target sil-kit-generate-version +``` + +The executable lands in `/sil-kit-generate-version` (with +multi-config generators such as Visual Studio: `//`). + +The tool is standalone: C++17 and the standard library only, no SIL Kit +dependencies, and it reads the git hash out of `.git` without invoking `git`. +It has to be, because it generates a header the library itself is built from. + +## Bump the version + +Run it from anywhere inside the source tree; it locates the tree root by looking +for `SilKit/cmake/SilKitVersion.cmake`. + +Look at the plan first: + +``` +sil-kit-generate-version --dry-run --major 5 --minor 0 --patch 9 +``` + +Then apply it: + +``` +sil-kit-generate-version --major 5 --minor 0 --patch 9 +``` + +That single command: + +1. sets the version in `SilKit/cmake/SilKitVersion.cmake` to `5.0.9`, +2. renames the current in-progress changelog entry to its release version and + date, archiving `docs/changelog/versions/latest.md` as + `docs/changelog/versions/5.0.8.md`, +3. adds `versions/5.0.8.md` to the toctree in `docs/changelog/overview.rst`, +4. resets `latest.md` to an empty `# [5.0.9] - UNRELEASED` stub, +5. regenerates `SilKit/include/silkit/capi/SilKitVersionMacros.h`. + +Either all of it happens or none of it does: every precondition is checked before +the first byte is written. + +Review the result with `git diff`. Exactly these files should have changed: + +``` +SilKit/cmake/SilKitVersion.cmake +SilKit/include/silkit/capi/SilKitVersionMacros.h +docs/changelog/overview.rst +docs/changelog/versions/latest.md +docs/changelog/versions/.md (new file) +``` + +The `SilKitVersion.cmake` diff must be three lines at most. If it is larger, +something went wrong; do not commit it. + +The archived entry gets today's date. Pass `--date YYYY-MM-DD` to set a +different release date, and `--suffix rc1` for a pre-release version (which +turns the version string into `5.0.9-rc1`). + +The commit convention for a bump is `version: bump to X.Y.Z (#PR)`. + +## Refresh the generated header without bumping + +After a rebase or a merge, the git hash in the committed header is stale, and if +the rebase pulled in someone else's bump the version numbers may disagree too. +Run the tool with no version arguments: + +``` +sil-kit-generate-version +``` + +This takes the version from `SilKitVersion.cmake`, refreshes the git hash, and +touches nothing else. No changelog rotation happens because the version did not +change. + +## Verify + +``` +sil-kit-generate-version --check +``` + +Exits non-zero if the header disagrees with `SilKitVersion.cmake`, and writes +nothing. Suitable for a CI guard. Reconfiguring the build is the other check: +`SilKit/source/CMakeLists.txt` fails outright if the generated header is missing. + +## Finish the changelog before a release + +`latest.md` is the entry for the version currently under development. Fill it in +as changes land, following `template.md`. It is what the packaging step ships as +`CHANGELOG.md`, so it must be complete before packaging. + +The heading stays `# [] - UNRELEASED` until the bump; the tool then +substitutes the release date automatically when it archives the file. There is no +need to edit the date by hand. + +## Build identity + +Nothing on this page stamps a build. This repository does not produce releases; +its GitHub workers run CI and component tests only. + +What identifies a build: + +- **Git hash.** `SILKIT_GIT_HASH` in the generated header, exposed at runtime as + `SilKit::Version::GitHash()`. Because the header is committed, the hash is + whatever HEAD was when the tool last ran, so for a bump commit it is that + commit's parent. It is no longer refreshed on every build, which is what makes + the build reproducible. +- **Reproducible builds.** `SILKIT_BUILD_REPRODUCIBLE` is `ON` by default and + omits timestamps and unique build ids, so the same sources produce the same + binaries. + +**Build number.** `SILKIT_BUILD_NUMBER` exists only because +`SilKit_Version_BuildNumber()` is part of the stable public C API. It is a +property of a build, not of the source tree, so it is *not* stored in the +generated header and `sil-kit-generate-version` cannot set it. To stamp one, pass +it to CMake: + +``` +cmake -B -DSILKIT_BUILD_NUMBER=42 +``` + +It reaches the library and the Windows `FILEVERSION` as a compile definition on +`I_SilKit`; `SilKitVersionMacros.h` only carries the `#ifndef` fallback of `0` for +consumers who compile against the installed header without CMake. Deliberately +setting it defeats reproducibility, so leave it alone unless something downstream +genuinely needs it. + +## Troubleshooting + +### `error: refusing to overwrite ` + +The file is not a generated version-macros header. You most likely passed the +wrong output path. Drop the path argument entirely; the default is correct. + +### `error: .md already exists` + +`SilKitVersion.cmake` names a version whose changelog entry has already been +archived, which usually means the tree is mid-rebase and the version numbers are +not what you think. Check `SilKitVersion.cmake` and `docs/changelog/versions/` +before doing anything else. `--force` overwrites the archived entry; +`--no-changelog` bumps the version and leaves the changelog alone. Nothing has +been written yet at this point. + +### `error: version drift` (from `--check`) + +The committed header and `SilKitVersion.cmake` disagree. Run +`sil-kit-generate-version` with no version arguments to regenerate the header. + +### `error: no SIL Kit source tree found` + +The working directory is outside the source tree. `cd` into it or pass +`--source-dir PATH`. + +### `warning: could not determine the git hash` + +`.git` was not found or could not be read; `SILKIT_GIT_HASH` is written as +`UNKNOWN`. Pass `--git-dir PATH`, or `--git-hash HASH` to set it directly. + +`sil-kit-generate-version --help` lists every option. + +## See also + +- {doc}`build` for build configuration and packaging +- `docs/for-developers/versioning.md` for what the version numbers promise From 1809be1d1aae87cdbb4ef0cad3487b571b3cd558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Tue, 1 Sep 2026 18:06:21 +0200 Subject: [PATCH 3/6] fixup! fixup! build: commit SilKitVersionMacros.h and regenerate with tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marius Börschig --- SilKit/source/util/GenerateVersion.cpp | 8 ++++++-- SilKit/source/util/GenerateVersion.hpp | 3 ++- SilKit/source/util/sil-kit-generate-version.cpp | 5 +++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/SilKit/source/util/GenerateVersion.cpp b/SilKit/source/util/GenerateVersion.cpp index f04d3c66d..2f93f59ab 100644 --- a/SilKit/source/util/GenerateVersion.cpp +++ b/SilKit/source/util/GenerateVersion.cpp @@ -4,7 +4,6 @@ #include "GenerateVersion.hpp" -#include #include #include #include @@ -275,8 +274,13 @@ std::string TodayIsoDate() #else localtime_r(&now, &local); #endif + // strftime rather than snprintf: GCC cannot prove that tm_mon and tm_mday + // are two digits wide and warns about a possibly truncated %02d. char buffer[16] = {}; - std::snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d", local.tm_year + 1900, local.tm_mon + 1, local.tm_mday); + if (std::strftime(buffer, sizeof(buffer), "%Y-%m-%d", &local) == 0) + { + return ""; + } return buffer; } diff --git a/SilKit/source/util/GenerateVersion.hpp b/SilKit/source/util/GenerateVersion.hpp index 138cb3a78..99de75117 100644 --- a/SilKit/source/util/GenerateVersion.hpp +++ b/SilKit/source/util/GenerateVersion.hpp @@ -76,7 +76,8 @@ std::string RenderChangelogStub(const Version& version); // false and fills 'error' if the anchor is missing or the entry already exists. bool InsertChangelogToctreeEntry(std::string& content, const Version& version, std::string& error); -// Today's date as YYYY-MM-DD in local time. +// Today's date as YYYY-MM-DD in local time, or "" if the clock cannot be +// formatted. std::string TodayIsoDate(); // True if the content uses CRLF line endings. The working tree may be either, diff --git a/SilKit/source/util/sil-kit-generate-version.cpp b/SilKit/source/util/sil-kit-generate-version.cpp index 2db6a3e1f..0840a49e9 100644 --- a/SilKit/source/util/sil-kit-generate-version.cpp +++ b/SilKit/source/util/sil-kit-generate-version.cpp @@ -603,6 +603,11 @@ int main(int argc, char* argv[]) } const std::string date = options.date.empty() ? SilKit::VersionGen::TodayIsoDate() : options.date; + if (date.empty()) + { + std::cerr << "error: cannot determine today's date; pass --date YYYY-MM-DD\n"; + return 1; + } const std::string latestContent = ReadFileFull(layout.latestMd); writes.push_back({archiveMd, SilKit::VersionGen::FinalizeChangelogHeading(latestContent, currentVersion, date), From 78c282e0b752fb67f033a916c999fe586db0c619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Tue, 1 Sep 2026 18:16:58 +0200 Subject: [PATCH 4/6] cmake: add option to build the generate-version tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marius Börschig --- CMakeLists.txt | 1 + SilKit/source/CMakeLists.txt | 4 +-- SilKit/source/util/CMakeLists.txt | 44 ++++++++++++++++++------------- docs/changelog/versions/latest.md | 3 +++ docs/development/build.rst | 4 +++ docs/development/release.md | 6 +++++ 6 files changed, 42 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d46ab619f..a304319af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ option(SILKIT_BUILD_DEMOS "Build the SIL Kit Demos" ON) option(SILKIT_BUILD_STATIC "Compile the SIL Kit as a static library" OFF) option(SILKIT_BUILD_TESTS "Enable unit and integration tests for the SIL Kit" ON) option(SILKIT_BUILD_UTILITIES "Build the SIL Kit utility tools" ON) +option(SILKIT_BUILD_GENERATE_VERSION "Build the sil-kit-generate-version maintainer tool" ON) option(SILKIT_BUILD_DOCS "Build documentation for the SIL Kit (requires Doxygen and Sphinx)" OFF) option(SILKIT_INSTALL_SOURCE "Install and package the source tree" OFF) option(SILKIT_ENABLE_COVERAGE "Enable coverage for builds (requires gcc, clang)" OFF) diff --git a/SilKit/source/CMakeLists.txt b/SilKit/source/CMakeLists.txt index 136a76002..91d335ae1 100644 --- a/SilKit/source/CMakeLists.txt +++ b/SilKit/source/CMakeLists.txt @@ -21,8 +21,8 @@ find_package(Threads REQUIRED) set(VERSION_MACROS_HPP "${CMAKE_CURRENT_LIST_DIR}/../include/silkit/capi/SilKitVersionMacros.h") if(NOT EXISTS "${VERSION_MACROS_HPP}") message(FATAL_ERROR - "SilKitVersionMacros.h not found. Build the sil-kit-generate-version target and run it" - " from the source tree root to create it:\n" + "SilKitVersionMacros.h not found. Configure with -DSILKIT_BUILD_GENERATE_VERSION=ON," + " build the sil-kit-generate-version target, and run it from the source tree root:\n" " sil-kit-generate-version\n" "See docs/development/release.md.") endif() diff --git a/SilKit/source/util/CMakeLists.txt b/SilKit/source/util/CMakeLists.txt index e59940fb4..8c6de17b1 100644 --- a/SilKit/source/util/CMakeLists.txt +++ b/SilKit/source/util/CMakeLists.txt @@ -87,24 +87,32 @@ add_silkit_test_to_executable(SilKitUnitTests # Maintainer tool for version bumps. Standalone on purpose: it must not depend # on I_SilKit, since it generates a header the library itself is built from. -add_library(O_SilKit_Util_GenerateVersion OBJECT - GenerateVersion.hpp - GenerateVersion.cpp -) - -# Root-relative includes as everywhere else in SilKit/source, so consumers write -# #include "util/GenerateVersion.hpp". -target_include_directories(O_SilKit_Util_GenerateVersion - PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/.." -) - -add_executable(sil-kit-generate-version sil-kit-generate-version.cpp) -target_link_libraries(sil-kit-generate-version - PRIVATE O_SilKit_Util_GenerateVersion -) -set_target_properties(sil-kit-generate-version PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$" -) +# +# The version logic is still built (and tested) when the tool itself is disabled, +# so a cross-compiled build keeps the test coverage without producing a +# maintainer binary that cannot run on the host. +if(SILKIT_BUILD_GENERATE_VERSION OR SILKIT_BUILD_TESTS) + add_library(O_SilKit_Util_GenerateVersion OBJECT + GenerateVersion.hpp + GenerateVersion.cpp + ) + + # Root-relative includes as everywhere else in SilKit/source, so consumers + # write #include "util/GenerateVersion.hpp". + target_include_directories(O_SilKit_Util_GenerateVersion + PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/.." + ) +endif() + +if(SILKIT_BUILD_GENERATE_VERSION) + add_executable(sil-kit-generate-version sil-kit-generate-version.cpp) + target_link_libraries(sil-kit-generate-version + PRIVATE O_SilKit_Util_GenerateVersion + ) + set_target_properties(sil-kit-generate-version PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$" + ) +endif() add_subdirectory(tests) diff --git a/docs/changelog/versions/latest.md b/docs/changelog/versions/latest.md index 2acbc7517..80fb7a08a 100644 --- a/docs/changelog/versions/latest.md +++ b/docs/changelog/versions/latest.md @@ -4,6 +4,9 @@ ## Added - Add Integration Test for Timestamp Behavior +- New CMake option `SILKIT_BUILD_GENERATE_VERSION` (default `ON`) to build the `sil-kit-generate-version` maintainer + tool. Turn it off when cross-compiling: the tool runs on the maintainer's machine, so building it for the target + architecture produces an unrunnable binary. ## Fixed diff --git a/docs/development/build.rst b/docs/development/build.rst index f52072fd7..a08fd6057 100644 --- a/docs/development/build.rst +++ b/docs/development/build.rst @@ -38,6 +38,10 @@ The following options are available: - Build the demo applications * - SILKIT_BUILD_DOCS - Build the documentation using Doxygen and Sphinx + * - SILKIT_BUILD_GENERATE_VERSION + - Build the ``sil-kit-generate-version`` maintainer tool (see :doc:`release`). + It is not installed or packaged; turn it off when cross-compiling, since the + resulting binary would not run on the host. * - SILKIT_INSTALL_SOURCE - Installs the source-tree (used for packaging releases). Implies SILKIT_BUILD_DOCS. diff --git a/docs/development/release.md b/docs/development/release.md index 3a8329597..4a1c17f68 100644 --- a/docs/development/release.md +++ b/docs/development/release.md @@ -52,6 +52,12 @@ The tool is standalone: C++17 and the standard library only, no SIL Kit dependencies, and it reads the git hash out of `.git` without invoking `git`. It has to be, because it generates a header the library itself is built from. +It is built by default and is never installed or packaged. +`-DSILKIT_BUILD_GENERATE_VERSION=OFF` skips it, which is what a cross-compiled +build wants: the tool runs on the maintainer's machine, so building it for the +target architecture produces something unrunnable. The version logic keeps its +unit test coverage either way. + ## Bump the version Run it from anywhere inside the source tree; it locates the tree root by looking From b5fa2b126cd9538a0f5889bfffd99d2df2985528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Tue, 1 Sep 2026 18:37:07 +0200 Subject: [PATCH 5/6] cmake: you can now pass SILKIT_GIT_HASH as a cmake variable to override the default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marius Börschig --- SilKit/cmake/SilKitVersion.cmake | 10 +- .../include/silkit/capi/SilKitVersionMacros.h | 12 +- SilKit/source/CMakeLists.txt | 36 +-- SilKit/source/util/GenerateVersion.cpp | 12 +- .../util/tests/Test_Util_GenerateVersion.cpp | 32 ++- docs/changelog/versions/latest.md | 7 +- docs/development/release.md | 215 +++++------------- 7 files changed, 120 insertions(+), 204 deletions(-) diff --git a/SilKit/cmake/SilKitVersion.cmake b/SilKit/cmake/SilKitVersion.cmake index cc451c2ef..464117c3f 100644 --- a/SilKit/cmake/SilKitVersion.cmake +++ b/SilKit/cmake/SilKitVersion.cmake @@ -8,13 +8,19 @@ # they are accessible from public headers at runtime. # * Do not edit the numbers below by hand. Run the sil-kit-generate-version tool, which keeps this file, the generated # header and the changelog in sync. See docs/development/release.md. -# * SILKIT_BUILD_NUMBER is a build-time override (cmake -DSILKIT_BUILD_NUMBER=N), not source tree state. It defaults to -# 0 and is passed to the sources as a compile definition; the generated header only carries the fallback. +# * SILKIT_BUILD_NUMBER and SILKIT_BUILD_GIT_HASH describe a build, not the source tree. Both are build-time +# overrides (cmake -DSILKIT_BUILD_NUMBER=N -DSILKIT_BUILD_GIT_HASH=) passed to the sources as compile +# definitions; the generated header only carries the fallbacks. CI should pass the hash it actually built, so the +# library reports that commit rather than the parent of the version bump. macro(configure_silkit_version project_name) set(SILKIT_VERSION_MAJOR 5) set(SILKIT_VERSION_MINOR 0) set(SILKIT_VERSION_PATCH 8) set(SILKIT_BUILD_NUMBER 0 CACHE STRING "The build number") + # Not named SILKIT_GIT_HASH: older build trees carry a stale INTERNAL cache + # entry under that name, and set(... CACHE ...) would not overwrite it. + set(SILKIT_BUILD_GIT_HASH "" CACHE STRING + "Git hash of the built sources; empty keeps the one in SilKitVersionMacros.h") set(SILKIT_VERSION_SUFFIX "") set(VERSION_STRING "${SILKIT_VERSION_MAJOR}.${SILKIT_VERSION_MINOR}.${SILKIT_VERSION_PATCH}") diff --git a/SilKit/include/silkit/capi/SilKitVersionMacros.h b/SilKit/include/silkit/capi/SilKitVersionMacros.h index 78a15a924..8ac8fbdf2 100644 --- a/SilKit/include/silkit/capi/SilKitVersionMacros.h +++ b/SilKit/include/silkit/capi/SilKitVersionMacros.h @@ -8,16 +8,20 @@ #pragma once -#define SILKIT_GIT_HASH "23932429ac68eecdb8ca7698f35783ee5d89a04b" #define SILKIT_VERSION_MAJOR 5 #define SILKIT_VERSION_MINOR 0 #define SILKIT_VERSION_PATCH 8 #define SILKIT_VERSION_STRING "5.0.8" #define SILKIT_VERSION_SUFFIX "" -// The build number is a property of the build, not of the source tree: pass -// -DSILKIT_BUILD_NUMBER=N to CMake to set it. This is the fallback for anyone -// compiling against the installed header without it. +// The build number and the git hash describe a build, not the source tree, so +// CMake supplies them: -DSILKIT_BUILD_NUMBER=N and -DSILKIT_GIT_HASH=. +// The values below are the fallbacks. The hash is the commit that was HEAD when +// this file was generated, i.e. the parent of the version bump. #ifndef SILKIT_BUILD_NUMBER #define SILKIT_BUILD_NUMBER 0 #endif + +#ifndef SILKIT_GIT_HASH +#define SILKIT_GIT_HASH "78c282e0b752fb67f033a916c999fe586db0c619" +#endif diff --git a/SilKit/source/CMakeLists.txt b/SilKit/source/CMakeLists.txt index 91d335ae1..bba75b74f 100644 --- a/SilKit/source/CMakeLists.txt +++ b/SilKit/source/CMakeLists.txt @@ -14,25 +14,6 @@ add_subdirectory(dashboard) find_package(Threads REQUIRED) -# SilKitVersionMacros.h is committed to source and regenerated by the -# sil-kit-generate-version tool. Nothing is generated at configure time, so any -# build tool can consume the file as a regular source file. -# See docs/development/release.md for the version bump procedure. -set(VERSION_MACROS_HPP "${CMAKE_CURRENT_LIST_DIR}/../include/silkit/capi/SilKitVersionMacros.h") -if(NOT EXISTS "${VERSION_MACROS_HPP}") - message(FATAL_ERROR - "SilKitVersionMacros.h not found. Configure with -DSILKIT_BUILD_GENERATE_VERSION=ON," - " build the sil-kit-generate-version target, and run it from the source tree root:\n" - " sil-kit-generate-version\n" - "See docs/development/release.md.") -endif() - -install(FILES - ${VERSION_MACROS_HPP} - DESTINATION ${INSTALL_INCLUDE_DIR}/silkit/capi - COMPONENT dev -) - set(silkitLibType SHARED) if(SILKIT_BUILD_STATIC) set(silkitLibType STATIC) @@ -47,15 +28,22 @@ target_include_directories(I_SilKit INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}" ) -# The build number is a property of a build, not of the source tree, so it is not -# stored in the generated SilKitVersionMacros.h. This definition carries it to -# both the C++ sources and the .rc resource scripts (CMake passes a target's -# compile definitions to the resource compiler); the header only provides the -# fallback of 0 for anyone compiling against the installed header without CMake. +# The build number and git hash describe a build, not the source tree, so they +# override the fallbacks in the generated SilKitVersionMacros.h. The build number +# also reaches the .rc resource scripts, which put it in FILEVERSION; CMake passes +# a target's compile definitions to the resource compiler. target_compile_definitions(I_SilKit INTERFACE SILKIT_BUILD_NUMBER=${SILKIT_BUILD_NUMBER} ) +# Only the C++ sources read the hash, and a quoted string define is fragile in +# the resource compiler, so keep this one away from the .rc files. +if(SILKIT_BUILD_GIT_HASH) + target_compile_definitions(I_SilKit + INTERFACE $<$:SILKIT_GIT_HASH="${SILKIT_BUILD_GIT_HASH}"> + ) +endif() + if (MSVC) target_compile_definitions(I_SilKit INTERFACE _WIN32_WINNT=0x0601 diff --git a/SilKit/source/util/GenerateVersion.cpp b/SilKit/source/util/GenerateVersion.cpp index 2f93f59ab..88f5284ef 100644 --- a/SilKit/source/util/GenerateVersion.cpp +++ b/SilKit/source/util/GenerateVersion.cpp @@ -25,19 +25,23 @@ constexpr auto kHeaderTemplate = R"(// SPDX-FileCopyrightText: 2022 Vector Infor #pragma once -#define SILKIT_GIT_HASH "@GIT_HASH@" #define SILKIT_VERSION_MAJOR @MAJOR@ #define SILKIT_VERSION_MINOR @MINOR@ #define SILKIT_VERSION_PATCH @PATCH@ #define SILKIT_VERSION_STRING "@VERSION_STRING@" #define SILKIT_VERSION_SUFFIX "@SUFFIX@" -// The build number is a property of the build, not of the source tree: pass -// -DSILKIT_BUILD_NUMBER=N to CMake to set it. This is the fallback for anyone -// compiling against the installed header without it. +// The build number and the git hash describe a build, not the source tree, so +// CMake supplies them: -DSILKIT_BUILD_NUMBER=N and -DSILKIT_BUILD_GIT_HASH=. +// The values below are the fallbacks. The hash is the commit that was HEAD when +// this file was generated, i.e. the parent of the version bump. #ifndef SILKIT_BUILD_NUMBER #define SILKIT_BUILD_NUMBER 0 #endif + +#ifndef SILKIT_GIT_HASH +#define SILKIT_GIT_HASH "@GIT_HASH@" +#endif )"; void ReplaceAll(std::string& s, const std::string& from, const std::string& to) diff --git a/SilKit/source/util/tests/Test_Util_GenerateVersion.cpp b/SilKit/source/util/tests/Test_Util_GenerateVersion.cpp index 7beecacdd..ce63d4b70 100644 --- a/SilKit/source/util/tests/Test_Util_GenerateVersion.cpp +++ b/SilKit/source/util/tests/Test_Util_GenerateVersion.cpp @@ -174,11 +174,23 @@ TEST(Test_Util_GenerateVersion, RenderedHeaderIsAsciiLfAndRoundTrips) EXPECT_EQ(ParseGitHashFromHeader(header), "deadbeef"); } -TEST(Test_Util_GenerateVersion, BuildNumberIsOnlyAFallbackInTheHeader) +// The build number and git hash are properties of a build, supplied by CMake as +// -DSILKIT_BUILD_NUMBER=N and -DSILKIT_GIT_HASH=. The header must guard +// both, or the definitions could never take effect. +void ExpectGuardedFallback(const std::string& header, const std::string& macroName) +{ + const auto guard = header.find("#ifndef " + macroName); + const auto define = header.find("#define " + macroName); + EXPECT_NE(guard, std::string::npos) << macroName << " is not guarded by #ifndef"; + EXPECT_LT(guard, define) << macroName << " is defined before its guard"; + + // Exactly one define, so nothing pins the value unconditionally. + EXPECT_EQ(header.find("#define " + macroName, define + 1), std::string::npos) + << macroName << " is defined more than once"; +} + +TEST(Test_Util_GenerateVersion, BuildInputsAreOnlyFallbacksInTheHeader) { - // The build number is a property of a build, supplied by CMake as - // -DSILKIT_BUILD_NUMBER=N. The header must not pin it, or the definition - // could never take effect. Version version; version.major = 5; version.minor = 0; @@ -186,14 +198,14 @@ TEST(Test_Util_GenerateVersion, BuildNumberIsOnlyAFallbackInTheHeader) const auto header = RenderHeader("abc", version); - EXPECT_NE(header.find("#ifndef SILKIT_BUILD_NUMBER"), std::string::npos); + ExpectGuardedFallback(header, "SILKIT_BUILD_NUMBER"); + ExpectGuardedFallback(header, "SILKIT_GIT_HASH"); + EXPECT_NE(header.find("#define SILKIT_BUILD_NUMBER 0"), std::string::npos); - EXPECT_NE(header.find("#endif"), std::string::npos); + EXPECT_EQ(ParseGitHashFromHeader(header), "abc"); - // The guarded define is the only one, and it comes after the #ifndef. - EXPECT_LT(header.find("#ifndef SILKIT_BUILD_NUMBER"), header.find("#define SILKIT_BUILD_NUMBER")); - EXPECT_EQ(header.find("#define SILKIT_BUILD_NUMBER", header.find("#define SILKIT_BUILD_NUMBER") + 1), - std::string::npos); + // The version itself is not guarded: it is source tree state. + EXPECT_EQ(header.find("#ifndef SILKIT_VERSION_MAJOR"), std::string::npos); } TEST(Test_Util_GenerateVersion, RenderedHeaderIsStable) diff --git a/docs/changelog/versions/latest.md b/docs/changelog/versions/latest.md index 80fb7a08a..b36e6bc50 100644 --- a/docs/changelog/versions/latest.md +++ b/docs/changelog/versions/latest.md @@ -19,9 +19,10 @@ - `SilKitVersionMacros.h` is now committed to the source tree instead of being generated at CMake configure time. The new `sil-kit-generate-version` maintainer tool regenerates it and performs a complete version bump (`SilKitVersion.cmake`, the generated header and the changelog) in one step. See `docs/development/release.md`. -- `SILKIT_BUILD_NUMBER` is now purely a build-time setting (`cmake -DSILKIT_BUILD_NUMBER=N`) rather than a value - stored in the source tree. The generated header only carries an `#ifndef` fallback of `0`, so - `SilKit_Version_BuildNumber()` reports `0` unless a build sets it. +- The build number and git hash are now build-time settings (`cmake -DSILKIT_BUILD_NUMBER=N`, + `-DSILKIT_BUILD_GIT_HASH=`) rather than values stored in the source tree. The generated header carries only + `#ifndef` fallbacks, so a build that passes its own hash makes `SilKit::Version::GitHash()` report the commit + actually built instead of the commit the header was generated at. - Changes to the SIL KIT MSI installer: - Default installation path changed from `\Vector SIL Kit ` to `\SIL Kit ` - Windows System Service Name changed from `VectorSilKitRegistry` to `SilKitRegistry` diff --git a/docs/development/release.md b/docs/development/release.md index 4a1c17f68..3ca2815ec 100644 --- a/docs/development/release.md +++ b/docs/development/release.md @@ -2,207 +2,108 @@ orphan: true --- -# !!! Releasing SIL Kit: Version Bumps and the Changelog +# !!! Version Bumps and the Changelog -This page is for SIL Kit maintainers. It describes how the version number is -stored in the source tree and how to change it. - -**Everything on this page is done by one tool: `sil-kit-generate-version`.** -Do not edit the version by hand: several files have to agree, and the tool is -what keeps them in sync. +For maintainers. One tool does all of it: `sil-kit-generate-version`. Do not edit +the version by hand, several files have to agree. ## Where the version lives | File | Who writes it | | --- | --- | -| `SilKit/cmake/SilKitVersion.cmake` | **Source of truth.** Patched by the tool. Everything in the build reads the version from here. | -| `SilKit/include/silkit/capi/SilKitVersionMacros.h` | **Generated. Never edit by hand.** Committed to the repository, and compiled into the library and into the Windows resources of the utilities. | -| `docs/changelog/versions/latest.md` | Hand-written during development, reset by the tool on a bump. | -| `docs/changelog/versions/.md` | Created by the tool when it archives `latest.md`. | -| `docs/changelog/overview.rst` | The toctree line for the archived entry is added by the tool. | - -The build number is deliberately absent from that list: it is set at configure -time, not stored in the source tree. See the "Build identity" section below. +| `SilKit/cmake/SilKitVersion.cmake` | **Source of truth.** Patched by the tool. | +| `SilKit/include/silkit/capi/SilKitVersionMacros.h` | **Generated. Never edit by hand.** Committed, and compiled into the library and the utilities' Windows resources. | +| `docs/changelog/versions/latest.md` | Hand-written as changes land; reset by the tool on a bump. | +| `docs/changelog/versions/.md` | Written by the tool when it archives `latest.md`. | +| `docs/changelog/overview.rst` | Toctree line for the archived entry, added by the tool. | ```{warning} -`silkit/capi/Version.h` and `silkit/SilKitVersion.hpp` are **hand-written public -API** headers that declare the version *query functions*. They contain no version -numbers and are never touched by a version bump. Only -`silkit/capi/SilKitVersionMacros.h` is generated. - -The tool refuses to overwrite any file that is not recognizably a generated -version-macros header, so a mistyped output path fails instead of destroying an -API header. +`silkit/capi/Version.h` and `silkit/SilKitVersion.hpp` are hand-written public API +declaring the version *query functions*. They hold no version numbers and a bump +never touches them. Only `SilKitVersionMacros.h` is generated, and the tool +refuses to write to anything that is not already a generated version header. ``` -`docs/changelog/versions/template.md` is authoring guidance for writing a -changelog entry. It is excluded from the documentation build and is not copied -anywhere by the tool. - ## Build the tool ``` cmake --build --target sil-kit-generate-version ``` -The executable lands in `/sil-kit-generate-version` (with -multi-config generators such as Visual Studio: `//`). +It lands in `/` (multi-config generators: `//`). It +is standalone C++17 with no SIL Kit dependencies, because it generates a header +the library is built from, and it reads `.git` directly rather than invoking +`git`. -The tool is standalone: C++17 and the standard library only, no SIL Kit -dependencies, and it reads the git hash out of `.git` without invoking `git`. -It has to be, because it generates a header the library itself is built from. - -It is built by default and is never installed or packaged. -`-DSILKIT_BUILD_GENERATE_VERSION=OFF` skips it, which is what a cross-compiled -build wants: the tool runs on the maintainer's machine, so building it for the -target architecture produces something unrunnable. The version logic keeps its -unit test coverage either way. +Built by default, never installed. `-DSILKIT_BUILD_GENERATE_VERSION=OFF` skips it +when cross-compiling, where a host tool built for the target is useless. The +version logic keeps its unit tests either way. ## Bump the version -Run it from anywhere inside the source tree; it locates the tree root by looking -for `SilKit/cmake/SilKitVersion.cmake`. - -Look at the plan first: +Run from anywhere in the source tree. Preview first: ``` sil-kit-generate-version --dry-run --major 5 --minor 0 --patch 9 -``` - -Then apply it: - -``` sil-kit-generate-version --major 5 --minor 0 --patch 9 ``` -That single command: +That one command sets the version in `SilKitVersion.cmake`, archives +`latest.md` as `5.0.8.md` with today's date, lists it in `overview.rst`, resets +`latest.md` to an empty `# [5.0.9] - UNRELEASED` stub, and regenerates the +header. Every precondition is checked before the first byte is written, so it +either all happens or none of it does. -1. sets the version in `SilKit/cmake/SilKitVersion.cmake` to `5.0.9`, -2. renames the current in-progress changelog entry to its release version and - date, archiving `docs/changelog/versions/latest.md` as - `docs/changelog/versions/5.0.8.md`, -3. adds `versions/5.0.8.md` to the toctree in `docs/changelog/overview.rst`, -4. resets `latest.md` to an empty `# [5.0.9] - UNRELEASED` stub, -5. regenerates `SilKit/include/silkit/capi/SilKitVersionMacros.h`. +Review with `git diff`; exactly five files change and the `SilKitVersion.cmake` +diff is at most three lines. Anything larger means something went wrong. -Either all of it happens or none of it does: every precondition is checked before -the first byte is written. +`--date YYYY-MM-DD` overrides the release date, `--suffix rc1` makes it a +pre-release. Commit as `version: bump to X.Y.Z (#PR)`. -Review the result with `git diff`. Exactly these files should have changed: +## Refresh the header without bumping -``` -SilKit/cmake/SilKitVersion.cmake -SilKit/include/silkit/capi/SilKitVersionMacros.h -docs/changelog/overview.rst -docs/changelog/versions/latest.md -docs/changelog/versions/.md (new file) -``` - -The `SilKitVersion.cmake` diff must be three lines at most. If it is larger, -something went wrong; do not commit it. - -The archived entry gets today's date. Pass `--date YYYY-MM-DD` to set a -different release date, and `--suffix rc1` for a pre-release version (which -turns the version string into `5.0.9-rc1`). - -The commit convention for a bump is `version: bump to X.Y.Z (#PR)`. - -## Refresh the generated header without bumping - -After a rebase or a merge, the git hash in the committed header is stale, and if -the rebase pulled in someone else's bump the version numbers may disagree too. -Run the tool with no version arguments: +After a rebase or merge the committed hash is stale, and the version may have +drifted from `SilKitVersion.cmake` too. With no version arguments the tool takes +the version from `SilKitVersion.cmake`, refreshes the hash, and rotates nothing: ``` sil-kit-generate-version +sil-kit-generate-version --check # non-zero on drift, writes nothing; for CI ``` -This takes the version from `SilKitVersion.cmake`, refreshes the git hash, and -touches nothing else. No changelog rotation happens because the version did not -change. - -## Verify - -``` -sil-kit-generate-version --check -``` - -Exits non-zero if the header disagrees with `SilKitVersion.cmake`, and writes -nothing. Suitable for a CI guard. Reconfiguring the build is the other check: -`SilKit/source/CMakeLists.txt` fails outright if the generated header is missing. - -## Finish the changelog before a release - -`latest.md` is the entry for the version currently under development. Fill it in -as changes land, following `template.md`. It is what the packaging step ships as -`CHANGELOG.md`, so it must be complete before packaging. - -The heading stays `# [] - UNRELEASED` until the bump; the tool then -substitutes the release date automatically when it archives the file. There is no -need to edit the date by hand. - ## Build identity -Nothing on this page stamps a build. This repository does not produce releases; -its GitHub workers run CI and component tests only. - -What identifies a build: - -- **Git hash.** `SILKIT_GIT_HASH` in the generated header, exposed at runtime as - `SilKit::Version::GitHash()`. Because the header is committed, the hash is - whatever HEAD was when the tool last ran, so for a bump commit it is that - commit's parent. It is no longer refreshed on every build, which is what makes - the build reproducible. -- **Reproducible builds.** `SILKIT_BUILD_REPRODUCIBLE` is `ON` by default and - omits timestamps and unique build ids, so the same sources produce the same - binaries. - -**Build number.** `SILKIT_BUILD_NUMBER` exists only because -`SilKit_Version_BuildNumber()` is part of the stable public C API. It is a -property of a build, not of the source tree, so it is *not* stored in the -generated header and `sil-kit-generate-version` cannot set it. To stamp one, pass -it to CMake: +The version is source tree state. The build number and git hash describe a +*build*, so the header carries only `#ifndef` fallbacks and CMake supplies the +real values: ``` -cmake -B -DSILKIT_BUILD_NUMBER=42 +cmake -B -DSILKIT_BUILD_GIT_HASH=$(git rev-parse HEAD) -DSILKIT_BUILD_NUMBER=42 ``` -It reaches the library and the Windows `FILEVERSION` as a compile definition on -`I_SilKit`; `SilKitVersionMacros.h` only carries the `#ifndef` fallback of `0` for -consumers who compile against the installed header without CMake. Deliberately -setting it defeats reproducibility, so leave it alone unless something downstream -genuinely needs it. +Pass `SILKIT_BUILD_GIT_HASH` and `SilKit::Version::GitHash()` reports the commit +actually built. Left unset, it reports the header's fallback, which is the commit +that was HEAD when the tool last ran, i.e. the parent of the bump commit. +`SILKIT_BUILD_NUMBER` defaults to `0` and also feeds the Windows `FILEVERSION`. -## Troubleshooting +Neither is refreshed per build, which is what keeps `SILKIT_BUILD_REPRODUCIBLE` +(`ON` by default) meaningful: the same sources and the same flags give the same +binary. -### `error: refusing to overwrite ` - -The file is not a generated version-macros header. You most likely passed the -wrong output path. Drop the path argument entirely; the default is correct. - -### `error: .md already exists` - -`SilKitVersion.cmake` names a version whose changelog entry has already been -archived, which usually means the tree is mid-rebase and the version numbers are -not what you think. Check `SilKitVersion.cmake` and `docs/changelog/versions/` -before doing anything else. `--force` overwrites the archived entry; -`--no-changelog` bumps the version and leaves the changelog alone. Nothing has -been written yet at this point. - -### `error: version drift` (from `--check`) - -The committed header and `SilKitVersion.cmake` disagree. Run -`sil-kit-generate-version` with no version arguments to regenerate the header. - -### `error: no SIL Kit source tree found` - -The working directory is outside the source tree. `cd` into it or pass -`--source-dir PATH`. - -### `warning: could not determine the git hash` +## Troubleshooting -`.git` was not found or could not be read; `SILKIT_GIT_HASH` is written as -`UNKNOWN`. Pass `--git-dir PATH`, or `--git-hash HASH` to set it directly. +- **`refusing to overwrite `** - not a generated version header, so you + passed the wrong output path. Drop the path argument; the default is right. +- **`.md already exists`** - `SilKitVersion.cmake` names a version whose + entry is already archived, usually a mid-rebase tree. Check the version numbers + first. `--force` overwrites the archived entry, `--no-changelog` skips + rotation. Nothing has been written yet. +- **`version drift`** (from `--check`) - header and `SilKitVersion.cmake` + disagree. Re-run with no version arguments. +- **`no SIL Kit source tree found`** - run inside the tree or pass + `--source-dir PATH`. +- **`could not determine the git hash`** - `.git` was unreadable, so the fallback + is written as `UNKNOWN`. Pass `--git-dir PATH` or `--git-hash HASH`. `sil-kit-generate-version --help` lists every option. From 210ed04263a79b851aa334c75a55ad1e9610a44d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Tue, 1 Sep 2026 19:08:12 +0200 Subject: [PATCH 6/6] cmake: use SILKIT_VERSION_SUFFIX to override the header default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marius Börschig --- SilKit/cmake/SilKitVersion.cmake | 12 +++-- .../include/silkit/capi/SilKitVersionMacros.h | 24 ++++++--- SilKit/source/CMakeLists.txt | 14 ++++- SilKit/source/util/GenerateVersion.cpp | 51 +++++++------------ SilKit/source/util/GenerateVersion.hpp | 21 ++++---- .../source/util/sil-kit-generate-version.cpp | 25 +++------ .../util/tests/Test_Util_GenerateVersion.cpp | 29 ++++++----- docs/changelog/versions/latest.md | 9 ++-- docs/development/release.md | 30 ++++++----- 9 files changed, 110 insertions(+), 105 deletions(-) diff --git a/SilKit/cmake/SilKitVersion.cmake b/SilKit/cmake/SilKitVersion.cmake index 464117c3f..1dd9c2c02 100644 --- a/SilKit/cmake/SilKitVersion.cmake +++ b/SilKit/cmake/SilKitVersion.cmake @@ -8,10 +8,12 @@ # they are accessible from public headers at runtime. # * Do not edit the numbers below by hand. Run the sil-kit-generate-version tool, which keeps this file, the generated # header and the changelog in sync. See docs/development/release.md. -# * SILKIT_BUILD_NUMBER and SILKIT_BUILD_GIT_HASH describe a build, not the source tree. Both are build-time -# overrides (cmake -DSILKIT_BUILD_NUMBER=N -DSILKIT_BUILD_GIT_HASH=) passed to the sources as compile -# definitions; the generated header only carries the fallbacks. CI should pass the hash it actually built, so the -# library reports that commit rather than the parent of the version bump. +# * SILKIT_BUILD_NUMBER, SILKIT_BUILD_GIT_HASH and SILKIT_VERSION_SUFFIX describe a build, not the source tree. All +# three are build-time overrides passed to the sources as compile definitions; the generated header only carries +# the fallbacks. CI should pass the hash it actually built, so the library reports that commit rather than the +# parent of the version bump, and may set a suffix to mark a pre-release: +# cmake -DSILKIT_BUILD_GIT_HASH= -DSILKIT_BUILD_NUMBER=N -DSILKIT_VERSION_SUFFIX=rc1 +# The suffix also flows into VERSION_STRING below, so CPack package names carry it too. macro(configure_silkit_version project_name) set(SILKIT_VERSION_MAJOR 5) set(SILKIT_VERSION_MINOR 0) @@ -21,7 +23,7 @@ macro(configure_silkit_version project_name) # entry under that name, and set(... CACHE ...) would not overwrite it. set(SILKIT_BUILD_GIT_HASH "" CACHE STRING "Git hash of the built sources; empty keeps the one in SilKitVersionMacros.h") - set(SILKIT_VERSION_SUFFIX "") + set(SILKIT_VERSION_SUFFIX "" CACHE STRING "Pre-release suffix, e.g. rc1; empty for a normal build") set(VERSION_STRING "${SILKIT_VERSION_MAJOR}.${SILKIT_VERSION_MINOR}.${SILKIT_VERSION_PATCH}") if (SILKIT_VERSION_SUFFIX) diff --git a/SilKit/include/silkit/capi/SilKitVersionMacros.h b/SilKit/include/silkit/capi/SilKitVersionMacros.h index 8ac8fbdf2..cc5e80759 100644 --- a/SilKit/include/silkit/capi/SilKitVersionMacros.h +++ b/SilKit/include/silkit/capi/SilKitVersionMacros.h @@ -11,17 +11,27 @@ #define SILKIT_VERSION_MAJOR 5 #define SILKIT_VERSION_MINOR 0 #define SILKIT_VERSION_PATCH 8 -#define SILKIT_VERSION_STRING "5.0.8" -#define SILKIT_VERSION_SUFFIX "" -// The build number and the git hash describe a build, not the source tree, so -// CMake supplies them: -DSILKIT_BUILD_NUMBER=N and -DSILKIT_GIT_HASH=. -// The values below are the fallbacks. The hash is the commit that was HEAD when -// this file was generated, i.e. the parent of the version bump. +// Everything below describes a build rather than the source tree, so CMake +// supplies it and the values here are only fallbacks: +// -DSILKIT_BUILD_NUMBER=N stamps a build number +// -DSILKIT_BUILD_GIT_HASH= the commit actually built; the fallback is +// the commit that was HEAD when this file was +// generated, i.e. the parent of the bump +// -DSILKIT_VERSION_SUFFIX=rc1 marks a pre-release, which also changes +// SILKIT_VERSION_STRING to "5.0.8-rc1" #ifndef SILKIT_BUILD_NUMBER #define SILKIT_BUILD_NUMBER 0 #endif #ifndef SILKIT_GIT_HASH -#define SILKIT_GIT_HASH "78c282e0b752fb67f033a916c999fe586db0c619" +#define SILKIT_GIT_HASH "b5fa2b126cd9538a0f5889bfffd99d2df2985528" +#endif + +#ifndef SILKIT_VERSION_SUFFIX +#define SILKIT_VERSION_SUFFIX "" +#endif + +#ifndef SILKIT_VERSION_STRING +#define SILKIT_VERSION_STRING "5.0.8" #endif diff --git a/SilKit/source/CMakeLists.txt b/SilKit/source/CMakeLists.txt index bba75b74f..898c157bf 100644 --- a/SilKit/source/CMakeLists.txt +++ b/SilKit/source/CMakeLists.txt @@ -36,14 +36,24 @@ target_compile_definitions(I_SilKit INTERFACE SILKIT_BUILD_NUMBER=${SILKIT_BUILD_NUMBER} ) -# Only the C++ sources read the hash, and a quoted string define is fragile in -# the resource compiler, so keep this one away from the .rc files. +# Only the C++ sources read these, and quoted string defines are fragile in the +# resource compiler, so keep them away from the .rc files. if(SILKIT_BUILD_GIT_HASH) target_compile_definitions(I_SilKit INTERFACE $<$:SILKIT_GIT_HASH="${SILKIT_BUILD_GIT_HASH}"> ) endif() +# A pre-release suffix changes the version string too, and CMake is the only +# place that knows how to compose it (VERSION_STRING in SilKitVersion.cmake). +if(SILKIT_VERSION_SUFFIX) + target_compile_definitions(I_SilKit + INTERFACE + $<$:SILKIT_VERSION_SUFFIX="${SILKIT_VERSION_SUFFIX}"> + $<$:SILKIT_VERSION_STRING="${VERSION_STRING}"> + ) +endif() + if (MSVC) target_compile_definitions(I_SilKit INTERFACE _WIN32_WINNT=0x0601 diff --git a/SilKit/source/util/GenerateVersion.cpp b/SilKit/source/util/GenerateVersion.cpp index 88f5284ef..70d229d2e 100644 --- a/SilKit/source/util/GenerateVersion.cpp +++ b/SilKit/source/util/GenerateVersion.cpp @@ -28,13 +28,15 @@ constexpr auto kHeaderTemplate = R"(// SPDX-FileCopyrightText: 2022 Vector Infor #define SILKIT_VERSION_MAJOR @MAJOR@ #define SILKIT_VERSION_MINOR @MINOR@ #define SILKIT_VERSION_PATCH @PATCH@ -#define SILKIT_VERSION_STRING "@VERSION_STRING@" -#define SILKIT_VERSION_SUFFIX "@SUFFIX@" -// The build number and the git hash describe a build, not the source tree, so -// CMake supplies them: -DSILKIT_BUILD_NUMBER=N and -DSILKIT_BUILD_GIT_HASH=. -// The values below are the fallbacks. The hash is the commit that was HEAD when -// this file was generated, i.e. the parent of the version bump. +// Everything below describes a build rather than the source tree, so CMake +// supplies it and the values here are only fallbacks: +// -DSILKIT_BUILD_NUMBER=N stamps a build number +// -DSILKIT_BUILD_GIT_HASH= the commit actually built; the fallback is +// the commit that was HEAD when this file was +// generated, i.e. the parent of the bump +// -DSILKIT_VERSION_SUFFIX=rc1 marks a pre-release, which also changes +// SILKIT_VERSION_STRING to "@VERSION_STRING@-rc1" #ifndef SILKIT_BUILD_NUMBER #define SILKIT_BUILD_NUMBER 0 #endif @@ -42,6 +44,14 @@ constexpr auto kHeaderTemplate = R"(// SPDX-FileCopyrightText: 2022 Vector Infor #ifndef SILKIT_GIT_HASH #define SILKIT_GIT_HASH "@GIT_HASH@" #endif + +#ifndef SILKIT_VERSION_SUFFIX +#define SILKIT_VERSION_SUFFIX "" +#endif + +#ifndef SILKIT_VERSION_STRING +#define SILKIT_VERSION_STRING "@VERSION_STRING@" +#endif )"; void ReplaceAll(std::string& s, const std::string& from, const std::string& to) @@ -63,12 +73,6 @@ std::regex CMakeIntSetter(const std::string& variable) return std::regex{"(set[ \t]*\\([ \t]*" + variable + "[ \t]+)([0-9]+)"}; } -// set(SILKIT_VERSION_SUFFIX "rc1") -std::regex CMakeStringSetter(const std::string& variable) -{ - return std::regex{"(set[ \t]*\\([ \t]*" + variable + "[ \t]+\")([^\"]*)(\")"}; -} - int MatchCMakeInt(const std::string& content, const std::regex& re) { std::smatch match; @@ -120,14 +124,9 @@ bool ReplaceCapture(std::string& content, const std::regex& re, int group, const } // namespace -std::string Version::ToShortString() const -{ - return std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(patch); -} - std::string Version::ToString() const { - return ToShortString() + (suffix.empty() ? "" : "-" + suffix); + return std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(patch); } Version ParseVersionFromCMake(const std::string& content) @@ -136,12 +135,6 @@ Version ParseVersionFromCMake(const std::string& content) version.major = MatchCMakeInt(content, CMakeIntSetter("SILKIT_VERSION_MAJOR")); version.minor = MatchCMakeInt(content, CMakeIntSetter("SILKIT_VERSION_MINOR")); version.patch = MatchCMakeInt(content, CMakeIntSetter("SILKIT_VERSION_PATCH")); - - std::smatch match; - if (std::regex_search(content, match, CMakeStringSetter("SILKIT_VERSION_SUFFIX"))) - { - version.suffix = match[2].str(); - } return version; } @@ -151,7 +144,6 @@ Version ParseVersionFromHeader(const std::string& content) version.major = DefineInt(content, "SILKIT_VERSION_MAJOR"); version.minor = DefineInt(content, "SILKIT_VERSION_MINOR"); version.patch = DefineInt(content, "SILKIT_VERSION_PATCH"); - DefineString(content, "SILKIT_VERSION_SUFFIX", version.suffix); return version; } @@ -191,12 +183,6 @@ bool PatchCMakeVersion(std::string& content, const Version& version, std::string } } - if (!ReplaceCapture(result, CMakeStringSetter("SILKIT_VERSION_SUFFIX"), 2, version.suffix)) - { - error = "no 'set(SILKIT_VERSION_SUFFIX \"...\")' found"; - return false; - } - content = result; return true; } @@ -209,7 +195,6 @@ std::string RenderHeader(const std::string& gitHash, const Version& version) SubstituteVar(result, "MINOR", std::to_string(version.minor)); SubstituteVar(result, "PATCH", std::to_string(version.patch)); SubstituteVar(result, "VERSION_STRING", version.ToString()); - SubstituteVar(result, "SUFFIX", version.suffix); return result; } @@ -237,7 +222,7 @@ std::string RenderChangelogStub(const Version& version) bool InsertChangelogToctreeEntry(std::string& content, const Version& version, std::string& error) { - const std::string entry = "versions/" + version.ToShortString() + ".md"; + const std::string entry = "versions/" + version.ToString() + ".md"; if (content.find(entry) != std::string::npos) { error = "'" + entry + "' is already listed"; diff --git a/SilKit/source/util/GenerateVersion.hpp b/SilKit/source/util/GenerateVersion.hpp index 99de75117..bc6e9030f 100644 --- a/SilKit/source/util/GenerateVersion.hpp +++ b/SilKit/source/util/GenerateVersion.hpp @@ -20,26 +20,28 @@ struct Version int major{-1}; int minor{-1}; int patch{-1}; - std::string suffix; bool IsValid() const { return major >= 0 && minor >= 0 && patch >= 0; } - // "5.0.8", without the suffix - std::string ToShortString() const; - - // "5.0.8" or "5.0.8-rc1" + // "5.0.8". There is no suffix here: a pre-release suffix describes a build + // and is supplied by CMake, not stored in the source tree. std::string ToString() const; - bool SameNumbers(const Version& other) const + bool operator==(const Version& other) const { return major == other.major && minor == other.minor && patch == other.patch; } + + bool operator!=(const Version& other) const + { + return !(*this == other); + } }; -// Parses SILKIT_VERSION_{MAJOR,MINOR,PATCH,SUFFIX} out of the contents of +// Parses SILKIT_VERSION_{MAJOR,MINOR,PATCH} out of the contents of // SilKit/cmake/SilKitVersion.cmake. Returns an invalid Version if a number is // missing. Version ParseVersionFromCMake(const std::string& content); @@ -59,8 +61,9 @@ bool LooksLikeGeneratedHeader(const std::string& content); // expected lines is missing. bool PatchCMakeVersion(std::string& content, const Version& version, std::string& error); -// Renders the full SilKitVersionMacros.h. The build number is deliberately not -// a parameter: it is a build-time CMake definition, not source tree state. +// Renders the full SilKitVersionMacros.h. 'gitHash' is written as the fallback +// value. The build number and pre-release suffix are not parameters at all: +// CMake supplies those at build time. std::string RenderHeader(const std::string& gitHash, const Version& version); // Rewrites the leading '# [x.y.z] - UNRELEASED' heading of a changelog entry to diff --git a/SilKit/source/util/sil-kit-generate-version.cpp b/SilKit/source/util/sil-kit-generate-version.cpp index 0840a49e9..01aa3b24f 100644 --- a/SilKit/source/util/sil-kit-generate-version.cpp +++ b/SilKit/source/util/sil-kit-generate-version.cpp @@ -251,7 +251,6 @@ void PrintUsage(const char* prog) << " --major N New version major (requires --minor and --patch)\n" << " --minor N New version minor\n" << " --patch N New version patch\n" - << " --suffix S Version suffix, e.g. rc1 (default: keep current)\n" << " --date YYYY-MM-DD Release date for the archived changelog entry\n" << " (default: today)\n" << " --source-dir PATH Source tree root (default: search upward from CWD)\n" @@ -280,8 +279,6 @@ struct Options int major{-1}; int minor{-1}; int patch{-1}; - bool haveSuffix{false}; - std::string suffix; std::string date; fs::path sourceDir; fs::path gitDir; @@ -327,11 +324,6 @@ bool ParseOptions(int argc, char* argv[], Options& options) { options.patch = std::atoi(requireNext().c_str()); } - else if (arg == "--suffix") - { - options.suffix = requireNext(); - options.haveSuffix = true; - } else if (arg == "--date") { options.date = requireNext(); @@ -430,7 +422,7 @@ int RunCheck(const Layout& layout, const fs::path& headerPath, const Version& cm std::cerr << "error: " << Show(headerPath) << " has no usable version macros\n"; return 1; } - if (!headerVersion.SameNumbers(cmakeVersion) || headerVersion.suffix != cmakeVersion.suffix) + if (headerVersion != cmakeVersion) { std::cerr << "error: version drift\n" << " " << Show(layout.cmakeVersionFile) << ": " << cmakeVersion.ToString() << "\n" @@ -509,13 +501,8 @@ int main(int argc, char* argv[]) newVersion.minor = options.minor; newVersion.patch = options.patch; } - if (options.haveSuffix) - { - newVersion.suffix = options.suffix; - } - const bool bumping = !newVersion.SameNumbers(currentVersion); - const bool cmakeChanged = bumping || newVersion.suffix != currentVersion.suffix; + const bool bumping = newVersion != currentVersion; // --- guard the output header -------------------------------------------- @@ -532,7 +519,7 @@ int main(int argc, char* argv[]) // Report drift instead of silently taking the header's word for it. const Version headerVersion = SilKit::VersionGen::ParseVersionFromHeader(existing); - if (headerVersion.IsValid() && !headerVersion.SameNumbers(currentVersion)) + if (headerVersion.IsValid() && headerVersion != currentVersion) { std::cout << "note: header was at " << headerVersion.ToString() << ", " << Show(layout.cmakeVersionFile) << " says " << currentVersion.ToString() << "; using the latter\n"; @@ -565,7 +552,7 @@ int main(int argc, char* argv[]) std::vector writes; const bool rotating = bumping && !options.noChangelog; - if (cmakeChanged) + if (bumping) { std::string error; if (!SilKit::VersionGen::PatchCMakeVersion(cmakeContent, newVersion, error)) @@ -579,7 +566,7 @@ int main(int argc, char* argv[]) if (rotating) { - const fs::path archiveMd = layout.versionsDir / (currentVersion.ToShortString() + ".md"); + const fs::path archiveMd = layout.versionsDir / (currentVersion.ToString() + ".md"); if (!fs::exists(layout.latestMd)) { @@ -620,7 +607,7 @@ int main(int argc, char* argv[]) if (SilKit::VersionGen::InsertChangelogToctreeEntry(overviewContent, currentVersion, error)) { writes.push_back({layout.overviewRst, overviewContent, - "changelog: list " + currentVersion.ToShortString() + ".md in the toctree"}); + "changelog: list " + currentVersion.ToString() + ".md in the toctree"}); } else if (!options.force) { diff --git a/SilKit/source/util/tests/Test_Util_GenerateVersion.cpp b/SilKit/source/util/tests/Test_Util_GenerateVersion.cpp index ce63d4b70..b02515fe0 100644 --- a/SilKit/source/util/tests/Test_Util_GenerateVersion.cpp +++ b/SilKit/source/util/tests/Test_Util_GenerateVersion.cpp @@ -68,7 +68,6 @@ TEST(Test_Util_GenerateVersion, ParseVersionFromCmake) EXPECT_EQ(version.major, 5); EXPECT_EQ(version.minor, 0); EXPECT_EQ(version.patch, 8); - EXPECT_EQ(version.suffix, ""); EXPECT_EQ(version.ToString(), "5.0.8"); } @@ -77,7 +76,7 @@ TEST(Test_Util_GenerateVersion, ParseVersionFromCmakeIgnoresBuildNumber) // SILKIT_BUILD_NUMBER carries a CACHE clause and must not be mistaken for a // version component. const auto version = ParseVersionFromCMake(kCMakeVersion); - EXPECT_EQ(version.ToShortString(), "5.0.8"); + EXPECT_EQ(version.ToString(), "5.0.8"); } TEST(Test_Util_GenerateVersion, ParseVersionFromCmakeWithoutVersionIsInvalid) @@ -118,7 +117,6 @@ TEST(Test_Util_GenerateVersion, PatchCmakeVersionOnlyTouchesTheVersionLines) version.major = 6; version.minor = 1; version.patch = 2; - version.suffix = "rc1"; std::string content{kCMakeVersion}; std::string error; @@ -127,14 +125,15 @@ TEST(Test_Util_GenerateVersion, PatchCmakeVersionOnlyTouchesTheVersionLines) EXPECT_NE(content.find("set(SILKIT_VERSION_MAJOR 6)"), std::string::npos); EXPECT_NE(content.find("set(SILKIT_VERSION_MINOR 1)"), std::string::npos); EXPECT_NE(content.find("set(SILKIT_VERSION_PATCH 2)"), std::string::npos); - EXPECT_NE(content.find("set(SILKIT_VERSION_SUFFIX \"rc1\")"), std::string::npos); - // Everything else survives verbatim. + // Everything else survives verbatim, the suffix included: it is a build + // input now, not something a version bump rewrites. + EXPECT_NE(content.find("set(SILKIT_VERSION_SUFFIX \"\")"), std::string::npos); EXPECT_NE(content.find("set(SILKIT_BUILD_NUMBER 0 CACHE STRING \"The build number\")"), std::string::npos); EXPECT_NE(content.find("macro(configure_silkit_version project_name)"), std::string::npos); EXPECT_NE(content.find("${SILKIT_VERSION_MAJOR}.${SILKIT_VERSION_MINOR}"), std::string::npos); - EXPECT_EQ(ParseVersionFromCMake(content).ToString(), "6.1.2-rc1"); + EXPECT_EQ(ParseVersionFromCMake(content).ToString(), "6.1.2"); } TEST(Test_Util_GenerateVersion, PatchCmakeVersionReportsAMissingSetter) @@ -156,7 +155,6 @@ TEST(Test_Util_GenerateVersion, RenderedHeaderIsAsciiLfAndRoundTrips) version.major = 5; version.minor = 0; version.patch = 9; - version.suffix = "rc2"; const auto header = RenderHeader("deadbeef", version); @@ -167,16 +165,16 @@ TEST(Test_Util_GenerateVersion, RenderedHeaderIsAsciiLfAndRoundTrips) } EXPECT_EQ(header.find('\r'), std::string::npos) << "the generated header must use LF line endings"; - EXPECT_NE(header.find("#define SILKIT_VERSION_STRING \"5.0.9-rc2\""), std::string::npos); + EXPECT_NE(header.find("#define SILKIT_VERSION_STRING \"5.0.9\""), std::string::npos); EXPECT_NE(header.find("DO NOT EDIT"), std::string::npos); - EXPECT_EQ(ParseVersionFromHeader(header).ToString(), "5.0.9-rc2"); + EXPECT_EQ(ParseVersionFromHeader(header).ToString(), "5.0.9"); EXPECT_EQ(ParseGitHashFromHeader(header), "deadbeef"); } -// The build number and git hash are properties of a build, supplied by CMake as -// -DSILKIT_BUILD_NUMBER=N and -DSILKIT_GIT_HASH=. The header must guard -// both, or the definitions could never take effect. +// The build number, git hash and pre-release suffix are properties of a build, +// supplied by CMake. The header must guard each of them, or the definitions +// could never take effect. void ExpectGuardedFallback(const std::string& header, const std::string& macroName) { const auto guard = header.find("#ifndef " + macroName); @@ -200,12 +198,17 @@ TEST(Test_Util_GenerateVersion, BuildInputsAreOnlyFallbacksInTheHeader) ExpectGuardedFallback(header, "SILKIT_BUILD_NUMBER"); ExpectGuardedFallback(header, "SILKIT_GIT_HASH"); + ExpectGuardedFallback(header, "SILKIT_VERSION_SUFFIX"); + // Guarded too, because a pre-release suffix changes it to "5.0.8-rc1". + ExpectGuardedFallback(header, "SILKIT_VERSION_STRING"); EXPECT_NE(header.find("#define SILKIT_BUILD_NUMBER 0"), std::string::npos); + EXPECT_NE(header.find("#define SILKIT_VERSION_SUFFIX \"\""), std::string::npos); EXPECT_EQ(ParseGitHashFromHeader(header), "abc"); - // The version itself is not guarded: it is source tree state. + // The version numbers are not guarded: they are source tree state. EXPECT_EQ(header.find("#ifndef SILKIT_VERSION_MAJOR"), std::string::npos); + EXPECT_EQ(header.find("#ifndef SILKIT_VERSION_PATCH"), std::string::npos); } TEST(Test_Util_GenerateVersion, RenderedHeaderIsStable) diff --git a/docs/changelog/versions/latest.md b/docs/changelog/versions/latest.md index b36e6bc50..a6b088f5b 100644 --- a/docs/changelog/versions/latest.md +++ b/docs/changelog/versions/latest.md @@ -19,10 +19,11 @@ - `SilKitVersionMacros.h` is now committed to the source tree instead of being generated at CMake configure time. The new `sil-kit-generate-version` maintainer tool regenerates it and performs a complete version bump (`SilKitVersion.cmake`, the generated header and the changelog) in one step. See `docs/development/release.md`. -- The build number and git hash are now build-time settings (`cmake -DSILKIT_BUILD_NUMBER=N`, - `-DSILKIT_BUILD_GIT_HASH=`) rather than values stored in the source tree. The generated header carries only - `#ifndef` fallbacks, so a build that passes its own hash makes `SilKit::Version::GitHash()` report the commit - actually built instead of the commit the header was generated at. +- The build number, git hash and pre-release suffix are now build-time settings (`cmake -DSILKIT_BUILD_NUMBER=N`, + `-DSILKIT_BUILD_GIT_HASH=`, `-DSILKIT_VERSION_SUFFIX=rc1`) rather than values stored in the source tree. + The generated header carries only `#ifndef` fallbacks, so a build that passes its own hash makes + `SilKit::Version::GitHash()` report the commit actually built, and a build that sets a suffix reports + `5.0.8-rc1` from `SilKit::Version::String()` and in the CPack archive name. - Changes to the SIL KIT MSI installer: - Default installation path changed from `\Vector SIL Kit ` to `\SIL Kit ` - Windows System Service Name changed from `VectorSilKitRegistry` to `SilKitRegistry` diff --git a/docs/development/release.md b/docs/development/release.md index 3ca2815ec..c9cd27cc7 100644 --- a/docs/development/release.md +++ b/docs/development/release.md @@ -57,8 +57,9 @@ either all happens or none of it does. Review with `git diff`; exactly five files change and the `SilKitVersion.cmake` diff is at most three lines. Anything larger means something went wrong. -`--date YYYY-MM-DD` overrides the release date, `--suffix rc1` makes it a -pre-release. Commit as `version: bump to X.Y.Z (#PR)`. +`--date YYYY-MM-DD` overrides the release date. Commit as +`version: bump to X.Y.Z (#PR)`. A pre-release is not a bump; see +"Build identity" below. ## Refresh the header without bumping @@ -73,22 +74,25 @@ sil-kit-generate-version --check # non-zero on drift, writes nothing; for CI ## Build identity -The version is source tree state. The build number and git hash describe a -*build*, so the header carries only `#ifndef` fallbacks and CMake supplies the -real values: +Only the version numbers are source tree state. The git hash, build number and +pre-release suffix describe a *build*, so the header carries only `#ifndef` +fallbacks and CMake supplies the real values: ``` -cmake -B -DSILKIT_BUILD_GIT_HASH=$(git rev-parse HEAD) -DSILKIT_BUILD_NUMBER=42 +cmake -B -DSILKIT_BUILD_GIT_HASH=$(git rev-parse HEAD) \ + -DSILKIT_BUILD_NUMBER=42 -DSILKIT_VERSION_SUFFIX=rc1 ``` -Pass `SILKIT_BUILD_GIT_HASH` and `SilKit::Version::GitHash()` reports the commit -actually built. Left unset, it reports the header's fallback, which is the commit -that was HEAD when the tool last ran, i.e. the parent of the bump commit. -`SILKIT_BUILD_NUMBER` defaults to `0` and also feeds the Windows `FILEVERSION`. +- `SILKIT_BUILD_GIT_HASH` makes `SilKit::Version::GitHash()` report the commit + actually built. Unset, it reports the header's fallback: the commit that was + HEAD when the tool last ran, i.e. the parent of the bump. +- `SILKIT_BUILD_NUMBER` defaults to `0` and also fills the Windows `FILEVERSION`. +- `SILKIT_VERSION_SUFFIX` marks a pre-release. It changes + `SilKit::Version::String()` to `5.0.8-rc1` and flows into `PROJECT_VERSION`, so + CPack archive names carry it too. -Neither is refreshed per build, which is what keeps `SILKIT_BUILD_REPRODUCIBLE` -(`ON` by default) meaningful: the same sources and the same flags give the same -binary. +None of them is derived per build, which keeps `SILKIT_BUILD_REPRODUCIBLE` (`ON` +by default) meaningful: the same sources and the same flags give the same binary. ## Troubleshooting