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.
This commit is contained in:
Rene Rivera
2026-06-24 00:17:31 -05:00
parent e3330f4e70
commit 50f853546c
12 changed files with 1278 additions and 20 deletions
+3
View File
@@ -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_
+10
View File
@@ -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.
+14
View File
@@ -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 <iostream>
int main()
{
std::cout << "Hello!\n";
}
// end::source[]
+15
View File
@@ -0,0 +1,15 @@
lib ext : : <name>ext10 <version>1.0 ;
lib ext : : <name>ext12 <version>1.2 ;
lib ext : : <name>ext20 <version>2.0 ;
explicit ext ;
exe hello : hello.cpp ext/<version>2 : <version>2.0 ;
exe hello : hello.cpp ext/<version>1 : <version>1.1 ;
explicit hello ;
explicit
[ exe hello20 : hello.cpp ext/<version>2 : <version>2.0 ]
[ exe hello11 : hello.cpp ext/<version>1.1 : <version>1.1 ]
[ exe hello10 : hello.cpp ext/<version>1.0 : <version>1.0 ] ;
alias all : hello/<version>2 hello/<version>1 hello20 hello11 hello10 ;
+57 -10
View File
@@ -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 <variant>debug : <define>_DEBUG <optimization>off ;
compose <variant>release : <define>NDEBUG <optimization>on ;
@@ -1411,6 +1451,13 @@ rule __test__ ( )
}
catch \"digital_mars\" is not a known value of feature <toolset> ;
validate-value-string <version> 1.2.3 ;
try ;
{
validate-value-string <version> a.b.c ;
}
catch \"a.b.c\" is not a valid semantic version for feature \"<version>\" ;
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 ;
{
+114 -3
View File
@@ -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 <variant>debug : <define>_DEBUG <optimization>off ;
feature.compose <variant>release : <define>NDEBUG <optimization>on ;
@@ -931,6 +1026,13 @@ rule __test__ ( )
: refine <toolset>gcc:<define>foo : <toolset>gcc:<define>bar
: $(test-space) ;
assert.result-set-equal <toolset>gcc <version>2.1
: refine <toolset>gcc <version>2.0
: <version>2.1 ;
assert.result-set-equal <toolset>gcc <version>3.0
: refine <toolset>gcc
: <version>3.0 ;
assert.result
: evaluate-conditionals-in-context
<variant>release,<rtti>off:<define>MY_RELEASE
@@ -980,6 +1082,11 @@ rule __test__ ( )
validate value : $(test-space) ;
catch \"value\" is not an implicit feature value ;
validate <version>1.2.3 ;
try ;
validate <version>a.b.c ;
catch \"a.b.c\" is not a valid semantic version for feature \"<version>\" ;
assert.result-set-equal <rtti>on
: remove free implicit : <toolset>gcc <define>foo <rtti>on : $(test-space) ;
@@ -1027,5 +1134,9 @@ rule __test__ ( )
: translate <toolset>gcc,!<rtti>off:<define>HELLO
: project-id : project-location : context-module ;
assert.true next-is-match <version>1.1 : <version>1.2 ;
assert.false next-is-match <version>1.1 : <version>2.0 ;
assert.false next-is-match <version>1.1 : <version>1.0 ;
feature.finish-test property-test-temp ;
}
+41 -4
View File
@@ -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
+7
View File
@@ -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 ;
}
+964
View File
@@ -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 <algorithm>
#include <limits>
#include <system_error>
#include <tuple>
#include <type_traits>
#include <vector>
#if defined(__cpp_lib_to_chars)
# include <charconv>
#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 <typename I>
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 <typename N, typename U>
constexpr N masked_max(U value)
{
return value & static_cast<U>(::std::numeric_limits<N>::max());
}
// Simple hash values combine.
template <class T, class... N>
T hash_combine(T seed, N... an)
{
T args[] = { an... };
// Init.
T result = seed;
// Combine.
for (auto a : args)
{
result ^= a + masked_max<T>(3772387269305686495)
+ (result << (30 * 2 / sizeof(T)))
+ (result >> (13 * 2 / sizeof(T)));
}
// Mix.
result ^= (result >> (16 * 2 / sizeof(T)));
result *= masked_max<T>(448100074733706);
result ^= (result << (8 * 2 / sizeof(T)));
result *= masked_max<T>(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 Number, ::std::size_t Count = 3>
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 <class... I>
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<Number>::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 <class N, ::std::size_t C>
friend from_chars_result from_chars(
const char * first, const char * last, version_core<N, C> & value);
template <class N, ::std::size_t C>
friend ::std::string to_string(const version_core<N, C> & value);
template <class N0, ::std::size_t C0, class N1, ::std::size_t C1>
friend int compare(
const version_core<N0, C0> & a, const version_core<N1, C1> & b);
template <class N0, ::std::size_t C0, class N1, ::std::size_t C1>
friend bool operator==(
const version_core<N0, C0> & a, const version_core<N1, C1> & b);
template <class N0, ::std::size_t C0, class N1, ::std::size_t C1>
friend bool operator<(
const version_core<N0, C0> & a, const version_core<N1, C1> & b);
template <class N, ::std::size_t C>
friend ::std::size_t hash(const version_core<N, C> & value);
// tag::version_core-sym[]
};
}} // namespace bfg::versioned
// end::version_core-sym[]
namespace bfg { namespace versioned {
template <class N, ::std::size_t C>
template <class... I>
version_core<N, C>::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 <class N, ::std::size_t C>
typename version_core<N, C>::element_t & version_core<N, C>::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 <class N, ::std::size_t C>
const typename version_core<N, C>::element_t & version_core<N, C>::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 <class N, ::std::size_t C>
from_chars_result from_chars(
const char * first, const char * last, version_core<N, C> & 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<N, C>::element_t
number[version_core<N, C>::element_c] { };
for (::std::size_t i = 0; i < version_core<N, C>::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<N, C>::element_c; ++i)
value.number[i] = number[i];
return result;
}
template <class N, ::std::size_t C>
::std::string to_string(const version_core<N, C> & value)
{
::std::string result;
for (::std::size_t i = version_core<N, C>::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 <class N0, ::std::size_t C0, class N1, ::std::size_t C1>
int compare(const version_core<N0, C0> & a, const version_core<N1, C1> & b)
{
for (::std::size_t i = 0; i < version_core<N0, C0>::element_c
&& i < version_core<N1, C1>::element_c;
++i)
{
auto c = a.number[i]
- static_cast<typename version_core<N0, C0>::element_t>(
b.number[i]);
if (c != 0) return int(c);
}
return int(version_core<N0, C0>::element_c)
- int(version_core<N1, C1>::element_c);
}
template <class N0, ::std::size_t C0, class N1, ::std::size_t C1>
bool operator==(const version_core<N0, C0> & a, const version_core<N1, C1> & b)
{
return compare(a, b) == 0;
}
template <class N0, ::std::size_t C0, class N1, ::std::size_t C1>
bool operator<(const version_core<N0, C0> & a, const version_core<N1, C1> & b)
{
return compare(a, b) < 0;
}
template <class N, ::std::size_t C>
::std::size_t hash(const version_core<N, C> & value)
{
::std::hash<typename version_core<N, C>::element_t> h { };
::std::size_t result = 0;
for (auto n : value.number) result = detail::hash_combine(result, h(n));
return result;
}
template <size_t I, class N, ::std::size_t C>
const typename version_core<N, C>::element_t & get(
const version_core<N, C> & value)
{
return value.at(I);
}
}} // namespace bfg::versioned
//
namespace std {
template <class N, ::std::size_t C>
struct hash<::bfg::versioned::version_core<N, C>>
{
::std::size_t operator()(
const ::bfg::versioned::version_core<N, C> & value) const noexcept
{
return ::bfg::versioned::hash(value);
}
};
#if defined(__clang__)
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wmismatched-tags"
#endif
template <class N, ::std::size_t C>
struct tuple_size<::bfg::versioned::version_core<N, C>>
: integral_constant<::std::size_t,
::bfg::versioned::version_core<N, C>::element_c>
{ };
template <size_t I, class N, ::std::size_t C>
struct tuple_element<I, ::bfg::versioned::version_core<N, C>>
{
using type = const typename ::bfg::versioned::version_core<N, C>::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<const char *, const char *>;
// 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 <class... S>
version_tag(const char * a0, S... an);
template <class... S>
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 <class... S>
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 <class... S>
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<decltype(value.info_)> { }(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 Number>
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 <class N0, class N1>
friend int compare(
const prerelease_version<N0> & a, const prerelease_version<N1> & b);
template <class N0, class N1>
friend bool operator==(
const prerelease_version<N0> & a, const prerelease_version<N1> & b);
template <class N0, class N1>
friend bool operator<(
const prerelease_version<N0> & a, const prerelease_version<N1> & b);
template <class N>
::std::size_t hash(const prerelease_version<N> & value);
// tag::prerelease_version-syn[]
};
}} // namespace bfg::versioned
// end::prerelease_version-syn[]
namespace bfg { namespace versioned {
template <class N>
typename prerelease_version<N>::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 <class N>
bool prerelease_version<N>::is_number_at(::std::size_t i) const
{
number_element_t n { };
return is_number_at(i, n);
}
template <class N0, class N1>
int compare(const prerelease_version<N0> & a, const prerelease_version<N1> & 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<N0>::number_element_t a_n { };
bool a_is_num = a.is_number_at(i, a_n);
typename prerelease_version<N1>::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<N0>::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 <class N0, class N1>
bool operator==(
const prerelease_version<N0> & a, const prerelease_version<N1> & b)
{
return compare(a, b) == 0;
}
template <class N0, class N1>
bool operator<(
const prerelease_version<N0> & a, const prerelease_version<N1> & b)
{
return compare(a, b) < 0;
}
template <class N>
::std::size_t hash(const prerelease_version<N> & value)
{
return hash(static_cast<const version_tag &>(value));
}
}} // namespace bfg::versioned
namespace std {
template <class N>
struct hash<::bfg::versioned::prerelease_version<N>>
{
::std::size_t operator()(
const ::bfg::versioned::prerelease_version<N> & 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 Number = int,
class Prerelease = prerelease_version<Number>,
class Build = build_metadata>
class semver
{
public:
// types
using version_t = version_core<Number, 3>;
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 <class N, class P, class B>
friend from_chars_result from_chars(
const char * first, const char * last, semver<N, P, B> & value);
template <class N, class P, class B>
friend ::std::string to_string(const semver<N, P, B> & value);
template <class N0, class P0, class B0, class N1, class P1, class B1>
friend int compare(
const semver<N0, P0, B0> & a, const semver<N1, P1, B1> & b);
template <class N0, class P0, class B0, class N1, class P1, class B1>
friend bool operator==(
const semver<N0, P0, B0> & a, const semver<N1, P1, B1> & b);
template <class N0, class P0, class B0, class N1, class P1, class B1>
friend bool operator<(
const semver<N0, P0, B0> & a, const semver<N1, P1, B1> & b);
template <class N, class P, class B>
friend ::std::size_t hash(const semver<N, P, B> & value);
// tag::semver-syn[]
};
}} // namespace bfg::versioned
// end::semver-syn[]
namespace bfg { namespace versioned {
template <class N, class P, class B>
semver<N, P, B>::semver(version_t v, prerelease_t p, build_t b)
: core_(v)
, pre_(p)
, build_(b)
{ }
template <class N, class P, class B>
const typename semver<N, P, B>::version_t & semver<N, P, B>::version() const
{
return core_;
}
template <class N, class P, class B>
typename semver<N, P, B>::version_t & semver<N, P, B>::version()
{
return core_;
}
template <class N, class P, class B>
const typename semver<N, P, B>::prerelease_t & semver<N, P, B>::prerelease()
const
{
return pre_;
}
template <class N, class P, class B>
typename semver<N, P, B>::prerelease_t & semver<N, P, B>::prerelease()
{
return pre_;
}
template <class N, class P, class B>
const typename semver<N, P, B>::build_t & semver<N, P, B>::build() const
{
return build_;
}
template <class N, class P, class B>
typename semver<N, P, B>::build_t & semver<N, P, B>::build()
{
return build_;
}
template <class N, class P, class B>
typename semver<N, P, B>::version_t::element_t semver<N, P, B>::major() const
{
return core_.at(0);
}
template <class N, class P, class B>
typename semver<N, P, B>::version_t::element_t semver<N, P, B>::minor() const
{
return core_.at(1);
}
template <class N, class P, class B>
typename semver<N, P, B>::version_t::element_t semver<N, P, B>::patch() const
{
return core_.at(2);
}
template <class N, class P, class B>
from_chars_result from_chars(
const char * first, const char * last, semver<N, P, B> & value)
{
if (first == last)
return from_chars_result { first, ::std::errc::invalid_argument };
from_chars_result result { };
typename semver<N, P, B>::version_t v;
{
result = from_chars(first, last, v);
if (result.ec != ::std::errc { }) return result;
first = result.ptr;
}
typename semver<N, P, B>::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<N, P, B>::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 <class N, class P, class B>
::std::string to_string(const semver<N, P, B> & 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 <class N0, class P0, class B0, class N1, class P1, class B1>
int compare(const semver<N0, P0, B0> & a, const semver<N1, P1, B1> & b)
{
auto x = compare(a.core_, b.core_);
if (x == 0) return compare(a.pre_, b.pre_);
return x;
}
template <class N0, class P0, class B0, class N1, class P1, class B1>
bool operator==(const semver<N0, P0, B0> & a, const semver<N1, P1, B1> & b)
{
return compare(a, b) == 0;
}
template <class N0, class P0, class B0, class N1, class P1, class B1>
bool operator<(const semver<N0, P0, B0> & a, const semver<N1, P1, B1> & b)
{
return compare(a, b) < 0;
}
template <class N, class P, class B>
::std::size_t hash(const semver<N, P, B> & 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<I == 0,
const typename ::std::tuple_element<I, semver<N, P, B>>::type>::type &
get(const semver<N, P, B> & value)
{
return value.version();
}
template <::std::size_t I, class N, class P, class B>
typename ::std::enable_if<I == 1,
const typename ::std::tuple_element<I, semver<N, P, B>>::type>::type &
get(const semver<N, P, B> & value)
{
return value.prerelease();
}
template <::std::size_t I, class N, class P, class B>
typename ::std::enable_if<I == 2,
const typename ::std::tuple_element<I, semver<N, P, B>>::type>::type &
get(const semver<N, P, B> & value)
{
return value.build();
}
}} // namespace bfg::versioned
namespace std {
template <class N, class P, class B>
struct hash<::bfg::versioned::semver<N, P, B>>
{
::std::size_t operator()(
const ::bfg::versioned::semver<N, P, B> & value) const noexcept
{
return ::bfg::versioned::hash(value);
}
};
#if defined(__clang__)
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wmismatched-tags"
#endif
template <class N, class P, class B>
struct tuple_size<::bfg::versioned::semver<N, P, B>>
: integral_constant<size_t, 3>
{ };
template <size_t I, class N, class P, class B>
struct tuple_element<I, ::bfg::versioned::semver<N, P, B>>
{
using version_t = typename ::bfg::versioned::semver<N, P, B>::version_t;
using prerelease_t =
typename ::bfg::versioned::semver<N, P, B>::prerelease_t;
using build_t = typename ::bfg::versioned::semver<N, P, B>::build_t;
using type = const typename conditional<I == 0,
version_t,
typename conditional<I == 1, prerelease_t, build_t>::type>::type;
};
#if defined(__clang__)
# pragma clang diagnostic pop
#endif
} // namespace std
#endif
+30
View File
@@ -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<int> & lhs, const std::vector<int> & rhs)
@@ -26,4 +29,31 @@ bool version_less(const std::vector<int> & lhs, const std::vector<int> & 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<bool>(r) && (r.ptr == v_last);
}
list_ref semver_resolve(std::tuple<value_ref, value_ref> 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
+22 -2
View File
@@ -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 <vector>
@@ -38,6 +40,23 @@ second version, `rhs`.
end::reference[] */
bool version_less(const std::vector<int> & lhs, const std::vector<int> & 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<value_ref, value_ref> ab);
struct version_module : b2::bind::module_<version_module>
{
const char * module_name = "version";
@@ -45,8 +64,9 @@ struct version_module : b2::bind::module_<version_module>
template <class Binder>
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
+1 -1
View File
@@ -16,4 +16,4 @@ feature.
feature.feature version
:
: free ;
: version ;