aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2022-03-02 22:16:28 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2022-03-04 19:18:25 +0300
commitedfeacac8a8f08f3b022cc561cc992d5a12fcf51 (patch)
treebc1c0d62b94716fa83114f05d9f6812d41c42e0a
parent48f4687510f9c7ad2b0c2baff3b14a181201f221 (diff)
Add support for --stdout-format to bdep-status command
-rw-r--r--bdep/common.cli76
-rw-r--r--bdep/config.cli4
-rw-r--r--bdep/options-types.hxx5
-rw-r--r--bdep/status.cli72
-rw-r--r--bdep/status.cxx322
-rw-r--r--bdep/types-parsers.cxx17
-rw-r--r--bdep/types-parsers.hxx13
-rw-r--r--tests/status.testscript46
8 files changed, 466 insertions, 89 deletions
diff --git a/bdep/common.cli b/bdep/common.cli
index cdf0e2c..1e95084 100644
--- a/bdep/common.cli
+++ b/bdep/common.cli
@@ -78,6 +78,14 @@ namespace bdep
\li|Even more detailed information.||"
}
+ bdep::stdout_format --stdout-format = bdep::stdout_format::lines
+ {
+ "<format>",
+ "Representation format to use for printing to \cb{stdout}. Valid values
+ for this option are \cb{lines} (default) and \cb{json}. See the JSON
+ OUTPUT section below for details on the \cb{json} format."
+ }
+
size_t --jobs|-j
{
"<num>",
@@ -231,4 +239,72 @@ namespace bdep
"Don't load default options files."
}
};
+
+ {
+ "",
+ "
+ \h|JSON OUTPUT|
+
+ Commands that support the JSON output specify their formats as a
+ serialized representation of a C++ \cb{struct} or an array thereof. For
+ example:
+
+ \
+ struct package
+ {
+ string name;
+ };
+
+ struct configuration
+ {
+ uint64_t id;
+ string path;
+ optional<string> name;
+ bool default;
+ vector<package> packages;
+ };
+ \
+
+ An example of the serialized JSON representation of \cb{struct}
+ \cb{configuration}:
+
+ \
+ {
+ \"id\": 1,
+ \"path\": \"/tmp/hello-gcc\",
+ \"name\": \"gcc\",
+ \"default\": true,
+ \"packages\": [
+ {
+ \"name\": \"hello\"
+ }
+ ]
+ }
+ \
+
+ This sections provides details on the overall properties of such formats
+ and the semantics of the \cb{struct} serialization.
+
+ The order of members in a JSON object is fixed as specified in the
+ corresponding \cb{struct}. While new members may be added in the
+ future (and should be ignored by older consumers), the semantics of the
+ existing members (including whether the top-level entry is an object or
+ array) may not change.
+
+ An object member is required unless its type is \cb{optional<>},
+ \cb{bool}, or \cb{vector<>} (array). For \cb{bool} members absent means
+ \cb{false}. For \cb{vector<>} members absent means empty. An empty
+ top-level array is always present.
+
+ For example, the following JSON text is a possible serialization of
+ the above \cb{struct} \cb{configuration}:
+
+ \
+ {
+ \"id\": 1,
+ \"path\": \"/tmp/hello-gcc\"
+ }
+ \
+ "
+ }
}
diff --git a/bdep/config.cli b/bdep/config.cli
index 7181ebc..ac67a54 100644
--- a/bdep/config.cli
+++ b/bdep/config.cli
@@ -132,8 +132,8 @@ namespace bdep
The \cb{list} subcommand prints the list of build configurations
associated with the project. Unless one or more configurations are
specified explicitly, \cb{list} prints all the associate
- configurations. Note that the output is written to \cb{STDOUT}, not
- \cb{STDERR}.|
+ configurations. Note that the output is written to \cb{stdout}, not
+ \cb{stderr}.|
\li|\cb{move}
diff --git a/bdep/options-types.hxx b/bdep/options-types.hxx
index fffb2a4..3c2a792 100644
--- a/bdep/options-types.hxx
+++ b/bdep/options-types.hxx
@@ -6,6 +6,11 @@
namespace bdep
{
+ enum class stdout_format
+ {
+ lines,
+ json
+ };
}
#endif // BDEP_OPTIONS_TYPES_HXX
diff --git a/bdep/status.cli b/bdep/status.cli
index 38e70e0..7abd7f3 100644
--- a/bdep/status.cli
+++ b/bdep/status.cli
@@ -42,10 +42,76 @@ namespace bdep
\c{\b{--immediate}|\b{-i}} or \c{\b{--recursive}|\b{-r}} options,
respectively.
- The status of each package is printed on a separate line. Note that the
- status is written to \cb{STDOUT}, not \cb{STDERR}. The semantics of
- <dep-spec> and the format of the status line are described in
+ In the default output format (see the \cb{--stdout-format} common
+ option), the status of each package is printed on a separate line. Note
+ that the status is written to \cb{stdout}, not \cb{stderr}. The semantics
+ of <dep-spec> and the format of the status line are described in
\l{bpkg-pkg-status(1)}.
+
+ If the output format is \cb{json}, then the output is a JSON array of
+ objects which are the serialized representation of the following C++
+ \cb{struct} \cb{configuration_package_status}:
+
+ \
+ struct configuration
+ {
+ uint64_t id;
+ string path;
+ optional<string> name;
+ };
+
+ struct configuration_package_status
+ {
+ configuration configuration;
+ vector<package_status> packages;
+ };
+ \
+
+ For example:
+
+ \
+ [
+ {
+ \"configuration\": {
+ \"id\": 1,
+ \"path\": \"/tmp/hello-gcc\",
+ \"name\": \"gcc\"
+ },
+ \"packages\": [
+ {
+ \"name\": \"hello\",
+ \"status\": \"configured\",
+ \"version\": \"1.0.0\",
+ \"hold_package\": true,
+ \"available_versions\": [
+ {
+ \"version\": \"1.0.1\"
+ },
+ {
+ \"version\": \"2.0.0\"
+ }
+ ],
+ \"dependencies\": [
+ {
+ \"name\": \"libhello\",
+ \"status\": \"configured\",
+ \"version\": \"1.0.2\"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ \
+
+ See the JSON OUTPUT section in \l{bdep-common-options(1)} for details on
+ the overall properties of this format and the semantics of the
+ \cb{struct} serialization.
+
+ Refer to the \cb{list} subcommand of \l{bdep-config(1)} for details on
+ the \cb{struct} \cb{configuration} members. Refer to
+ \l{bpkg-pkg-status(1)} for the definition of \cb{struct}
+ \cb{package_status}.
"
}
diff --git a/bdep/status.cxx b/bdep/status.cxx
index b64a9a4..d932e58 100644
--- a/bdep/status.cxx
+++ b/bdep/status.cxx
@@ -5,6 +5,8 @@
#include <iostream> // cout
+#include <libbutl/json/serializer.hxx>
+
#include <bdep/project.hxx>
#include <bdep/database.hxx>
#include <bdep/diagnostics.hxx>
@@ -15,12 +17,39 @@ using namespace std;
namespace bdep
{
- static void
- cmd_status (const cmd_status_options& o,
- const dir_path& prj,
- const dir_path& cfg,
- const strings& pkgs,
- bool fetch)
+ // If the specified package list is not empty, then return only those
+ // packages which are initialized in the specified configuration. Otherwise,
+ // return all packages that have been initialized in this configuration.
+ //
+ static strings
+ config_packages (const configuration& cfg, const package_locations& pkgs)
+ {
+ strings r;
+
+ bool all (pkgs.empty ());
+ for (const package_state& s: cfg.packages)
+ {
+ if (all ||
+ find_if (pkgs.begin (),
+ pkgs.end (),
+ [&s] (const package_location& p)
+ {
+ return p.name == s.name;
+ }) != pkgs.end ())
+ r.push_back (s.name.string ());
+ }
+
+ return r;
+ }
+
+ static process
+ start_bpkg_status (const cmd_status_options& o,
+ int out,
+ const dir_path& prj,
+ const dir_path& cfg,
+ const strings& pkgs,
+ bool fetch,
+ const char* format)
{
// Shallow fetch the project to make sure we show latest iterations and
// pick up any new repositories.
@@ -38,16 +67,202 @@ namespace bdep
// Don't show the hold status since the only packages that will normally
// be held are the project's. But do show dependency constraints.
//
- run_bpkg (2,
- o,
- "status",
- "-d", cfg,
- "--no-hold",
- "--constraint",
- (o.old_available () ? "--old-available" : nullptr),
- (o.immediate () ? "--immediate" :
- o.recursive () ? "--recursive" : nullptr),
- pkgs);
+ return start_bpkg (2 /* verbosity */,
+ o,
+ out,
+ 2 /* stderr */,
+ "status",
+ "-d", cfg,
+ "--no-hold",
+ "--constraint",
+ (o.old_available () ? "--old-available" : nullptr),
+ (o.immediate () ? "--immediate" :
+ o.recursive () ? "--recursive" : nullptr),
+ "--stdout-format", format,
+ pkgs);
+ }
+
+ static void
+ cmd_status_lines (const cmd_status_options& o,
+ const project_packages& prj_pkgs,
+ const configurations& cfgs,
+ const strings& dep_pkgs)
+ {
+ tracer trace ("status_lines");
+
+ // Print status in each configuration, skipping fetching repositories in
+ // those where no package statuses needs to be printed.
+ //
+ bool first (true);
+ for (const shared_ptr<configuration>& c: cfgs)
+ {
+ // Collect the packages to print, unless the dependency packages are
+ // specified.
+ //
+ strings pkgs;
+
+ if (dep_pkgs.empty ())
+ pkgs = config_packages (*c, prj_pkgs.packages);
+
+ // If we are printing multiple configurations, separate them with a
+ // blank line and print the configuration name/directory.
+ //
+ if (verb && cfgs.size () > 1)
+ {
+ cout << (first ? "" : "\n")
+ << "in configuration " << *c << ':' << endl;
+
+ first = false;
+ }
+
+ if (!c->packages.empty () && (!pkgs.empty () || !dep_pkgs.empty ()))
+ {
+ const dir_path& prj (prj_pkgs.project);
+
+ bool fetch (o.fetch () || o.fetch_full ());
+
+ if (fetch)
+ cmd_fetch (o, prj, c, o.fetch_full ());
+
+ // Status for either packages or their dependencies must be printed,
+ // but not for both.
+ //
+ assert (pkgs.empty () == !dep_pkgs.empty ());
+
+ process pr (start_bpkg_status (o,
+ 1 /* stdout */,
+ prj,
+ c->path,
+ !pkgs.empty () ? pkgs : dep_pkgs,
+ !fetch,
+ "lines"));
+
+ finish_bpkg (o, pr);
+ }
+ else
+ {
+ if (verb)
+ {
+ diag_record dr (info);
+
+ if (c->packages.empty ())
+ dr << "no packages ";
+ else
+ dr << "none of specified packages ";
+
+ dr << "initialized in configuration " << *c << ", skipping";
+ }
+ }
+ }
+ }
+
+ static void
+ cmd_status_json (const cmd_status_options& o,
+ const project_packages& prj_pkgs,
+ const configurations& cfgs,
+ const strings& dep_pkgs)
+ {
+ tracer trace ("status_json");
+
+ butl::json::stream_serializer ss (cout);
+
+ ss.begin_array ();
+
+ // Print status in each configuration, skipping fetching repositories in
+ // those where no package statuses need to be printed.
+ //
+ for (const shared_ptr<configuration>& c: cfgs)
+ {
+ // Collect the packages to print, unless the dependency packages are
+ // specified.
+ //
+ strings pkgs;
+
+ if (dep_pkgs.empty ())
+ pkgs = config_packages (*c, prj_pkgs.packages);
+
+ ss.begin_object ();
+ ss.member_name ("configuration");
+ ss.begin_object ();
+ ss.member ("id", *c->id);
+ ss.member ("path", c->path.string ());
+
+ if (c->name)
+ ss.member ("name", *c->name);
+
+ ss.end_object ();
+
+ if (!c->packages.empty () && (!pkgs.empty () || !dep_pkgs.empty ()))
+ {
+ const dir_path& prj (prj_pkgs.project);
+
+ bool fetch (o.fetch () || o.fetch_full ());
+
+ if (fetch)
+ cmd_fetch (o, prj, c, o.fetch_full ());
+
+ // Status for either packages or their dependencies must be printed,
+ // but not for both.
+ //
+ assert (pkgs.empty () == !dep_pkgs.empty ());
+
+ // Save the JSON representation of package statuses into a string from
+ // bpkg-status' stdout and use it as a pre-serialized value for the
+ // packages member of the configuration packages status object.
+ //
+ string ps;
+
+ fdpipe pipe (open_pipe ()); // Text mode seems appropriate.
+
+ process pr (start_bpkg_status (o,
+ pipe.out.get (),
+ prj,
+ c->path,
+ !pkgs.empty () ? pkgs : dep_pkgs,
+ !fetch,
+ "json"));
+
+ // Shouldn't throw, unless something is severely damaged.
+ //
+ pipe.out.close ();
+
+ bool io (false);
+ try
+ {
+ ifdstream is (move (pipe.in));
+ ps = is.read_text ();
+ is.close ();
+ }
+ catch (const io_error&)
+ {
+ // Presumably the child process failed and issued diagnostics so let
+ // finish_bpkg() try to deal with that first.
+ //
+ io = true;
+ }
+
+ finish_bpkg (o, pr, io);
+
+ // Trim the trailing newline, which must be present. Let's however
+ // check that it is, for good measure.
+ //
+ if (!ps.empty () && ps.back () == '\n')
+ ps.pop_back ();
+
+ ss.member_name ("packages");
+ ss.value_json_text (ps);
+ }
+ else
+ {
+ // Not that unlike in the lines output we don't tell the user that
+ // there are no packages.
+ }
+
+ ss.end_object ();
+ }
+
+ ss.end_array ();
+ cout << endl;
}
int
@@ -90,79 +305,18 @@ namespace bdep
cfgs = move (cs.first);
}
- // Print status in each configuration, skipping those where no package
- // statuses needs to be printed.
- //
- bool first (true);
- for (const shared_ptr<configuration>& c: cfgs)
+ switch (o.stdout_format ())
{
- // Collect the packages to print, unless the dependency packages are
- // specified.
- //
- // If no packages were explicitly specified, then we print the status
- // for all that have been initialized in the configuration. Otherwise,
- // only for specified packages initialized in the (specified)
- // configurations.
- //
- strings pkgs;
-
- if (dep_pkgs.empty ())
+ case stdout_format::lines:
{
- const package_locations& ps (pp.packages);
- bool all (ps.empty ());
-
- for (const package_state& s: c->packages)
- {
- if (all ||
- find_if (ps.begin (),
- ps.end (),
- [&s] (const package_location& p)
- {
- return p.name == s.name;
- }) != ps.end ())
- pkgs.push_back (s.name.string ());
- }
+ cmd_status_lines (o, pp, cfgs, dep_pkgs);
+ break;
}
-
- // If we are printing multiple configurations, separate them with a
- // blank line and print the configuration name/directory.
- //
- if (verb && cfgs.size () > 1)
+ case stdout_format::json:
{
- cout << (first ? "" : "\n")
- << "in configuration " << *c << ':' << endl;
-
- first = false;
+ cmd_status_json (o, pp, cfgs, dep_pkgs);
+ break;
}
-
- if (c->packages.empty () || (pkgs.empty () && dep_pkgs.empty ()))
- {
- if (verb)
- {
- diag_record dr (info);
-
- if (c->packages.empty ())
- dr << "no packages ";
- else
- dr << "none of specified packages ";
-
- dr << "initialized in configuration " << *c << ", skipping";
- }
-
- continue;
- }
-
- bool fetch (o.fetch () || o.fetch_full ());
-
- if (fetch)
- cmd_fetch (o, prj, c, o.fetch_full ());
-
- // Status for either packages or their dependencies must be printed, but
- // not for both.
- //
- assert (pkgs.empty () == !dep_pkgs.empty ());
-
- cmd_status (o, prj, c->path, !pkgs.empty () ? pkgs : dep_pkgs, !fetch);
}
return 0;
diff --git a/bdep/types-parsers.cxx b/bdep/types-parsers.cxx
index 7707b3c..8f693fa 100644
--- a/bdep/types-parsers.cxx
+++ b/bdep/types-parsers.cxx
@@ -68,5 +68,22 @@ namespace bdep
xs = true;
parse_path (x, s);
}
+
+ void parser<stdout_format>::
+ parse (stdout_format& r, bool& xs, scanner& s)
+ {
+ const char* o (s.next ());
+
+ if (!s.more ())
+ throw missing_value (o);
+
+ string v (s.next ());
+
+ if (v == "lines") r = stdout_format::lines;
+ else if (v == "json") r = stdout_format::json;
+ else throw invalid_value (o, v);
+
+ xs = true;
+ }
}
}
diff --git a/bdep/types-parsers.hxx b/bdep/types-parsers.hxx
index 9e76237..3da355c 100644
--- a/bdep/types-parsers.hxx
+++ b/bdep/types-parsers.hxx
@@ -48,6 +48,19 @@ namespace bdep
static void
merge (dir_path& b, const dir_path& a) {b = a;}
};
+
+ template <>
+ struct parser<stdout_format>
+ {
+ static void
+ parse (stdout_format&, bool&, scanner&);
+
+ static void
+ merge (stdout_format& b, const stdout_format& a)
+ {
+ b = a;
+ }
+ };
}
}
diff --git a/tests/status.testscript b/tests/status.testscript
index bf4339b..2977e4f 100644
--- a/tests/status.testscript
+++ b/tests/status.testscript
@@ -30,6 +30,23 @@ deinit += -d prj
$* >'prj configured 0.1.0-a.0.19700101000000';
+ $* --stdout-format 'json' >>/~"%EOO%";
+ [
+ {
+ "configuration": {
+ "id": 1,
+ "path": "$~/prj-cfg",
+ "name": "cfg"
+ },
+ "packages": [
+ {
+ %.+
+ }
+ ]
+ }
+ ]
+ EOO
+
$deinit 2>>/"EOE"
deinitializing in project $~/prj/
synchronizing:
@@ -55,6 +72,35 @@ deinit += -d prj
prj configured 0.1.0-a.0.19700101000000
EOO
+ $* --all --stdout-format 'json' >>/~"%EOO%";
+ [
+ {
+ "configuration": {
+ "id": 1,
+ "path": "$~/prj-cfg1",
+ "name": "cfg1"
+ },
+ "packages": [
+ {
+ %.+
+ }
+ ]
+ },
+ {
+ "configuration": {
+ "id": 2,
+ "path": "$~/prj-cfg2",
+ "name": "cfg2"
+ },
+ "packages": [
+ {
+ %.+
+ }
+ ]
+ }
+ ]
+ EOO
+
$deinit 2>>/"EOE"
deinitializing in project $~/prj/
synchronizing: