From 76246456268b240a5556ce9b6e38f4bdaa51194b Mon Sep 17 00:00:00 2001 From: Ivan Kravets Date: Thu, 24 Mar 2022 14:17:18 +0200 Subject: [PATCH] Implement `pio pkg list` command // Issue #3373 --- docs | 2 +- platformio/commands/pkg.py | 2 + platformio/package/commands/list.py | 219 ++++++++++++++++++++++++++++ tests/commands/pkg/test_list.py | 145 ++++++++++++++++++ 4 files changed, 367 insertions(+), 1 deletion(-) create mode 100644 platformio/package/commands/list.py create mode 100644 tests/commands/pkg/test_list.py diff --git a/docs b/docs index cbf179f8..a04cdef3 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit cbf179f826faa674846da782cbf3917a9f0a475f +Subproject commit a04cdef33cd508420fd837a7baf09cebc738faad diff --git a/platformio/commands/pkg.py b/platformio/commands/pkg.py index 1ec965ba..0ec9f411 100644 --- a/platformio/commands/pkg.py +++ b/platformio/commands/pkg.py @@ -16,6 +16,7 @@ import click from platformio.package.commands.exec import package_exec_cmd from platformio.package.commands.install import package_install_cmd +from platformio.package.commands.list import package_list_cmd from platformio.package.commands.outdated import package_outdated_cmd from platformio.package.commands.pack import package_pack_cmd from platformio.package.commands.publish import package_publish_cmd @@ -29,6 +30,7 @@ from platformio.package.commands.update import package_update_cmd commands=[ package_exec_cmd, package_install_cmd, + package_list_cmd, package_outdated_cmd, package_pack_cmd, package_publish_cmd, diff --git a/platformio/package/commands/list.py b/platformio/package/commands/list.py new file mode 100644 index 00000000..bd84c3de --- /dev/null +++ b/platformio/package/commands/list.py @@ -0,0 +1,219 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from typing import List + +import click + +from platformio import fs +from platformio.package.manager.library import LibraryPackageManager +from platformio.package.manager.platform import PlatformPackageManager +from platformio.package.manager.tool import ToolPackageManager +from platformio.package.meta import PackageItem, PackageSpec +from platformio.platform.factory import PlatformFactory +from platformio.project.config import ProjectConfig + + +@click.command("list", short_help="List installed packages") +@click.option( + "-d", + "--project-dir", + default=os.getcwd, + type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True), +) +@click.option("-e", "--environment", "environments", multiple=True) +@click.option("-p", "--platform", "platforms", metavar="SPECIFICATION", multiple=True) +@click.option("-t", "--tool", "tools", metavar="SPECIFICATION", multiple=True) +@click.option("-l", "--library", "libraries", metavar="SPECIFICATION", multiple=True) +@click.option("-g", "--global", is_flag=True, help="List globally installed packages") +@click.option( + "--storage-dir", + default=None, + type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True), + help="Custom Package Manager storage for global 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-libraries", is_flag=True, help="List only library packages") +@click.option("-v", "--verbose", is_flag=True) +def package_list_cmd(**options): + if options.get("global"): + list_global_packages(options) + else: + list_project_packages(options) + + +def humanize_package(pkg, spec=None, verbose=False): + if spec and not isinstance(spec, PackageSpec): + spec = PackageSpec(spec) + data = [click.style("{name}@{version}".format(**pkg.metadata.as_dict()), fg="cyan")] + extra_data = ["required: %s" % (spec.humanize() if spec else "Any")] + if verbose: + extra_data.append(pkg.path) + data.append("(%s)" % ", ".join(extra_data)) + return " ".join(data) + + +def print_dependency_tree(pm, specs=None, filter_specs=None, level=0, verbose=False): + filtered_pkgs = [ + pm.get_package(spec) for spec in filter_specs or [] if pm.get_package(spec) + ] + candidates = {} + if specs: + for spec in specs: + pkg = pm.get_package(spec) + if not pkg: + continue + candidates[pkg.path] = (pkg, spec) + else: + candidates = {pkg.path: (pkg, pkg.metadata.spec) for pkg in pm.get_installed()} + if not candidates: + return + candidates = sorted(candidates.values(), key=lambda item: item[0].metadata.name) + for index, (pkg, spec) in enumerate(candidates): + if filtered_pkgs and not _pkg_tree_contains(pm, pkg, filtered_pkgs): + continue + dependencies = pm.get_pkg_dependencies(pkg) + click.echo( + "%s%s %s" + % ( + "│ " * level, + "├──" if index < len(candidates) - 1 else "└──", + humanize_package( + pkg, + spec=spec, + verbose=verbose, + ), + ) + ) + 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]): + if root in children: + return True + for dependency in pm.get_pkg_dependencies(root) or []: + pkg = pm.get_package(pm.dependency_to_spec(dependency)) + if pkg and _pkg_tree_contains(pm, pkg, children): + return True + return False + + +def list_global_packages(options): + data = [ + ("platforms", PlatformPackageManager(options.get("storage_dir"))), + ("tools", ToolPackageManager(options.get("storage_dir"))), + ("libraries", LibraryPackageManager(options.get("storage_dir"))), + ] + only_packages = any( + options.get(type_) or options.get(f"only_{type_}") for (type_, _) in data + ) + for (type_, pm) in data: + skip_conds = [ + only_packages + and not options.get(type_) + and not options.get(f"only_{type_}"), + not pm.get_installed(), + ] + if any(skip_conds): + continue + click.secho(type_.capitalize(), bold=True) + print_dependency_tree( + pm, filter_specs=options.get(type_), verbose=options.get("verbose") + ) + click.echo() + + +def list_project_packages(options): + environments = options["environments"] + only_packages = any( + options.get(type_) or options.get(f"only_{type_}") + for type_ in ("platforms", "tools", "libraries") + ) + only_platform_packages = any( + options.get(type_) or options.get(f"only_{type_}") + for type_ in ("platforms", "tools") + ) + only_library_packages = options.get("libraries") or options.get(f"only_libraries") + + with fs.cd(options["project_dir"]): + config = ProjectConfig.get_instance() + config.validate(environments) + for env in config.envs(): + if environments and env not in environments: + continue + click.echo( + "Resolving %s environment packages..." % click.style(env, fg="cyan") + ) + found = False + if not only_packages or only_platform_packages: + _found = print_project_env_platform_packages(env, options) + found = found or _found + if not only_packages or only_library_packages: + _found = print_project_env_library_packages(env, options) + found = found or _found + if not found: + click.echo("No packages") + if (not environments and len(config.envs()) > 1) or len(environments) > 1: + click.echo() + + +def print_project_env_platform_packages(project_env, options): + config = ProjectConfig.get_instance() + platform = config.get(f"env:{project_env}", "platform") + if not platform: + return None + pkg = PlatformPackageManager().get_package(platform) + if not pkg: + return None + click.echo( + "Platform %s" + % (humanize_package(pkg, platform, verbose=options.get("verbose"))) + ) + p = PlatformFactory.new(pkg) + if project_env: + p.configure_project_packages(project_env) + print_dependency_tree( + p.pm, + specs=[p.get_package_spec(name) for name in p.packages], + filter_specs=options.get("tools"), + ) + click.echo() + return True + + +def print_project_env_library_packages(project_env, options): + config = ProjectConfig.get_instance() + lib_deps = config.get(f"env:{project_env}", "lib_deps") + lm = LibraryPackageManager( + os.path.join(config.get("platformio", "libdeps_dir"), project_env) + ) + if not lib_deps or not lm.get_installed(): + return None + click.echo("Libraries") + print_dependency_tree( + lm, + lib_deps, + filter_specs=options.get("libraries"), + verbose=options.get("verbose"), + ) + return True diff --git a/tests/commands/pkg/test_list.py b/tests/commands/pkg/test_list.py new file mode 100644 index 00000000..4297023c --- /dev/null +++ b/tests/commands/pkg/test_list.py @@ -0,0 +1,145 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=unused-argument + +import os + +from platformio import fs +from platformio.package.commands.install import package_install_cmd +from platformio.package.commands.list import package_list_cmd +from platformio.package.manager.library import LibraryPackageManager +from platformio.package.manager.platform import PlatformPackageManager +from platformio.package.manager.tool import ToolPackageManager +from platformio.package.meta import PackageSpec +from platformio.project.config import ProjectConfig + +PROJECT_CONFIG_TPL = """ +[env] +platform = platformio/atmelavr@^3.4.0 + +[env:baremetal] +board = uno + +[env:devkit] +framework = arduino +board = attiny88 +lib_deps = + milesburton/DallasTemperature@^3.9.1 + https://github.com/bblanchon/ArduinoJson.git#v6.19.0 +""" + + +def test_project(clirunner, validate_cliresult, isolated_pio_core, tmp_path): + project_dir = tmp_path / "project" + project_dir.mkdir() + (project_dir / "platformio.ini").write_text(PROJECT_CONFIG_TPL) + result = clirunner.invoke( + package_install_cmd, + ["-d", str(project_dir)], + ) + validate_cliresult(result) + + # test all envs + result = clirunner.invoke( + package_list_cmd, + ["-d", str(project_dir)], + ) + validate_cliresult(result) + assert all(token in result.output for token in ("baremetal", "devkit")) + assert result.output.count("Platform atmelavr@3.4.0") == 2 + assert ( + result.output.count( + "toolchain-atmelavr@1.70300.191015 (required: " + "platformio/toolchain-atmelavr @ ~1.70300.0)" + ) + == 2 + ) + assert result.output.count("Libraries") == 1 + assert ( + "ArduinoJson@6.19.0+sha.9693fd2 (required: " + "git+https://github.com/bblanchon/ArduinoJson.git#v6.19.0)" + ) in result.output + assert "OneWire@2" in result.output + + # test "baremetal" + result = clirunner.invoke( + package_list_cmd, + ["-d", str(project_dir), "-e", "baremetal"], + ) + validate_cliresult(result) + assert "Platform atmelavr@3" in result.output + assert "Libraries" not in result.output + + # filter by "tool" package + result = clirunner.invoke( + package_list_cmd, + ["-d", str(project_dir), "-t", "toolchain-atmelavr@~1.70300.0"], + ) + assert "framework-arduino" not in result.output + assert "Libraries" not in result.output + + # list only libraries + result = clirunner.invoke( + package_list_cmd, + ["-d", str(project_dir), "--only-libraries"], + ) + assert "Platform atmelavr" not in result.output + + # list only libraries for baremetal + result = clirunner.invoke( + package_list_cmd, + ["-d", str(project_dir), "-e", "baremetal", "--only-libraries"], + ) + assert "No packages" in result.output + + +def test_global_packages(clirunner, validate_cliresult, isolated_pio_core, tmp_path): + result = clirunner.invoke(package_list_cmd, ["-g"]) + validate_cliresult(result) + assert "atmelavr@3" in result.output + assert "framework-arduino-avr-attiny" in result.output + + # only tools + result = clirunner.invoke(package_list_cmd, ["-g", "--only-tools"]) + validate_cliresult(result) + assert "toolchain-atmelavr" in result.output + assert "Platforms" not in result.output + + # find tool package + result = clirunner.invoke(package_list_cmd, ["-g", "-t", "toolchain-atmelavr"]) + validate_cliresult(result) + assert "toolchain-atmelavr" in result.output + assert "framework-arduino-avr-attiny@" not in result.output + + # only libraries - no packages + result = clirunner.invoke(package_list_cmd, ["-g", "--only-libraries"]) + validate_cliresult(result) + assert not result.output.strip() + + # check global libs + result = clirunner.invoke( + package_install_cmd, ["-g", "-l", "milesburton/DallasTemperature@^3.9.1"] + ) + validate_cliresult(result) + result = clirunner.invoke(package_list_cmd, ["-g", "--only-libraries"]) + validate_cliresult(result) + assert "DallasTemperature" in result.output + assert "OneWire" in result.output + + # filter by lib + result = clirunner.invoke(package_list_cmd, ["-g", "-l", "OneWire"]) + validate_cliresult(result) + assert "DallasTemperature" in result.output + assert "OneWire" in result.output