From a4fcc10f5f30934bc3c79b01981ae13cb63da4ff Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 8 May 2026 17:25:09 -0600 Subject: [PATCH] Make version logic tolerate prerelease semver --- CMakeLists.txt | 12 +-- src/dusk/update_check.cpp | 167 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 168 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 02eaeb183e..60aeb4e3fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,13 +48,15 @@ else () message(STATUS "Unable to find git, commit information will not be available") endif () -if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)(-([0-9]+).*)?$") +if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$") set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}") - if (CMAKE_MATCH_5) - set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${CMAKE_MATCH_5}") - else () - set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.0") + set(DUSK_VERSION_TWEAK "0") + if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$") + set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}") + elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$") + set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}") endif () + set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}") else () set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION") set(DUSK_VERSION_STRING "0.0.0.0") diff --git a/src/dusk/update_check.cpp b/src/dusk/update_check.cpp index 11c11795af..c1e332c432 100644 --- a/src/dusk/update_check.cpp +++ b/src/dusk/update_check.cpp @@ -5,9 +5,12 @@ #include "nlohmann/json.hpp" #include "version.h" +#include #include #include +#include #include +#include namespace dusk::update_check { namespace { @@ -20,8 +23,7 @@ struct Version { int major = 0; int minor = 0; int patch = 0; - - friend auto operator<=>(const Version&, const Version&) = default; + std::vector prerelease; }; std::string json_string(const json& value, const char* key) { @@ -57,6 +59,134 @@ bool consume(std::string_view& value, char expected) { return true; } +bool is_digit(char value) { + return value >= '0' && value <= '9'; +} + +bool is_identifier_char(char value) { + return is_digit(value) || (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z') || value == '-'; +} + +bool is_numeric_identifier(std::string_view value) { + if (value.empty()) { + return false; + } + for (const char c : value) { + if (!is_digit(c)) { + return false; + } + } + return true; +} + +bool is_identifier_list(std::string_view value) { + if (value.empty()) { + return false; + } + + bool expectingIdentifier = true; + for (const char c : value) { + if (c == '.') { + if (expectingIdentifier) { + return false; + } + expectingIdentifier = true; + continue; + } + if (!is_identifier_char(c)) { + return false; + } + expectingIdentifier = false; + } + + return !expectingIdentifier; +} + +std::string_view trim_git_describe_suffix(std::string_view value) { + if (value.ends_with("-dirty")) { + value.remove_suffix(6); + } + if (is_numeric_identifier(value)) { + return {}; + } + + const size_t suffixStart = value.rfind('-'); + if (suffixStart != std::string_view::npos && value.substr(0, suffixStart).find('.') != std::string_view::npos + && is_numeric_identifier(value.substr(suffixStart + 1))) { + value.remove_suffix(value.size() - suffixStart); + } + return value; +} + +void split_identifiers(std::string_view value, std::vector& identifiers) { + while (!value.empty()) { + const size_t separator = value.find('.'); + if (separator == std::string_view::npos) { + identifiers.push_back(value); + return; + } + identifiers.push_back(value.substr(0, separator)); + value.remove_prefix(separator + 1); + } +} + +std::string_view trim_leading_zeroes(std::string_view value) { + while (value.size() > 1 && value.front() == '0') { + value.remove_prefix(1); + } + return value; +} + +int compare_identifier(std::string_view lhs, std::string_view rhs) { + const bool lhsNumeric = is_numeric_identifier(lhs); + const bool rhsNumeric = is_numeric_identifier(rhs); + if (lhsNumeric && rhsNumeric) { + lhs = trim_leading_zeroes(lhs); + rhs = trim_leading_zeroes(rhs); + if (lhs.size() != rhs.size()) { + return lhs.size() < rhs.size() ? -1 : 1; + } + } else if (lhsNumeric != rhsNumeric) { + return lhsNumeric ? -1 : 1; + } + + const int result = lhs.compare(rhs); + if (result < 0) { + return -1; + } + if (result > 0) { + return 1; + } + return 0; +} + +int compare_version(const Version& lhs, const Version& rhs) { + if (lhs.major != rhs.major) { + return lhs.major < rhs.major ? -1 : 1; + } + if (lhs.minor != rhs.minor) { + return lhs.minor < rhs.minor ? -1 : 1; + } + if (lhs.patch != rhs.patch) { + return lhs.patch < rhs.patch ? -1 : 1; + } + if (lhs.prerelease.empty() != rhs.prerelease.empty()) { + return lhs.prerelease.empty() ? 1 : -1; + } + + const size_t commonSize = std::min(lhs.prerelease.size(), rhs.prerelease.size()); + for (size_t i = 0; i < commonSize; ++i) { + const int result = compare_identifier(lhs.prerelease[i], rhs.prerelease[i]); + if (result != 0) { + return result; + } + } + if (lhs.prerelease.size() != rhs.prerelease.size()) { + return lhs.prerelease.size() < rhs.prerelease.size() ? -1 : 1; + } + return 0; +} + std::optional parse_version(std::string_view value) { if (!value.empty() && value.front() == 'v') { value.remove_prefix(1); @@ -75,13 +205,38 @@ std::optional parse_version(std::string_view value) { if (!patch) { return std::nullopt; } - if (!value.empty() && value.front() != '-' && value.front() != '+') { - return std::nullopt; - } version.major = *major; version.minor = *minor; version.patch = *patch; + + if (value.empty()) { + return version; + } + if (value.front() == '+') { + value.remove_prefix(1); + if (!is_identifier_list(value)) { + return std::nullopt; + } + return version; + } + if (!consume(value, '-')) { + return std::nullopt; + } + + const size_t buildStart = value.find('+'); + std::string_view prerelease = value.substr(0, buildStart); + if (!is_identifier_list(prerelease)) { + return std::nullopt; + } + if (buildStart != std::string_view::npos && !is_identifier_list(value.substr(buildStart + 1))) { + return std::nullopt; + } + + prerelease = trim_git_describe_suffix(prerelease); + if (!prerelease.empty()) { + split_identifiers(prerelease, version.prerelease); + } return version; } @@ -185,7 +340,7 @@ Result check_latest_github_release(std::string_view owner, std::string_view repo }; } - const bool updateAvailable = *latestVersion > *currentVersion; + const bool updateAvailable = compare_version(*latestVersion, *currentVersion) > 0; return { .status = updateAvailable ? Status::UpdateAvailable : Status::UpToDate, .message = updateAvailable ? "Update available" : "Dusk is up to date",