aboutsummaryrefslogtreecommitdiff
path: root/libbbot
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2017-04-29 23:23:07 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2017-04-30 21:47:39 +0300
commit79640be325c333d77b4078d37f7668b74d5682e3 (patch)
tree5b165704351e9914e1d0fa87b787d95603a970c1 /libbbot
parentd3c88705b3e3b77150f60aed2527fa60d658991e (diff)
Add hxx extension for headers and lib prefix for library dirs
Diffstat (limited to 'libbbot')
-rw-r--r--libbbot/.gitignore1
-rw-r--r--libbbot/build-config.cxx125
-rw-r--r--libbbot/build-config.hxx53
-rw-r--r--libbbot/buildfile37
-rw-r--r--libbbot/export.hxx41
-rw-r--r--libbbot/manifest.cxx988
-rw-r--r--libbbot/manifest.hxx297
-rw-r--r--libbbot/version.hxx.in48
8 files changed, 1590 insertions, 0 deletions
diff --git a/libbbot/.gitignore b/libbbot/.gitignore
new file mode 100644
index 0000000..426db9e
--- /dev/null
+++ b/libbbot/.gitignore
@@ -0,0 +1 @@
+version.hxx
diff --git a/libbbot/build-config.cxx b/libbbot/build-config.cxx
new file mode 100644
index 0000000..d902d42
--- /dev/null
+++ b/libbbot/build-config.cxx
@@ -0,0 +1,125 @@
+// file : libbbot/build-config.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <libbbot/build-config.hxx>
+
+#include <string>
+#include <cstddef> // size_t
+#include <utility> // move()
+#include <stdexcept> // invalid_argument
+
+#include <butl/path>
+#include <butl/fdstream>
+#include <butl/tab-parser>
+
+#include <libbbot/manifest.hxx> // task_manifest::check_config()
+
+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)
+ {
+ // Offset beyond the end-of-line is meaningless.
+ //
+ assert (i <= n);
+
+ throw tab_parsing (name,
+ tl.line,
+ i == n
+ ? tl.end_column
+ : tl[i].column,
+ 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)
+ {
+ task_manifest::check_config (tl[i].value);
+ config.vars.emplace_back (move (tl[i].value));
+ }
+ }
+ catch (const invalid_argument& e)
+ {
+ bad_line (e.what ());
+ }
+
+ // 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/libbbot/build-config.hxx b/libbbot/build-config.hxx
new file mode 100644
index 0000000..8a2e2d7
--- /dev/null
+++ b/libbbot/build-config.hxx
@@ -0,0 +1,53 @@
+// file : libbbot/build-config.hxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef LIBBBOT_BUILD_CONFIG_HXX
+#define LIBBBOT_BUILD_CONFIG_HXX
+
+#include <string>
+#include <vector>
+#include <iosfwd>
+
+#include <butl/path>
+#include <butl/optional>
+#include <butl/tab-parser>
+#include <butl/target-triplet>
+
+#include <libbbot/export.hxx>
+#include <libbbot/version.hxx>
+
+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;
+
+ std::vector<std::string> 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 // LIBBBOT_BUILD_CONFIG_HXX
diff --git a/libbbot/buildfile b/libbbot/buildfile
new file mode 100644
index 0000000..c143032
--- /dev/null
+++ b/libbbot/buildfile
@@ -0,0 +1,37 @@
+# file : libbbot/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 }{ version } \
+ $int_libs
+
+hxx{version}: in{version} $src_root/file{manifest}
+hxx{version}: dist = true
+
+# 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 $version.pre_release
+ lib{bbot}: bin.lib.version = @"-$version.project_id"
+else
+ lib{bbot}: bin.lib.version = @"-$version.major.$version.minor"
+
+cxx.poptions =+ "-I$out_root" "-I$src_root"
+obja{*}: cxx.poptions += -DLIBBBOT_STATIC_BUILD
+objs{*}: cxx.poptions += -DLIBBBOT_SHARED_BUILD
+
+lib{bbot}: cxx.export.poptions = "-I$out_root" "-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 libbbot/ subdirectory of, say, /usr/include/.
+#
+install.include = $install.include/libbbot/
diff --git a/libbbot/export.hxx b/libbbot/export.hxx
new file mode 100644
index 0000000..eef4cf7
--- /dev/null
+++ b/libbbot/export.hxx
@@ -0,0 +1,41 @@
+// file : libbbot/export.hxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef LIBBBOT_EXPORT_HXX
+#define LIBBBOT_EXPORT_HXX
+
+// 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 // LIBBBOT_EXPORT_HXX
diff --git a/libbbot/manifest.cxx b/libbbot/manifest.cxx
new file mode 100644
index 0000000..3ea9d19
--- /dev/null
+++ b/libbbot/manifest.cxx
@@ -0,0 +1,988 @@
+// file : libbbot/manifest.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <libbbot/manifest.hxx>
+
+#include <vector>
+#include <string>
+#include <cctype> // isxdigit()
+#include <cassert>
+#include <sstream>
+#include <cstddef> // size_t
+#include <utility> // move()
+#include <cstdint> // uint64_t
+#include <stdexcept> // invalid_argument
+
+#include <butl/utility> // digit()
+#include <butl/tab-parser>
+#include <butl/string-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>;
+
+ // result_status
+ //
+ string
+ to_string (result_status s)
+ {
+ switch (s)
+ {
+ case result_status::success: return "success";
+ case result_status::warning: return "warning";
+ case result_status::error: return "error";
+ case result_status::abort: return "abort";
+ case result_status::abnormal: return "abnormal";
+ }
+
+ assert (false);
+ return string ();
+ }
+
+ result_status
+ to_result_status (const string& s)
+ {
+ if (s == "success") return result_status::success;
+ else if (s == "warning") return result_status::warning;
+ else if (s == "error") return result_status::error;
+ else if (s == "abort") return result_status::abort;
+ else if (s == "abnormal") return result_status::abnormal;
+ else throw invalid_argument ("invalid result status '" + s + "'");
+ }
+
+ // Utility functions
+ //
+ 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;
+ }
+
+ inline static bool
+ valid_fingerprint (const string& f) noexcept
+ {
+ size_t n (f.size ());
+ if (n != 32 * 3 - 1)
+ return false;
+
+ for (size_t i (0); i < n; ++i)
+ {
+ char c (f[i]);
+ if ((i + 1) % 3 == 0)
+ {
+ if (c != ':')
+ return false;
+ }
+ else if (!isxdigit (c))
+ return false;
+ }
+
+ return true;
+ }
+
+ // machine_header_manifest
+ //
+ machine_header_manifest::
+ machine_header_manifest (parser& p, bool iu)
+ : machine_header_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 machine header manifest expected");
+ }
+
+ machine_header_manifest::
+ machine_header_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)
+ {
+ 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 header 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 == "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 header 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 (summary.empty ())
+ bad_value ("no machine summary specified");
+ }
+
+ void machine_header_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);
+ 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)
+ 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");
+
+ // Parse machine header manifests.
+ //
+ for (nv = p.next (); !nv.empty (); nv = p.next ())
+ machines.emplace_back (machine_header_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);
+
+ if (fingerprint)
+ s.next ("fingerprint", *fingerprint);
+
+ s.next ("", ""); // End of manifest.
+
+ for (const machine_header_manifest& m: machines)
+ 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 == "trust")
+ {
+ if (v != "yes" && !valid_fingerprint (v))
+ bad_value ("invalid repository certificate fingerprint");
+
+ trust.emplace_back (move (v));
+ }
+ 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
+ {
+ check_config (tf.value);
+ }
+ catch (const invalid_argument& e)
+ {
+ bad_value (string ("invalid task configuration: ") + e.what (),
+ tf.column - 1,
+ tl.line - 1);
+ }
+
+ config.emplace_back (move (tf.value));
+ }
+ }
+ }
+ 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 ());
+
+ for (const auto& v: trust)
+ s.next ("trust", v);
+
+ 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.
+ }
+
+ strings task_manifest::
+ unquoted_config () const
+ {
+ return string_parser::unquote (config);
+ }
+
+ void task_manifest::
+ check_config (const string& s)
+ {
+ auto i (s.begin ());
+ auto e (s.end ());
+
+ // Iterate until the variable name end and check that it contains no
+ // whitespaces.
+ //
+ for (; i != e; ++i)
+ {
+ char c (*i);
+
+ if (c == ' ' || c == '\t') // Whitespace in name.
+ throw invalid_argument ("expected variable assignment");
+ else if (c == '=')
+ break;
+ }
+
+ if (i == e)
+ throw invalid_argument ("no variable value");
+ }
+
+ // 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 (n == "result-url")
+ {
+ if (result_url)
+ bad_name ("task response result url redefinition");
+
+ if (v.empty ())
+ bad_value ("empty task response result url");
+
+ result_url = 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 may, and the result url
+ // must, be present, otherwise they shouldn't.
+ //
+ if (!session.empty ())
+ {
+ if (!result_url)
+ bad_value ("no task response result url specified");
+ }
+ else
+ {
+ if (challenge)
+ bad_value ("unexpected task response challenge");
+
+ if (result_url)
+ bad_value ("unexpected task response result url");
+ }
+
+ // 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);
+
+ if (result_url)
+ s.next ("result-url", *result_url);
+
+ 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
+ {
+ try
+ {
+ return to_result_status (v);
+ }
+ catch (const invalid_argument&)
+ {
+ bad_value ("invalid " + what);
+ }
+
+ // Can't be here. Would be redundant if it were possible to declare
+ // lambda with the [[noreturn]] attribute. Note that GCC (non-portably)
+ // supports that.
+ //
+ return result_status::abnormal;
+ };
+
+ // 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 ());
+ s.next ("status", to_string (status));
+
+ // Serialize *-status values.
+ //
+ for (const auto& r: results)
+ s.next (r.operation + "-status", to_string (r.status));
+
+ // 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)
+ 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");
+
+ 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);
+
+ if (challenge)
+ s.next ("challenge", *challenge);
+
+ s.next ("", ""); // End of manifest.
+
+ result.serialize (s);
+ s.next ("", ""); // End of stream.
+ }
+}
diff --git a/libbbot/manifest.hxx b/libbbot/manifest.hxx
new file mode 100644
index 0000000..615c4f9
--- /dev/null
+++ b/libbbot/manifest.hxx
@@ -0,0 +1,297 @@
+// file : libbbot/manifest.hxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef LIBBBOT_MANIFEST_HXX
+#define LIBBBOT_MANIFEST_HXX
+
+#include <string>
+#include <vector>
+#include <ostream>
+
+#include <butl/optional>
+#include <butl/target-triplet>
+#include <butl/manifest-forward>
+
+#include <bpkg/manifest> // version, repository_location
+
+#include <libbbot/export.hxx>
+#include <libbbot/version.hxx>
+
+namespace bbot
+{
+ using strings = std::vector<std::string>;
+
+ class LIBBBOT_EXPORT machine_header_manifest
+ {
+ public:
+ std::string id;
+ std::string name;
+ std::string summary;
+
+ machine_header_manifest (std::string i, std::string n, std::string s)
+ : id (std::move (i)), name (std::move (n)), summary (std::move (s)) {}
+
+ public:
+ machine_header_manifest () = default; // VC export.
+ machine_header_manifest (butl::manifest_parser&,
+ bool ignore_unknown = false);
+ machine_header_manifest (butl::manifest_parser&,
+ butl::manifest_name_value start,
+ bool ignore_unknown = false);
+
+ void
+ serialize (butl::manifest_serializer&) const;
+ };
+
+ using machine_header_manifests = std::vector<machine_header_manifest>;
+
+ 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
+ //
+ butl::optional<std::string> fingerprint;
+
+ machine_header_manifests machines;
+
+ task_request_manifest (std::string a,
+ butl::optional<std::string> f,
+ machine_header_manifests m)
+ : agent (std::move (a)),
+ fingerprint (std::move (f)),
+ machines (std::move (m)) {}
+
+ 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.
+
+ // The SHA256 repositories certificates fingerprints to trust. The special
+ // 'yes' value can be specified instead of fingerprint (in which case all
+ // repositories will be trusted without authentication).
+ //
+ strings trust;
+
+ // 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).
+ // Note: could be quoted.
+ //
+ strings config;
+
+ strings
+ unquoted_config () const;
+
+ task_manifest (std::string nm,
+ bpkg::version vr,
+ bpkg::repository_location rl,
+ strings tr,
+ std::string mn,
+ butl::optional<butl::target_triplet> tg,
+ strings cf)
+ : name (std::move (nm)),
+ version (std::move (vr)),
+ repository (std::move (rl)),
+ trust (tr),
+ machine (std::move (mn)),
+ target (std::move (tg)),
+ config (std::move (cf)) {}
+
+ 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;
+
+ // Check that a string has the name=value format. The name must not
+ // contain spaces. Throw invalid_argument if the string doesn't conform to
+ // these rules.
+ //
+ static void
+ check_config (const std::string&);
+ };
+
+ class LIBBBOT_EXPORT task_response_manifest
+ {
+ public:
+ // If empty then no task available.
+ //
+ std::string session;
+
+ // Challenge, result url and task are absent if session is empty.
+ //
+ butl::optional<std::string> challenge;
+ butl::optional<std::string> result_url;
+ butl::optional<task_manifest> task;
+
+ task_response_manifest (std::string s,
+ butl::optional<std::string> c,
+ butl::optional<std::string> u,
+ butl::optional<task_manifest> t)
+ : session (std::move (s)),
+ challenge (std::move (c)),
+ result_url (std::move (u)),
+ task (std::move (t)) {}
+
+ 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
+ };
+
+ LIBBBOT_EXPORT std::string
+ to_string (result_status);
+
+ LIBBBOT_EXPORT result_status
+ to_result_status (const std::string&); // May throw invalid_argument.
+
+ inline std::ostream&
+ operator<< (std::ostream& os, result_status s) {return os << to_string (s);}
+
+ 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;
+ }
+
+ // Return true if the result is "bad", that is, error or worse.
+ //
+ inline bool
+ operator! (result_status s)
+ {
+ return static_cast<std::uint8_t> (s) >=
+ static_cast<std::uint8_t> (result_status::error);
+ }
+
+ struct operation_result
+ {
+ std::string operation; // "configure", "update", "test", etc.
+ result_status status;
+ std::string log;
+ };
+
+ using operation_results = std::vector<operation_result>;
+
+ class LIBBBOT_EXPORT result_manifest
+ {
+ public:
+ // Built package.
+ //
+ // If the version is 0 (which signifies a stub package that cannot be
+ // possibly built) then both name and version are "unknown". This is used
+ // by the worker to signal abnormal termination before being able to
+ // obtain the package name/version.
+ //
+ 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 (std::string n,
+ bpkg::version v,
+ result_status s,
+ operation_results r)
+ : name (std::move (n)),
+ version (std::move (v)),
+ status (s),
+ results (std::move (r)) {}
+
+ public:
+ 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.
+
+ // The answer to challenge in the task response.
+ //
+ butl::optional<std::string> challenge;
+
+ result_manifest result;
+
+ result_request_manifest (std::string s,
+ butl::optional<std::string> c,
+ result_manifest r)
+ : session (std::move (s)),
+ challenge (std::move (c)),
+ result (std::move (r)) {}
+
+ public:
+ result_request_manifest () = default; // VC export.
+ result_request_manifest (butl::manifest_parser&,
+ bool ignore_unknown = false);
+
+ void
+ serialize (butl::manifest_serializer&) const;
+ };
+}
+
+#endif // LIBBBOT_MANIFEST_HXX
diff --git a/libbbot/version.hxx.in b/libbbot/version.hxx.in
new file mode 100644
index 0000000..6fca06c
--- /dev/null
+++ b/libbbot/version.hxx.in
@@ -0,0 +1,48 @@
+// file : libbbot/version.hxx.in -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef LIBBBOT_VERSION // Note: using the version macro itself.
+
+// Note: using build2 standard versioning scheme. The numeric version format
+// is AAABBBCCCDDDE where:
+//
+// AAA - major version number
+// BBB - minor version number
+// CCC - bugfix version number
+// DDD - alpha / beta (DDD + 500) version number
+// E - final (0) / snapshot (1)
+//
+// When DDDE is not 0, 1 is subtracted from AAABBBCCC. For example:
+//
+// Version AAABBBCCCDDDE
+//
+// 0.1.0 0000010000000
+// 0.1.2 0000010010000
+// 1.2.3 0010020030000
+// 2.2.0-a.1 0020019990010
+// 3.0.0-b.2 0029999995020
+// 2.2.0-a.1.z 0020019990011
+//
+#define LIBBBOT_VERSION $libbbot.version.project_number$ULL
+#define LIBBBOT_VERSION_STR "$libbbot.version.project$"
+#define LIBBBOT_VERSION_ID "$libbbot.version.project_id$"
+
+#define LIBBBOT_VERSION_MAJOR $libbbot.version.major$
+#define LIBBBOT_VERSION_MINOR $libbbot.version.minor$
+#define LIBBBOT_VERSION_PATCH $libbbot.version.patch$
+
+#define LIBBBOT_PRE_RELEASE $libbbot.version.pre_release$
+
+#define LIBBBOT_SNAPSHOT $libbbot.version.snapshot_sn$ULL
+#define LIBBBOT_SNAPSHOT_ID "$libbbot.version.snapshot_id$"
+
+#include <butl/version>
+
+$libbutl.check(LIBBUTL_VERSION, LIBBUTL_SNAPSHOT)$
+
+#include <bpkg/version>
+
+$libbpkg.check(LIBBPKG_VERSION, LIBBPKG_SNAPSHOT)$
+
+#endif // LIBBBOT_VERSION