# 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 import json import re from glob import glob from os.path import isdir, join import arrow import click from platformio import app, commands, exception, util from platformio.managers.package import BasePkgManager class LibraryManager(BasePkgManager): def __init__(self, package_dir=None): if not package_dir: package_dir = join(util.get_home_dir(), "lib") BasePkgManager.__init__(self, 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(util.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], basestring): 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], basestring): 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 = arrow.get(datestr1) date2 = arrow.get(datestr2) if date1 == date2: return 0 return -1 if date1 < date2 else 1 semver_spec = self.parse_semver_spec( requirements) if requirements else None item = None 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.get_pkg_id_by_name( name, requirements, silent=silent), cache_valid="1h")['versions'], requirements) return item['name'] if item else None def get_pkg_id_by_name(self, name, requirements, silent=False, interactive=False): if name.startswith("id="): return int(name[3:]) # try to find ID from installed packages package_dir = self.get_package_dir(name, requirements) if package_dir: manifest = self.load_manifest(package_dir) if "id" in manifest: return int(manifest['id']) return int( self.search_for_library({ "name": name }, silent, interactive)['id']) 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("enable_ssl") else dl_data['url'], requirements) def install( # pylint: disable=arguments-differ self, name, requirements=None, silent=False, trigger_event=True, interactive=False, force=False): pkg_dir = None try: _name, _requirements, _url = self.parse_pkg_uri(name, requirements) if not _url: name = "id=%d" % self.get_pkg_id_by_name( _name, _requirements, silent=silent, interactive=interactive) requirements = _requirements pkg_dir = BasePkgManager.install( self, name, requirements, silent=silent, trigger_event=trigger_event, force=force) except exception.InternetIsOffline as e: if not silent: click.secho(str(e), fg="yellow") return None 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") for filters in self.normalize_dependencies(manifest['dependencies']): assert "name" in filters if any([s in filters.get("version", "") for s in ("\\", "/")]): self.install( "{name}={version}".format(**filters), silent=silent, trigger_event=trigger_event, interactive=interactive, force=force) else: try: lib_info = self.search_for_library(filters, silent, interactive) except exception.LibNotFound as e: if not silent: click.secho("Warning! %s" % e, fg="yellow") continue if filters.get("version"): self.install( lib_info['id'], filters.get("version"), silent=silent, trigger_event=trigger_event, interactive=interactive, force=force) else: self.install( lib_info['id'], silent=silent, trigger_event=trigger_event, interactive=interactive, force=force) return pkg_dir @staticmethod def search_for_library( # pylint: disable=too-many-branches filters, silent=False, interactive=False): assert isinstance(filters, dict) assert "name" in filters 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) for item in result['items']: commands.lib.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 filters.keys() == ["name"]: raise exception.LibNotFound(filters['name']) else: raise exception.LibNotFound(str(filters)) if not silent: click.echo("Found: %s" % click.style( "http://platformio.org/lib/show/{id}/{name}".format( **lib_info), fg="blue")) return lib_info