aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2018-11-17 23:39:15 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2018-11-29 21:06:49 +0300
commitc8775bf46f337e2dca4d161251eb89595aef4051 (patch)
tree38593dff2e970ff3cb9afb9d4df662fea6584f8e
parent93c043d0bc755f6c9cffea489116ae742795a152 (diff)
Add support for builds manifest value
-rw-r--r--libbpkg/manifest.cxx363
-rw-r--r--libbpkg/manifest.hxx122
-rw-r--r--tests/build-class-expr/buildfile8
-rw-r--r--tests/build-class-expr/driver.cxx101
-rw-r--r--tests/build-class-expr/testscript125
-rw-r--r--tests/manifest/testscript21
6 files changed, 737 insertions, 3 deletions
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 <libbutl/url.mxx>
#include <libbutl/path.mxx>
#include <libbutl/base64.mxx>
-#include <libbutl/utility.mxx> // casecmp(), lcase(), alpha(),
- // digit(), xdigit()
+#include <libbutl/utility.mxx> // casecmp(), lcase(), alnum(),
+ // digit(), xdigit(), next_word()
#include <libbutl/manifest-parser.mxx>
#include <libbutl/manifest-serializer.mxx>
#include <libbutl/standard-version.mxx>
@@ -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 (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<build_class_term> (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<build_class_term> (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<build_class_term>
+ parse_build_class_expr (const string& s, size_t* p = nullptr)
+ {
+ vector<build_class_term> 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<build_class_term> 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<build_class_term> ({move (t)});
+ }
+
+ expr = move (r);
+ }
+
+ // Return string representation of the build class expression.
+ //
+ static string
+ to_string (const vector<build_class_term>& 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<build_class_term>& 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<build_class_term> 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<build_class_term> 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<build_class_term> 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<email_type> build_error_email;
std::vector<dependency_alternatives> dependencies;
std::vector<requirement_alternatives> requirements;
+ std::vector<build_class_expr> builds;
std::vector<build_constraint> 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 <ios>
+#include <string>
+#include <iostream>
+
+#include <libbutl/utility.mxx> // eof(), operator<<(ostream, exception)
+#include <libbutl/optional.mxx>
+
+#include <libbpkg/manifest.hxx>
+
+// Usages:
+//
+// argv[0] -p
+// argv[0] [<classes>]
+//
+// 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<strings> 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 >>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 <<EOI 2>"stdin:4:9: error: invalid package builds: class expression separator ':' expected" != 0
+ : 1
+ sha256sum: a2b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+ :
+ builds: default -gcc
+ EOI
+ }
+ }
}
: repositories