blob: abcb6c701848fccb1d17fa16e2c1d3401556beca [file] [log] [blame]
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""The `cros query` CLI.
This is merely a lightweight wrapper around lib/build_query.py, which,
in turn, lets lib/portage_util.py do much of the heavy lifting. Go to
those files if you want to see how the guts of the query logic works.
"""
import collections
import logging
from typing import Any, Callable
from chromite.cli import command
from chromite.lib import build_query
from chromite.lib import commandline
from chromite.lib import constants
# A minimal set of globals for -f expression evaluation. We want to give people
# some capabilities, and we cannot stop them from accessing larger sets (eval is
# not safe), but this command line isn't used in a secure context: the input is
# from users executing code on their own machine. So our only goal here is
# to limit the API boundary of things that you might normally do.
_GLOBALS = {
"BAD": build_query.Stability.BAD,
"Board": build_query.Board,
"Ebuild": build_query.Ebuild,
"Overlay": build_query.Overlay,
"Profile": build_query.Profile,
"Query": build_query.Query,
"STABLE": build_query.Stability.STABLE,
"UNSPECIFIED": build_query.Stability.UNSPECIFIED,
"UNSTABLE": build_query.Stability.UNSTABLE,
"__builtins__": {},
"all": all,
"any": any,
"bool": bool,
"dict": dict,
"int": int,
"len": len,
"list": list,
"max": max,
"min": min,
"set": set,
"sorted": sorted,
"str": str,
"sum": sum,
"tuple": tuple,
}
class ObjectMapping(collections.abc.Mapping):
"""Provides a dictionary view of any object.
For example, given an object x and ObjectMapping y, x.attr can be
referenced as y["attr"].
"""
def __init__(self, obj: Any) -> None:
self._obj = obj
def __getitem__(self, item):
try:
return getattr(self._obj, item)
except AttributeError as e:
# The contract for __getitem__ expects a KeyError. Translate.
raise KeyError from e
def __iter__(self):
return iter(dir(self._obj))
def __len__(self) -> int:
return len(dir(self._obj))
def compile_filter(arg: str) -> Callable[[build_query.QueryTarget], bool]:
"""Take a command-line filter argument and compile it to a function.
Args:
arg: A command-line Python string.
Returns:
A callable which takes a QueryTarget and returns True when the
filter accepts, and False when the filter rejects.
"""
code = compile(arg, "<command_line>", "eval")
def _result(query_result: build_query.QueryTarget) -> bool:
mapping = ObjectMapping(query_result)
try:
# pylint: disable=eval-used
return bool(eval(code, _GLOBALS, mapping))
except Exception:
logging.error(
"Failed to evaluate expression %r on %r.", arg, query_result
)
raise
return _result
def compile_formatter(arg: str) -> Callable[[build_query.QueryTarget], str]:
"""Take a command-line output format argument and compile it to a function.
Args:
arg: A command-line Python formatting string.
Returns:
A callable which takes a QueryTarget and returns a string with the
formatted output.
"""
f_string = f"f{arg!r}"
code = compile(f_string, "<command_line>", "eval")
def _result(query_result: build_query.QueryTarget) -> str:
mapping = ObjectMapping(query_result)
try:
# pylint: disable=eval-used
return eval(code, _GLOBALS, mapping)
except Exception:
logging.error(
"Failed to evaluate f-string %s on %r.", f_string, query_result
)
raise
return _result
def tree_result(
result: build_query.QueryTarget,
fmt: Callable[[build_query.QueryTarget], str],
) -> None:
"""Output a tree of the result.
Args:
result: An object returned from a query.
fmt: The formatter function to use.
"""
def _rec(item, prefix, indent) -> None:
# If the child is not of the result type, we cannot use the
# user-provided format string, as it's specific to the output result's
# type.
fmt_item = fmt if isinstance(item, type(result)) else str
print(f"{indent[:-len(prefix)]}{prefix}{fmt_item(item)}")
children = list(item.tree())
if children:
for child in children[:-1]:
_rec(child, "├─", indent + "│ ")
_rec(children[-1], "╰─", indent + " ")
_rec(result, "", "")
QUERY_TARGETS = {
"boards": build_query.Board,
"ebuilds": build_query.Ebuild,
"overlays": build_query.Overlay,
"profiles": build_query.Profile,
}
@command.command_decorator("query")
class QueryCommand(command.CliCommand):
"""Query information from the build system."""
@classmethod
def AddParser(cls, parser: commandline.ArgumentParser):
"""Build the parser.
Args:
parser: The parser.
"""
super().AddParser(parser)
parser.add_argument(
"query_target",
choices=list(QUERY_TARGETS.values()),
metavar=f"{{{','.join(QUERY_TARGETS)}}}",
type=QUERY_TARGETS.get,
help="Target type to query.",
)
parser.add_argument(
"-o",
"--format",
type=compile_formatter,
default=str,
help="Output format.",
)
parser.add_argument(
"-f",
"--filter",
dest="filters",
action="append",
default=[],
help="Filter outputs with expressions.",
)
parser.add_argument(
"-t",
"--tree",
action="store_true",
help="Provide a tree-like output format for some types.",
)
parser.add_argument(
"-b",
"--board",
"--build-target",
help="Limit overlays to only the overlays for this board.",
)
parser.add_argument(
"--public",
action="store_const",
dest="overlays",
const=constants.PUBLIC_OVERLAYS,
default=constants.BOTH_OVERLAYS,
help=(
"Artificially limit overlays to only public overlays, even if "
"running in a private checkout."
),
)
parser.epilog = """Examples:
Get a list of all boards known to the build system:
cros query boards
Show only boards that have the "bootimage" USE flag set:
cros query boards -f '"bootimage" in use_flags'
For each board, show the path to its top level overlay:
cros query boards -o '{name} {top_level_overlay}'
Show the computed global USE flags for the "volteer" board:
cros query boards -f 'name == "volteer"' -o '{use_flags}'
Show which profiles modify (set or unset) the "bootimage" USE flag:
cros query profiles -f '"bootimage" in use_flags_set | use_flags_unset'
Show which ebuilds IUSE the "bootimage" flag:
cros query ebuilds -f '"bootimage" in iuse'
Show all ebuilds which inherit python-r1 and have EAPI <= 6:
cros query ebuilds -f '"python-r1" in eclasses' -f 'eapi <= 6'
"""
return parser
def Run(self) -> None:
query = build_query.Query(
self.options.query_target,
board=self.options.board,
overlays=self.options.overlays,
)
for filter_code in self.options.filters:
query = query.filter(compile_filter(filter_code))
for result in query:
if self.options.tree:
tree_result(result, self.options.format)
else:
print(self.options.format(result))