# 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=too-many-branches, too-many-locals import json import logging import os import time from urllib.parse import quote import click from tabulate import tabulate from platformio import exception, fs, util from platformio.commands import PlatformioCLI from platformio.commands.lib.helpers import get_builtin_libs, save_project_libdeps from platformio.package.exception import NotGlobalLibDir, UnknownPackageError from platformio.package.manager.library import LibraryPackageManager from platformio.package.meta import PackageItem, PackageSpec from platformio.proc import is_ci from platformio.project.config import ProjectConfig from platformio.project.helpers import get_project_dir, is_platformio_project CTX_META_INPUT_DIRS_KEY = __name__ + ".input_dirs" CTX_META_PROJECT_ENVIRONMENTS_KEY = __name__ + ".project_environments" CTX_META_STORAGE_DIRS_KEY = __name__ + ".storage_dirs" CTX_META_STORAGE_LIBDEPS_KEY = __name__ + ".storage_lib_deps" def get_project_global_lib_dir(): return ProjectConfig.get_instance().get("platformio", "globallib_dir") @click.group(short_help="Library manager", hidden=True) @click.option( "-d", "--storage-dir", multiple=True, default=None, type=click.Path( exists=True, file_okay=False, dir_okay=True, writable=True, resolve_path=True ), help="Manage custom library storage", ) @click.option( "-g", "--global", is_flag=True, help="Manage global PlatformIO library storage" ) @click.option( "-e", "--environment", multiple=True, help=( "Manage libraries for the specific project build environments " "declared in `platformio.ini`" ), ) @click.pass_context def cli(ctx, **options): in_silence = PlatformioCLI.in_silence() storage_cmds = ("install", "uninstall", "update", "list") # skip commands that don't need storage folder if ctx.invoked_subcommand not in storage_cmds or ( len(ctx.args) == 2 and ctx.args[1] in ("-h", "--help") ): return storage_dirs = list(options["storage_dir"]) if options["global"]: storage_dirs.append(get_project_global_lib_dir()) if not storage_dirs: if is_platformio_project(): storage_dirs = [get_project_dir()] elif is_ci(): storage_dirs = [get_project_global_lib_dir()] click.secho( "Warning! Global library storage is used automatically. " "Please use `platformio lib --global %s` command to remove " "this warning." % ctx.invoked_subcommand, fg="yellow", ) if not storage_dirs: raise NotGlobalLibDir( get_project_dir(), get_project_global_lib_dir(), ctx.invoked_subcommand ) ctx.meta[CTX_META_PROJECT_ENVIRONMENTS_KEY] = options["environment"] ctx.meta[CTX_META_INPUT_DIRS_KEY] = storage_dirs ctx.meta[CTX_META_STORAGE_DIRS_KEY] = [] ctx.meta[CTX_META_STORAGE_LIBDEPS_KEY] = {} for storage_dir in storage_dirs: if not is_platformio_project(storage_dir): ctx.meta[CTX_META_STORAGE_DIRS_KEY].append(storage_dir) continue with fs.cd(storage_dir): config = ProjectConfig.get_instance( os.path.join(storage_dir, "platformio.ini") ) config.validate(options["environment"], silent=in_silence) libdeps_dir = config.get("platformio", "libdeps_dir") for env in config.envs(): if options["environment"] and env not in options["environment"]: continue storage_dir = os.path.join(libdeps_dir, env) ctx.meta[CTX_META_STORAGE_DIRS_KEY].append(storage_dir) ctx.meta[CTX_META_STORAGE_LIBDEPS_KEY][storage_dir] = config.get( "env:" + env, "lib_deps", [] ) @cli.command("install", short_help="Install library") @click.argument("libraries", required=False, nargs=-1, metavar="[LIBRARY...]") @click.option( "--save/--no-save", is_flag=True, default=True, help="Save installed libraries into the `platformio.ini` dependency list" " (enabled by default)", ) @click.option("-s", "--silent", is_flag=True, help="Suppress progress reporting") @click.option( "--interactive", is_flag=True, help="Deprecated! Please use a strict dependency specification (owner/libname)", ) @click.option( "-f", "--force", is_flag=True, help="Reinstall/redownload library if exists" ) @click.pass_context def lib_install( # pylint: disable=too-many-arguments,unused-argument ctx, libraries, save, silent, interactive, force ): click.secho( "\nWARNING: This command is deprecated and will be removed in " "the next releases. \nPlease use `pio pkg install` instead.\n", fg="yellow", ) storage_dirs = ctx.meta[CTX_META_STORAGE_DIRS_KEY] storage_libdeps = ctx.meta.get(CTX_META_STORAGE_LIBDEPS_KEY, []) installed_pkgs = {} for storage_dir in storage_dirs: if not silent and (libraries or storage_dir in storage_libdeps): print_storage_header(storage_dirs, storage_dir) lm = LibraryPackageManager(storage_dir) lm.set_log_level(logging.WARN if silent else logging.DEBUG) if libraries: installed_pkgs = { library: lm.install(library, force=force) for library in libraries } elif storage_dir in storage_libdeps: for library in storage_libdeps[storage_dir]: lm.install(library, force=force) if save and installed_pkgs: _save_deps(ctx, installed_pkgs) def _save_deps(ctx, pkgs, action="add"): specs = [] for library, pkg in pkgs.items(): spec = PackageSpec(library) if spec.external: specs.append(spec) else: specs.append( PackageSpec( owner=pkg.metadata.spec.owner, name=pkg.metadata.spec.name, requirements=spec.requirements or ( ("^%s" % pkg.metadata.version) if not pkg.metadata.version.build else pkg.metadata.version ), ) ) input_dirs = ctx.meta.get(CTX_META_INPUT_DIRS_KEY, []) project_environments = ctx.meta[CTX_META_PROJECT_ENVIRONMENTS_KEY] for input_dir in input_dirs: if not is_platformio_project(input_dir): continue save_project_libdeps(input_dir, specs, project_environments, action=action) @cli.command("uninstall", short_help="Remove libraries") @click.argument("libraries", nargs=-1, metavar="[LIBRARY...]") @click.option( "--save/--no-save", is_flag=True, default=True, help="Remove libraries from the `platformio.ini` dependency list and save changes" " (enabled by default)", ) @click.option("-s", "--silent", is_flag=True, help="Suppress progress reporting") @click.pass_context def lib_uninstall(ctx, libraries, save, silent): click.secho( "\nWARNING: This command is deprecated and will be removed in " "the next releases. \nPlease use `pio pkg uninstall` instead.\n", fg="yellow", ) storage_dirs = ctx.meta[CTX_META_STORAGE_DIRS_KEY] uninstalled_pkgs = {} for storage_dir in storage_dirs: print_storage_header(storage_dirs, storage_dir) lm = LibraryPackageManager(storage_dir) lm.set_log_level(logging.WARN if silent else logging.DEBUG) uninstalled_pkgs = {library: lm.uninstall(library) for library in libraries} if save and uninstalled_pkgs: _save_deps(ctx, uninstalled_pkgs, action="remove") @cli.command("update", short_help="Update installed libraries") @click.argument("libraries", required=False, nargs=-1, metavar="[LIBRARY...]") @click.option( "-c", "--only-check", is_flag=True, help="DEPRECATED. Please use `--dry-run` instead", ) @click.option( "--dry-run", is_flag=True, help="Do not update, only check for the new versions" ) @click.option("-s", "--silent", is_flag=True, help="Suppress progress reporting") @click.option("--json-output", is_flag=True) @click.pass_context def lib_update( # pylint: disable=too-many-arguments ctx, libraries, only_check, dry_run, silent, json_output ): only_check = dry_run or only_check if only_check and not json_output: raise exception.UserSideException( "This command is deprecated, please use `pio pkg outdated` instead" ) if not json_output: click.secho( "\nWARNING: This command is deprecated and will be removed in " "the next releases. \nPlease use `pio pkg update` instead.\n", fg="yellow", ) storage_dirs = ctx.meta[CTX_META_STORAGE_DIRS_KEY] json_result = {} for storage_dir in storage_dirs: if not json_output: print_storage_header(storage_dirs, storage_dir) lib_deps = ctx.meta.get(CTX_META_STORAGE_LIBDEPS_KEY, {}).get(storage_dir, []) lm = LibraryPackageManager(storage_dir) lm.set_log_level(logging.WARN if silent else logging.DEBUG) _libraries = libraries or lib_deps or lm.get_installed() if only_check and json_output: result = [] for library in _libraries: spec = None pkg = None if isinstance(library, PackageItem): pkg = library else: spec = PackageSpec(library) pkg = lm.get_package(spec) if not pkg: continue outdated = lm.outdated(pkg, spec) if not outdated.is_outdated(allow_incompatible=True): continue manifest = lm.legacy_load_manifest(pkg) manifest["versionWanted"] = ( str(outdated.wanted) if outdated.wanted else None ) manifest["versionLatest"] = ( str(outdated.latest) if outdated.latest else None ) result.append(manifest) json_result[storage_dir] = result else: for library in _libraries: to_spec = ( None if isinstance(library, PackageItem) else PackageSpec(library) ) try: lm.update(library, to_spec=to_spec) except UnknownPackageError as e: if library not in lib_deps: raise e if json_output: return click.echo( json.dumps( json_result[storage_dirs[0]] if len(storage_dirs) == 1 else json_result ) ) return True @cli.command("list", short_help="List installed libraries") @click.option("--json-output", is_flag=True) @click.pass_context def lib_list(ctx, json_output): if not json_output: click.secho( "\nWARNING: This command is deprecated and will be removed in " "the next releases. \nPlease use `pio pkg list` instead.\n", fg="yellow", ) storage_dirs = ctx.meta[CTX_META_STORAGE_DIRS_KEY] json_result = {} for storage_dir in storage_dirs: if not json_output: print_storage_header(storage_dirs, storage_dir) lm = LibraryPackageManager(storage_dir) items = lm.legacy_get_installed() if json_output: json_result[storage_dir] = items elif items: for item in sorted(items, key=lambda i: i["name"]): print_lib_item(item) else: click.echo("No items found") if json_output: return click.echo( json.dumps( json_result[storage_dirs[0]] if len(storage_dirs) == 1 else json_result ) ) return True @cli.command("search", short_help="Search for a library") @click.argument("query", required=False, nargs=-1) @click.option("--json-output", is_flag=True) @click.option("--page", type=click.INT, default=1) @click.option("--id", multiple=True) @click.option("-o", "--owner", multiple=True) @click.option("-n", "--name", multiple=True) @click.option("-a", "--author", multiple=True) @click.option("-k", "--keyword", multiple=True) @click.option("-f", "--framework", multiple=True) @click.option("-p", "--platform", multiple=True) @click.option("-i", "--header", multiple=True) @click.option( "--noninteractive", is_flag=True, help="Do not prompt, automatically paginate with delay", ) def lib_search(query, json_output, page, noninteractive, **filters): if not json_output: click.secho( "\nWARNING: This command is deprecated and will be removed in " "the next releases. \nPlease use `pio pkg search` instead.\n", fg="yellow", ) regclient = LibraryPackageManager().get_registry_client_instance() if not query: query = [] if not isinstance(query, list): query = list(query) for key, values in filters.items(): for value in values: query.append('%s:"%s"' % (key, value)) result = regclient.fetch_json_data( "get", "/v2/lib/search", params=dict(query=" ".join(query), page=page), x_cache_valid="1d", ) if json_output: click.echo(json.dumps(result)) return if result["total"] == 0: click.secho( "Nothing has been found by your request\n" "Try a less-specific search or use truncation (or wildcard) " "operator", fg="yellow", nl=False, ) click.secho(" *", fg="green") click.secho("For example: DS*, PCA*, DHT* and etc.\n", fg="yellow") click.echo( "For more examples and advanced search syntax, please use documentation:" ) click.secho( "https://docs.platformio.org/page/userguide/lib/cmd_search.html\n", fg="cyan", ) return click.secho( "Found %d libraries:\n" % result["total"], fg="green" if result["total"] else "yellow", ) while True: for item in result["items"]: print_lib_item(item) if int(result["page"]) * int(result["perpage"]) >= int(result["total"]): break if noninteractive: click.echo() click.secho( "Loading next %d libraries... Press Ctrl+C to stop!" % result["perpage"], fg="yellow", ) click.echo() time.sleep(5) elif not click.confirm("Show next libraries?"): break result = regclient.fetch_json_data( "get", "/v2/lib/search", params=dict(query=" ".join(query), page=int(result["page"]) + 1), x_cache_valid="1d", ) @cli.command("builtin", short_help="List built-in libraries") @click.option("--storage", multiple=True) @click.option("--json-output", is_flag=True) def lib_builtin(storage, json_output): items = get_builtin_libs(storage) if json_output: return click.echo(json.dumps(items)) for storage_ in items: if not storage_["items"]: continue click.secho(storage_["name"], fg="green") click.echo("*" * len(storage_["name"])) click.echo() for item in sorted(storage_["items"], key=lambda i: i["name"]): print_lib_item(item) return True @cli.command("show", short_help="Show detailed info about a library") @click.argument("library", metavar="[LIBRARY]") @click.option("--json-output", is_flag=True) def lib_show(library, json_output): if not json_output: click.secho( "\nWARNING: This command is deprecated and will be removed in " "the next releases. \nPlease use `pio pkg show` instead.\n", fg="yellow", ) lm = LibraryPackageManager() lm.set_log_level(logging.ERROR if json_output else logging.DEBUG) lib_id = lm.reveal_registry_package_id(library) regclient = lm.get_registry_client_instance() lib = regclient.fetch_json_data( "get", "/v2/lib/info/%d" % lib_id, x_cache_valid="1h" ) if json_output: return click.echo(json.dumps(lib)) title = "{ownername}/{name}".format(**lib) click.secho(title, fg="cyan") click.echo("=" * len(title)) click.echo(lib["description"]) click.echo() click.secho("ID: %d" % lib["id"]) click.echo( "Version: %s, released %s" % ( lib["version"]["name"], util.parse_datetime(lib["version"]["released"]).strftime("%c"), ) ) click.echo("Manifest: %s" % lib["confurl"]) for key in ("homepage", "repository", "license"): if key not in lib or not lib[key]: continue if isinstance(lib[key], list): click.echo("%s: %s" % (key.capitalize(), ", ".join(lib[key]))) else: click.echo("%s: %s" % (key.capitalize(), lib[key])) blocks = [] _authors = [] for author in lib.get("authors", []): _data = [] for key in ("name", "email", "url", "maintainer"): if not author.get(key): continue if key == "email": _data.append("<%s>" % author[key]) elif key == "maintainer": _data.append("(maintainer)") else: _data.append(author[key]) _authors.append(" ".join(_data)) if _authors: blocks.append(("Authors", _authors)) blocks.append(("Keywords", lib["keywords"])) for key in ("frameworks", "platforms"): if key not in lib or not lib[key]: continue blocks.append(("Compatible %s" % key, [i["title"] for i in lib[key]])) blocks.append(("Headers", lib["headers"])) blocks.append(("Examples", lib["examples"])) blocks.append( ( "Versions", [ "%s, released %s" % (v["name"], util.parse_datetime(v["released"]).strftime("%c")) for v in lib["versions"] ], ) ) blocks.append( ( "Unique Downloads", [ "Today: %s" % lib["dlstats"]["day"], "Week: %s" % lib["dlstats"]["week"], "Month: %s" % lib["dlstats"]["month"], ], ) ) for (title, rows) in blocks: click.echo() click.secho(title, bold=True) click.echo("-" * len(title)) for row in rows: click.echo(row) return True @cli.command("register", short_help="Deprecated") @click.argument("config_url") def lib_register(config_url): # pylint: disable=unused-argument raise exception.UserSideException( "This command is deprecated. Please use `pio pkg publish` command." ) @cli.command("stats", short_help="Library Registry Statistics") @click.option("--json-output", is_flag=True) def lib_stats(json_output): regclient = LibraryPackageManager().get_registry_client_instance() result = regclient.fetch_json_data("get", "/v2/lib/stats", x_cache_valid="1h") if json_output: return click.echo(json.dumps(result)) for key in ("updated", "added"): tabular_data = [ ( click.style(item["name"], fg="cyan"), util.parse_datetime(item["date"]).strftime("%c"), "https://platformio.org/lib/show/%s/%s" % (item["id"], quote(item["name"])), ) for item in result.get(key, []) ] table = tabulate( tabular_data, headers=[click.style("RECENTLY " + key.upper(), bold=True), "Date", "URL"], ) click.echo(table) click.echo() for key in ("lastkeywords", "topkeywords"): tabular_data = [ ( click.style(name, fg="cyan"), "https://platformio.org/lib/search?query=" + quote("keyword:%s" % name), ) for name in result.get(key, []) ] table = tabulate( tabular_data, headers=[ click.style( ("RECENT" if key == "lastkeywords" else "POPULAR") + " KEYWORDS", bold=True, ), "URL", ], ) click.echo(table) click.echo() for key, title in (("dlday", "Today"), ("dlweek", "Week"), ("dlmonth", "Month")): tabular_data = [ ( click.style(item["name"], fg="cyan"), "https://platformio.org/lib/show/%s/%s" % (item["id"], quote(item["name"])), ) for item in result.get(key, []) ] table = tabulate( tabular_data, headers=[click.style("FEATURED: " + title.upper(), bold=True), "URL"], ) click.echo(table) click.echo() return True def print_storage_header(storage_dirs, storage_dir): if storage_dirs and storage_dirs[0] != storage_dir: click.echo("") click.echo( click.style("Library Storage: ", bold=True) + click.style(storage_dir, fg="blue") ) def print_lib_item(item): click.secho(item["name"], fg="cyan") click.echo("=" * len(item["name"])) if "id" in item: click.secho("#ID: %d" % item["id"], bold=True) if "description" in item or "url" in item: click.echo(item.get("description", item.get("url", ""))) click.echo() for key in ("version", "homepage", "license", "keywords"): if key not in item or not item[key]: continue if isinstance(item[key], list): click.echo("%s: %s" % (key.capitalize(), ", ".join(item[key]))) else: click.echo("%s: %s" % (key.capitalize(), item[key])) for key in ("frameworks", "platforms"): if key not in item: continue click.echo( "Compatible %s: %s" % ( key, ", ".join( [i["title"] if isinstance(i, dict) else i for i in item[key]] ), ) ) if "authors" in item or "authornames" in item: click.echo( "Authors: %s" % ", ".join( item.get( "authornames", [a.get("name", "") for a in item.get("authors", [])] ) ) ) if "__src_url" in item: click.secho("Source: %s" % item["__src_url"]) click.echo()