Added --json-output support for pkg list command

This commit is contained in:
Ivan Kravets
2024-03-29 20:46:32 +02:00
parent d9a5b9def3
commit 76b6de55d1
2 changed files with 199 additions and 102 deletions

View File

@@ -12,8 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json
import os import os
from typing import List
import click import click
@@ -21,13 +21,13 @@ from platformio import fs
from platformio.package.manager.library import LibraryPackageManager from platformio.package.manager.library import LibraryPackageManager
from platformio.package.manager.platform import PlatformPackageManager from platformio.package.manager.platform import PlatformPackageManager
from platformio.package.manager.tool import ToolPackageManager from platformio.package.manager.tool import ToolPackageManager
from platformio.package.meta import PackageItem, PackageSpec from platformio.package.meta import PackageInfo, PackageItem, PackageSpec
from platformio.platform.exception import UnknownPlatform from platformio.platform.exception import UnknownPlatform
from platformio.platform.factory import PlatformFactory from platformio.platform.factory import PlatformFactory
from platformio.project.config import ProjectConfig from platformio.project.config import ProjectConfig
@click.command("list", short_help="List installed packages") @click.command("list", short_help="List project packages")
@click.option( @click.option(
"-d", "-d",
"--project-dir", "--project-dir",
@@ -48,79 +48,116 @@ from platformio.project.config import ProjectConfig
@click.option("--only-platforms", is_flag=True, help="List only platform packages") @click.option("--only-platforms", is_flag=True, help="List only platform packages")
@click.option("--only-tools", is_flag=True, help="List only tool packages") @click.option("--only-tools", is_flag=True, help="List only tool packages")
@click.option("--only-libraries", is_flag=True, help="List only library packages") @click.option("--only-libraries", is_flag=True, help="List only library packages")
@click.option("--json-output", is_flag=True)
@click.option("-v", "--verbose", is_flag=True) @click.option("-v", "--verbose", is_flag=True)
def package_list_cmd(**options): def package_list_cmd(**options):
if options.get("global"): data = (
list_global_packages(options) list_global_packages(options)
if options.get("global")
else list_project_packages(options)
)
if options.get("json_output"):
return click.echo(_dump_to_json(data, options))
def _print_items(typex, items):
click.secho(typex.capitalize(), bold=True)
print_dependency_tree(items, verbose=options.get("verbose"))
click.echo()
if options.get("global"):
for typex, items in data.items():
_print_items(typex, items)
else: else:
list_project_packages(options) for env, env_data in data.items():
click.echo("Resolving %s dependencies..." % click.style(env, fg="cyan"))
for typex, items in env_data.items():
_print_items(typex, items)
return None
def humanize_package(pkg, spec=None, verbose=False): def _dump_to_json(data, options):
if spec and not isinstance(spec, PackageSpec): result = {}
spec = PackageSpec(spec)
data = [ if options.get("global"):
click.style(pkg.metadata.name, fg="cyan"), for typex, items in data.items():
click.style(f"@ {str(pkg.metadata.version)}", bold=True), result[typex] = [info.as_dict(with_manifest=True) for info in items]
] else:
extra_data = ["required: %s" % (spec.humanize() if spec else "Any")] for env, env_data in data.items():
if verbose: result[env] = {}
extra_data.append(pkg.path) for typex, items in env_data.items():
data.append("(%s)" % ", ".join(extra_data)) result[env][typex] = [
return " ".join(data) info.as_dict(with_manifest=True) for info in items
]
return json.dumps(result)
def print_dependency_tree(pm, specs=None, filter_specs=None, level=0, verbose=False): def build_package_info(pm, specs=None, filter_specs=None, resolve_dependencies=True):
filtered_pkgs = [ filtered_pkgs = [
pm.get_package(spec) for spec in filter_specs or [] if pm.get_package(spec) pm.get_package(spec) for spec in filter_specs if pm.get_package(spec)
] ]
candidates = {} candidates = []
if specs: if specs:
for spec in specs: for spec in specs:
pkg = pm.get_package(spec) candidates.append(
if not pkg: PackageInfo(
continue spec if isinstance(spec, PackageSpec) else PackageSpec(spec),
candidates[pkg.path] = (pkg, spec) pm.get_package(spec),
)
)
else: else:
candidates = {pkg.path: (pkg, pkg.metadata.spec) for pkg in pm.get_installed()} candidates = [PackageInfo(pkg.metadata.spec, pkg) for pkg in pm.get_installed()]
if not candidates: if not candidates:
return return []
candidates = sorted(candidates.values(), key=lambda item: item[0].metadata.name)
for index, (pkg, spec) in enumerate(candidates): candidates = sorted(
if filtered_pkgs and not _pkg_tree_contains(pm, pkg, filtered_pkgs): candidates,
continue key=lambda info: info.item.metadata.name if info.item else info.spec.humanize(),
printed_pkgs = pm.memcache_get("__printed_pkgs", []) )
if printed_pkgs and pkg.path in printed_pkgs:
continue
printed_pkgs.append(pkg.path)
pm.memcache_set("__printed_pkgs", printed_pkgs)
click.echo( result = []
"%s%s %s" for info in candidates:
% ( if filter_specs and (
"" * level, not info.item or not _pkg_tree_contains(pm, info.item, filtered_pkgs)
"├──" if index < len(candidates) - 1 else "└──", ):
humanize_package( continue
pkg, if not info.item:
spec=spec, if not info.spec.external and not info.spec.owner: # built-in library?
verbose=verbose, continue
result.append(info)
continue
visited_pkgs = pm.memcache_get("__visited_pkgs", [])
if visited_pkgs and info.item.path in visited_pkgs:
continue
visited_pkgs.append(info.item.path)
pm.memcache_set("__visited_pkgs", visited_pkgs)
result.append(
PackageInfo(
info.spec,
info.item,
(
build_package_info(
pm,
specs=[
pm.dependency_to_spec(item)
for item in pm.get_pkg_dependencies(info.item)
],
filter_specs=filter_specs,
resolve_dependencies=True,
)
if resolve_dependencies and pm.get_pkg_dependencies(info.item)
else []
), ),
) )
) )
dependencies = pm.get_pkg_dependencies(pkg) return result
if dependencies:
print_dependency_tree(
pm,
specs=[pm.dependency_to_spec(item) for item in dependencies],
filter_specs=filter_specs,
level=level + 1,
verbose=verbose,
)
def _pkg_tree_contains(pm, root: PackageItem, children: List[PackageItem]): def _pkg_tree_contains(pm, root: PackageItem, children: list[PackageItem]):
if root in children: if root in children:
return True return True
for dependency in pm.get_pkg_dependencies(root) or []: for dependency in pm.get_pkg_dependencies(root) or []:
@@ -139,6 +176,7 @@ def list_global_packages(options):
only_packages = any( only_packages = any(
options.get(typex) or options.get(f"only_{typex}") for (typex, _) in data options.get(typex) or options.get(f"only_{typex}") for (typex, _) in data
) )
result = {}
for typex, pm in data: for typex, pm in data:
skip_conds = [ skip_conds = [
only_packages only_packages
@@ -148,82 +186,115 @@ def list_global_packages(options):
] ]
if any(skip_conds): if any(skip_conds):
continue continue
click.secho(typex.capitalize(), bold=True) result[typex] = build_package_info(pm, filter_specs=options.get(typex))
print_dependency_tree(
pm, filter_specs=options.get(typex), verbose=options.get("verbose") return result
)
click.echo()
def list_project_packages(options): def list_project_packages(options):
environments = options["environments"] environments = options["environments"]
only_packages = any( only_filtered_packages = any(
options.get(typex) or options.get(f"only_{typex}") options.get(typex) or options.get(f"only_{typex}")
for typex in ("platforms", "tools", "libraries") for typex in ("platforms", "tools", "libraries")
) )
only_platform_packages = any( only_platform_package = options.get("platforms") or options.get("only_platforms")
options.get(typex) or options.get(f"only_{typex}") only_tool_packages = options.get("tools") or options.get("only_tools")
for typex in ("platforms", "tools")
)
only_library_packages = options.get("libraries") or options.get("only_libraries") only_library_packages = options.get("libraries") or options.get("only_libraries")
result = {}
with fs.cd(options["project_dir"]): with fs.cd(options["project_dir"]):
config = ProjectConfig.get_instance() config = ProjectConfig.get_instance()
config.validate(environments) config.validate(environments)
for env in config.envs(): for env in config.envs():
if environments and env not in environments: if environments and env not in environments:
continue continue
click.echo("Resolving %s dependencies..." % click.style(env, fg="cyan")) result[env] = {}
found = False if not only_filtered_packages or only_platform_package:
if not only_packages or only_platform_packages: result[env]["platforms"] = list_project_env_platform_package(
_found = print_project_env_platform_packages(env, options) env, options
found = found or _found )
if not only_packages or only_library_packages: if not only_filtered_packages or only_tool_packages:
_found = print_project_env_library_packages(env, options) result[env]["tools"] = list_project_env_tool_packages(env, options)
found = found or _found if not only_filtered_packages or only_library_packages:
if not found: result[env]["libraries"] = list_project_env_library_packages(
click.echo("No packages") env, options
if (not environments and len(config.envs()) > 1) or len(environments) > 1: )
click.echo()
return result
def print_project_env_platform_packages(project_env, options): def list_project_env_platform_package(project_env, options):
try: pm = PlatformPackageManager()
p = PlatformFactory.from_env(project_env) return build_package_info(
except UnknownPlatform: pm,
return None specs=[PackageSpec(pm.config.get(f"env:{project_env}", "platform"))],
click.echo( filter_specs=options.get("platforms"),
"Platform %s" resolve_dependencies=False,
% (
humanize_package(
PlatformPackageManager().get_package(p.get_dir()),
p.config.get(f"env:{project_env}", "platform"),
verbose=options.get("verbose"),
)
)
) )
print_dependency_tree(
def list_project_env_tool_packages(project_env, options):
try:
p = PlatformFactory.from_env(project_env, targets=["upload"])
except UnknownPlatform:
return []
return build_package_info(
p.pm, p.pm,
specs=[p.get_package_spec(name) for name in p.packages], specs=[
p.get_package_spec(name)
for name, options in p.packages.items()
if not options.get("optional")
],
filter_specs=options.get("tools"), filter_specs=options.get("tools"),
) )
click.echo()
return True
def print_project_env_library_packages(project_env, options): def list_project_env_library_packages(project_env, options):
config = ProjectConfig.get_instance() config = ProjectConfig.get_instance()
lib_deps = config.get(f"env:{project_env}", "lib_deps") lib_deps = config.get(f"env:{project_env}", "lib_deps")
lm = LibraryPackageManager( lm = LibraryPackageManager(
os.path.join(config.get("platformio", "libdeps_dir"), project_env) os.path.join(config.get("platformio", "libdeps_dir"), project_env)
) )
if not lib_deps or not lm.get_installed(): return build_package_info(
return None
click.echo("Libraries")
print_dependency_tree(
lm, lm,
lib_deps, lib_deps,
filter_specs=options.get("libraries"), filter_specs=options.get("libraries"),
verbose=options.get("verbose"),
) )
return True
def humanize_package(info, verbose=False):
data = (
[
click.style(info.item.metadata.name, fg="cyan"),
click.style(f"@ {str(info.item.metadata.version)}", bold=True),
]
if info.item
else ["Not installed"]
)
extra_data = ["required: %s" % (info.spec.humanize() if info.spec else "Any")]
if verbose and info.item:
extra_data.append(info.item.path)
data.append("(%s)" % ", ".join(extra_data))
return " ".join(data)
def print_dependency_tree(items, verbose=False, level=0):
for index, info in enumerate(items):
click.echo(
"%s%s %s"
% (
"" * level,
"├──" if index < len(items) - 1 else "└──",
humanize_package(
info,
verbose=verbose,
),
)
)
if info.dependencies:
print_dependency_tree(
info.dependencies,
verbose=verbose,
level=level + 1,
)

View File

@@ -23,7 +23,7 @@ import semantic_version
from platformio import fs from platformio import fs
from platformio.compat import get_object_members, hashlib_encode_data, string_types from platformio.compat import get_object_members, hashlib_encode_data, string_types
from platformio.package.manifest.parser import ManifestFileType from platformio.package.manifest.parser import ManifestFileType, ManifestParserFactory
from platformio.package.version import SemanticVersionError, cast_version_to_semver from platformio.package.version import SemanticVersionError, cast_version_to_semver
from platformio.util import items_in_list from platformio.util import items_in_list
@@ -561,3 +561,29 @@ class PackageItem:
break break
assert location assert location
return self.metadata.dump(os.path.join(location, self.METAFILE_NAME)) return self.metadata.dump(os.path.join(location, self.METAFILE_NAME))
def as_dict(self):
return {"path": self.path, "metadata": self.metadata.as_dict()}
class PackageInfo:
def __init__(self, spec: PackageSpec, item: PackageItem = None, dependencies=None):
assert isinstance(spec, PackageSpec)
self.spec = spec
self.item = item
self.dependencies = dependencies or []
def as_dict(self, with_manifest=False):
result = {
"spec": self.spec.as_dict(),
"item": self.item.as_dict() if self.item else None,
"dependencies": [d.as_dict() for d in self.dependencies],
}
if with_manifest:
result["manifest"] = (
ManifestParserFactory.new_from_dir(self.item.path).as_dict()
if self.item
else None
)
return result