From 1a1f5674dfbabf6a9b91933b15304acd7f3e64f0 Mon Sep 17 00:00:00 2001 From: Todd Gamblin Date: Sun, 1 Sep 2019 16:42:53 -0700 Subject: [PATCH] command: add `spack find --json` This is another machine-readable version of `spack find`. Supplying the `--json` argument causes specs to be written out as json records, easily filered with tools like jq. e.g.: $ spack find --json python | jq -C ".[] | { name, version } " [ { "name": "python", "version": "2.7.16" }, { "name": "bzip2", "version": "1.0.8" } ] --- lib/spack/spack/cmd/__init__.py | 19 +++++++++++++++++++ lib/spack/spack/cmd/find.py | 11 +++++++---- lib/spack/spack/environment.py | 1 + lib/spack/spack/spec.py | 25 +++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py index b4de2a88c3..4c80025e05 100644 --- a/lib/spack/spack/cmd/__init__.py +++ b/lib/spack/spack/cmd/__init__.py @@ -21,6 +21,7 @@ import spack.paths import spack.spec import spack.store +import spack.util.spack_json as sjson from spack.error import SpackError @@ -214,6 +215,24 @@ def display_formatted_specs(specs, format_string, deps=False): print(" " * depth, dep.format(format_string)) +def display_specs_as_json(specs, deps=False): + """Convert specs to a list of json records.""" + seen = set() + records = [] + for spec in specs: + if spec.dag_hash() not in seen: + seen.add(spec.dag_hash()) + records.append(spec.to_record_dict()) + + if deps: + for dep in spec.traverse(): + if dep.dag_hash() not in seen: + seen.add(spec.dag_hash()) + records.append(dep.to_record_dict()) + + sjson.dump(records, sys.stdout) + + def display_specs(specs, args=None, **kwargs): """Display human readable specs with customizable formatting. diff --git a/lib/spack/spack/cmd/find.py b/lib/spack/spack/cmd/find.py index ef300a83cd..afe8cf9b7c 100644 --- a/lib/spack/spack/cmd/find.py +++ b/lib/spack/spack/cmd/find.py @@ -2,6 +2,7 @@ # Spack Project Developers. See the top-level COPYRIGHT file for details. # # SPDX-License-Identifier: (Apache-2.0 OR MIT) + from __future__ import print_function import llnl.util.tty as tty @@ -12,7 +13,6 @@ import spack.repo import spack.cmd as cmd import spack.cmd.common.arguments as arguments -from spack.cmd import display_specs from spack.util.string import plural description = "list and search installed packages" @@ -36,6 +36,9 @@ def setup_parser(subparser): format_group.add_argument( "--format", action="store", default=None, help="output specs with the specified format string") + format_group.add_argument( + "--json", action="store_true", default=False, + help="output specs as machine-readable json records") # TODO: separate this entirely from the "mode" option -- it's # TODO: orthogonal, but changing it for all commands that use it with @@ -159,14 +162,14 @@ def display_env(env, args, decorator): else: tty.msg('Root specs') # TODO: Change this to not print extraneous deps and variants - display_specs( + cmd.display_specs( env.user_specs, args, decorator=lambda s, f: color.colorize('@*{%s}' % f)) print() if args.show_concretized: tty.msg('Concretized roots') - display_specs( + cmd.display_specs( env.specs_by_hash.values(), args, decorator=decorator) print() @@ -205,4 +208,4 @@ def find(parser, args): if env: display_env(env, args, decorator) tty.msg("%s" % plural(len(results), 'installed package')) - display_specs(results, args, decorator=decorator, all_headers=True) + cmd.display_specs(results, args, decorator=decorator, all_headers=True) diff --git a/lib/spack/spack/environment.py b/lib/spack/spack/environment.py index 1a279e4ea9..56b6fde6f3 100644 --- a/lib/spack/spack/environment.py +++ b/lib/spack/spack/environment.py @@ -952,6 +952,7 @@ def _shell_vars(self): ('PKG_CONFIG_PATH', ['lib/pkgconfig', 'lib64/pkgconfig']), ('CMAKE_PREFIX_PATH', ['']), ] + path_updates = list() if default_view_name in self.views: for var, subdirs in updates: diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py index d6cdc59924..3586cb2aae 100644 --- a/lib/spack/spack/spec.py +++ b/lib/spack/spack/spec.py @@ -1540,6 +1540,31 @@ def to_dict(self, hash=ht.dag_hash): return syaml_dict([('spec', node_list)]) + def to_record_dict(self): + """Return a "flat" dictionary with name and hash as top-level keys. + + This is similar to ``to_node_dict()``, but the name and the hash + are "flattened" into the dictionary for easiler parsing by tools + like ``jq``. Instead of being keyed by name or hash, the + dictionary "name" and "hash" fields, e.g.:: + + { + "name": "openssl" + "hash": "3ws7bsihwbn44ghf6ep4s6h4y2o6eznv" + "version": "3.28.0", + "arch": { + ... + } + + But is otherwise the same as ``to_node_dict()``. + + """ + dictionary = syaml_dict() + dictionary["name"] = self.name + dictionary["hash"] = self.dag_hash() + dictionary.update(self.to_node_dict()[self.name]) + return dictionary + def to_yaml(self, stream=None, hash=ht.dag_hash): return syaml.dump( self.to_dict(hash), stream=stream, default_flow_style=False)