From edfeacac8a8f08f3b022cc561cc992d5a12fcf51 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Wed, 2 Mar 2022 22:16:28 +0300 Subject: Add support for --stdout-format to bdep-status command --- bdep/common.cli | 76 ++++++++++++ bdep/config.cli | 4 +- bdep/options-types.hxx | 5 + bdep/status.cli | 72 ++++++++++- bdep/status.cxx | 322 +++++++++++++++++++++++++++++++++++------------- bdep/types-parsers.cxx | 17 +++ bdep/types-parsers.hxx | 13 ++ tests/status.testscript | 46 +++++++ 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 + { + "", + "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 { "", @@ -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 name; + bool default; + vector 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 - 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 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 name; + }; + + struct configuration_package_status + { + configuration configuration; + vector 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 // cout +#include + #include #include #include @@ -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& 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& 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& 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:: + 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 + { + 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: -- cgit v1.1