aboutsummaryrefslogtreecommitdiff
path: root/libbpkg
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2021-12-08 22:46:50 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2022-01-14 14:29:01 +0300
commit6a68b1fd2161357a5905b875e9d59609a2b829b1 (patch)
tree52f925120b329928d1cdcfeab3e35cd48319928e /libbpkg
parentfc5da517f670a7ddf844527bfb3dbb9c9c2d482d (diff)
Add support for package dependency and requirement alternatives representation new syntax
Diffstat (limited to 'libbpkg')
-rw-r--r--libbpkg/buildfile-scanner.cxx38
-rw-r--r--libbpkg/buildfile-scanner.hxx96
-rw-r--r--libbpkg/buildfile-scanner.txx272
-rw-r--r--libbpkg/manifest.cxx1471
-rw-r--r--libbpkg/manifest.hxx248
5 files changed, 2014 insertions, 111 deletions
diff --git a/libbpkg/buildfile-scanner.cxx b/libbpkg/buildfile-scanner.cxx
new file mode 100644
index 0000000..ec73d83
--- /dev/null
+++ b/libbpkg/buildfile-scanner.cxx
@@ -0,0 +1,38 @@
+// file : libbpkg/buildfile-scanner.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <libbpkg/buildfile-scanner.hxx>
+
+#include <string>
+
+using namespace std;
+
+namespace bpkg
+{
+ // buildfile_scanning
+ //
+ static inline string
+ format (const string& n, uint64_t l, uint64_t c, const string& d)
+ {
+ string r;
+ if (!n.empty ())
+ {
+ r += n;
+ r += ':';
+ }
+
+ r += to_string (l);
+ r += ':';
+ r += to_string (c);
+ r += ": error: ";
+ r += d;
+ return r;
+ }
+
+ buildfile_scanning::
+ buildfile_scanning (const string& n, uint64_t l, uint64_t c, const string& d)
+ : runtime_error (format (n, l, c, d)),
+ name (n), line (l), column (c), description (d)
+ {
+ }
+}
diff --git a/libbpkg/buildfile-scanner.hxx b/libbpkg/buildfile-scanner.hxx
new file mode 100644
index 0000000..8b5f0d7
--- /dev/null
+++ b/libbpkg/buildfile-scanner.hxx
@@ -0,0 +1,96 @@
+// file : libbpkg/buildfile-scanner.hxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#ifndef LIBBPKG_BUILDFILE_SCANNER_HXX
+#define LIBBPKG_BUILDFILE_SCANNER_HXX
+
+#include <string>
+#include <cstdint> // uint64_t
+#include <cstddef> // size_t
+#include <stdexcept> // runtime_error
+
+#include <libbutl/char-scanner.hxx>
+
+#include <libbpkg/export.hxx>
+
+namespace bpkg
+{
+ // Scan buildfile fragments, respecting the single- and double-quoted
+ // character sequences, backslash-escaping, comments, evaluation
+ // contexts, and nested blocks.
+ //
+ class LIBBPKG_EXPORT buildfile_scanning: public std::runtime_error
+ {
+ public:
+ buildfile_scanning (const std::string& name,
+ std::uint64_t line,
+ std::uint64_t column,
+ const std::string& description);
+
+ std::string name;
+ std::uint64_t line;
+ std::uint64_t column;
+ std::string description;
+ };
+
+ template <typename V, std::size_t N>
+ class buildfile_scanner
+ {
+ public:
+ // Note that name is stored by shallow reference.
+ //
+ buildfile_scanner (butl::char_scanner<V, N>& s, const std::string& name)
+ : scan_ (s), name_ (name) {}
+
+ // Scan the buildfile line and return the scanned fragment. Optionally,
+ // specify an additional stop character. Leave the newline (or the stop
+ // character) in the stream. Throw buildfile_scanning on error
+ // (unterminated quoted sequence, etc).
+ //
+ std::string
+ scan_line (char stop = '\0');
+
+ // Scan the buildfile line until an unbalanced ')' character is encountered
+ // and return the scanned fragment, leaving ')' in the stream. Throw
+ // buildfile_scanning on error or if eos or newline is reached.
+ //
+ std::string
+ scan_eval ();
+
+ // Scan the buildfile block until an unbalanced block closing '}' character
+ // is encountered and return the scanned fragment. Throw buildfile_scanning
+ // on error or if eos is reached.
+ //
+ // Note that the block opening '{' and closing '}' characters are only
+ // considered as such, if they are the only characters on the line besides
+ // whitespaces and comments. Also note that the fragment terminating '}'
+ // line is consumed from the stream but is not included into the fragment.
+ //
+ std::string
+ scan_block ();
+
+ private:
+ using scanner = butl::char_scanner<V, N>;
+ using xchar = typename scanner::xchar;
+
+ xchar
+ peek ();
+
+ // Scan the buildfile line, saving the scanned characters into the
+ // specified string, leaving newline and the stop character, if specified,
+ // in the stream. Return '{' if this line is a block-opening curly brace,
+ // '}' if it is a block-closing curly brace, and '\0' otherwise.
+ //
+ char
+ scan_line (std::string& l, char stop = '\0');
+
+ private:
+ scanner& scan_;
+ const std::string& name_;
+ std::string ebuf_; // Error message buffer.
+ };
+}
+
+#include <libbpkg/buildfile-scanner.txx>
+
+#endif // LIBBPKG_BUILDFILE_SCANNER_HXX
diff --git a/libbpkg/buildfile-scanner.txx b/libbpkg/buildfile-scanner.txx
new file mode 100644
index 0000000..acd9037
--- /dev/null
+++ b/libbpkg/buildfile-scanner.txx
@@ -0,0 +1,272 @@
+// file : libbpkg/buildfile-scanner.txx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <cassert>
+
+#include <libbutl/optional.hxx>
+
+namespace bpkg
+{
+ template <typename V, std::size_t N>
+ typename buildfile_scanner<V, N>::xchar buildfile_scanner<V, N>::
+ peek ()
+ {
+ xchar c (scan_.peek (ebuf_));
+
+ if (scanner::invalid (c))
+ throw buildfile_scanning (name_, scan_.line, scan_.column, ebuf_);
+
+ return c;
+ }
+
+ template <typename V, std::size_t N>
+ char buildfile_scanner<V, N>::
+ scan_line (std::string& l, char stop)
+ {
+ using namespace std;
+
+ auto fail = [this] (const string& d)
+ {
+ throw buildfile_scanning (name_, scan_.line, scan_.column, d);
+ };
+
+ xchar c (peek ());
+
+ auto next = [&l, &c, this] ()
+ {
+ l += c;
+ scan_.get (c);
+ };
+
+ butl::optional<char> r;
+ bool double_quoted (false);
+
+ for (;
+ !scanner::eos (c) && (double_quoted || (c != '\n' && c != stop));
+ c = peek ())
+ {
+ switch (c)
+ {
+ case '\"':
+ {
+ // Start or finish scanning the double-quoted sequence.
+ //
+ double_quoted = !double_quoted;
+
+ r = '\0';
+ break;
+ }
+ case '\\':
+ {
+ next ();
+
+ c = peek ();
+
+ if (scanner::eos (c))
+ fail (double_quoted
+ ? "unterminated double-quoted sequence"
+ : "unterminated escape sequence");
+
+ r = '\0';
+ break;
+ }
+ case '(':
+ {
+ next ();
+
+ scan_line (l, ')');
+
+ c = peek ();
+
+ if (c != ')')
+ fail ("unterminated evaluation context");
+
+ next ();
+
+ r = '\0';
+ continue;
+ }
+ case '\'':
+ {
+ if (!double_quoted)
+ {
+ next ();
+
+ for (;;)
+ {
+ c = peek ();
+
+ if (scanner::eos (c))
+ fail ("unterminated single-quoted sequence");
+
+ next ();
+
+ if (c == '\'')
+ break;
+ }
+
+ r = '\0';
+ continue;
+ }
+
+ break;
+ }
+ case '#':
+ {
+ if (!double_quoted)
+ {
+ next ();
+
+ // See if this is a multi-line comment in the form:
+ //
+ /*
+ #\
+ ...
+ #\
+ */
+ auto ml = [&c, &next, this] () -> bool
+ {
+ if ((c = peek ()) == '\\')
+ {
+ next ();
+
+ if ((c = peek ()) == '\n' || scanner::eos (c))
+ return true;
+ }
+
+ return false;
+ };
+
+ if (ml ())
+ {
+ // Scan until we see the closing one.
+ //
+ for (;;)
+ {
+ if (c == '#' && ml ())
+ break;
+
+ if (scanner::eos (c = peek ()))
+ fail ("unterminated multi-line comment");
+
+ next ();
+ }
+ }
+ else
+ {
+ // Read until newline or eos.
+ //
+ for (; !scanner::eos (c) && c != '\n'; c = peek ())
+ next ();
+ }
+
+ continue;
+ }
+
+ break;
+ }
+ case '{':
+ case '}':
+ {
+ if (!double_quoted)
+ r = !r ? static_cast<char> (c) : '\0';
+
+ break;
+ }
+ default:
+ {
+ if (!double_quoted && c != ' ' && c != '\t')
+ r = '\0';
+
+ break;
+ }
+ }
+
+ next ();
+ }
+
+ if (double_quoted)
+ fail ("unterminated double-quoted sequence");
+
+ return r ? *r : '\0';
+ }
+
+ template <typename V, std::size_t N>
+ std::string buildfile_scanner<V, N>::
+ scan_line (char stop)
+ {
+ std::string r;
+ scan_line (r, stop);
+ return r;
+ }
+
+ template <typename V, std::size_t N>
+ std::string buildfile_scanner<V, N>::
+ scan_eval ()
+ {
+ std::string r;
+ scan_line (r, ')');
+
+ if (peek () != ')')
+ throw buildfile_scanning (name_,
+ scan_.line,
+ scan_.column,
+ "unterminated evaluation context");
+
+ return r;
+ }
+
+ template <typename V, std::size_t N>
+ std::string buildfile_scanner<V, N>::
+ scan_block ()
+ {
+ using namespace std;
+
+ auto fail = [this] (const string& d)
+ {
+ throw buildfile_scanning (name_, scan_.line, scan_.column, d);
+ };
+
+ string r;
+ for (size_t level (0);; )
+ {
+ if (scanner::eos (peek ()))
+ fail ("unterminated buildfile block");
+
+ size_t n (r.size ());
+ char bc (scan_line (r));
+
+ xchar c (peek ());
+
+ // Append the newline unless this is eos.
+ //
+ if (c == '\n')
+ {
+ r += c;
+ scan_.get (c);
+ }
+ else
+ assert (scanner::eos (c));
+
+ if (bc == '{')
+ {
+ ++level;
+ }
+ else if (bc == '}')
+ {
+ // If this is the fragment terminating line, then strip it from the
+ // fragment and bail out.
+ //
+ if (level == 0)
+ {
+ r.resize (n);
+ break;
+ }
+ else
+ --level;
+ }
+ }
+
+ return r;
+ }
+}
diff --git a/libbpkg/manifest.cxx b/libbpkg/manifest.cxx
index bbe8749..6a5ce25 100644
--- a/libbpkg/manifest.cxx
+++ b/libbpkg/manifest.cxx
@@ -18,15 +18,19 @@
#include <libbutl/url.hxx>
#include <libbutl/path.hxx>
+#include <libbutl/utf8.hxx>
#include <libbutl/base64.hxx>
#include <libbutl/utility.hxx> // icasecmp(), lcase(), alnum(),
// digit(), xdigit(), next_word()
#include <libbutl/filesystem.hxx> // dir_exist()
#include <libbutl/small-vector.hxx>
+#include <libbutl/char-scanner.hxx>
#include <libbutl/manifest-parser.hxx>
#include <libbutl/manifest-serializer.hxx>
#include <libbutl/standard-version.hxx>
+#include <libbpkg/buildfile-scanner.hxx>
+
using namespace std;
using namespace butl;
@@ -1123,141 +1127,1434 @@ namespace bpkg
// dependency_alternative
//
- dependency_alternative::
- dependency_alternative (const std::string& s)
+ string dependency_alternative::
+ string () const
{
- push_back (dependency (s)); // @@ DEP
+ std::string r (size () > 1 ? "{" : "");
+
+ bool first (true);
+ for (const dependency& d: *this)
+ {
+ if (!first)
+ r += ' ';
+ else
+ first = false;
+
+ r += d.string ();
+ }
+
+ if (size () > 1)
+ r += '}';
+
+ if (single_line ())
+ {
+ if (enable)
+ {
+ r += " ? (";
+ r += *enable;
+ r += ')';
+ }
+
+ if (reflect)
+ {
+ r += ' ';
+ r += *reflect;
+ }
+ }
+ else
+ {
+ // Add an extra newline between the clauses.
+ //
+ bool first (true);
+
+ r += "\n{";
+
+ if (enable)
+ {
+ first = false;
+
+ r += "\n enable (";
+ r += *enable;
+ r += ')';
+ }
+
+ if (prefer)
+ {
+ if (!first)
+ r += '\n';
+ else
+ first = false;
+
+ r += "\n prefer\n {\n";
+ r += *prefer;
+ r += " }";
+
+ assert (accept);
+
+ r += "\n\n accept (";
+ r += *accept;
+ r += ')';
+ }
+ else if (require)
+ {
+ if (!first)
+ r += '\n';
+ else
+ first = false;
+
+ r += "\n require\n {\n";
+ r += *require;
+ r += " }";
+ }
+
+ if (reflect)
+ {
+ if (!first)
+ r += '\n';
+ else
+ first = false;
+
+ r += "\n reflect\n {\n";
+ r += *reflect;
+ r += " }";
+ }
+
+ r += "\n}";
+ }
+
+ return r;
}
- string dependency_alternative::
- string () const
+ bool dependency_alternative::
+ single_line () const
{
- assert (size () == 1); // @@ DEP
- return front ().string ();
+ return !prefer &&
+ !require &&
+ (!reflect || reflect->find ('\n') == string::npos);
}
// dependency_alternatives
//
- dependency_alternatives::
- dependency_alternatives (const std::string& s)
+ class dependency_alternatives_lexer: public char_scanner<utf8_validator>
{
- using std::string;
+ public:
+ enum class token_type
+ {
+ eos,
+ newline,
+ word,
+ buildfile,
+
+ question, // ?
+
+ lcbrace, // {
+ rcbrace, // }
+
+ lparen, // (
+ rparen, // )
+
+ lsbrace, // [
+ rsbrace, // ]
- // Allow specifying ?* in any order.
+ equal, // ==
+ not_equal, // !=
+ less, // <
+ greater, // >
+ less_equal, // <=
+ greater_equal, // >=
+
+ tilde, // ~
+ caret, // ^
+
+ bit_or // |
+ };
+
+ struct token
+ {
+ token_type type;
+ std::string value;
+
+ uint64_t line;
+ uint64_t column;
+
+ std::string
+ string (bool diag = true) const;
+ };
+
+ public:
+ // Note that name is stored by shallow reference.
//
- size_t n (s.size ());
- size_t cond ((n > 0 && s[0] == '?') || (n > 1 && s[1] == '?') ? 1 : 0);
- size_t btim ((n > 0 && s[0] == '*') || (n > 1 && s[1] == '*') ? 1 : 0);
+ dependency_alternatives_lexer (istream& is,
+ const string& name,
+ uint64_t line,
+ uint64_t column)
+ : char_scanner (is,
+ utf8_validator (codepoint_types::graphic, U"\n\r\t"),
+ true /* crlf */,
+ line,
+ column),
+ name_ (name),
+ buildfile_scan_ (*this, name_) {}
+
+ // The following functions throw manifest_parsing on invalid UTF-8
+ // sequence.
+ //
+
+ // Peek the next non-whitespace character.
+ //
+ xchar
+ peek_char ();
+
+ // Extract next token (other than of the buildfile type) from the stream.
+ //
+ // Note that it is ok to call next() again after getting eos.
+ //
+ token
+ next ();
+
+ // The next_*() functions extract the buildfile token from the stream.
+ // Throw manifest_parsing on error (invalid buildfile fragment, etc).
+ //
+ // Note that they are just thin wrappers around the scan_*() functions
+ // (see buildfile-scanner.hxx for details).
+ //
+ token
+ next_eval ();
+
+ token
+ next_line (char stop);
+
+ token
+ next_block ();
+
+ private:
+ using base = char_scanner<utf8_validator>;
+
+ xchar
+ get ()
+ {
+ xchar c (base::get (ebuf_));
+
+ if (invalid (c))
+ throw parsing (name_, c.line, c.column, ebuf_);
+
+ return c;
+ }
+
+ void
+ get (const xchar& peeked)
+ {
+ base::get (peeked);
+ }
+
+ xchar
+ peek ()
+ {
+ xchar c (base::peek (ebuf_));
+
+ if (invalid (c))
+ throw parsing (name_, c.line, c.column, ebuf_);
+
+ return c;
+ }
+
+ void
+ skip_spaces ();
+
+ private:
+ const string& name_;
+
+ // Buffer for a get()/peek() potential error.
+ //
+ string ebuf_;
+
+ buildfile_scanner<utf8_validator, 1> buildfile_scan_;
+ };
+
+ dependency_alternatives_lexer::token dependency_alternatives_lexer::
+ next ()
+ {
+ using type = token_type;
+
+ skip_spaces ();
+
+ uint64_t ln (line);
+ uint64_t cl (column);
+
+ xchar c (get ());
+
+ auto make_token = [ln, cl] (type t, string v = string ())
+ {
+ return token {t, move (v), ln, cl};
+ };
+
+ if (eos (c))
+ return make_token (type::eos);
+
+ // NOTE: don't forget to also update the below separators list if changing
+ // anything here.
+ //
+ switch (c)
+ {
+ case '\n': return make_token (type::newline);
+ case '?': return make_token (type::question);
+ case '(': return make_token (type::lparen);
+ case ')': return make_token (type::rparen);
+ case '{': return make_token (type::lcbrace);
+ case '}': return make_token (type::rcbrace);
+ case '[': return make_token (type::lsbrace);
+ case ']': return make_token (type::rsbrace);
+
+ case '=':
+ case '!':
+ {
+ if ((peek ()) == '=')
+ {
+ get ();
+ return make_token (c == '=' ? type::equal : type::not_equal);
+ }
+ break;
+ }
+
+ case '<':
+ {
+ if ((c = peek ()) == '=')
+ {
+ get (c);
+ return make_token (type::less_equal);
+ }
+ else
+ return make_token (type::less);
+ }
+
+ case '>':
+ {
+ if ((c = peek ()) == '=')
+ {
+ get (c);
+ return make_token (type::greater_equal);
+ }
+ else
+ return make_token (type::greater);
+ }
+
+ case '~': return make_token (type::tilde);
+ case '^': return make_token (type::caret);
+
+ case '|': return make_token (type::bit_or);
+ }
+
+ // Otherwise it is a word.
+ //
+ // Starts with a non-whitespace character which has not been recognized as
+ // a part of some other token.
+ //
+ string r (1, c);
+
+ // Add subsequent characters until eos or separator is encountered.
+ //
+ const char* s (" \n\t?(){}[]=!<>~^|");
+ for (c = peek (); !eos (c) && strchr (s, c) == nullptr; c = peek ())
+ {
+ r += c;
+ get (c);
+ }
+
+ return make_token (type::word, move (r));
+ }
+
+ dependency_alternatives_lexer::token dependency_alternatives_lexer::
+ next_eval ()
+ {
+ skip_spaces ();
+
+ uint64_t ln (line);
+ uint64_t cl (column);
+
+ try
+ {
+ // Strip the trailing whitespaces.
+ //
+ return token {token_type::buildfile,
+ trim (buildfile_scan_.scan_eval ()),
+ ln,
+ cl};
+ }
+ catch (const buildfile_scanning& e)
+ {
+ throw parsing (e.name, e.line, e.column, e.description);
+ }
+ }
+
+ dependency_alternatives_lexer::token dependency_alternatives_lexer::
+ next_line (char stop)
+ {
+ skip_spaces ();
+
+ uint64_t ln (line);
+ uint64_t cl (column);
+
+ try
+ {
+ // Strip the trailing whitespaces.
+ //
+ return token {token_type::buildfile,
+ trim (buildfile_scan_.scan_line (stop)),
+ ln,
+ cl};
+ }
+ catch (const buildfile_scanning& e)
+ {
+ throw parsing (e.name, e.line, e.column, e.description);
+ }
+ }
+
+ dependency_alternatives_lexer::token dependency_alternatives_lexer::
+ next_block ()
+ {
+ uint64_t ln (line);
+ uint64_t cl (column);
+
+ try
+ {
+ // Don't trim the token value not to strip the potential block indenting
+ // on the first line.
+ //
+ return token {token_type::buildfile,
+ buildfile_scan_.scan_block (),
+ ln,
+ cl};
+ }
+ catch (const buildfile_scanning& e)
+ {
+ throw parsing (e.name, e.line, e.column, e.description);
+ }
+ }
+
+ dependency_alternatives_lexer::xchar dependency_alternatives_lexer::
+ peek_char ()
+ {
+ skip_spaces ();
+ return peek ();
+ }
+
+ void dependency_alternatives_lexer::
+ skip_spaces ()
+ {
+ xchar c (peek ());
+ bool start (c.column == 1);
+
+ for (; !eos (c); c = peek ())
+ {
+ switch (c)
+ {
+ case ' ':
+ case '\t': break;
+
+ case '\n':
+ {
+ // Skip empty lines.
+ //
+ if (start)
+ break;
+ }
+ // Fall through.
+ default: return;
+ }
+
+ get (c);
+ }
+ }
+
+ std::string dependency_alternatives_lexer::token::
+ string (bool diag) const
+ {
+ std::string q (diag ? "'" : "");
+
+ switch (type)
+ {
+ case token_type::eos: return diag ? "<end of stream>" : "";
+ case token_type::newline: return diag ? "<newline>" : "\n";
+ case token_type::word: return q + value + q;
+ case token_type::buildfile: return (diag
+ ? "<buildfile fragment>"
+ : value);
+ case token_type::question: return q + "?" + q;
+ case token_type::lparen: return q + "(" + q;
+ case token_type::rparen: return q + ")" + q;
+ case token_type::lcbrace: return q + "{" + q;
+ case token_type::rcbrace: return q + "}" + q;
+ case token_type::lsbrace: return q + "[" + q;
+ case token_type::rsbrace: return q + "]" + q;
+ case token_type::equal: return q + "==" + q;
+ case token_type::not_equal: return q + "!=" + q;
+ case token_type::less: return q + "<" + q;
+ case token_type::greater: return q + ">" + q;
+ case token_type::less_equal: return q + "<=" + q;
+ case token_type::greater_equal: return q + ">=" + q;
+ case token_type::tilde: return q + "~" + q;
+ case token_type::caret: return q + "^" + q;
+ case token_type::bit_or: return q + "|" + q;
+ }
+
+ assert (false); // Can't be here.
+ return "";
+ }
+
+ class dependency_alternatives_parser
+ {
+ public:
+
+ // If the requirements flavor is specified, then only enable and reflect
+ // clauses are allowed in the multi-line representation.
+ //
+ explicit
+ dependency_alternatives_parser (bool requirements = false)
+ : requirements_ (requirements) {}
+
+ // Throw manifest_parsing if representation is invalid.
+ //
+ void
+ parse (const package_name& dependent,
+ istream&,
+ const string& name,
+ uint64_t line,
+ uint64_t column,
+ dependency_alternatives&);
+
+ private:
+ using lexer = dependency_alternatives_lexer;
+ using token = lexer::token;
+ using token_type = lexer::token_type;
+
+ token_type
+ next (token&, token_type&);
+
+ token_type
+ next_eval (token&, token_type&);
+
+ token_type
+ next_line (token&, token_type&);
+
+ token_type
+ next_block (token&, token_type&);
+
+ // Receive the token/type from which it should start consuming and in
+ // return the token/type contains the first token that has not been
+ // consumed (normally eos, newline, or '|').
+ //
+ dependency_alternative
+ parse_alternative (token&, token_type&, bool first);
+
+ // Helpers.
+ //
+ // Throw manifest_parsing with the `<what> expected instead of <token>`
+ // description.
+ //
+ [[noreturn]] void
+ unexpected_token (const token&, string&& what);
+
+ bool requirements_;
+
+ const package_name* dependent_;
+ const string* name_;
+ lexer* lexer_;
+ dependency_alternatives* result_;
+ };
+
+ [[noreturn]] void dependency_alternatives_parser::
+ unexpected_token (const token& t, string&& w)
+ {
+ w += " expected";
+
+ // Don't add the `instead of...` part, if the unexpected token is eos or
+ // an empty word/buildfile.
+ //
+ if (t.type != token_type::eos &&
+ ((t.type != token_type::word && t.type != token_type::buildfile) ||
+ !t.value.empty ()))
+ {
+ w += " instead of ";
+ w += t.string ();
+ }
+
+ throw parsing (*name_, t.line, t.column, w);
+ }
+
+ void dependency_alternatives_parser::
+ parse (const package_name& dependent,
+ istream& is,
+ const string& name,
+ uint64_t line,
+ uint64_t column,
+ dependency_alternatives& result)
+ {
+ lexer lexer (is, name, line, column);
+
+ dependent_ = &dependent;
+ name_ = &name;
+ lexer_ = &lexer;
+ result_ = &result;
+
+ string what (requirements_ ? "requirement" : "dependency");
+
+ token t;
+ token_type tt;
+ next (t, tt);
+
+ // Make sure the representation is not empty, unless we are in the
+ // requirements mode. In the latter case fallback to creating a simple
+ // unconditional requirement. Note that it's the caller's responsibility
+ // to verify that a non-empty comment is specified in this case.
+ //
+ if (tt == token_type::eos)
+ {
+ if (!requirements_)
+ unexpected_token (t, what + " alternatives");
+
+ dependency_alternative da;
+ da.push_back (dependency ());
+
+ result_->push_back (move (da));
+ return;
+ }
+
+ for (bool first (true); tt != token_type::eos; )
+ {
+ dependency_alternative da (parse_alternative (t, tt, first));
+
+ // Skip newline after the dependency alternative, if present.
+ //
+ if (tt == token_type::newline)
+ next (t, tt);
+
+ // Make sure that the simple requirement has the only alternative in the
+ // representation.
+ //
+ if (requirements_ &&
+ da.size () == 1 &&
+ (da[0].name.empty () || (da.enable && da.enable->empty ())))
+ {
+ assert (first);
+
+ if (tt != token_type::eos)
+ throw parsing (*name_,
+ t.line,
+ t.column,
+ "end of simple requirement expected");
+ }
+ else
+ {
+ if (tt != token_type::eos && tt != token_type::bit_or)
+ unexpected_token (t, "end of " + what + " alternatives or '|'");
+ }
+
+ if (tt == token_type::bit_or)
+ {
+ next (t, tt);
+
+ // Skip newline after '|', if present.
+ //
+ if (tt == token_type::newline)
+ next (t, tt);
+
+ // Make sure '|' is not followed by eos.
+ //
+ if (tt == token_type::eos)
+ unexpected_token (t, move (what));
+ }
+
+ result_->push_back (move (da));
+
+ first = false;
+ }
+ }
+
+ dependency_alternative dependency_alternatives_parser::
+ parse_alternative (token& t, token_type& tt, bool first)
+ {
+ using type = token_type;
+
+ dependency_alternative r;
+
+ string what (requirements_ ? "requirement" : "dependency");
+ string config ("config." + dependent_->variable () + ".");
+
+ auto bad_token = [&t, this] (string&& what)
+ {
+ unexpected_token (t, move (what));
+ };
+
+ // Check that the current token type matches the expected one. Throw
+ // manifest_parsing if that's not the case. Use the expected token type
+ // name for the error description or the custom name, if specified. For
+ // the word and buildfile token types the custom name must be specified.
+ //
+ // Only move from the custom name argument if throwing exception.
+ //
+ auto expect_token = [&tt, &bad_token] (type et,
+ string&& what = string ())
+ {
+ assert ((et != type::word && et != type::buildfile) || !what.empty ());
+
+ if (tt != et)
+ {
+ if (what.empty ())
+ {
+ token e {et, "", 0, 0};
+ bad_token (e.string ());
+ }
+ else
+ bad_token (move (what));
+ }
+ };
+
+ // Parse dependencies.
+ //
+ // If the current token starts the version constraint, then read its
+ // tokens, rejoin them, and return the constraint string representation.
+ // Otherwise return nullopt.
+ //
+ // Note that normally the caller reads the dependency package name, reads
+ // the version constraint and, if present, appends it to the dependency,
+ // and then creates the dependency object with a single constructor call.
+ //
+ // Note: doesn't read token that follows the constraint.
+ //
+ auto try_scan_version_constraint =
+ [&t, &tt, &bad_token, &expect_token, this] () -> optional<string>
+ {
+ switch (t.type)
+ {
+ case type::lparen:
+ case type::lsbrace:
+ {
+ string r (t.string (false /* diag */));
+
+ next (t, tt);
+
+ expect_token (type::word, "version");
+
+ r += t.string (false /* diag */);
+ r += ' ';
+
+ next (t, tt);
+
+ expect_token (type::word, "version");
+
+ r += t.string (false /* diag */);
+
+ next (t, tt);
+
+ if (tt != type::rparen && tt != type::rsbrace)
+ bad_token ("')' or ']'");
+
+ r += t.string (false /* diag */);
+
+ return optional<string> (move (r));
+ }
+
+ case type::equal:
+ case type::not_equal:
+ case type::less:
+ case type::greater:
+ case type::less_equal:
+ case type::greater_equal:
+ case type::tilde:
+ case type::caret:
+ {
+ string r (t.string (false /* diag */));
+
+ next (t, tt);
+
+ expect_token (type::word, "version");
+
+ r += t.string (false /* diag */);
+
+ return optional<string> (move (r));
+ }
+
+ default: return nullopt;
+ }
+ };
+
+ // Parse the evaluation context including the left and right parenthesis
+ // and return the enclosed buildfile fragment.
+ //
+ // Note: no token is read after terminating ')'.
+ //
+ auto parse_eval = [&t, &tt, &expect_token, &bad_token, this] ()
+ {
+ next (t, tt);
+ expect_token (type::lparen);
+
+ next_eval (t, tt);
+
+ if (t.value.empty ())
+ bad_token ("condition");
+
+ string r (move (t.value));
+
+ next (t, tt);
+ expect_token (type::rparen);
+
+ return r;
+ };
+
+ const char* vccs ("([<>=!~^");
+
+ bool group (tt == type::lcbrace); // Dependency group.
+
+ if (group)
+ {
+ next (t, tt);
+
+ if (tt == type::rcbrace)
+ bad_token (move (what));
+
+ while (tt != type::rcbrace)
+ {
+ expect_token (type::word, what + " or '}'");
+
+ string d (move (t.value));
+ uint64_t dl (t.line);
+ uint64_t dc (t.column);
+
+ next (t, tt);
+
+ optional<string> vc (try_scan_version_constraint ());
+
+ if (vc)
+ {
+ d += *vc;
+
+ next (t, tt);
+ }
+
+ try
+ {
+ r.emplace_back (d);
+ }
+ catch (const invalid_argument& e)
+ {
+ throw parsing (*name_, dl, dc, e.what ());
+ }
+ }
+
+ // See if a common version constraint follows the dependency group and
+ // parse it if that's the case.
+ //
+ // Note that we need to be accurate not to consume what may end up to be
+ // a part of the reflect config.
+ //
+ lexer::xchar c (lexer_->peek_char ());
+
+ if (!lexer::eos (c) && strchr (vccs, c) != nullptr)
+ {
+ next (t, tt);
+
+ uint64_t vcl (t.line);
+ uint64_t vcc (t.column);
+
+ optional<string> vc (try_scan_version_constraint ());
+
+ if (!vc)
+ bad_token ("version constraint");
+
+ try
+ {
+ version_constraint c (*vc);
+
+ for (dependency& d: r)
+ {
+ if (!d.constraint)
+ d.constraint = c;
+ }
+ }
+ catch (const invalid_argument& e)
+ {
+ throw parsing (*name_,
+ vcl,
+ vcc,
+ string ("invalid version constraint: ") + e.what ());
+ }
+ }
+ }
+ else // Single dependency.
+ {
+ // If we see the question mark instead of a word in the requirements
+ // mode, then this is a simple requirement. In this case parse the
+ // evaluation context, if present, and bail out.
+ //
+ if (requirements_ && first && tt == type::question)
+ {
+ r.emplace_back (dependency ());
+ r.enable = lexer_->peek_char () == '(' ? parse_eval () : string ();
+
+ next (t, tt);
+ return r;
+ }
+
+ expect_token (type::word, move (what));
+
+ string d (move (t.value));
+ uint64_t dl (t.line);
+ uint64_t dc (t.column);
+
+ // See if a version constraint follows the dependency package name and
+ // parse it if that's the case.
+ //
+ lexer::xchar c (lexer_->peek_char ());
+
+ if (!lexer::eos (c) && strchr (vccs, c) != nullptr)
+ {
+ next (t, tt);
+
+ optional<string> vc (try_scan_version_constraint ());
+
+ if (!vc)
+ bad_token ("version constraint");
+
+ d += *vc;
+ }
+
+ try
+ {
+ r.emplace_back (d);
+ }
+ catch (const invalid_argument& e)
+ {
+ throw parsing (*name_, dl, dc, e.what ());
+ }
+ }
+
+ // See if there is an enable condition and parse it if that's the case.
+ //
+ {
+ lexer::xchar c (lexer_->peek_char ());
+
+ if (c == '?')
+ {
+ next (t, tt);
+ expect_token (type::question);
+
+ // If we don't see the opening parenthesis in the requirements mode,
+ // then this is a simple requirement. In this case set the enable
+ // condition to an empty string and bail out.
+ //
+ c = lexer_->peek_char ();
+
+ if (requirements_ && first && !group && c != '(')
+ {
+ r.enable = "";
+
+ next (t, tt);
+ return r;
+ }
+
+ r.enable = parse_eval ();
+ }
+ }
+
+ // See if there is a reflect config and parse it if that's the case.
+ //
+ {
+ lexer::xchar c (lexer_->peek_char ());
+
+ if (!lexer::eos (c) && strchr ("|\n", c) == nullptr)
+ {
+ next_line (t, tt);
+
+ string& l (t.value);
+ if (l.compare (0, config.size (), config) != 0)
+ bad_token (config + "* variable assignment");
+
+ r.reflect = move (l);
+ }
+ }
+
+ // If the dependencies are terminated with the newline, then check if the
+ // next token is '{'. If that's the case, then this is a multi-line
+ // representation.
+ //
+ next (t, tt);
+
+ if (tt == type::newline)
+ {
+ next (t, tt);
+
+ if (tt == type::lcbrace)
+ {
+ if (r.enable)
+ throw parsing (
+ *name_,
+ t.line,
+ t.column,
+ "multi-line " + what + " form with inline enable clause");
+
+ if (r.reflect)
+ throw parsing (
+ *name_,
+ t.line,
+ t.column,
+ "multi-line " + what + " form with inline reflect clause");
+
+ next (t, tt);
+ expect_token (type::newline);
+
+ // Parse the clauses.
+ //
+ for (next (t, tt); tt == type::word; next (t, tt))
+ {
+ auto fail_dup = [&t, this] ()
+ {
+ throw parsing (*name_, t.line, t.column, "duplicate clause");
+ };
+
+ auto fail_precede = [&t, this] (const char* what)
+ {
+ throw parsing (
+ *name_,
+ t.line,
+ t.column,
+ t.value + " clause should precede " + what + " clause");
+ };
+
+ auto fail_conflict = [&t, this] (const char* what)
+ {
+ throw parsing (
+ *name_,
+ t.line,
+ t.column,
+ t.value + " and " + what + " clauses are mutually exclusive");
+ };
+
+ auto fail_requirements = [&t, this] ()
+ {
+ throw parsing (
+ *name_,
+ t.line,
+ t.column,
+ t.value + " clause is not permitted for requirements");
+ };
+
+ // Parse the buildfile fragment block including the left and right
+ // curly braces (expected to be on the separate lines) and return
+ // the enclosed fragment.
+ //
+ auto parse_block = [&t, &tt, &expect_token, &bad_token, this] ()
+ {
+ next (t, tt);
+ expect_token (type::newline);
+
+ next (t, tt);
+ expect_token (type::lcbrace);
+
+ next (t, tt);
+ expect_token (type::newline);
+
+ next_block (t, tt);
+
+ string r (move (t.value));
+
+ // Fail if the buildfile fragment is empty.
+ //
+ if (r.find_first_not_of (" \t\n") == string::npos)
+ bad_token ("buildfile fragment");
+
+ return r;
+ };
+
+ const string& v (t.value);
+
+ if (v == "enable")
+ {
+ if (r.enable)
+ fail_dup ();
+
+ if (r.prefer)
+ fail_precede ("prefer");
+
+ if (r.require)
+ fail_precede ("require");
+
+ if (r.reflect)
+ fail_precede ("reflect");
+
+ r.enable = parse_eval ();
+
+ next (t, tt);
+ expect_token (type::newline);
+ }
+ else if (v == "prefer")
+ {
+ if (requirements_)
+ fail_requirements ();
+
+ if (r.prefer)
+ fail_dup ();
+
+ if (r.require)
+ fail_conflict ("require");
+
+ if (r.reflect)
+ fail_precede ("reflect");
+
+ r.prefer = parse_block ();
+
+ // The accept clause must follow, so parse it.
+ //
+ next (t, tt);
+
+ if (tt != type::word || t.value != "accept")
+ bad_token ("accept clause");
+
+ r.accept = parse_eval ();
+
+ next (t, tt);
+ expect_token (type::newline);
+ }
+ else if (v == "require")
+ {
+ if (requirements_)
+ fail_requirements ();
+
+ if (r.require)
+ fail_dup ();
+
+ if (r.prefer)
+ fail_conflict ("prefer");
+
+ if (r.reflect)
+ fail_precede ("reflect");
+
+ r.require = parse_block ();
+ }
+ else if (v == "reflect")
+ {
+ if (r.reflect)
+ fail_dup ();
+
+ r.reflect = parse_block ();
+ }
+ else if (v == "accept")
+ {
+ if (requirements_)
+ fail_requirements ();
+
+ throw parsing (*name_,
+ t.line,
+ t.column,
+ "accept clause should follow prefer clause");
+ }
+ else
+ bad_token (what + " alternative clause");
+ }
+
+ expect_token (type::rcbrace);
+ next (t, tt);
+ }
+ }
+
+ return r;
+ }
+
+ dependency_alternatives_parser::token_type dependency_alternatives_parser::
+ next (token& t, token_type& tt)
+ {
+ t = lexer_->next ();
+ tt = t.type;
+ return tt;
+ }
+
+ dependency_alternatives_parser::token_type dependency_alternatives_parser::
+ next_eval (token& t, token_type& tt)
+ {
+ t = lexer_->next_eval ();
+ tt = t.type;
+ return tt;
+ }
+
+ dependency_alternatives_parser::token_type dependency_alternatives_parser::
+ next_line (token& t, token_type& tt)
+ {
+ t = lexer_->next_line ('|');
+ tt = t.type;
+ return tt;
+ }
+
+ dependency_alternatives_parser::token_type dependency_alternatives_parser::
+ next_block (token& t, token_type& tt)
+ {
+ t = lexer_->next_block ();
+ tt = t.type;
+ return tt;
+ }
+
+ dependency_alternatives::
+ dependency_alternatives (const std::string& s,
+ const package_name& dependent,
+ const std::string& name,
+ uint64_t line,
+ uint64_t column)
+ {
+ using std::string;
auto vc (parser::split_comment (s));
- conditional = (cond != 0);
- buildtime = (btim != 0);
- comment = move (vc.second);
+ comment = move (vc.second);
- const string& vl (vc.first);
+ const string& v (vc.first);
+ buildtime = (v[0] == '*');
- string::const_iterator b (vl.begin ());
- string::const_iterator e (vl.end ());
+ string::const_iterator b (v.begin ());
+ string::const_iterator e (v.end ());
- if (conditional || buildtime)
+ if (buildtime)
{
- string::size_type p (vl.find_first_not_of (spaces, cond + btim));
+ string::size_type p (v.find_first_not_of (spaces, 1));
b = p == string::npos ? e : b + p;
}
- list_parser lp (b, e, '|');
- for (string lv (lp.next ()); !lv.empty (); lv = lp.next ())
- push_back (dependency_alternative (lv));
+ dependency_alternatives_parser p;
+ istringstream is (b == v.begin () ? v : string (b, e));
+ p.parse (dependent, is, name, line, column, *this);
}
string dependency_alternatives::
string () const
{
- std::string r (conditional
- ? (buildtime ? "?* " : "? ")
- : (buildtime ? "* " : ""));
+ std::string r (buildtime ? "* " : "");
- bool f (true);
+ const dependency_alternative* prev (nullptr);
for (const dependency_alternative& da: *this)
{
- r += (f ? (f = false, "") : " | ");
+ if (prev != nullptr)
+ {
+ r += prev->single_line () ? " |" : "\n|";
+ r += !da.single_line () || !prev->single_line () ? '\n' : ' ';
+ }
+
r += da.string ();
+ prev = &da;
}
return serializer::merge_comment (r, comment);
}
- // requirement_alternative
- //
- requirement_alternative::
- requirement_alternative (const std::string& s)
+ bool dependency_alternatives::
+ conditional () const
{
- push_back (s); // @@ DEP
+ for (const dependency_alternative& da: *this)
+ {
+ if (da.enable)
+ return true;
+ }
+
+ return false;
}
+ // requirement_alternative
+ //
string requirement_alternative::
string () const
{
- assert (size () == 1); // @@ DEP
- return front ();
+ using std::string;
+
+ string r (size () > 1 ? "{" : "");
+
+ bool first (true);
+ for (const string& rq: *this)
+ {
+ if (!first)
+ r += ' ';
+ else
+ first = false;
+
+ r += rq;
+ }
+
+ if (size () > 1)
+ r += '}';
+
+ if (single_line ())
+ {
+ if (enable)
+ {
+ if (!simple ())
+ {
+ r += " ? (";
+ r += *enable;
+ r += ')';
+ }
+ else
+ {
+ // Note that the (single) requirement id may or may not be empty.
+ //
+ if (!r.empty ())
+ r += ' ';
+
+ r += '?';
+
+ if (!enable->empty ())
+ {
+ r += " (";
+ r += *enable;
+ r += ')';
+ }
+ }
+ }
+
+ if (reflect)
+ {
+ r += ' ';
+ r += *reflect;
+ }
+ }
+ else
+ {
+ r += "\n{";
+
+ if (enable)
+ {
+ r += "\n enable (";
+ r += *enable;
+ r += ')';
+ }
+
+ if (reflect)
+ {
+ if (enable)
+ r += '\n';
+
+ r += "\n reflect\n {\n";
+ r += *reflect;
+ r += " }";
+ }
+
+ r += "\n}";
+ }
+
+ return r;
+ }
+
+ bool requirement_alternative::
+ single_line () const
+ {
+ return !reflect || reflect->find ('\n') == string::npos;
}
// requirement_alternatives
//
requirement_alternatives::
- requirement_alternatives (const std::string& v)
+ requirement_alternatives (const std::string& s,
+ const package_name& dependent,
+ const std::string& name,
+ uint64_t line,
+ uint64_t column)
{
using std::string;
- // Allow specifying ?* in any order.
- //
- size_t n (v.size ());
- size_t cond ((n > 0 && v[0] == '?') || (n > 1 && v[1] == '?') ? 1 : 0);
- size_t btim ((n > 0 && v[0] == '*') || (n > 1 && v[1] == '*') ? 1 : 0);
-
- auto vc (parser::split_comment (v));
+ auto vc (parser::split_comment (s));
- conditional = (cond != 0);
- buildtime = (btim != 0);
- comment = move (vc.second);
+ comment = move (vc.second);
- const string& vl (vc.first);
+ const string& v (vc.first);
+ buildtime = (v[0] == '*');
- string::const_iterator b (vl.begin ());
- string::const_iterator e (vl.end ());
+ string::const_iterator b (v.begin ());
+ string::const_iterator e (v.end ());
- if (conditional || buildtime)
+ if (buildtime)
{
- string::size_type p (vl.find_first_not_of (spaces, cond + btim));
+ string::size_type p (v.find_first_not_of (spaces, 1));
b = p == string::npos ? e : b + p;
}
- list_parser lp (b, e, '|');
- for (string lv (lp.next ()); !lv.empty (); lv = lp.next ())
- push_back (requirement_alternative (lv));
+ // We will use the dependency alternatives parser to parse the
+ // representation into a temporary dependency alternatives in the
+ // requirements mode. Then we will move the dependency alternatives into
+ // the requirement alternatives using the string representation of the
+ // dependencies.
+ //
+ dependency_alternatives_parser p (true /* requirements */);
+ istringstream is (b == v.begin () ? v : string (b, e));
- if (empty () && comment.empty ())
- throw invalid_argument ("empty package requirement specification");
+ dependency_alternatives das;
+ p.parse (dependent, is, name, line, column, das);
+
+ for (dependency_alternative& da: das)
+ {
+ requirement_alternative ra (move (da.enable), move (da.reflect));
+
+ // Also handle the simple requirement.
+ //
+ for (dependency& d: da)
+ ra.push_back (!d.name.empty () ? d.string () : string ());
+
+ push_back (move (ra));
+ }
+
+ // Make sure that the simple requirement is accompanied with a non-empty
+ // comment.
+ //
+ if (simple () && comment.empty ())
+ {
+ // Let's describe the following error cases differently:
+ //
+ // requires: ?
+ // requires:
+ //
+ throw parsing (name,
+ line,
+ column,
+ (back ().enable
+ ? "no comment specified for simple requirement"
+ : "requirement or comment expected"));
+ }
}
std::string requirement_alternatives::
string () const
{
- std::string r (conditional
- ? (buildtime ? "?* " : "? ")
- : (buildtime ? "* " : ""));
+ using std::string;
+
+ string r (buildtime ? "* " : "");
- bool f (true);
+ const requirement_alternative* prev (nullptr);
for (const requirement_alternative& ra: *this)
{
- r += (f ? (f = false, "") : " | ");
+ if (prev != nullptr)
+ {
+ r += prev->single_line () ? " |" : "\n|";
+ r += !ra.single_line () || !prev->single_line () ? '\n' : ' ';
+ }
+
r += ra.string ();
+ prev = &ra;
}
+ // For better readability separate the comment from the question mark for
+ // the simple requirement with an empty condition.
+ //
+ if (simple () && conditional () && back ().enable->empty ())
+ r += ' ';
+
return serializer::merge_comment (r, comment);
}
+ bool requirement_alternatives::
+ conditional () const
+ {
+ for (const requirement_alternative& ra: *this)
+ {
+ if (ra.enable)
+ return true;
+ }
+
+ return false;
+ }
+
// build_class_term
//
build_class_term::
@@ -1966,9 +3263,11 @@ namespace bpkg
// We will cache the depends and the test dependency manifest values to
// parse and, if requested, complete the version constraints later, after
- // the version value is parsed.
+ // the version value is parsed. We will also cache the requires values to
+ // parse them later, after the package name is parsed.
//
vector<name_value> dependencies;
+ vector<name_value> requirements;
small_vector<name_value, 1> tests;
// We will cache the description and its type values to validate them
@@ -2254,16 +3553,13 @@ namespace bpkg
m.license_alternatives.push_back (move (l));
}
+ else if (n == "depends")
+ {
+ dependencies.push_back (move (nv));
+ }
else if (n == "requires")
{
- try
- {
- m.requirements.push_back (requirement_alternatives (v));
- }
- catch (const invalid_argument& e)
- {
- bad_value (e.what ());
- }
+ requirements.push_back (move (nv));
}
else if (n == "builds")
{
@@ -2280,10 +3576,6 @@ namespace bpkg
m.build_constraints.push_back (
parse_build_constraint (nv, true /* exclusion */, name));
}
- else if (n == "depends")
- {
- dependencies.push_back (move (nv));
- }
// @@ TMP time to drop *-0.14.0?
//
else if (n == "tests" || n == "tests-0.14.0" ||
@@ -2467,8 +3759,8 @@ namespace bpkg
}
catch (const invalid_argument& e)
{
- throw invalid_argument (string ("invalid package constraint: ") +
- e.what ());
+ throw invalid_argument ("invalid package constraint '" +
+ dep.constraint->string () + "': " + e.what ());
}
return move (dep);
@@ -2480,16 +3772,15 @@ namespace bpkg
{
nv = move (d); // Restore as bad_value() uses its line/column.
- const string& v (nv.value);
-
// Parse dependency alternatives.
//
try
{
- dependency_alternatives das (v);
-
- if (das.empty ())
- bad_value ("empty package dependency specification");
+ dependency_alternatives das (nv.value,
+ m.name,
+ name,
+ nv.value_line,
+ nv.value_column);
for (dependency_alternative& da: das)
{
@@ -2505,6 +3796,18 @@ namespace bpkg
}
}
+ // Parse the requirements.
+ //
+ for (const name_value& r: requirements)
+ {
+ m.requirements.push_back (
+ requirement_alternatives (r.value,
+ m.name,
+ name,
+ r.value_line,
+ r.value_column));
+ }
+
// Parse the test dependencies.
//
for (name_value& t: tests)
diff --git a/libbpkg/manifest.hxx b/libbpkg/manifest.hxx
index d72a55e..499d64d 100644
--- a/libbpkg/manifest.hxx
+++ b/libbpkg/manifest.hxx
@@ -8,7 +8,7 @@
#include <string>
#include <vector>
#include <cassert>
-#include <cstdint> // uint16_t
+#include <cstdint> // uint*_t
#include <ostream>
#include <utility> // move()
#include <stdexcept> // logic_error
@@ -455,45 +455,175 @@ namespace bpkg
// depends
//
+ // The dependency alternative can be represented in one of the following
+ // forms.
+ //
+ // Single-line form:
+ //
+ // <dependencies> ['?' <enable-condition>] [<reflect-config>]
+ //
+ // <dependencies> = <dependency> |
+ // ({ <dependency> [ <dependency>]* } [<version-constraint>])
+ //
+ // <enable-condition> - buildfile evaluation context
+ // <reflect-config> - dependent package configuration variable assignment
+ //
+ // If the version constraint is specified after the dependency group, it
+ // only applies to dependencies without a version constraint.
+ //
+ // Multi-line forms:
+ //
+ // <dependencies>
+ // {
+ // enable <enable-condition>
+ //
+ // prefer
+ // {
+ // <prefer-config>
+ // }
+ //
+ // accept <accept-condition>
+ //
+ // reflect
+ // {
+ // <reflect-config>
+ // }
+ // }
+ // |
+ // <dependencies>
+ // {
+ // enable <enable-condition>
+ //
+ // require
+ // {
+ // <require-config>
+ // }
+ //
+ // reflect
+ // {
+ // <reflect-config>
+ // }
+ // }
+ //
+ // <prefer-config> - buildfile fragment containing dependency packages
+ // configuration variables assignments
+ //
+ // <accept-condition> - buildfile evaluation context
+ //
+ // <require-config> - buildfile fragment containing dependency packages
+ // configuration variables assignments
+ //
+ // <reflect-config> - buildfile fragment containing dependent package
+ // configuration variables assignments
+ //
+ // The dependency alternative is only considered by bpkg if the enable
+ // condition evaluates to true. If the enable clause is not specified, then
+ // it is always considered.
+ //
+ // The prefer clause specifies the preferred dependency package
+ // configuration that may potentially differ from the resulting
+ // configuration after the preferred/required configurations from all the
+ // selected dependency alternatives of all the dependent packages are
+ // "negotiated" by bpkg. The accept clause is used to verify that the
+ // resulting configuration is still acceptable for the dependent
+ // package. The accept clause must always be specified if the prefer clause
+ // is specified.
+ //
+ // The require clause specifies the only acceptable dependency packages
+ // configuration. It is a shortcut for specifying the prefer/accept clauses,
+ // where the accept condition verifies all the variable values assigned in
+ // the prefer clause. The require clause and the prefer/accept clause pair
+ // are optional and are mutually exclusive.
+ //
+ // The reflect clause specifies the dependent package configuration that
+ // should be used if the alternative is selected.
+ //
+ // All clauses are optional but at least one of them must be specified.
+ //
class dependency_alternative: public butl::small_vector<dependency, 1>
{
public:
butl::optional<std::string> enable;
+ butl::optional<std::string> reflect;
+ butl::optional<std::string> prefer;
+ butl::optional<std::string> accept;
+ butl::optional<std::string> require;
dependency_alternative () = default;
- dependency_alternative (butl::optional<std::string> e)
- : enable (std::move (e)) {}
-
- // Parse the dependency alternative string representation.
+ dependency_alternative (butl::optional<std::string> e,
+ butl::optional<std::string> r,
+ butl::optional<std::string> p,
+ butl::optional<std::string> a,
+ butl::optional<std::string> q)
+ : enable (std::move (e)),
+ reflect (std::move (r)),
+ prefer (std::move (p)),
+ accept (std::move (a)),
+ require (std::move (q)) {}
+
+ // Return the single-line representation if possible (the prefer and
+ // require clauses are absent and the reflect clause either absent or
+ // contains no newlines).
//
- explicit LIBBPKG_EXPORT
- dependency_alternative (const std::string&);
-
LIBBPKG_EXPORT std::string
string () const;
+
+ // Return true if the string() function would return the single-line
+ // representation.
+ //
+ LIBBPKG_EXPORT bool
+ single_line () const;
};
class dependency_alternatives:
public butl::small_vector<dependency_alternative, 1>
{
public:
- bool conditional;
bool buildtime;
std::string comment;
dependency_alternatives () = default;
- dependency_alternatives (bool d, bool b, std::string c)
- : conditional (d), buildtime (b), comment (std::move (c)) {}
+ dependency_alternatives (bool b, std::string c)
+ : buildtime (b), comment (std::move (c)) {}
- // Parse the dependency alternatives string representation in the
- // `[?][*] <dependency> [ '|' <dependency>]* [; <comment>]` form. Throw
- // std::invalid_argument if the value is invalid. @@ DEP @@ TMP update.
+ // Parse the dependency alternatives string representation in the form:
+ //
+ // [*] <alternative> [ '|' <alternative>]* [; <comment>]
+ //
+ // Where <alternative> can be single or multi-line (see above). Note also
+ // that leading `*` and trailing comment can be on separate lines. Throw
+ // manifest_parsing if the value is invalid.
+ //
+ // Use the dependent package name to verify that the reflect clauses in
+ // the dependency alternative representations refer to the dependent
+ // package configuration variable.
+ //
+ // Optionally, specify the stream name to use when creating the
+ // manifest_parsing exception. The start line and column arguments can be
+ // used to align the exception information with a containing stream. This
+ // is useful when the alternatives representation is a part of some larger
+ // text (manifest, etc).
+ //
+ // Note that semicolons inside alternatives must be escaped with the
+ // backslash (not to be treated as the start of a comment). Backslashes at
+ // the end of buildfile fragment lines need to also be escaped, if
+ // dependency alternatives representation comes from the manifest file
+ // (since trailing backslashes in manifest lines has special semantics).
//
explicit LIBBPKG_EXPORT
- dependency_alternatives (const std::string&);
+ dependency_alternatives (const std::string&,
+ const package_name& dependent,
+ const std::string& name = std::string (),
+ std::uint64_t line = 1,
+ std::uint64_t column = 1);
LIBBPKG_EXPORT std::string
string () const;
+
+ // Return true if there is a conditional alternative in the list.
+ //
+ LIBBPKG_EXPORT bool
+ conditional () const;
};
inline std::ostream&
@@ -504,45 +634,109 @@ namespace bpkg
// requires
//
+ // The requirement alternative string representation is similar to that of
+ // the dependency alternative with the following differences:
+ //
+ // - The requirement id (with or without version) can mean anything (but
+ // must still be a valid package name).
+ //
+ // - Only the enable and reflect clauses are permitted (reflect is allowed
+ // for potential future support of recognized requirement alternatives,
+ // for example, C++ standard).
+ //
+ // - The simplified representation syntax, where the comment carries the
+ // main information and thus is mandatory, is also supported (see
+ // requirement_alternatives for details). For example:
+ //
+ // requires: ; X11 libs.
+ // requires: ? ($windows) ; Only 64-bit.
+ // requires: ? ; Only 64-bit if on Windows.
+ // requires: x86_64 ? ; Only if on Windows.
+ //
class requirement_alternative: public butl::small_vector<std::string, 1>
{
public:
butl::optional<std::string> enable;
+ butl::optional<std::string> reflect;
requirement_alternative () = default;
- requirement_alternative (butl::optional<std::string> e)
- : enable (std::move (e)) {}
+ requirement_alternative (butl::optional<std::string> e,
+ butl::optional<std::string> r)
+ : enable (std::move (e)), reflect (std::move (r)) {}
- // Parse the requirement alternative string representation.
+ // Return the single-line representation if possible (the reflect clause
+ // either absent or contains no newlines).
//
- explicit LIBBPKG_EXPORT
- requirement_alternative (const std::string&);
-
LIBBPKG_EXPORT std::string
string () const;
+
+ // Return true if the string() function would return the single-line
+ // representation.
+ //
+ LIBBPKG_EXPORT bool
+ single_line () const;
+
+ // Return true if this is a single requirement with an empty id or an
+ // empty enable condition.
+ //
+ bool
+ simple () const
+ {
+ return size () == 1 && (back ().empty () || (enable && enable->empty ()));
+ }
};
class requirement_alternatives:
public butl::small_vector<requirement_alternative, 1>
{
public:
- bool conditional;
bool buildtime;
std::string comment;
requirement_alternatives () = default;
- requirement_alternatives (bool d, bool b, std::string c)
- : conditional (d), buildtime (b), comment (std::move (c)) {}
+ requirement_alternatives (bool b, std::string c)
+ : buildtime (b), comment (std::move (c)) {}
// Parse the requirement alternatives string representation in the
- // `[?] [<requirement> [ '|' <requirement>]*] [; <comment>]` form. Throw
- // std::invalid_argument if the value is invalid. @@ DEP @@ TMP update.
+ // following forms:
+ //
+ // [*] <alternative> [ '|' <alternative>]* [; <comment>]
+ // [*] [<requirement-id>] [? [<enable-condition>]] ; <comment>
+ //
+ // Parsing the second form ends up with a single alternative with a single
+ // potentially empty requirement id, potentially with an enable condition
+ // with potentially empty value (see examples above).
+ //
+ // Throw manifest_parsing if the value is invalid.
+ //
+ // Optionally, specify the stream name to use when creating the
+ // manifest_parsing exception. The start line and column arguments can be
+ // used to align the exception information with a containing stream. This
+ // is useful when the alternatives representation is a part of some larger
+ // text (manifest, etc).
//
explicit LIBBPKG_EXPORT
- requirement_alternatives (const std::string&);
+ requirement_alternatives (const std::string&,
+ const package_name& dependent,
+ const std::string& name = std::string (),
+ std::uint64_t line = 1,
+ std::uint64_t column = 1);
LIBBPKG_EXPORT std::string
string () const;
+
+ // Return true if there is a conditional alternative in the list.
+ //
+ LIBBPKG_EXPORT bool
+ conditional () const;
+
+ // Return true if this is a single simple requirement alternative.
+ //
+ bool
+ simple () const
+ {
+ return size () == 1 && back ().simple ();
+ }
};
inline std::ostream&