From c8775bf46f337e2dca4d161251eb89595aef4051 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Sat, 17 Nov 2018 23:39:15 +0300 Subject: Add support for builds manifest value --- libbpkg/manifest.cxx | 363 +++++++++++++++++++++++++++++++++++++- libbpkg/manifest.hxx | 122 +++++++++++++ tests/build-class-expr/buildfile | 8 + tests/build-class-expr/driver.cxx | 101 +++++++++++ tests/build-class-expr/testscript | 125 +++++++++++++ tests/manifest/testscript | 21 +++ 6 files changed, 737 insertions(+), 3 deletions(-) create mode 100644 tests/build-class-expr/buildfile create mode 100644 tests/build-class-expr/driver.cxx create mode 100644 tests/build-class-expr/testscript diff --git a/libbpkg/manifest.cxx b/libbpkg/manifest.cxx index e93f0cb..73d2311 100644 --- a/libbpkg/manifest.cxx +++ b/libbpkg/manifest.cxx @@ -17,8 +17,8 @@ #include #include #include -#include // casecmp(), lcase(), alpha(), - // digit(), xdigit() +#include // casecmp(), lcase(), alnum(), + // digit(), xdigit(), next_word() #include #include #include @@ -390,7 +390,7 @@ namespace bpkg } default: { - if (!digit (c) && !alpha (c)) + if (!alnum (c)) bad_arg ("alpha-numeric characters expected in a component"); } } @@ -889,6 +889,340 @@ namespace bpkg return o; } + // build_class_term + // + build_class_term:: + ~build_class_term () + { + if (simple) + name.~string (); + else + expr.~vector (); + } + + build_class_term:: + build_class_term (build_class_term&& t) + : operation (t.operation), + inverted (t.inverted), + simple (t.simple) + { + if (simple) + new (&name) string (move (t.name)); + else + new (&expr) vector (move (t.expr)); + } + + build_class_term:: + build_class_term (const build_class_term& t) + : operation (t.operation), + inverted (t.inverted), + simple (t.simple) + { + if (simple) + new (&name) string (t.name); + else + new (&expr) vector (t.expr); + } + + build_class_term& build_class_term:: + operator= (build_class_term&& t) + { + if (this != &t) + { + this->~build_class_term (); + + // Assume noexcept move-construction. + // + new (this) build_class_term (move (t)); + } + return *this; + } + + build_class_term& build_class_term:: + operator= (const build_class_term& t) + { + if (this != &t) + *this = build_class_term (t); // Reduce to move-assignment. + return *this; + } + + bool build_class_term:: + validate_name (const string& s) + { + if (s.empty ()) + throw invalid_argument ("empty class name"); + + size_t i (0); + char c (s[i++]); + + if (!(alnum (c) || c == '_')) + throw invalid_argument ( + "class name '" + s + "' starts with '" + c + "'"); + + for (; i != s.size (); ++i) + { + if (!(alnum (c = s[i]) || c == '+' || c == '-' || c == '_' || c == '.')) + throw invalid_argument ( + "class name '" + s + "' contains '" + c + "'"); + } + + return s[0] == '_'; + } + + // build_class_expr + // + // Parse the string representation of a space-separated, potentially empty + // build class expression. + // + // Calls itself recursively when a nested expression is encountered. In this + // case the second parameter points to the position at which the nested + // expression starts (right after the opening parenthesis). Updates the + // position to refer the nested expression end (right after the closing + // parenthesis). + // + static vector + parse_build_class_expr (const string& s, size_t* p = nullptr) + { + vector r; + + bool root (p == nullptr); + size_t e (0); + + if (root) + p = &e; + + size_t n; + for (size_t b (0); (n = next_word (s, b, *p)); ) + { + string t (s, b, n); + + // Check for the nested expression end. + // + if (t == ")") + { + if (root) + throw invalid_argument ("class term expected instead of ')'"); + + break; + } + + // Parse the term. + // + char op (t[0]); // Can be '\0'. + + if (op != '+') + { + if (op != '-' && op != '&') + throw invalid_argument ( + "class term '" + t + "' must start with '+', '-', or '&'"); + + // Only the root expression may start with a term having the '-' or + // '&' operation. + // + if (r.empty () && !root) + throw invalid_argument ( + "class term '" + t + "' must start with '+'"); + } + + bool inv (t[1] == '!'); // Can be '\0'. + string nm (t, inv ? 2 : 1); + + // Append the compound term. + // + if (nm == "(") + r.emplace_back (parse_build_class_expr (s, p), op, inv); + + // Append the simple term. + // + else + { + build_class_term::validate_name (nm); + r.emplace_back (move (nm), op, inv); + } + } + + // Verify that the nested expression is terminated with the closing + // parenthesis and is not empty. + // + if (!root) + { + // The zero-length of the last term means that we escaped the loop due + // to the eos. + // + if (n == 0) + throw invalid_argument ( + "nested class expression must be closed with ')'"); + + if (r.empty ()) + throw invalid_argument ("empty nested class expression"); + } + + return r; + } + + build_class_expr:: + build_class_expr (const std::string& s, std::string c) + : comment (move (c)) + { + using std::string; + + size_t eb (0); // Start of expression. + + // Parse the underlying classes until the expression term, ':', or eos is + // encountered. + // + for (size_t b (0); next_word (s, b, eb); ) + { + string nm (s, b, eb - b); + + if (nm[0] == '+' || nm[0] == '-' || nm[0] == '&') + { + // Expression must always be separated with ':' from the underlying + // classes. + // + if (!underlying_classes.empty ()) + throw invalid_argument ("class expression separator ':' expected"); + + eb = b; // Reposition to the term beginning. + break; + } + else if (nm == ":") + { + // The ':' separator must follow the underlying class set. + // + if (underlying_classes.empty ()) + throw invalid_argument ("underlying class set expected"); + + break; + } + + build_class_term::validate_name (nm); + underlying_classes.emplace_back (move (nm)); + } + + expr = parse_build_class_expr (eb == 0 ? s : string (s, eb)); + + // At least one of the expression or underlying class set should be + // present in the representation. + // + if (expr.empty () && underlying_classes.empty ()) + throw invalid_argument ("empty class expression"); + } + + build_class_expr:: + build_class_expr (const strings& cs, char op, std::string c) + : comment (move (c)) + { + vector r; + + for (const std::string& c: cs) + r.emplace_back (c, op == '-' ? '-' : '+', false /* inverse */); + + if (op == '&' && !r.empty ()) + { + build_class_term t (move (r), '&', false /* inverse */); + r = vector ({move (t)}); + } + + expr = move (r); + } + + // Return string representation of the build class expression. + // + static string + to_string (const vector& expr) + { + string r; + for (const build_class_term& t: expr) + { + if (!r.empty ()) + r += ' '; + + r += t.operation; + + if (t.inverted) + r += '!'; + + r += t.simple ? t.name : "( " + to_string (t.expr) + " )"; + } + + return r; + } + + string build_class_expr:: + string () const + { + using std::string; + + string r; + for (const string& c: underlying_classes) + { + if (!r.empty ()) + r += ' '; + + r += c; + } + + if (!expr.empty ()) + { + if (!r.empty ()) + r += " : " + to_string (expr); + else + r = to_string (expr); + } + + return r; + } + + // Match build configuration classes against an expression, updating the + // result. + // + static void + match_classes (const strings& cs, + const vector& expr, + bool& r) + { + for (const build_class_term& t: expr) + { + // Note that the '+' operation may only invert false and the '-' and '&' + // operations may only invert true (see below). So, let's optimize it a + // bit. + // + if ((t.operation == '+') == r) + continue; + + bool m; + + // We don't expect the class list to be long, so the linear search should + // be fine. + // + if (t.simple) + m = find (cs.begin (), cs.end (), t.name) != cs.end (); + else + { + m = false; + match_classes (cs, t.expr, m); + } + + if (t.inverted) + m = !m; + + switch (t.operation) + { + case '+': if (m) r = true; break; + case '-': if (m) r = false; break; + case '&': r &= m; break; + default: assert (false); + } + } + } + + void build_class_expr:: + match (const strings& cs, bool& r) const + { + match_classes (cs, expr, r); + } + // pkg_package_manifest // static void @@ -1229,6 +1563,26 @@ namespace bpkg m.requirements.push_back (move (ra)); } + else if (n == "builds") + { + try + { + auto vc (parser::split_comment (v)); + build_class_expr expr (vc.first, move (vc.second)); + + // Underlying build configuration class set may appear only in the + // first builds value. + // + if (!expr.underlying_classes.empty () && !m.builds.empty ()) + throw invalid_argument ("unexpected underlying class set"); + + m.builds.emplace_back (move (expr)); + } + catch (const invalid_argument& e) + { + bad_value (string ("invalid package builds: ") + e.what ()); + } + } else if (n == "build-include") { add_build_constraint (false, v); @@ -1541,6 +1895,9 @@ namespace bpkg : (r.buildtime ? "* " : "")) + serializer::merge_comment (concatenate (r, " | "), r.comment)); + for (const build_class_expr& e: m.builds) + s.next ("builds", serializer::merge_comment (e.string (), e.comment)); + for (const auto& c: m.build_constraints) s.next (c.exclusion ? "build-exclude" : "build-include", serializer::merge_comment (!c.target diff --git a/libbpkg/manifest.hxx b/libbpkg/manifest.hxx index 20c9caf..55e9d15 100644 --- a/libbpkg/manifest.hxx +++ b/libbpkg/manifest.hxx @@ -430,6 +430,127 @@ namespace bpkg return x |= y; } + // Build configuration class term. + // + class LIBBPKG_EXPORT build_class_term + { + public: + char operation; // '+', '-' or '&' + bool inverted; // Operation is followed by '!'. + bool simple; // Name if true, expr otherwise. + union + { + std::string name; // Class name. + std::vector expr; // Parenthesized expression. + }; + + // Create the simple term object (class name). + // + build_class_term (std::string n, char o, bool i) + : operation (o), inverted (i), simple (true), name (std::move (n)) {} + + // Create the compound term object (parenthesized expression). + // + build_class_term (std::vector e, char o, bool i) + : operation (o), inverted (i), simple (false), expr (std::move (e)) {} + + // Required by VC for some reason. + // + build_class_term () + : operation ('\0'), inverted (false), simple (true), name () {} + + build_class_term (build_class_term&&); + build_class_term (const build_class_term&); + build_class_term& operator= (build_class_term&&); + build_class_term& operator= (const build_class_term&); + + ~build_class_term (); + + // Check that the specified string is a valid class name, that is + // non-empty, containing only alpha-numeric characters, '_', '+', '-', '.' + // (except as the first character for the last three). Return true if the + // name is reserved (starts with '_'). Throw std::invalid_argument if + // invalid. + // + static bool + validate_name (const std::string&); + }; + + // Build configuration class expression. Includes comment and optional + // underlying set. + // + class LIBBPKG_EXPORT build_class_expr + { + public: + std::string comment; + strings underlying_classes; + std::vector expr; + + public: + build_class_expr () = default; + + // Parse the string representation of a space-separated build class + // expression, potentially prepended with a space-separated underlying + // build class set, in which case the expression can be empty. If both, + // underlying class set and expression are present, then they should be + // separated with the semicolon. Throw std::invalid_argument if the + // representation is invalid. Some expression examples: + // + // +gcc + // -msvc -clang + // default leagacy + // default leagacy : + // default leagacy : -msvc + // default leagacy : &gcc + // + build_class_expr (const std::string&, std::string comment); + + // Create the expression object from a class list (c1, c2, ...) using the + // specified operation (+/-/&) according to the following rules: + // + // + -> +c1 +c2 ... + // - -> -c1 -c2 ... + // & -> &( +c1 +c2 ... ) + // + // An empty class list results in an empty expression. + // + // Note: it is assumed that the class names are valid. + // + build_class_expr (const strings& classes, + char operation, + std::string comment); + + // Return the string representation of the build class expression, + // potentially prepended with the underlying class set. + // + std::string + string () const; + + // Match a build configuration that belongs to the specified list of + // classes against the expression. Either return or update the result (the + // latter allows to sequentially matching against a list of expressions). + // + // Note: the underlying class set doesn't affect the match in any way (it + // should have been used to pre-filter the set of build configurations). + // + void + match (const strings&, bool& result) const; + + bool + match (const strings& cs) const + { + bool r (false); + match (cs, r); + return r; + } + }; + + inline std::ostream& + operator<< (std::ostream& os, const build_class_expr& bce) + { + return os << bce.string (); + } + class LIBBPKG_EXPORT package_manifest { public: @@ -458,6 +579,7 @@ namespace bpkg butl::optional build_error_email; std::vector dependencies; std::vector requirements; + std::vector builds; std::vector build_constraints; // The following values are only valid in the manifest list (and only for diff --git a/tests/build-class-expr/buildfile b/tests/build-class-expr/buildfile new file mode 100644 index 0000000..8d63d65 --- /dev/null +++ b/tests/build-class-expr/buildfile @@ -0,0 +1,8 @@ +# file : tests/build-class-expr/buildfile +# copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +import libs = libbutl%lib{butl} +import libs += libbpkg%lib{bpkg} + +exe{driver}: {hxx cxx}{*} $libs testscript diff --git a/tests/build-class-expr/driver.cxx b/tests/build-class-expr/driver.cxx new file mode 100644 index 0000000..343c3b5 --- /dev/null +++ b/tests/build-class-expr/driver.cxx @@ -0,0 +1,101 @@ +// file : tests/build-class-expr/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include +#include +#include + +#include // eof(), operator<<(ostream, exception) +#include + +#include + +// Usages: +// +// argv[0] -p +// argv[0] [] +// +// Parse stdin lines as build configuration class expressions and print them +// or evaluate. +// +// In the first form print expressions to stdout, one per line. +// +// In the second form sequentially match the configuration classes passed as +// arguments against the expressions, updating the match result. If the first +// expression has an underlying class set specified, then transform the +// combined expression, making the underlying class set a starting set for the +// original expression and a restricting set, simultaneously. +// +// On error print the exception description to stderr and exit with the two +// status. Otherwise, if the combined expression doesn't match then exit with +// the one status. Otherwise, exit with zero status. +// +int +main (int argc, char* argv[]) +{ + using namespace std; + using namespace butl; + using namespace bpkg; + + using butl::optional; + + bool print (argc != 1 && argv[1] == string ("-p")); + + assert (!print || argc == 2); + + cin.exceptions (ios::badbit); + + strings cs; + + if (print) + cout.exceptions (ios::failbit | ios::badbit); + else + { + for (int i (1); i != argc; ++i) + cs.push_back (argv[i]); + } + + try + { + string s; + bool r (false); + optional underlying_cls; + + while (!eof (getline (cin, s))) + { + build_class_expr expr (s, "" /* comment */); + + if (print) + cout << expr << endl; + else + { + if (!underlying_cls) + { + underlying_cls = move (expr.underlying_classes); + + if (!underlying_cls->empty ()) + { + build_class_expr expr (*underlying_cls, '+', "" /* comment */); + expr.match (cs, r); + } + } + + expr.match (cs, r); + } + } + + if (underlying_cls && !underlying_cls->empty ()) + { + build_class_expr expr (*underlying_cls, '&', "" /* comment */); + expr.match (cs, r); + } + + return print || r ? 0 : 1; + } + catch (const exception& e) + { + cerr << e << endl; + return 2; + } +} diff --git a/tests/build-class-expr/testscript b/tests/build-class-expr/testscript new file mode 100644 index 0000000..cfa1400 --- /dev/null +++ b/tests/build-class-expr/testscript @@ -0,0 +1,125 @@ +# file : tests/build-class-expr/testscript +# copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +: valid +: +{ + test.options += -p + + : roundtrip + : + $* <>EOF + a + a b + a : -b + a : -b +c + +g + +gcc + +gcc-libc++ + +!gcc + +gcc -windows + +gcc &linux + +gcc &linux +( +msvc +windows &!optimized ) + +!windows &( +gcc +clang ) +( +windows &msvc ) + -windows + EOF + + $* <'a :' >'a' : no-expr +} + +: invalid +: +{ + test.options += -p + + $* <'' 2>"empty class expression" != 0 : empty1 + $* <'+( )' 2>"empty nested class expression" != 0 : empty2 + $* <':' 2>"underlying class set expected" != 0 : und-exprected1 + $* <': a' 2>"underlying class set expected" != 0 : und-exprected2 + $* <'~a' 2>"class name '~a' starts with '~'" != 0 : invalid-und + $* <'x : a' 2>"class term 'a' must start with '+', '-', or '&'" != 0 : no-op + $* <'+' 2>"empty class name" != 0 : no-name1 + $* <'+!' 2>"empty class name" != 0 : no-name2 + $* <'+a=b' 2>"class name 'a=b' contains '='" != 0 : invalid-char1 + $* <'+-a' 2>"class name '-a' starts with '-'" != 0 : invalid-char2 + $* <'+( +a' 2>"nested class expression must be closed with ')'" != 0 : not-closed + $* <'+a )' 2>"class term expected instead of ')'" != 0 : expected-name1 + $* <'+( +b ) )' 2>"class term expected instead of ')'" != 0 : expected-name2 + $* <'+( -a )' 2>"class term '-a' must start with '+'" != 0 : first-subtract +} + +: match +: +{ + : non-empty-class-list + : + { + test.arguments += a b c + + $* <'+a' + $* <'+!x' + $* <'+a +x' + $* <'+a +!x' + $* <'+a -x' + $* <'+a -!b' + $* <'+a -b +c' + $* <'+a &b' + $* <'+a &!y' + + $* <'+a +( +b )' + $* <'+a +( +x )' + $* <'+a +!( +b )' + $* <'+a +!( +x )' + $* <'+a &( +b )' + $* <'+a &!( +x )' + $* <'+a -( +x )' + $* <'+a -!( +b )' + $* <'+( +b -c +a )' + $* <'+a &( +b -c +a )' + $* <'+a &( +b -c +( +a -b ) +c )' + + $* <'a : +c' + $* <'a : -x' + } + + : empty-class-list + : + { + $* <'+!x' + } +} + +: mismatch +: +{ + : non-empty-class-list + : + { + test.arguments += a b c + + $* <'+!a' == 1 + $* <'+a -b' == 1 + $* <'+a -!x' == 1 + $* <'+a &x' == 1 + $* <'+a &!b' == 1 + + $* <'+a -( +b )' == 1 + $* <'+a -!( +x )' == 1 + $* <'+a &( +x )' == 1 + $* <'+a &!( +b )' == 1 + $* <'+a -c +( +x )' == 1 + $* <'+a -c +!( +b )' == 1 + $* <'+a -( +x +b )' == 1 + $* <'+a &( +b -c +( +a +b ) &x )' == 1 + + $* <'x : +a' == 1 + $* <'a : -c' == 1 + } + + : empty-class-list + : + { + $* <'+a' == 1 + } +} diff --git a/tests/manifest/testscript b/tests/manifest/testscript index 4f9a9a9..b77740f 100644 --- a/tests/manifest/testscript +++ b/tests/manifest/testscript @@ -127,6 +127,7 @@ requires: ? ; libc++ standard library if using Clang on Mac OS X. requires: zlib; Most Linux/UNIX systems already have one; or get it at\ www.zlib.net. + builds: +!windows &( +gcc +clang ) +( +windows &msvc ) build-include: linux* build-include: freebsd* build-exclude: *; Only supports Linux and FreeBSD. @@ -142,6 +143,8 @@ email: libbar-users@example.org build-email: depends: libbaz (1- 2-) | libbaz [3 4-) | libbaz (5 6] | libbaz [7 8] + builds: default legacy; Default and legacy. + builds: -windows; Not on Windows. build-exclude: *-msvc_14*/i?86-*; Linker crash. location: bar/libbar-3.4A.5+6.tbz sha256sum: d4b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -153,6 +156,7 @@ url: http://www.example.org/projects/libbar/ email: libbaz-users@example.org build-error-email: libbaz-issues@example.org; Email for libbaz issues. + builds: default experimental location: libbaz/libbaz-+2-3.4A.5+3.tar.gz sha256sum: b5b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 EOF @@ -204,6 +208,23 @@ fragment: ca602c2d46b0dca7a9ebc856871767b0ba6b74f3 EOF } + + : builds + : + { + : invalid + : + { + : empty + : + $* -pp <"stdin:4:9: error: invalid package builds: class expression separator ':' expected" != 0 + : 1 + sha256sum: a2b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + : + builds: default -gcc + EOI + } + } } : repositories -- cgit v1.1