From 50f853546c35e9ccf515b2724c583d9622027cca Mon Sep 17 00:00:00 2001 From: Rene Rivera Date: Wed, 24 Jun 2026 00:17:31 -0500 Subject: [PATCH] Experimental "version" feature type. This adds a "version" type attribute to features that follow semver semantics. The implementation follows the semver rules for matching property sets and requirements, and for matching target alternatives. The implementation relies on the bfgroup/versioned library for the semver parsing and checking. --- doc/src/history.adoc | 3 + doc/src/reference.adoc | 10 + example/versioning/hello.cpp | 14 + example/versioning/jamroot.jam | 15 + src/build/feature.jam | 67 +- src/build/property.jam | 117 ++- src/build/targets.jam | 45 +- src/build/version.jam | 7 + src/engine/ext_bfgroup_versioned.hpp | 964 +++++++++++++++++++++++++ src/engine/mod_version.cpp | 30 + src/engine/mod_version.h | 24 +- src/tools/features/version-feature.jam | 2 +- 12 files changed, 1278 insertions(+), 20 deletions(-) create mode 100644 example/versioning/hello.cpp create mode 100644 example/versioning/jamroot.jam create mode 100644 src/engine/ext_bfgroup_versioned.hpp diff --git a/doc/src/history.adoc b/doc/src/history.adoc index b56a8a5bc..1075502a7 100644 --- a/doc/src/history.adoc +++ b/doc/src/history.adoc @@ -14,6 +14,9 @@ * *New*: Add reading/parsing of property database from JSON data to base CPS support from. -- _René Ferdinand Rivera Morell_ +* *New*: Add `version` feature attribute to tag and manage semantic versioning + of targets. + -- _René Ferdinand Rivera Morell_ * Fix Jam/{CPP} bind definitions of 4 or more values in a single declared argument not actually adding all the definitions. -- _Paolo Pastori_ diff --git a/doc/src/reference.adoc b/doc/src/reference.adoc index 2479218ce..d9bc3c3a9 100644 --- a/doc/src/reference.adoc +++ b/doc/src/reference.adoc @@ -1091,6 +1091,16 @@ on library B. As the result, whenever an application will link to A, it will also link to B. Specifying B as dependency of A is different from adding B to the sources of A. +* [[b2.reference.features.attributes.version]] _version_ ++ +The value of a version feature is a valid Semantic Version. +footnote:[Semantic Versioning (https://semver.org/)] Like free features, the +set of values are unbounded. When a version feature is specified it will use +the semnantic versioning rules to match them against each other. ++ +_Note, this is an experimental feature and how it works is subject to future +changes as use cases get discovered._ + [[b2.reference.features.attributes.base]] Features that are neither free nor incidental are called _base_ features. diff --git a/example/versioning/hello.cpp b/example/versioning/hello.cpp new file mode 100644 index 000000000..888e78b1c --- /dev/null +++ b/example/versioning/hello.cpp @@ -0,0 +1,14 @@ +// Copyright (c) 2003 Vladimir Prus +// +// Distributed under the Boost Software License, Version 1.0. (See +// accompanying file LICENSE.txt or copy at +// https://www.bfgroup.xyz/b2/LICENSE.txt) + +// tag::source[] +#include + +int main() +{ + std::cout << "Hello!\n"; +} +// end::source[] diff --git a/example/versioning/jamroot.jam b/example/versioning/jamroot.jam new file mode 100644 index 000000000..11e7c3386 --- /dev/null +++ b/example/versioning/jamroot.jam @@ -0,0 +1,15 @@ +lib ext : : ext10 1.0 ; +lib ext : : ext12 1.2 ; +lib ext : : ext20 2.0 ; +explicit ext ; + +exe hello : hello.cpp ext/2 : 2.0 ; +exe hello : hello.cpp ext/1 : 1.1 ; +explicit hello ; + +explicit + [ exe hello20 : hello.cpp ext/2 : 2.0 ] + [ exe hello11 : hello.cpp ext/1.1 : 1.1 ] + [ exe hello10 : hello.cpp ext/1.0 : 1.0 ] ; + +alias all : hello/2 hello/1 hello20 hello11 hello10 ; diff --git a/src/build/feature.jam b/src/build/feature.jam index 7c1be2985..f6da77fb1 100644 --- a/src/build/feature.jam +++ b/src/build/feature.jam @@ -1,5 +1,5 @@ # Copyright 2001, 2002, 2003 Dave Abrahams -# Copyright 2002, 2006 Rene Rivera +# Copyright 2002, 2006, 2026 Rene Rivera # Copyright 2002, 2003, 2004, 2005, 2006 Vladimir Prus # Distributed under the Boost Software License, Version 1.0. # (See accompanying file LICENSE.txt or copy at @@ -29,6 +29,7 @@ local rule setup ( ) subfeature order-sensitive hidden + version ; .all-features = ; @@ -113,14 +114,22 @@ rule feature ( { error = feature already "defined:" ; } - else if implicit in $(attributes) && free in $(attributes) + else if free in $(attributes) && implicit in $(attributes) { - error = free features cannot also be implicit ; + error = free features cannot be implicit ; } else if free in $(attributes) && propagated in $(attributes) { error = free features cannot be propagated ; } + else if version in $(attributes) && implicit in $(attributes) + { + error = version features cannot be implicit ; + } + else if version in $(attributes) && free in $(attributes) + { + error = version features cannot be free ; + } else { if [ MATCH (=) : $(values) ] @@ -170,6 +179,10 @@ rule set-default ( feature : value ) { bad-attribute = optional ; } + else if version in $(a) + { + bad-attribute = version ; + } if $(bad-attribute) { import errors ; @@ -194,7 +207,7 @@ rule defaults ( features * ) { local gf = $(:E=:G=$(f)) ; local a = $($(gf).attributes) ; - if ( free in $(a) ) || ( optional in $(a) ) + if ( free in $(a) ) || ( optional in $(a) ) || ( version in $(a) ) { } else @@ -462,7 +475,18 @@ local rule extend-feature ( feature : values * ) # rule validate-value-string ( feature value-string ) { - if ! ( + if ( version in $($(feature).attributes) ) + { + # Check that the value is a valid semver. + import version ; + if ! [ version.semver-verify $(value-string) ] + { + import errors ; + errors.error \"$(value-string)\" + "is not a valid semantic version for feature" \"$(feature)\" ; + } + } + else if ! ( free in $($(feature).attributes) || ( $(value-string) in $($(feature).values) ) ) @@ -738,6 +762,20 @@ rule free-features ( ) } +#| +Is a feature with a unbounded/variable set of values, like free and version. +|# +local rule is-variable-feature ( f ) +{ + local attr = [ attributes $(f) ] ; + if free in $(attr) || feature in $(attr) + { + return true ; + } + return ; +} + + # Expand all composite properties in the set so that all components are # explicitly expressed. # @@ -757,7 +795,7 @@ rule expand-composites ( properties * ) { local f = $(x:G) ; - if $(f) in $(free.features) + if $(f) in $(free.features) || $(f) in $(version.features) { result += $(x) ; } @@ -1085,7 +1123,7 @@ rule add-defaults ( properties * ) local to-remove ; for local f in $(properties:G) { - if ! free in [ attributes $(f) ] + if ! [ is-variable-feature $(f) ] && ! feature in [ attributes $(f) ] { to-remove += $(f) ; } @@ -1112,9 +1150,10 @@ rule add-defaults ( properties * ) { if ! $(m:G) in $(to-remove) { + local is-variable = [ is-variable-feature $(m:G) ] ; local att = [ attributes $(m:G) ] ; if $(m:G) in $(expanded-from-composite) && - ! free in $(att) && + ! $(is-variable) && ! $(m) in $(more) { import errors ; @@ -1125,7 +1164,7 @@ rule add-defaults ( properties * ) expanded-from-composite += $(m:G) ; } more += $(m) ; - if ! subfeature in $(att) && ! free in $(att) + if ! subfeature in $(att) && ! $(is-variable) { worklist += $(m) ; } @@ -1225,6 +1264,7 @@ rule __test__ ( ) feature variant : debug release profile : implicit composite symmetric ; feature stdlib : native stlport ; feature magic : : free ; + feature version : : version ; compose debug : _DEBUG off ; compose release : NDEBUG on ; @@ -1411,6 +1451,13 @@ rule __test__ ( ) } catch \"digital_mars\" is not a known value of feature ; + validate-value-string 1.2.3 ; + try ; + { + validate-value-string a.b.c ; + } + catch \"a.b.c\" is not a valid semantic version for feature \"\" ; + try ; { feature foobar : : baz ; @@ -1428,7 +1475,7 @@ rule __test__ ( ) { feature feature2 : : free implicit ; } - catch free features cannot also be implicit ; + catch free features cannot be implicit ; try ; { diff --git a/src/build/property.jam b/src/build/property.jam index fb66601ad..87ac7fb1e 100644 --- a/src/build/property.jam +++ b/src/build/property.jam @@ -15,6 +15,7 @@ import sequence ; import set ; import utility ; import args ; +import version ; # Refines 'properties' by overriding any non-free and non-conditional properties @@ -25,6 +26,7 @@ rule refine ( properties * : requirements * ) { local result ; local unset ; + local to-resolve-version ; # Collect all non-free features in requirements for local r in $(requirements) @@ -32,7 +34,11 @@ rule refine ( properties * : requirements * ) # Do not consider conditional requirements. if ! [ MATCH "(:<)" : $(r:G=) ] && ! free in [ feature.attributes $(r:G) ] { - if ! $(r) in $(properties) + if version in [ feature.attributes $(r:G) ] + { + to-resolve-version += $(r) ; + } + else if ! $(r) in $(properties) { # Kill subfeatures of properties that we're changing local sub = [ modules.peek feature : $(r:G).subfeatures ] ; @@ -48,16 +54,49 @@ rule refine ( properties * : requirements * ) } } + # The version props to resolve get removed from both the props and + # requirements. As the resolved version gets added in tot he result. + requirements = [ set.difference $(requirements) : $(to-resolve-version) ] ; + # Remove properties that are overridden by requirements for local p in $(properties) { - if [ MATCH "(:<)" : $(p:G=) ] || ! $(p:G) in $(unset) + if [ MATCH "(:<)" : $(p:G=) ] { + # Keep conditional properties in result. + result += $(p) ; + } + else if $(p:G) in $(to-resolve-version:G) + { + # For version feature need to resolve which one is compatible. + local resolved = $(p:G=) ; + for local r in $(to-resolve-version) + { + if $(p:G) = $(r:G) + { + resolved = [ version.semver-resolve $(resolved) $(r:G=) ] $(resolved) ; + resolved = $(resolved[1]) ; + } + } + result += $(p:G)$(resolved) ; + } + else if ! $(p:G) in $(unset) + { + # Keep anything that is not overridden, i.e. not in the requirements. result += $(p) ; } } - return [ sequence.unique $(result) $(requirements) ] ; + for local r in $(to-resolve-version) + { + if ! $(r:G) in $(result:G) + { + result += $(r) ; + } + } + + local result = [ sequence.unique $(result) $(requirements) ] ; + return $(result) ; } @@ -877,6 +916,61 @@ class property-map } +rule next-is-match ( current * : next * ) +{ + if $(current) in $(next) + { + return true ; + } + local current-plain ; + local current-version ; + local next-plain ; + local next-version ; + for local c in $(current) + { + if version in [ feature.attributes $(c:G) ] + { + current-version += $(c) ; + } + else + { + current-plain += $(c) ; + } + } + for local n in $(next) + { + if version in [ feature.attributes $(n:G) ] + { + next-version += $(n) ; + } + else + { + next-plain += $(n) ; + } + } + if ! $(current-plain) in $(next-plain) + { + return ; + } + if ! $(current-version:G) in $(next-version:G) + { + return ; + } + local next-match ; + for local p in $(current-version) + { + for local q in $(next-version) + { + if $(p:G) = $(q:G) && ! [ version.semver-resolve $(p:G=) $(q:G=) ] + { + return ; + } + } + } + return true ; +} + + rule __test__ ( ) { import assert ; @@ -897,6 +991,7 @@ rule __test__ ( ) feature.feature optimization : on off ; feature.feature variant : debug release : implicit composite symmetric ; feature.feature rtti : on off : link-incompatible ; + feature.feature version : : version ; feature.compose debug : _DEBUG off ; feature.compose release : NDEBUG on ; @@ -931,6 +1026,13 @@ rule __test__ ( ) : refine gcc:foo : gcc:bar : $(test-space) ; + assert.result-set-equal gcc 2.1 + : refine gcc 2.0 + : 2.1 ; + assert.result-set-equal gcc 3.0 + : refine gcc + : 3.0 ; + assert.result : evaluate-conditionals-in-context release,off:MY_RELEASE @@ -980,6 +1082,11 @@ rule __test__ ( ) validate value : $(test-space) ; catch \"value\" is not an implicit feature value ; + validate 1.2.3 ; + try ; + validate a.b.c ; + catch \"a.b.c\" is not a valid semantic version for feature \"\" ; + assert.result-set-equal on : remove free implicit : gcc foo on : $(test-space) ; @@ -1027,5 +1134,9 @@ rule __test__ ( ) : translate gcc,!off:HELLO : project-id : project-location : context-module ; + assert.true next-is-match 1.1 : 1.2 ; + assert.false next-is-match 1.1 : 2.0 ; + assert.false next-is-match 1.1 : 1.0 ; + feature.finish-test property-test-temp ; } diff --git a/src/build/targets.jam b/src/build/targets.jam index e6ea08d96..e514f1816 100644 --- a/src/build/targets.jam +++ b/src/build/targets.jam @@ -633,6 +633,7 @@ local rule end-building ( main-target-instance ) # class main-target : abstract-target { + import property ; import property-set ; import sequence ; import set ; @@ -713,6 +714,11 @@ class main-target : abstract-target best = $(v) ; best-properties = $(properties) ; } + else if [ property.next-is-match $(best-properties) : $(properties) ] + { + best = $(v) ; + best-properties = $(properties) ; + } else { bad = true ; @@ -1148,6 +1154,7 @@ class basic-target : abstract-target import sequence ; import set ; import targets ; + import version ; import virtual-target ; rule __init__ ( name : project : sources * : requirements * : @@ -1229,13 +1236,38 @@ class basic-target : abstract-target local condition = [ set.difference $(bcondition) : $(ccondition) ] ; condition = [ feature.expand-subfeatures $(condition) : unchecked ] ; - if $(condition) in [ $(property-set).raw ] + if [ $(property-set).contains-features $(condition:G) ] { - if $(debug) + # The condition could match. But we need to resolve any features + # that use non-equality matching. + for local c in $(condition) { - ECHO " matched:" $(condition:E=(empty)) ; + if version in [ feature.attributes $(c:G) ] + { + local resolved ; + for local v in [ $(property-set).get $(c:G) ] + { + resolved ?= $(c:G=) ; + resolved = [ version.semver-resolve $(v) $(resolved) ] ; + if ! $(resolved) + { + if $(debug) + { + ECHO " no match:" $(condition:E=(empty)) ; + } + return no-match ; + } + } + } + else if ! $(c:G=) in [ $(property-set).get $(c:G) ] + { + if $(debug) + { + ECHO " no match:" $(condition:E=(empty)) ; + } + return no-match ; + } } - return $(condition) ; } else { @@ -1245,6 +1277,11 @@ class basic-target : abstract-target } return no-match ; } + if $(debug) + { + ECHO " matched:" $(condition:E=(empty)) ; + } + return $(condition) ; } # Takes a target reference, which might be either target id or a dependency diff --git a/src/build/version.jam b/src/build/version.jam index 040e8329b..03fa4e134 100644 --- a/src/build/version.jam +++ b/src/build/version.jam @@ -194,5 +194,12 @@ rule __test__ ( ) assert.true version-compatible 04 : 4 ; assert.true version-compatible 04 00 : 04 ; assert.true version-compatible 04 : 04 00 ; + + assert.true semver-verify 1.2.3 ; + assert.false semver-verify a.b.c ; + assert.false semver-verify "1.2.3 extra" ; + + assert.true semver-resolve 1.2 1.3 ; + assert.false semver-resolve 1.3 1.2 ; } diff --git a/src/engine/ext_bfgroup_versioned.hpp b/src/engine/ext_bfgroup_versioned.hpp new file mode 100644 index 000000000..a6097f812 --- /dev/null +++ b/src/engine/ext_bfgroup_versioned.hpp @@ -0,0 +1,964 @@ +// Copyright René Ferdinand Rivera Morell +// Distributed under the Boost Software License, Version 1.0. (See +// versioned_narrowc_accumulateompanying file LICENSE.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +#ifndef VERSIONED_VERSIONED_HPP +#define VERSIONED_VERSIONED_HPP + +#include +#include +#include +#include +#include +#include + +#if defined(__cpp_lib_to_chars) +# include +#endif + +#define VERSIONED_VERSION_MAJOR 0 +#define VERSIONED_VERSION_MINOR 1 +#define VERSIONED_VERSION_PATCH 0 + +#define VERSIONED_VERSION \ + (((VERSIONED_VERSION_MAJOR) * 10000000) \ + + ((VERSIONED_VERSION_MINOR) * 100000) + (VERSIONED_VERSION_PATCH)) + +// Some platform define inconvenient macros. +#ifdef major +# undef major +#endif +#ifdef minor +# undef minor +#endif + +namespace bfg { namespace versioned { namespace detail { + +// The decimal digits. +inline constexpr bool is_digit(const char c) { return c >= '0' && c <= '9'; } + +// Alphanumeric, for identifiers. +inline constexpr bool is_alphanum(const char c) +{ + return is_digit(c) || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +// Identifier allowed character. +inline constexpr bool is_ident(const char c) +{ + return is_alphanum(c) || (c == '-'); +} + +// Base 10 only positive integer parsing. Believe it or not, this rather +// simple code provides the fastest throughput for parsing sequential +// integers. +template +const char * from_chars_10(const char * first, const char * last, I & value) +{ + I e = 0; + while (first < last && is_digit(*first)) e = e * 10 + (*(first++) - '0'); + value = e; + return first; +} + +// Compare, as ASCII, two string ranges. +inline int chars_cmp( + const char * a0, const char * an, const char * b0, const char * bn) +{ + while ((a0 < an) && (b0 < bn) && (*a0 == *b0)) + { + a0 += 1; + b0 += 1; + } + return *a0 - *b0; +} + +template +constexpr N masked_max(U value) +{ + return value & static_cast(::std::numeric_limits::max()); +} + +// Simple hash values combine. +template +T hash_combine(T seed, N... an) +{ + T args[] = { an... }; + // Init. + T result = seed; + // Combine. + for (auto a : args) + { + result ^= a + masked_max(3772387269305686495) + + (result << (30 * 2 / sizeof(T))) + + (result >> (13 * 2 / sizeof(T))); + } + // Mix. + result ^= (result >> (16 * 2 / sizeof(T))); + result *= masked_max(448100074733706); + result ^= (result << (8 * 2 / sizeof(T))); + result *= masked_max(190056597654806); + result ^= (result >> (11 * 2 / sizeof(T))); + return result; +} + +// Backfill to_string as it's missing on some runtimes. +inline ::std::string to_string_10(::std::size_t value) +{ + ::std::string result; + result.reserve(::std::numeric_limits<::std::size_t>::max_digits10); + do + { + result += '0' + (value % 10); + value /= 10; + } + while (value > 0); + ::std::reverse(result.begin(), result.end()); + return result; +} + +}}} // namespace bfg::versioned::detail + +namespace bfg { namespace versioned { +struct from_chars_result +{ + const char * ptr; + ::std::errc ec; + + constexpr explicit operator bool() const noexcept + { + return ec == ::std::errc { }; + } + + friend constexpr bool operator==( + const from_chars_result & a, const from_chars_result & b) + { + return (a.ptr == b.ptr) && (a.ec == b.ec); + } + +#if defined(__cpp_lib_to_chars) && (__cpp_lib_to_chars >= 201611L) + constexpr operator std::from_chars_result() { return { ptr, ec }; } + constexpr from_chars_result & operator=( + const std::from_chars_result & other) noexcept + { + return *this = from_chars_result { other.ptr, other.ec }; + } +#endif +}; +}} // namespace bfg::versioned + +// tag::version_core-sym[] +namespace bfg { namespace versioned { +template +class version_core +{ + public: + // types + using element_t = Number; + static constexpr ::std::size_t element_c = Count; + + // construction, destruction, and assignment + version_core() = default; + version_core(version_core &&) = default; + version_core(const version_core &) = default; + version_core & operator=(const version_core &) = default; + template + version_core(element_t a0, I... an); + + // observers + element_t & at(::std::size_t i); + const element_t & at(::std::size_t i) const; + + private: + static_assert(::std::is_integral::value, + "Version element type is not an integral type."); // exposition only + static_assert(Count > 0, + "Version must contain at least one part."); // exposition only + + element_t number[element_c] = { }; // exposition only + // end::version_core-sym[] + + template + friend from_chars_result from_chars( + const char * first, const char * last, version_core & value); + + template + friend ::std::string to_string(const version_core & value); + + template + friend int compare( + const version_core & a, const version_core & b); + + template + friend bool operator==( + const version_core & a, const version_core & b); + + template + friend bool operator<( + const version_core & a, const version_core & b); + + template + friend ::std::size_t hash(const version_core & value); + // tag::version_core-sym[] +}; +}} // namespace bfg::versioned +// end::version_core-sym[] + +namespace bfg { namespace versioned { +template +template +version_core::version_core(element_t a0, I... an) +{ + element_t args[] = { a0, an... }; + ::std::size_t i = 0; + for (auto a : args) number[i++] = a; +} + +template +typename version_core::element_t & version_core::at(::std::size_t i) +{ + if (i >= element_c) + throw ::std::out_of_range( + "No such component: " + detail::to_string_10(i)); + return number[i]; +} + +template +const typename version_core::element_t & version_core::at( + ::std::size_t i) const +{ + if (i >= element_c) + throw ::std::out_of_range( + "No such component: " + detail::to_string_10(i)); + return number[i]; +} + +template +from_chars_result from_chars( + const char * first, const char * last, version_core & value) +{ + if (first == last || !first || !last) + return from_chars_result { first, ::std::errc::invalid_argument }; + + from_chars_result result { first, ::std::errc(0) }; + typename version_core::element_t + number[version_core::element_c] { }; + + for (::std::size_t i = 0; i < version_core::element_c; ++i) + { + result.ptr = detail::from_chars_10(result.ptr, last, number[i]); + if (result.ptr == last || *result.ptr != '.') break; + result.ptr += 1; + } + + for (::std::size_t i = 0; i < version_core::element_c; ++i) + value.number[i] = number[i]; + + return result; +} + +template +::std::string to_string(const version_core & value) +{ + ::std::string result; + for (::std::size_t i = version_core::element_c; i > 0; --i) + { + ::std::size_t n = i - 1; + if (n == 0) + result = detail::to_string_10(value.number[n]) + result; + else if (value.number[n] > 0) + result = "." + detail::to_string_10(value.number[n]) + result; + } + return result; +} + +template +int compare(const version_core & a, const version_core & b) +{ + for (::std::size_t i = 0; i < version_core::element_c + && i < version_core::element_c; + ++i) + { + auto c = a.number[i] + - static_cast::element_t>( + b.number[i]); + if (c != 0) return int(c); + } + return int(version_core::element_c) + - int(version_core::element_c); +} + +template +bool operator==(const version_core & a, const version_core & b) +{ + return compare(a, b) == 0; +} + +template +bool operator<(const version_core & a, const version_core & b) +{ + return compare(a, b) < 0; +} + +template +::std::size_t hash(const version_core & value) +{ + ::std::hash::element_t> h { }; + ::std::size_t result = 0; + for (auto n : value.number) result = detail::hash_combine(result, h(n)); + return result; +} + +template +const typename version_core::element_t & get( + const version_core & value) +{ + return value.at(I); +} +}} // namespace bfg::versioned + +// +namespace std { +template +struct hash<::bfg::versioned::version_core> +{ + ::std::size_t operator()( + const ::bfg::versioned::version_core & value) const noexcept + { + return ::bfg::versioned::hash(value); + } +}; + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wmismatched-tags" +#endif + +template +struct tuple_size<::bfg::versioned::version_core> + : integral_constant<::std::size_t, + ::bfg::versioned::version_core::element_c> +{ }; + +template +struct tuple_element> +{ + using type = const typename ::bfg::versioned::version_core::element_t; +}; + +#if defined(__clang__) +# pragma clang diagnostic pop +#endif +} // namespace std + +// tag::version_tag-syn[] +namespace bfg { namespace versioned { +class version_tag +{ + public: + // types + using element_t = ::std::string; + using range_element_t = ::std::tuple; + + // construction, destruction, and assignment + version_tag() = default; + version_tag(version_tag &&) = default; + version_tag(const version_tag &) = default; + version_tag & operator=(const version_tag &) = default; + template + version_tag(const char * a0, S... an); + template + version_tag(const ::std::string & a0, S... an); + + // observers + element_t at(::std::size_t i) const; + range_element_t range_at(::std::size_t i) const; + bool empty() const; + ::std::size_t size() const; + // end::version_tag-syn[] + + private: + // The info contains the part that is parsed from any original data. + ::std::string info_; + // The parts hold the index to the end of each parsed segment. + ::std::vector<::std::size_t> parts_; + + friend from_chars_result from_chars( + const char * first, const char * last, version_tag & value); + + friend ::std::string to_string(const version_tag & value); + + friend int compare(const version_tag & a, const version_tag & b); + + friend bool operator==(const version_tag & a, const version_tag & b); + friend bool operator<(const version_tag & a, const version_tag & b); + + friend ::std::size_t hash(const version_tag & value); + // tag::version_tag-syn[] +}; +}} // namespace bfg::versioned +// end::version_tag-syn[] + +namespace bfg { namespace versioned { +template +version_tag::version_tag(const char * a0, S... an) +{ + info_ += a0; + parts_.emplace_back(info_.size()); + const char * args[] = { an... }; + for (auto a : args) + { + info_ += "."; + info_ += a; + parts_.emplace_back(info_.size()); + } +} + +template +version_tag::version_tag(const ::std::string & a0, S... an) +{ + info_ += a0; + parts_.emplace_back(info_.size()); + const char * args[] = { an... }; + for (auto a : args) + { + info_ += "."; + info_ += a; + parts_.emplace_back(info_.size()); + } +} + +inline version_tag::element_t version_tag::at(::std::size_t i) const +{ + auto r = this->range_at(i); + return ::std::string(::std::get<0>(r), ::std::get<1>(r)); +} + +inline version_tag::range_element_t version_tag::range_at(::std::size_t i) const +{ + if (i >= parts_.size()) + throw ::std::out_of_range( + "No such component: " + detail::to_string_10(i)); + return ::std::make_tuple(info_.c_str() + (i == 0 ? 0 : parts_[i - 1] + 1), + info_.c_str() + parts_[i]); +} + +inline bool version_tag::empty() const { return info_.empty(); } + +inline ::std::size_t version_tag::size() const { return parts_.size(); } + +inline from_chars_result from_chars( + const char * first, const char * last, version_tag & value) +{ + if (first == last) + return from_chars_result { first, ::std::errc::invalid_argument }; + + from_chars_result result { }; + ::std::vector<::std::size_t> parts; + + for (auto begin = first; begin < last;) + { + auto end = ::std::find_if(begin, last, + [](char c) { return c == '.' || !detail::is_ident(c); }); + if (end == first) + { + // No valid info parts found. Indicate an error, as nothing is + // not an allowed input. + result.ptr = end; + result.ec = ::std::errc::invalid_argument; + break; + } + else if (end == last || *end == '.') + { + // Found the EOS or a part separator. We have a part. And may + // have more. + parts.emplace_back(end - first); + begin = end + 1; + } + else if (!detail::is_ident(*end)) + { + // We parsed a valid part, but reached the end of the valid + // characters. + parts.emplace_back(end - first); + break; + } + else + { + // Should never happen. But check, and report an error if it + // does. + result.ptr = nullptr; + result.ec = ::std::errc::invalid_argument; + break; + } + } + + if (result.ec == ::std::errc { }) + { + result.ptr = first + parts.back(); + value.info_ = ::std::string(first, result.ptr); + value.parts_ = parts; + } + return result; +} + +inline ::std::string to_string(const version_tag & value) +{ + return value.info_; +} + +inline int compare(const version_tag & a, const version_tag & b) +{ + if (!a.empty() && b.empty()) return -1; + if (a.empty() && !b.empty()) return 1; + for (::std::size_t i = 0; i < a.size() && i < b.size(); ++i) + { + auto a_chars = a.range_at(i); + auto b_chars = b.range_at(i); + auto c + = detail::chars_cmp(::std::get<0>(a_chars), ::std::get<1>(a_chars), + ::std::get<0>(b_chars), ::std::get<1>(b_chars)); + if (c != 0) return c; + } + return int(a.size()) - int(b.size()); +} + +inline bool operator==(const version_tag & a, const version_tag & b) +{ + return compare(a, b) == 0; +} + +inline bool operator<(const version_tag & a, const version_tag & b) +{ + return compare(a, b) < 0; +} + +inline ::std::size_t hash(const version_tag & value) +{ + return ::std::hash { }(value.info_); +} +}} // namespace bfg::versioned + +namespace std { +template <> +struct hash<::bfg::versioned::version_tag> +{ + ::std::size_t operator()( + const ::bfg::versioned::version_tag & value) const noexcept + { + return ::bfg::versioned::hash(value); + } +}; +} // namespace std + +// tag::prerelease_version-syn[] +namespace bfg { namespace versioned { +template +class prerelease_version : public version_tag +{ + public: + // types + using number_element_t = Number; + + // construction, destruction, and assignment + using version_tag::version_tag; + + // observers + number_element_t number_at(::std::size_t i) const; + bool is_number_at(::std::size_t i) const; + // end::prerelease_version-syn[] + + private: + bool is_number_at(::std::size_t i, number_element_t & n) const + { + auto r = this->range_at(i); + const char * e + = detail::from_chars_10(::std::get<0>(r), ::std::get<1>(r), n); + return (e == ::std::get<1>(r)); + } + + template + friend int compare( + const prerelease_version & a, const prerelease_version & b); + + template + friend bool operator==( + const prerelease_version & a, const prerelease_version & b); + + template + friend bool operator<( + const prerelease_version & a, const prerelease_version & b); + + template + ::std::size_t hash(const prerelease_version & value); + // tag::prerelease_version-syn[] +}; +}} // namespace bfg::versioned +// end::prerelease_version-syn[] + +namespace bfg { namespace versioned { +template +typename prerelease_version::number_element_t prerelease_version< + N>::number_at(::std::size_t i) const +{ + number_element_t n { }; + if (!is_number_at(i, n)) + throw ::std::invalid_argument("Element is not a number."); + return n; +} + +template +bool prerelease_version::is_number_at(::std::size_t i) const +{ + number_element_t n { }; + return is_number_at(i, n); +} + +template +int compare(const prerelease_version & a, const prerelease_version & b) +{ + if (!a.empty() && b.empty()) return -1; + if (a.empty() && !b.empty()) return 1; + for (::std::size_t i = 0; i < a.size() && i < b.size(); ++i) + { + typename prerelease_version::number_element_t a_n { }; + bool a_is_num = a.is_number_at(i, a_n); + typename prerelease_version::number_element_t b_n { }; + bool b_is_num = b.is_number_at(i, b_n); + if (a_is_num && b_is_num) + { + auto b_e = static_cast< + typename prerelease_version::number_element_t>(b_n); + if (a_n < b_e) return -1; + if (b_e < a_n) return 1; + } + else if (a_is_num) + return -1; + else if (b_is_num) + return 1; + else + { + auto a_chars = a.range_at(i); + auto b_chars = b.range_at(i); + auto c = detail::chars_cmp(::std::get<0>(a_chars), + ::std::get<1>(a_chars), ::std::get<0>(b_chars), + ::std::get<1>(b_chars)); + if (c != 0) return c; + } + } + return int(a.size()) - int(b.size()); +} + +template +bool operator==( + const prerelease_version & a, const prerelease_version & b) +{ + return compare(a, b) == 0; +} + +template +bool operator<( + const prerelease_version & a, const prerelease_version & b) +{ + return compare(a, b) < 0; +} + +template +::std::size_t hash(const prerelease_version & value) +{ + return hash(static_cast(value)); +} +}} // namespace bfg::versioned + +namespace std { +template +struct hash<::bfg::versioned::prerelease_version> +{ + ::std::size_t operator()( + const ::bfg::versioned::prerelease_version & value) const noexcept + { + return ::bfg::versioned::hash(value); + } +}; +} // namespace std + +// tag::build_metadata-syn[] +namespace bfg { namespace versioned { +class build_metadata : public version_tag +{ + public: + // construction, destruction, and assignment + using version_tag::version_tag; +}; +}} // namespace bfg::versioned +// end::build_metadata-syn[] + +// tag::semver-syn[] +namespace bfg { namespace versioned { +template , + class Build = build_metadata> +class semver +{ + public: + // types + using version_t = version_core; + using prerelease_t = Prerelease; + using build_t = Build; + + // construction, destruction, and assignment + semver() = default; + semver(semver &&) = default; + semver(const semver &) = default; + semver(version_t, prerelease_t = { }, build_t = { }); + semver & operator=(semver &&) = default; + + // observers + const version_t & version() const; + version_t & version(); + const prerelease_t & prerelease() const; + prerelease_t & prerelease(); + const build_t & build() const; + build_t & build(); + typename version_t::element_t major() const; + typename version_t::element_t minor() const; + typename version_t::element_t patch() const; + + private: + version_t core_; // exposition only + prerelease_t pre_; // exposition only + build_t build_; // exposition only + // end::semver-syn[] + + template + friend from_chars_result from_chars( + const char * first, const char * last, semver & value); + + template + friend ::std::string to_string(const semver & value); + + template + friend int compare( + const semver & a, const semver & b); + + template + friend bool operator==( + const semver & a, const semver & b); + + template + friend bool operator<( + const semver & a, const semver & b); + + template + friend ::std::size_t hash(const semver & value); + // tag::semver-syn[] +}; +}} // namespace bfg::versioned +// end::semver-syn[] + +namespace bfg { namespace versioned { + +template +semver::semver(version_t v, prerelease_t p, build_t b) + : core_(v) + , pre_(p) + , build_(b) +{ } + +template +const typename semver::version_t & semver::version() const +{ + return core_; +} + +template +typename semver::version_t & semver::version() +{ + return core_; +} + +template +const typename semver::prerelease_t & semver::prerelease() + const +{ + return pre_; +} + +template +typename semver::prerelease_t & semver::prerelease() +{ + return pre_; +} + +template +const typename semver::build_t & semver::build() const +{ + return build_; +} + +template +typename semver::build_t & semver::build() +{ + return build_; +} + +template +typename semver::version_t::element_t semver::major() const +{ + return core_.at(0); +} + +template +typename semver::version_t::element_t semver::minor() const +{ + return core_.at(1); +} + +template +typename semver::version_t::element_t semver::patch() const +{ + return core_.at(2); +} + +template +from_chars_result from_chars( + const char * first, const char * last, semver & value) +{ + if (first == last) + return from_chars_result { first, ::std::errc::invalid_argument }; + + from_chars_result result { }; + + typename semver::version_t v; + { + result = from_chars(first, last, v); + if (result.ec != ::std::errc { }) return result; + first = result.ptr; + } + + typename semver::prerelease_t p; + if (first != last && *first == '-') + { + result = from_chars(first + 1, last, p); + if (result.ec != ::std::errc { }) return result; + first = result.ptr; + } + + typename semver::build_t b; + if (first != last && *first == '+') + { + result = from_chars(first + 1, last, b); + if (result.ec != ::std::errc { }) return result; + first = result.ptr; + } + + value.core_ = v; + value.pre_ = p; + value.build_ = b; + return result; +} + +template +::std::string to_string(const semver & value) +{ + ::std::string result = to_string(value.core_); + if (!value.pre_.empty()) result += "-" + to_string(value.pre_); + if (!value.build_.empty()) result += "+" + to_string(value.build_); + return result; +} + +template +int compare(const semver & a, const semver & b) +{ + auto x = compare(a.core_, b.core_); + if (x == 0) return compare(a.pre_, b.pre_); + return x; +} + +template +bool operator==(const semver & a, const semver & b) +{ + return compare(a, b) == 0; +} + +template +bool operator<(const semver & a, const semver & b) +{ + return compare(a, b) < 0; +} + +template +::std::size_t hash(const semver & value) +{ + return detail::hash_combine(::bfg::versioned::hash(value.core_), + ::bfg::versioned::hash(value.pre_), + ::bfg::versioned::hash(value.build_)); +} + +template <::std::size_t I, class N, class P, class B> +typename ::std::enable_if>::type>::type & + get(const semver & value) +{ + return value.version(); +} + +template <::std::size_t I, class N, class P, class B> +typename ::std::enable_if>::type>::type & + get(const semver & value) +{ + return value.prerelease(); +} + +template <::std::size_t I, class N, class P, class B> +typename ::std::enable_if>::type>::type & + get(const semver & value) +{ + return value.build(); +} + +}} // namespace bfg::versioned + +namespace std { + +template +struct hash<::bfg::versioned::semver> +{ + ::std::size_t operator()( + const ::bfg::versioned::semver & value) const noexcept + { + return ::bfg::versioned::hash(value); + } +}; + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wmismatched-tags" +#endif + +template +struct tuple_size<::bfg::versioned::semver> + : integral_constant +{ }; + +template +struct tuple_element> +{ + using version_t = typename ::bfg::versioned::semver::version_t; + using prerelease_t = + typename ::bfg::versioned::semver::prerelease_t; + using build_t = typename ::bfg::versioned::semver::build_t; + using type = const typename conditional::type>::type; +}; + +#if defined(__clang__) +# pragma clang diagnostic pop +#endif + +} // namespace std + +#endif diff --git a/src/engine/mod_version.cpp b/src/engine/mod_version.cpp index eb9a02dba..498f36877 100644 --- a/src/engine/mod_version.cpp +++ b/src/engine/mod_version.cpp @@ -6,6 +6,9 @@ Distributed under the Boost Software License, Version 1.0. #include "mod_version.h" +#include "ext_bfgroup_versioned.hpp" +#include "value.h" + namespace b2 { bool version_less(const std::vector & lhs, const std::vector & rhs) @@ -26,4 +29,31 @@ bool version_less(const std::vector & lhs, const std::vector & rhs) return false; } +bool semver_verify(value_ref v) +{ + bfg::versioned::semver<> v_semver; + auto v_string = v->as_string(); + auto v_first = v_string.cbegin(); + auto v_last = v_first + v_string.size(); + auto r = from_chars(v_first, v_last, v_semver); + return static_cast(r) && (r.ptr == v_last); +} + +list_ref semver_resolve(std::tuple ab) +{ + list_ref result; + bfg::versioned::semver<> a_semver; + bfg::versioned::semver<> b_semver; + auto a_string = std::get<0>(ab)->as_string(); + auto b_string = std::get<1>(ab)->as_string(); + auto a_result = from_chars( + a_string.cbegin(), a_string.cbegin() + a_string.size(), a_semver); + auto b_result = from_chars( + b_string.cbegin(), b_string.cbegin() + b_string.size(), b_semver); + if (a_result && b_result && a_semver.major() == b_semver.major() + && (a_semver < b_semver || a_semver == b_semver)) + result.push_back(std::get<1>(ab)); + return result; +} + } // namespace b2 diff --git a/src/engine/mod_version.h b/src/engine/mod_version.h index 36c288e5c..4d1c6c3f4 100644 --- a/src/engine/mod_version.h +++ b/src/engine/mod_version.h @@ -10,6 +10,8 @@ Distributed under the Boost Software License, Version 1.0. #include "config.h" #include "bind.h" +#include "lists.h" +#include "value.h" #include @@ -38,6 +40,23 @@ second version, `rhs`. end::reference[] */ bool version_less(const std::vector & lhs, const std::vector & rhs); +/* tag::reference[] + +== `b2::version_verify` + +==== +[horizontal] +Jam:: `rule semver-verify ( semver )` +{CPP}:: `bool semver_verify(value_ref semver);` +==== + +Returns `true` if the given `semver` value is a fully valid semantic version. + +end::reference[] */ +bool semver_verify(value_ref semver); + +list_ref semver_resolve(std::tuple ab); + struct version_module : b2::bind::module_ { const char * module_name = "version"; @@ -45,8 +64,9 @@ struct version_module : b2::bind::module_ template void def(Binder & binder) { - binder.def( - &b2::version_less, "version-less", "lhs" * _1n | "rhs" * _1n); + binder.def(&b2::version_less, "version-less", "lhs" * _1n | "rhs" * _1n); + binder.def(&b2::semver_verify, "semver-verify", "semver" * _1); + binder.def(&b2::semver_resolve, "semver-resolve", "a" * _1 + "b" * _1); } }; } // namespace b2 diff --git a/src/tools/features/version-feature.jam b/src/tools/features/version-feature.jam index 0a4e97f2c..0d18bdee3 100644 --- a/src/tools/features/version-feature.jam +++ b/src/tools/features/version-feature.jam @@ -16,4 +16,4 @@ feature. feature.feature version : - : free ; + : version ;