# 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-arguments, too-many-locals, too-many-branches # pylint: disable=too-many-return-statements import json import re from glob import glob from os.path import isdir, join import click import semantic_version from platformio import app, exception, util from platformio.compat import glob_escape, string_types from platformio.managers.package import BasePkgManager from platformio.managers.platform import PlatformFactory, PlatformManager from platformio.project.helpers import get_project_global_lib_dir class LibraryManager(BasePkgManager): FILE_CACHE_VALID = "30d" # 1 month def __init__(self, package_dir=None): if not package_dir: package_dir = get_project_global_lib_dir() super(LibraryManager, self).__init__(package_dir) @property def manifest_names(self): return [".library.json", "library.json", "library.properties", "module.json"] def get_manifest_path(self, pkg_dir): path = BasePkgManager.get_manifest_path(self, pkg_dir) if path: return path # if library without manifest, returns first source file src_dir = join(glob_escape(pkg_dir)) if isdir(join(pkg_dir, "src")): src_dir = join(src_dir, "src") chs_files = glob(join(src_dir, "*.[chS]")) if chs_files: return chs_files[0] cpp_files = glob(join(src_dir, "*.cpp")) if cpp_files: return cpp_files[0] return None def load_manifest(self, pkg_dir): manifest = BasePkgManager.load_manifest(self, pkg_dir) if not manifest: return manifest # if Arduino library.properties if "sentence" in manifest: manifest["frameworks"] = ["arduino"] manifest["description"] = manifest["sentence"] del manifest["sentence"] if "author" in manifest: if isinstance(manifest["author"], dict): manifest["authors"] = [manifest["author"]] else: manifest["authors"] = [{"name": manifest["author"]}] del manifest["author"] if "authors" in manifest and not isinstance(manifest["authors"], list): manifest["authors"] = [manifest["authors"]] if "keywords" not in manifest: keywords = [] for keyword in re.split( r"[\s/]+", manifest.get("category", "Uncategorized") ): keyword = keyword.strip() if not keyword: continue keywords.append(keyword.lower()) manifest["keywords"] = keywords if "category" in manifest: del manifest["category"] # don't replace VCS URL if "url" in manifest and "description" in manifest: manifest["homepage"] = manifest["url"] del manifest["url"] if "architectures" in manifest: platforms = [] platforms_map = { "avr": "atmelavr", "sam": "atmelsam", "samd": "atmelsam", "esp8266": "espressif8266", "esp32": "espressif32", "arc32": "intel_arc32", } for arch in manifest["architectures"].split(","): arch = arch.strip() if arch == "*": platforms = "*" break if arch in platforms_map: platforms.append(platforms_map[arch]) manifest["platforms"] = platforms del manifest["architectures"] # convert listed items via comma to array for key in ("keywords", "frameworks", "platforms"): if key not in manifest or not isinstance(manifest[key], string_types): continue manifest[key] = [i.strip() for i in manifest[key].split(",") if i.strip()] return manifest @staticmethod def normalize_dependencies(dependencies): if not dependencies: return [] items = [] if isinstance(dependencies, dict): if "name" in dependencies: items.append(dependencies) else: for name, version in dependencies.items(): items.append({"name": name, "version": version}) elif isinstance(dependencies, list): items = [d for d in dependencies if "name" in d] for item in items: for k in ("frameworks", "platforms"): if k not in item or isinstance(k, list): continue if item[k] == "*": del item[k] elif isinstance(item[k], string_types): item[k] = [i.strip() for i in item[k].split(",") if i.strip()] return items def max_satisfying_repo_version(self, versions, requirements=None): def _cmp_dates(datestr1, datestr2): date1 = util.parse_date(datestr1) date2 = util.parse_date(datestr2) if date1 == date2: return 0 return -1 if date1 < date2 else 1 semver_spec = None try: semver_spec = ( semantic_version.SimpleSpec(requirements) if requirements else None ) except ValueError: pass item = {} for v in versions: semver_new = self.parse_semver_version(v["name"]) if semver_spec: if not semver_new or semver_new not in semver_spec: continue if not item or self.parse_semver_version(item["name"]) < semver_new: item = v elif requirements: if requirements == v["name"]: return v else: if not item or _cmp_dates(item["released"], v["released"]) == -1: item = v return item def get_latest_repo_version(self, name, requirements, silent=False): item = self.max_satisfying_repo_version( util.get_api_result( "/lib/info/%d" % self.search_lib_id( {"name": name, "requirements": requirements}, silent=silent ), cache_valid="1h", )["versions"], requirements, ) return item["name"] if item else None def _install_from_piorepo(self, name, requirements): assert name.startswith("id="), name version = self.get_latest_repo_version(name, requirements) if not version: raise exception.UndefinedPackageVersion( requirements or "latest", util.get_systype() ) dl_data = util.get_api_result( "/lib/download/" + str(name[3:]), dict(version=version), cache_valid="30d" ) assert dl_data return self._install_from_url( name, dl_data["url"].replace("http://", "https://") if app.get_setting("strict_ssl") else dl_data["url"], requirements, ) def search_lib_id( # pylint: disable=too-many-branches self, filters, silent=False, interactive=False ): assert isinstance(filters, dict) assert "name" in filters # try to find ID within installed packages lib_id = self._get_lib_id_from_installed(filters) if lib_id: return lib_id # looking in PIO Library Registry if not silent: click.echo( "Looking for %s library in registry" % click.style(filters["name"], fg="cyan") ) query = [] for key in filters: if key not in ("name", "authors", "frameworks", "platforms"): continue values = filters[key] if not isinstance(values, list): values = [v.strip() for v in values.split(",") if v] for value in values: query.append( '%s:"%s"' % (key[:-1] if key.endswith("s") else key, value) ) lib_info = None result = util.get_api_result( "/v2/lib/search", dict(query=" ".join(query)), cache_valid="1h" ) if result["total"] == 1: lib_info = result["items"][0] elif result["total"] > 1: if silent and not interactive: lib_info = result["items"][0] else: click.secho( "Conflict: More than one library has been found " "by request %s:" % json.dumps(filters), fg="yellow", err=True, ) from platformio.commands.lib import print_lib_item for item in result["items"]: print_lib_item(item) if not interactive: click.secho( "Automatically chose the first available library " "(use `--interactive` option to make a choice)", fg="yellow", err=True, ) lib_info = result["items"][0] else: deplib_id = click.prompt( "Please choose library ID", type=click.Choice([str(i["id"]) for i in result["items"]]), ) for item in result["items"]: if item["id"] == int(deplib_id): lib_info = item break if not lib_info: if list(filters) == ["name"]: raise exception.LibNotFound(filters["name"]) raise exception.LibNotFound(str(filters)) if not silent: click.echo( "Found: %s" % click.style( "https://platformio.org/lib/show/{id}/{name}".format(**lib_info), fg="blue", ) ) return int(lib_info["id"]) def _get_lib_id_from_installed(self, filters): if filters["name"].startswith("id="): return int(filters["name"][3:]) package_dir = self.get_package_dir( filters["name"], filters.get("requirements", filters.get("version")) ) if not package_dir: return None manifest = self.load_manifest(package_dir) if "id" not in manifest: return None for key in ("frameworks", "platforms"): if key not in filters: continue if key not in manifest: return None if not util.items_in_list( util.items_to_list(filters[key]), util.items_to_list(manifest[key]) ): return None if "authors" in filters: if "authors" not in manifest: return None manifest_authors = manifest["authors"] if not isinstance(manifest_authors, list): manifest_authors = [manifest_authors] manifest_authors = [ a["name"] for a in manifest_authors if isinstance(a, dict) and "name" in a ] filter_authors = filters["authors"] if not isinstance(filter_authors, list): filter_authors = [filter_authors] if not set(filter_authors) <= set(manifest_authors): return None return int(manifest["id"]) def install( # pylint: disable=arguments-differ self, name, requirements=None, silent=False, after_update=False, interactive=False, force=False, ): _name, _requirements, _url = self.parse_pkg_uri(name, requirements) if not _url: name = "id=%d" % self.search_lib_id( {"name": _name, "requirements": _requirements}, silent=silent, interactive=interactive, ) requirements = _requirements pkg_dir = BasePkgManager.install( self, name, requirements, silent=silent, after_update=after_update, force=force, ) if not pkg_dir: return None manifest = self.load_manifest(pkg_dir) if "dependencies" not in manifest: return pkg_dir if not silent: click.secho("Installing dependencies", fg="yellow") builtin_lib_storages = None for filters in self.normalize_dependencies(manifest["dependencies"]): assert "name" in filters # avoid circle dependencies if not self.INSTALL_HISTORY: self.INSTALL_HISTORY = [] history_key = str(filters) if history_key in self.INSTALL_HISTORY: continue self.INSTALL_HISTORY.append(history_key) if any(s in filters.get("version", "") for s in ("\\", "/")): self.install( "{name}={version}".format(**filters), silent=silent, after_update=after_update, interactive=interactive, force=force, ) else: try: lib_id = self.search_lib_id(filters, silent, interactive) except exception.LibNotFound as e: if builtin_lib_storages is None: builtin_lib_storages = get_builtin_libs() if not silent or is_builtin_lib( builtin_lib_storages, filters["name"] ): click.secho("Warning! %s" % e, fg="yellow") continue if filters.get("version"): self.install( lib_id, filters.get("version"), silent=silent, after_update=after_update, interactive=interactive, force=force, ) else: self.install( lib_id, silent=silent, after_update=after_update, interactive=interactive, force=force, ) return pkg_dir def get_builtin_libs(storage_names=None): items = [] storage_names = storage_names or [] pm = PlatformManager() for manifest in pm.get_installed(): p = PlatformFactory.newPlatform(manifest["__pkg_dir"]) for storage in p.get_lib_storages(): if storage_names and storage["name"] not in storage_names: continue lm = LibraryManager(storage["path"]) items.append( { "name": storage["name"], "path": storage["path"], "items": lm.get_installed(), } ) return items def is_builtin_lib(storages, name): for storage in storages or []: if any(l.get("name") == name for l in storage["items"]): return True return False