aboutsummaryrefslogtreecommitdiff
path: root/bbot
diff options
context:
space:
mode:
Diffstat (limited to 'bbot')
-rw-r--r--bbot/build-config54
-rw-r--r--bbot/build-config.cxx121
-rw-r--r--bbot/buildfile35
-rw-r--r--bbot/export41
-rw-r--r--bbot/manifest215
-rw-r--r--bbot/manifest.cxx931
-rw-r--r--bbot/variable49
-rw-r--r--bbot/variable.cxx94
-rw-r--r--bbot/version37
9 files changed, 1577 insertions, 0 deletions
diff --git a/bbot/build-config b/bbot/build-config
new file mode 100644
index 0000000..0a3ecb3
--- /dev/null
+++ b/bbot/build-config
@@ -0,0 +1,54 @@
+// file : bbot/build-config -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BBOT_BUILD_CONFIG
+#define BBOT_BUILD_CONFIG
+
+#include <string>
+#include <vector>
+#include <iosfwd>
+
+#include <butl/path>
+#include <butl/optional>
+#include <butl/tab-parser>
+#include <butl/target-triplet>
+
+#include <bbot/export>
+
+#include <bbot/variable>
+
+namespace bbot
+{
+ // Build configuration matching specific machine names. Used by bbot
+ // controllers.
+ //
+ struct build_config
+ {
+ std::string machine_pattern; // Machine name pattern.
+ std::string name; // Configuration name.
+
+ butl::optional<butl::target_triplet> target;
+
+ variables vars;
+ };
+
+ using build_configs = std::vector<build_config>;
+
+ // Parse buildtab stream or file. Throw tab_parsing on parsing error,
+ // ios::failure on the underlying OS error.
+ //
+ // buildtab consists of lines in the following format:
+ //
+ // <machine-name-pattern> <config-name> [<target>] [<config-vars>]
+ //
+ using butl::tab_parsing;
+
+ LIBBBOT_EXPORT build_configs
+ parse_buildtab (std::istream&, const std::string& name);
+
+ LIBBBOT_EXPORT build_configs
+ parse_buildtab (const butl::path&);
+}
+
+#endif // BBOT_BUILD_CONFIG
diff --git a/bbot/build-config.cxx b/bbot/build-config.cxx
new file mode 100644
index 0000000..226ead3
--- /dev/null
+++ b/bbot/build-config.cxx
@@ -0,0 +1,121 @@
+// file : bbot/build-config.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <bbot/build-config>
+
+#include <string>
+#include <cstddef> // size_t
+#include <utility> // move()
+#include <stdexcept> // invalid_argument
+
+#include <butl/path>
+#include <butl/fdstream>
+#include <butl/tab-parser>
+
+using namespace std;
+using namespace butl;
+
+namespace bbot
+{
+ LIBBBOT_EXPORT build_configs
+ parse_buildtab (istream& is, const string& name)
+ {
+ build_configs r;
+ tab_parser parser (is, name);
+
+ tab_fields tl;
+ while (!(tl = parser.next ()).empty ())
+ {
+ size_t n (tl.size ()); // Fields count.
+ size_t i (0); // The field currently being processed.
+
+ // Throw tab_parsing for the field currently being processed. If i == n
+ // then we refer to the end-of-line column (presumably reporting a missed
+ // field).
+ //
+ auto bad_line = [&name, &tl, &i, n] (const string& d, size_t offset = 0)
+ {
+ // Offset beyond the end-of-line is meaningless.
+ //
+ assert (i < n || (i == n && offset == 0));
+
+ throw tab_parsing (name,
+ tl.line,
+ i == n
+ ? tl.end_column
+ : tl[i].column + offset,
+ d);
+ };
+
+ build_config config;
+ config.machine_pattern = move (tl[i++].value);
+
+ // Configuration name field is a required one.
+ //
+ if (i == n)
+ bad_line ("no configuration name found");
+
+ config.name = move (tl[i].value);
+
+ // Make sure the name is unique.
+ //
+ for (const auto& c: r)
+ if (c.name == config.name)
+ bad_line ("duplicate configuration name");
+
+ // If there is no target nor configuration variables then save the
+ // configuration and proceed with the next line.
+ //
+ if (++i == n)
+ {
+ r.emplace_back (move (config));
+ continue;
+ }
+
+ // If the third field doesn't contain '=' character, then we will treat
+ // it as a target.
+ //
+ if (tl[i].value.find ('=') == string::npos)
+ {
+ try
+ {
+ config.target = target_triplet (tl[i].value);
+ }
+ catch (const invalid_argument& e)
+ {
+ bad_line (e.what ());
+ }
+
+ ++i;
+ }
+
+ try
+ {
+ for (; i < n; ++i)
+ config.vars.emplace_back (variable (move (tl[i].value)));
+ }
+ catch (const invalid_variable& e)
+ {
+ bad_line (e.what (), e.pos); // Note that tl[i].value is moved from,
+ // but happily we don't use it
+ }
+
+ // Save the configuration.
+ //
+ r.emplace_back (move (config));
+ }
+
+ return r;
+ }
+
+ build_configs
+ parse_buildtab (const path& p)
+ {
+ ifdstream ifs (p);
+ build_configs r (parse_buildtab (ifs, p.string ()));
+
+ ifs.close (); // Throws on failure.
+ return r;
+ }
+}
diff --git a/bbot/buildfile b/bbot/buildfile
new file mode 100644
index 0000000..1bb608f
--- /dev/null
+++ b/bbot/buildfile
@@ -0,0 +1,35 @@
+# file : bbot/buildfile
+# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+import int_libs = libbutl%lib{butl} libbpkg%lib{bpkg}
+
+lib{bbot}: \
+{hxx cxx}{ build-config } \
+{hxx }{ export } \
+{hxx cxx}{ manifest } \
+{hxx cxx}{ variable } \
+{hxx }{ version } \
+ $int_libs
+
+# For pre-releases use the complete version to make sure they cannot be used
+# in place of another pre-release or the final version.
+#
+if $abi_prerelease
+ lib{bbot}: bin.lib.version = @-$version
+else
+ lib{bbot}: bin.lib.version = @-$abi_major.$abi_minor
+
+cxx.poptions =+ "-I$src_root"
+obja{*}: cxx.poptions += -DLIBBBOT_STATIC_BUILD
+objs{*}: cxx.poptions += -DLIBBBOT_SHARED_BUILD
+
+lib{bbot}: cxx.export.poptions = "-I$src_root"
+liba{bbot}: cxx.export.poptions += -DLIBBBOT_STATIC
+libs{bbot}: cxx.export.poptions += -DLIBBBOT_SHARED
+
+lib{bbot}: cxx.export.libs = $int_libs
+
+# Install into the bbot/ subdirectory of, say, /usr/include/.
+#
+install.include = $install.include/bbot/
diff --git a/bbot/export b/bbot/export
new file mode 100644
index 0000000..6947752
--- /dev/null
+++ b/bbot/export
@@ -0,0 +1,41 @@
+// file : bbot/export -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BBOT_EXPORT
+#define BBOT_EXPORT
+
+// Normally we don't export class templates (but do complete specializations),
+// inline functions, and classes with only inline member functions. Exporting
+// classes that inherit from non-exported/imported bases (e.g., std::string)
+// will end up badly. The only known workarounds are to not inherit or to not
+// export. Also, MinGW GCC doesn't like seeing non-exported function being
+// used before their inline definition. The workaround is to reorder code. In
+// the end it's all trial and error.
+
+#if defined(LIBBBOT_STATIC) // Using static.
+# define LIBBBOT_EXPORT
+#elif defined(LIBBBOT_STATIC_BUILD) // Building static.
+# define LIBBBOT_EXPORT
+#elif defined(LIBBBOT_SHARED) // Using shared.
+# ifdef _WIN32
+# define LIBBBOT_EXPORT __declspec(dllimport)
+# else
+# define LIBBBOT_EXPORT
+# endif
+#elif defined(LIBBBOT_SHARED_BUILD) // Building shared.
+# ifdef _WIN32
+# define LIBBBOT_EXPORT __declspec(dllexport)
+# else
+# define LIBBBOT_EXPORT
+# endif
+#else
+// If none of the above macros are defined, then we assume we are being used
+// by some third-party build system that cannot/doesn't signal the library
+// type. Note that this fallback works for both static and shared but in case
+// of shared will be sub-optimal compared to having dllimport.
+//
+# define LIBBBOT_EXPORT // Using static or shared.
+#endif
+
+#endif // BBOT_EXPORT
diff --git a/bbot/manifest b/bbot/manifest
new file mode 100644
index 0000000..5122dc6
--- /dev/null
+++ b/bbot/manifest
@@ -0,0 +1,215 @@
+// file : bbot/manifest -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BBOT_MANIFEST
+#define BBOT_MANIFEST
+
+#include <string>
+#include <vector>
+#include <iosfwd>
+
+#include <butl/optional>
+#include <butl/small-vector>
+#include <butl/target-triplet>
+#include <butl/manifest-forward>
+
+#include <bpkg/manifest> // version, repository_location
+
+#include <bbot/export>
+
+#include <bbot/variable>
+
+namespace bbot
+{
+ enum class machine_type {vm, container};
+
+ class LIBBBOT_EXPORT machine_manifest
+ {
+ public:
+ std::string id;
+ std::string name;
+
+ // Absent if inside task_request_manifest.
+ //
+ butl::optional<machine_type> type;
+
+ std::string summary;
+
+ public:
+ machine_manifest () = default; // VC export.
+ machine_manifest (butl::manifest_parser&, bool ignore_unknown = false);
+ machine_manifest (butl::manifest_parser&,
+ butl::manifest_name_value start,
+ bool ignore_unknown = false);
+
+ void
+ serialize (butl::manifest_serializer&) const;
+
+ private:
+ machine_manifest (butl::manifest_parser&,
+ butl::manifest_name_value start,
+ bool in_list,
+ bool ignore_unknown);
+ };
+
+ class LIBBBOT_EXPORT task_request_manifest
+ {
+ public:
+ std::string agent;
+
+ // Agent's public key SHA256 fingerprint.
+ //
+ // @@ How the fingerpring for openssl public key will be produced? Seems
+ // there is no "standard" for it. Possibly we will use the following
+ // command result (plain SHA256).
+ //
+ // $ cat key.pub | openssl sha256
+ //
+ std::string fingerprint;
+
+ std::vector<machine_manifest> machines;
+
+ public:
+ task_request_manifest () = default; // VC export.
+ task_request_manifest (butl::manifest_parser&,
+ bool ignore_unknown = false);
+
+ void
+ serialize (butl::manifest_serializer&) const;
+ };
+
+ class LIBBBOT_EXPORT task_manifest
+ {
+ public:
+ // Package to build.
+ //
+ std::string name;
+ bpkg::version version;
+ bpkg::repository_location repository; // Remote or absolute.
+
+ // Build machine to use for building the package.
+ //
+ std::string machine;
+
+ // Default for the machine if absent.
+ //
+ butl::optional<butl::target_triplet> target;
+
+ // Build system configuration variables (in addition to build environment
+ // configuration variables).
+ //
+ variables config;
+
+ public:
+ task_manifest () = default; // VC export.
+ task_manifest (butl::manifest_parser&, bool ignore_unknown = false);
+ task_manifest (butl::manifest_parser&,
+ butl::manifest_name_value start,
+ bool ignore_unknown = false);
+
+ void
+ serialize (butl::manifest_serializer&) const;
+ };
+
+ class LIBBBOT_EXPORT task_response_manifest
+ {
+ public:
+ // If empty then no task available.
+ //
+ std::string session;
+
+ // Challenge and task are absent if session is empty.
+ //
+ butl::optional<std::string> challenge;
+ butl::optional<task_manifest> task;
+
+ public:
+ task_response_manifest () = default; // VC export.
+ task_response_manifest (butl::manifest_parser&,
+ bool ignore_unknown = false);
+
+ void
+ serialize (butl::manifest_serializer&) const;
+ };
+
+ // Build task or operation result status.
+ //
+ enum class result_status: std::uint8_t
+ {
+ // The order of the enumerators is arranged so that their integral values
+ // indicate whether one "overrides" the other in the "merge" operator|
+ // (see below).
+ //
+ success,
+ warning,
+ error,
+ abort,
+ abnormal
+ };
+
+ std::ostream&
+ operator<< (std::ostream&, result_status);
+
+ inline result_status&
+ operator |= (result_status& l, result_status r)
+ {
+ if (static_cast<std::uint8_t> (r) > static_cast<std::uint8_t> (l))
+ l = r;
+ return l;
+ }
+
+ struct operation_result
+ {
+ std::string operation; // "configure", "update", "test", etc.
+ result_status status;
+ std::string log;
+ };
+
+ using operation_results = butl::small_vector<operation_result, 3>;
+
+ class LIBBBOT_EXPORT result_manifest
+ {
+ public:
+ // Built package.
+ //
+ std::string name;
+ bpkg::version version;
+
+ result_status status;
+
+ // Ordered (ascending) by operation value. May not contain all the
+ // operations if the task failed in the middle, but should have no gaps
+ // (operation can not start unless all previous ones succeeded).
+ //
+ operation_results results;
+
+ result_manifest () = default; // VC export.
+ result_manifest (butl::manifest_parser&, bool ignore_unknown = false);
+ result_manifest (butl::manifest_parser&,
+ butl::manifest_name_value start,
+ bool ignore_unknown = false);
+
+ void
+ serialize (butl::manifest_serializer&) const;
+ };
+
+ class LIBBBOT_EXPORT result_request_manifest
+ {
+ public:
+ std::string session; // The task response session.
+ std::string challenge; // The answer to challenge in the task response.
+
+ result_manifest result;
+
+ public:
+ result_request_manifest () = default; // VC export.
+ result_request_manifest (butl::manifest_parser&,
+ bool ignore_unknown = false);
+
+ void
+ serialize (butl::manifest_serializer&) const;
+ };
+}
+
+#endif // BBOT_MANIFEST
diff --git a/bbot/manifest.cxx b/bbot/manifest.cxx
new file mode 100644
index 0000000..6c9fe4d
--- /dev/null
+++ b/bbot/manifest.cxx
@@ -0,0 +1,931 @@
+// file : bbot/manifest.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <bbot/manifest>
+
+#include <vector>
+#include <string>
+#include <cassert>
+#include <sstream>
+#include <cstddef> // size_t
+#include <utility> // move()
+#include <cstdint> // uint64_t
+#include <algorithm> // find()
+
+#include <butl/utility> // digit()
+#include <butl/tab-parser>
+#include <butl/manifest-parser>
+#include <butl/manifest-serializer>
+
+using namespace std;
+using namespace butl;
+using namespace bpkg;
+
+namespace bbot
+{
+ using parser = manifest_parser;
+ using parsing = manifest_parsing;
+ using serializer = manifest_serializer;
+ using serialization = manifest_serialization;
+ using name_value = manifest_name_value;
+
+ using strings = vector<string>;
+
+ static const strings machine_type_names ({"vm", "container"});
+
+ static const strings result_status_names ({
+ "success",
+ "warning",
+ "error",
+ "abort",
+ "abnormal"});
+
+ ostream&
+ operator<< (ostream& o, result_status s)
+ {
+ size_t i (static_cast<size_t> (s));
+ assert (i < result_status_names.size ());
+ o << result_status_names[i];
+ return o;
+ }
+
+ inline static bool
+ valid_sha256 (const string& s) noexcept
+ {
+ if (s.size () != 64)
+ return false;
+
+ for (const auto& c: s)
+ {
+ if ((c < 'a' || c > 'f' ) && !digit (c))
+ return false;
+ }
+
+ return true;
+ }
+
+ // machine_manifest
+ //
+ machine_manifest::
+ machine_manifest (parser& p, bool iu)
+ : machine_manifest (p, p.next (), false, iu)
+ {
+ // Make sure this is the end.
+ //
+ name_value nv (p.next ());
+ if (!nv.empty ())
+ throw parsing (p.name (), nv.name_line, nv.name_column,
+ "single machine manifest expected");
+ }
+
+ machine_manifest::
+ machine_manifest (parser& p, name_value nv, bool iu)
+ : machine_manifest (p, nv, true, iu)
+ {
+ }
+
+ machine_manifest::
+ machine_manifest (parser& p, name_value nv, bool il, bool iu)
+ {
+ auto bad_name = [&p, &nv] (const string& d)
+ {
+ throw parsing (p.name (), nv.name_line, nv.name_column, d);
+ };
+
+ auto bad_value = [&p, &nv] (const string& d)
+ {
+ throw parsing (p.name (), nv.value_line, nv.value_column, d);
+ };
+
+ // Make sure this is the start and we support the version.
+ //
+ if (!nv.name.empty ())
+ bad_name ("start of machine manifest expected");
+
+ if (nv.value != "1")
+ bad_value ("unsupported format version");
+
+ for (nv = p.next (); !nv.empty (); nv = p.next ())
+ {
+ string& n (nv.name);
+ string& v (nv.value);
+
+ if (n == "id")
+ {
+ if (!id.empty ())
+ bad_name ("machine id redefinition");
+
+ if (v.empty ())
+ bad_value ("empty machine id");
+
+ id = move (v);
+ }
+ else if (n == "name")
+ {
+ if (!name.empty ())
+ bad_name ("machine name redefinition");
+
+ if (v.empty ())
+ bad_value ("empty machine name");
+
+ name = move (v);
+ }
+ else if (n == "type")
+ {
+ if (il)
+ bad_name ("machine type not allowed");
+
+ if (type)
+ bad_name ("machine type redefinition");
+
+ auto b (machine_type_names.cbegin ());
+ auto e (machine_type_names.cend ());
+ auto i (find (b, e, v));
+
+ if (i == e)
+ bad_value ("invalid machine type");
+
+ type = static_cast<machine_type> (i - b);
+ }
+ else if (n == "summary")
+ {
+ if (!summary.empty ())
+ bad_name ("machine summary redefinition");
+
+ if (v.empty ())
+ bad_value ("empty machine summary");
+
+ summary = move (v);
+ }
+ else if (!iu)
+ bad_name ("unknown name '" + n + "' in machine manifest");
+ }
+
+ // Verify all non-optional values were specified.
+ //
+ if (id.empty ())
+ bad_value ("no machine id specified");
+
+ if (name.empty ())
+ bad_value ("no machine name specified");
+
+ if (!il && !type)
+ bad_value ("no machine type specified");
+
+ if (summary.empty ())
+ bad_value ("no machine summary specified");
+ }
+
+ void machine_manifest::
+ serialize (serializer& s) const
+ {
+ // @@ Should we check that all non-optional values are specified and all
+ // values are valid?
+ //
+ s.next ("", "1"); // Start of manifest.
+ s.next ("id", id);
+ s.next ("name", name);
+
+ if (type)
+ {
+ size_t v (static_cast<size_t> (*type));
+ assert (v < machine_type_names.size ());
+ s.next ("type", machine_type_names[v]);
+ }
+
+ s.next ("summary", summary);
+ s.next ("", ""); // End of manifest.
+ }
+
+ // task_request_manifest
+ //
+ task_request_manifest::
+ task_request_manifest (parser& p, bool iu)
+ {
+ name_value nv (p.next ());
+
+ auto bad_name = [&p, &nv] (const string& d)
+ {
+ throw parsing (p.name (), nv.name_line, nv.name_column, d);
+ };
+
+ auto bad_value = [&p, &nv] (const string& d)
+ {
+ throw parsing (p.name (), nv.value_line, nv.value_column, d);
+ };
+
+ // Make sure this is the start and we support the version.
+ //
+ if (!nv.name.empty ())
+ bad_name ("start of task request manifest expected");
+
+ if (nv.value != "1")
+ bad_value ("unsupported format version");
+
+ // Parse the task request manifest.
+ //
+ for (nv = p.next (); !nv.empty (); nv = p.next ())
+ {
+ string& n (nv.name);
+ string& v (nv.value);
+
+ if (n == "agent")
+ {
+ if (!agent.empty ())
+ bad_name ("task request agent redefinition");
+
+ if (v.empty ())
+ bad_value ("empty task request agent");
+
+ agent = move (v);
+ }
+ else if (n == "fingerprint")
+ {
+ if (!fingerprint.empty ())
+ bad_name ("task request fingerprint redefinition");
+
+ if (!valid_sha256 (v))
+ bad_value ("invalid task request fingerprint");
+
+ fingerprint = move (v);
+ }
+ else if (!iu)
+ bad_name ("unknown name '" + n + "' in task request manifest");
+ }
+
+ // Verify all non-optional values were specified.
+ //
+ if (agent.empty ())
+ bad_value ("no task request agent specified");
+
+ if (fingerprint.empty ())
+ bad_value ("no task request fingerprint specified");
+
+ // Parse machine manifests.
+ //
+ for (nv = p.next (); !nv.empty (); nv = p.next ())
+ machines.emplace_back (machine_manifest (p, nv, iu));
+
+ if (machines.empty ())
+ bad_value ("no task request machines specified");
+ }
+
+ void task_request_manifest::
+ serialize (serializer& s) const
+ {
+ // @@ Should we check that all non-optional values are specified and all
+ // values are valid?
+ //
+ s.next ("", "1"); // Start of manifest.
+ s.next ("agent", agent);
+ s.next ("fingerprint", fingerprint);
+ s.next ("", ""); // End of manifest.
+
+ for (const machine_manifest& m: machines)
+ {
+ if (m.type)
+ throw serialization (s.name (), "machine type is forbidden");
+
+ m.serialize (s);
+ }
+
+ s.next ("", ""); // End of stream.
+ }
+
+ // task_manifest
+ //
+ task_manifest::
+ task_manifest (parser& p, bool iu)
+ : task_manifest (p, p.next (), iu)
+ {
+ // Make sure this is the end.
+ //
+ name_value nv (p.next ());
+ if (!nv.empty ())
+ throw parsing (p.name (), nv.name_line, nv.name_column,
+ "single task manifest expected");
+ }
+
+ task_manifest::
+ task_manifest (parser& p, name_value nv, bool iu)
+ {
+ auto bad_name = [&p, &nv] (const string& d)
+ {
+ throw parsing (p.name (), nv.name_line, nv.name_column, d);
+ };
+
+ // Offsets are used to tie an error to the specific position inside a
+ // manifest value (possibly a multiline one).
+ //
+ auto bad_value = [&p, &nv] (
+ const string& d, uint64_t column_offset = 0, uint64_t line_offset = 0)
+ {
+ throw parsing (p.name (),
+ nv.value_line + line_offset,
+ (line_offset == 0 ? nv.value_column : 1) + column_offset,
+ d);
+ };
+
+ // Make sure this is the start and we support the version.
+ //
+ if (!nv.name.empty ())
+ bad_name ("start of task manifest expected");
+
+ if (nv.value != "1")
+ bad_value ("unsupported format version");
+
+ // Parse the task manifest.
+ //
+ for (nv = p.next (); !nv.empty (); nv = p.next ())
+ {
+ string& n (nv.name);
+ string& v (nv.value);
+
+ if (n == "name")
+ {
+ if (!name.empty ())
+ bad_name ("task package name redefinition");
+
+ if (v.empty ())
+ bad_value ("empty task package name");
+
+ name = move (v);
+ }
+ else if (n == "version")
+ {
+ if (!version.empty ())
+ bad_name ("task package version redefinition");
+
+ try
+ {
+ version = bpkg::version (move (v));
+ }
+ catch (const invalid_argument& e)
+ {
+ bad_value (string ("invalid task package version: ") + e.what ());
+ }
+
+ // Versions like 1.2.3- are forbidden in manifest as intended to be
+ // used for version constrains rather than actual releases.
+ //
+ if (version.release && version.release->empty ())
+ bad_value ("invalid task package version release");
+ }
+ else if (n == "repository")
+ {
+ if (!repository.empty ())
+ bad_name ("task repository redefinition");
+
+ if (v.empty ())
+ bad_value ("empty task repository");
+
+ try
+ {
+ // Call remote/absolute repository location constructor (throws
+ // invalid_argument for relative location).
+ //
+ repository = repository_location (move (v));
+ }
+ catch (const invalid_argument& e)
+ {
+ bad_value (string ("invalid task repository: ") + e.what ());
+ }
+ }
+ else if (n == "machine")
+ {
+ if (!machine.empty ())
+ bad_name ("task machine redefinition");
+
+ if (v.empty ())
+ bad_value ("empty task machine");
+
+ machine = move (v);
+ }
+ else if (n == "target")
+ {
+ if (target)
+ bad_name ("task target redefinition");
+
+ try
+ {
+ target = target_triplet (v);
+ }
+ catch (const invalid_argument& e)
+ {
+ bad_value (string ("invalid task target: ") + e.what ());
+ }
+ }
+ else if (n == "config")
+ {
+ if (!config.empty ())
+ bad_name ("task configuration redefinition");
+
+ // Note that when reporting errors we combine the manifest value
+ // position with the respective field and error positions.
+ //
+ try
+ {
+ istringstream is (v);
+ tab_parser parser (is, "");
+
+ // Here we naturally support multiline config manifest.
+ //
+ tab_fields tl;
+ while (!(tl = parser.next ()).empty ())
+ {
+ for (auto& tf: tl)
+ {
+ try
+ {
+ config.emplace_back (variable (move (tf.value)));
+ }
+ catch (const invalid_variable& e)
+ {
+ bad_value (string ("invalid task configuration: ") + e.what (),
+ tf.column - 1 + e.pos,
+ tl.line - 1);
+ }
+ }
+ }
+ }
+ catch (const tab_parsing& e)
+ {
+ bad_value ("invalid task configuration: " + e.description,
+ e.column - 1,
+ e.line - 1);
+ }
+
+ if (config.empty ())
+ bad_value ("empty task configuration");
+ }
+ else if (!iu)
+ bad_name ("unknown name '" + n + "' in task manifest");
+ }
+
+ // Verify all non-optional values were specified.
+ //
+ if (name.empty ())
+ bad_value ("no task package name specified");
+
+ if (version.empty ())
+ bad_value ("no task package version specified");
+
+ if (repository.empty ())
+ bad_value ("no task repository specified");
+
+ if (machine.empty ())
+ bad_value ("no task machine specified");
+ }
+
+ void task_manifest::
+ serialize (serializer& s) const
+ {
+ // @@ Should we check that all non-optional values are specified and all
+ // values are valid?
+ //
+ s.next ("", "1"); // Start of manifest.
+ s.next ("name", name);
+ s.next ("version", version.string ());
+ s.next ("repository", repository.string ());
+ s.next ("machine", machine);
+
+ if (target)
+ s.next ("target", target->string ());
+
+ // Recompose config string as a space-separated variable list,
+ //
+ if (!config.empty ())
+ {
+ string v;
+ for (auto b (config.cbegin ()), i (b), e (config.cend ()); i != e; ++i)
+ {
+ if (i != b)
+ v += ' ';
+
+ v += *i;
+ }
+
+ s.next ("config", v);
+ }
+
+ s.next ("", ""); // End of manifest.
+ }
+
+ // task_response_manifest
+ //
+ task_response_manifest::
+ task_response_manifest (parser& p, bool iu)
+ {
+ name_value nv (p.next ());
+
+ auto bad_name = [&p, &nv] (const string& d)
+ {
+ throw parsing (p.name (), nv.name_line, nv.name_column, d);
+ };
+
+ auto bad_value = [&p, &nv] (const string& d)
+ {
+ throw parsing (p.name (), nv.value_line, nv.value_column, d);
+ };
+
+ // Make sure this is the start and we support the version.
+ //
+ if (!nv.name.empty ())
+ bad_name ("start of task response manifest expected");
+
+ if (nv.value != "1")
+ bad_value ("unsupported format version");
+
+ // Parse the task response manifest.
+ //
+ // Note that we need to distinguish an empty and absent session.
+ //
+ optional<string> sess;
+ for (nv = p.next (); !nv.empty (); nv = p.next ())
+ {
+ string& n (nv.name);
+ string& v (nv.value);
+
+ if (n == "session")
+ {
+ if (sess)
+ bad_name ("task response session redefinition");
+
+ sess = move (v);
+ }
+ else if (n == "challenge")
+ {
+ if (challenge)
+ bad_name ("task response challenge redefinition");
+
+ if (v.empty ())
+ bad_value ("empty task response challenge");
+
+ challenge = move (v);
+ }
+ else if (!iu)
+ bad_name ("unknown name '" + n + "' in task response manifest");
+ }
+
+ // Verify all non-optional values were specified, and all values are
+ // expected.
+ //
+ if (!sess)
+ bad_value ("no task response session specified");
+
+ session = move (*sess);
+
+ // If session is not empty then the challenge must present, otherwise it
+ // shouldn't.
+ //
+ if (!session.empty ())
+ {
+ if (!challenge)
+ bad_value ("no task response challenge specified");
+ }
+ else if (challenge)
+ bad_value ("unexpected task response challenge");
+
+ // If session is not empty then the task manifest must follow, otherwise it
+ // shouldn't.
+ //
+ nv = p.next ();
+
+ if (!session.empty ())
+ {
+ if (nv.empty ())
+ bad_value ("task manifest expected");
+
+ task = task_manifest (p, nv, iu);
+
+ nv = p.next ();
+ }
+
+ // Make sure this is the end.
+ //
+ if (!nv.empty ())
+ throw parsing (p.name (), nv.name_line, nv.name_column,
+ "single task response manifest expected");
+ }
+
+ void task_response_manifest::
+ serialize (serializer& s) const
+ {
+ // @@ Should we check that all non-optional values are specified and all
+ // values are valid?
+ //
+ s.next ("", "1"); // Start of manifest.
+ s.next ("session", session);
+
+ if (challenge)
+ s.next ("challenge", *challenge);
+
+ s.next ("", ""); // End of manifest.
+
+ if (task)
+ task->serialize (s);
+
+ s.next ("", ""); // End of stream.
+ }
+
+ // result_manifest
+ //
+ result_manifest::
+ result_manifest (parser& p, bool iu)
+ : result_manifest (p, p.next (), iu)
+ {
+ // Make sure this is the end.
+ //
+ name_value nv (p.next ());
+ if (!nv.empty ())
+ throw parsing (p.name (), nv.name_line, nv.name_column,
+ "single result manifest expected");
+ }
+
+ result_manifest::
+ result_manifest (parser& p, name_value nv, bool iu)
+ {
+ auto bad_name = [&p, &nv] (const string& d)
+ {
+ throw parsing (p.name (), nv.name_line, nv.name_column, d);
+ };
+
+ auto bad_value = [&p, &nv] (const string& d, size_t offset = 0)
+ {
+ throw parsing (p.name (), nv.value_line, nv.value_column + offset, d);
+ };
+
+ auto result_stat =
+ [&bad_value] (const string& v, const string& what) -> result_status
+ {
+ auto b (result_status_names.cbegin ());
+ auto e (result_status_names.cend ());
+ auto i (find (b, e, v));
+
+ if (i == e)
+ bad_value ("invalid " + what);
+
+ return static_cast<result_status> (i - b);
+ };
+
+ // Make sure this is the start and we support the version.
+ //
+ if (!nv.name.empty ())
+ bad_name ("start of result manifest expected");
+
+ if (nv.value != "1")
+ bad_value ("unsupported format version");
+
+ // Parse the result manifest.
+ //
+ optional<result_status> stat;
+
+ // Number of parsed *-log values. Also denotes the next expected log type.
+ //
+ size_t nlog (0);
+
+ for (nv = p.next (); !nv.empty (); nv = p.next ())
+ {
+ string& n (nv.name);
+ string& v (nv.value);
+
+ if (n == "name")
+ {
+ if (!name.empty ())
+ bad_name ("result package name redefinition");
+
+ if (v.empty ())
+ bad_value ("empty result package name");
+
+ name = move (v);
+ }
+ else if (n == "version")
+ {
+ if (!version.empty ())
+ bad_name ("result package version redefinition");
+
+ try
+ {
+ version = bpkg::version (move (v));
+ }
+ catch (const invalid_argument& e)
+ {
+ bad_value (string ("invalid result package version: ") + e.what ());
+ }
+
+ // Versions like 1.2.3- are forbidden in manifest as intended to be
+ // used for version constrains rather than actual releases.
+ //
+ if (version.release && version.release->empty ())
+ bad_value ("invalid result package version release");
+ }
+ else if (n == "status")
+ {
+ if (stat)
+ bad_name ("result status redefinition");
+
+ stat = result_stat (v, "result status");
+ }
+ else
+ {
+ size_t nn (n.size ()); // Name length.
+
+ // Note: returns false if nothing preceeds a suffix.
+ //
+ auto suffix = [&n, nn] (const char* s, size_t sn) -> bool
+ {
+ return nn > sn && n.compare (nn - sn, sn, s) == 0;
+ };
+
+ size_t sn;
+ if (suffix ("-status", sn = 7))
+ {
+ if (!stat)
+ bad_name ("result status must appear first");
+
+ if (nlog > 0) // Some logs have already been parsed.
+ bad_name (n + " after operations logs");
+
+ string op (n, 0, nn - sn);
+
+ // Make sure the operation result status is not redefined.
+ //
+ for (const auto& r: results)
+ {
+ if (r.operation == op)
+ bad_name ("result " + n + " redefinition");
+ }
+
+ // Add the operation result (log will come later).
+ //
+ results.push_back ({move (op), result_stat (v, n), string ()});
+ }
+ else if (suffix ("-log", sn = 4))
+ {
+ string op (n, 0, nn - sn);
+
+ // Check that specifically this operation log is expected.
+ //
+ if (nlog >= results.size ())
+ bad_name ("unexpected " + n);
+
+ if (results[nlog].operation != op)
+ bad_name (results[nlog].operation + "-log is expected");
+
+ // Save operation log.
+ //
+ results[nlog++].log = move (v);
+ }
+ else if (!iu)
+ bad_name ("unknown name '" + n + "' in result manifest");
+ }
+ }
+
+ // Verify all non-optional values were specified.
+ //
+ if (name.empty ())
+ bad_value ("no result package name specified");
+
+ if (version.empty ())
+ bad_value ("no result package version specified");
+
+ if (!stat)
+ bad_value ("no result status specified");
+
+ // @@ Checking that the result status is consistent with operations
+ // statuses is a bit hairy, so let's postpone for now.
+ //
+ status = move (*stat);
+
+ // Check that we have log for every operation result status.
+ //
+ if (nlog < results.size ())
+ bad_name ("no result " + results[nlog].operation + "-log specified");
+ }
+
+ void result_manifest::
+ serialize (serializer& s) const
+ {
+ // @@ Should we check that all non-optional values are specified and all
+ // values are valid?
+ //
+ s.next ("", "1"); // Start of manifest.
+ s.next ("name", name);
+ s.next ("version", version.string ());
+
+ size_t v (static_cast<size_t> (status));
+ assert (v < result_status_names.size ());
+ s.next ("status", result_status_names[v]);
+
+ // Serialize *-status values.
+ //
+ for (const auto& r: results)
+ {
+ size_t rs (static_cast<size_t> (r.status));
+ assert (rs < result_status_names.size ());
+
+ s.next (r.operation + "-status", result_status_names[rs]);
+ }
+
+ // Serialize *-log values.
+ //
+ for (const auto& r: results)
+ s.next (r.operation + "-log", r.log);
+
+ s.next ("", ""); // End of manifest.
+ }
+
+ // result_request_manifest
+ //
+ result_request_manifest::
+ result_request_manifest (parser& p, bool iu)
+ {
+ name_value nv (p.next ());
+
+ auto bad_name = [&p, &nv] (const string& d)
+ {
+ throw parsing (p.name (), nv.name_line, nv.name_column, d);
+ };
+
+ auto bad_value = [&p, &nv] (const string& d)
+ {
+ throw parsing (p.name (), nv.value_line, nv.value_column, d);
+ };
+
+ // Make sure this is the start and we support the version.
+ //
+ if (!nv.name.empty ())
+ bad_name ("start of result request manifest expected");
+
+ if (nv.value != "1")
+ bad_value ("unsupported format version");
+
+ // Parse the result request manifest.
+ //
+ for (nv = p.next (); !nv.empty (); nv = p.next ())
+ {
+ string& n (nv.name);
+ string& v (nv.value);
+
+ if (n == "session")
+ {
+ if (!session.empty ())
+ bad_name ("result request session redefinition");
+
+ if (v.empty ())
+ bad_value ("empty result request session");
+
+ session = move (v);
+ }
+ else if (n == "challenge")
+ {
+ if (!challenge.empty ())
+ bad_name ("result request challenge redefinition");
+
+ if (v.empty ())
+ bad_value ("empty result request challenge");
+
+ challenge = move (v);
+ }
+ else if (!iu)
+ bad_name ("unknown name '" + n + "' in result request manifest");
+ }
+
+ // Verify all non-optional values were specified.
+ //
+ if (session.empty ())
+ bad_value ("no result request session specified");
+
+ if (challenge.empty ())
+ bad_value ("no result request challenge specified");
+
+ nv = p.next ();
+ if (nv.empty ())
+ bad_value ("result manifest expected");
+
+ result = result_manifest (p, nv, iu);
+
+ // Make sure this is the end.
+ //
+ nv = p.next ();
+ if (!nv.empty ())
+ throw parsing (p.name (), nv.name_line, nv.name_column,
+ "single result request manifest expected");
+ }
+
+ void result_request_manifest::
+ serialize (serializer& s) const
+ {
+ // @@ Should we check that all non-optional values are specified and all
+ // values are valid?
+ //
+ s.next ("", "1"); // Start of manifest.
+ s.next ("session", session);
+ s.next ("challenge", challenge);
+ s.next ("", ""); // End of manifest.
+
+ result.serialize (s);
+ s.next ("", ""); // End of stream.
+ }
+}
diff --git a/bbot/variable b/bbot/variable
new file mode 100644
index 0000000..c210dc0
--- /dev/null
+++ b/bbot/variable
@@ -0,0 +1,49 @@
+// file : bbot/variable -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BBOT_VARIABLE
+#define BBOT_VARIABLE
+
+#include <string>
+#include <vector>
+#include <cstdint> // uint64_t
+#include <stdexcept> // invalid_argument
+
+#include <bbot/export>
+
+namespace bbot
+{
+ //@@ invalid_argument
+ //
+ class LIBBBOT_EXPORT invalid_variable: public std::invalid_argument
+ {
+ public:
+ invalid_variable (std::uint64_t p, const std::string& d)
+ : invalid_argument (d), pos (p) {}
+
+ std::uint64_t pos; // Zero-based.
+ };
+
+ // String in the name=value format. Can contain single or double quoted
+ // substrings. No escaping is supported. The name must not contain spaces.
+ // Throw variable_error if the string doesn't conform with the above
+ // constraints.
+ //
+ struct LIBBBOT_EXPORT variable: std::string
+ {
+ variable (std::string);
+
+ // Remove a single level of quotes. Don't validate the format or the
+ // correctness of the quotation. Note that the variable can potentially be
+ // modified through the std::string interface in a way that breaks
+ // format/quoting.
+ //
+ std::string
+ unquoted () const;
+ };
+
+ using variables = std::vector<variable>;
+}
+
+#endif // BBOT_VARIABLE
diff --git a/bbot/variable.cxx b/bbot/variable.cxx
new file mode 100644
index 0000000..a25b007
--- /dev/null
+++ b/bbot/variable.cxx
@@ -0,0 +1,94 @@
+// file : bbot/variable.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <bbot/variable>
+
+#include <utility> // move()
+
+using namespace std;
+
+namespace bbot
+{
+ variable::
+ variable (string v): string (move (v))
+ {
+ // Scan the string untill the end to check that the quoting is terminated.
+ // We will also make sure that the name doesn't contain spaces and the
+ // value is provided.
+ //
+ char quoting ('\0'); // Current quoting mode, can be used as bool.
+ bool name (true); // True while we are parsing the variable name.
+
+ auto b (cbegin ());
+ auto i (b);
+
+ auto bad_variable = [&b, &i] (const string& d)
+ {
+ throw invalid_variable (i - b, d);
+ };
+
+ for (auto e (cend ()); i != e; ++i)
+ {
+ char c (*i);
+
+ if (!quoting)
+ {
+ if (c == '"' || c == '\'') // Begin of quoted string,
+ {
+ quoting = c;
+ continue;
+ }
+ }
+ else if (c == quoting) // End of quoted string,
+ {
+ quoting = '\0';
+ continue;
+ }
+
+ if (name)
+ {
+ if (c == ' ' || c == '\t')
+ bad_variable ("expected variable assignment");
+ else if (c == '=')
+ name = false;
+ }
+ }
+
+ if (quoting)
+ bad_variable ("unterminated quoted string");
+
+ if (name)
+ bad_variable ("no variable value");
+ }
+
+ string variable::
+ unquoted () const
+ {
+ string r;
+ char quoting ('\0'); // Current quoting mode, can be used as bool.
+
+ for (auto i (cbegin ()), e (cend ()); i != e; ++i)
+ {
+ char c (*i);
+
+ if (!quoting)
+ {
+ if (c == '"' || c == '\'') // Begin of quoted string.
+ {
+ quoting = c;
+ continue;
+ }
+ }
+ else if (c == quoting) // End of quoted string.
+ {
+ quoting = '\0';
+ continue;
+ }
+
+ r += c;
+ }
+
+ return r;
+ }
+}
diff --git a/bbot/version b/bbot/version
new file mode 100644
index 0000000..9454762
--- /dev/null
+++ b/bbot/version
@@ -0,0 +1,37 @@
+// file : bbot/version -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef LIBBBOT_VERSION // Note: using the version macro itself.
+
+#include <butl/version> // LIBBUTL_VERSION
+
+// Version format is AABBCCDD where
+//
+// AA - major version number
+// BB - minor version number
+// CC - bugfix version number
+// DD - alpha / beta (DD + 50) version number
+//
+// When DD is not 00, 1 is subtracted from AABBCC. For example:
+//
+// Version AABBCCDD
+// 2.0.0 02000000
+// 2.1.0 02010000
+// 2.1.1 02010100
+// 2.2.0-a1 02019901
+// 3.0.0-b2 02999952
+//
+#define LIBBBOT_VERSION 49901
+#define LIBBBOT_VERSION_STR "0.5.0-a1"
+
+// Generally, we expect minor versions to be source code backwards-
+// compatible, thought we might have a minimum version requirement.
+//
+// Note: does not apply during early development.
+//
+#if LIBBUTL_VERSION != 49901
+# error incompatible libbutl version
+#endif
+
+#endif // LIBBBOT_VERSION