# 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-instance-attributes, too-many-public-methods # pylint: disable=assignment-from-no-return, unused-argument, too-many-lines import hashlib import io import os import re import sys import click import SCons.Scanner # pylint: disable=import-error from SCons.Script import ARGUMENTS # pylint: disable=import-error from SCons.Script import DefaultEnvironment # pylint: disable=import-error from platformio import exception, fs from platformio.builder.tools import piobuild from platformio.compat import IS_WINDOWS, hashlib_encode_data, string_types from platformio.http import HTTPClientError, InternetConnectionError from platformio.package.exception import ( MissingPackageManifestError, UnknownPackageError, ) from platformio.package.manager.library import LibraryPackageManager from platformio.package.manifest.parser import ( ManifestParserError, ManifestParserFactory, ) from platformio.package.meta import PackageCompatibility, PackageItem from platformio.project.options import ProjectOptions class LibBuilderFactory: @staticmethod def new(env, path, verbose=int(ARGUMENTS.get("PIOVERBOSE", 0))): clsname = "UnknownLibBuilder" if os.path.isfile(os.path.join(path, "library.json")): clsname = "PlatformIOLibBuilder" else: used_frameworks = LibBuilderFactory.get_used_frameworks(env, path) common_frameworks = set(env.get("PIOFRAMEWORK", [])) & set(used_frameworks) if common_frameworks: clsname = "%sLibBuilder" % list(common_frameworks)[0].capitalize() elif used_frameworks: clsname = "%sLibBuilder" % used_frameworks[0].capitalize() obj = globals()[clsname](env, path, verbose=verbose) # Handle PlatformIOLibBuilder.manifest.build.builder # pylint: disable=protected-access if isinstance(obj, PlatformIOLibBuilder) and obj._manifest.get("build", {}).get( "builder" ): obj = globals()[obj._manifest.get("build", {}).get("builder")]( env, path, verbose=verbose ) assert isinstance(obj, LibBuilderBase) return obj @staticmethod def get_used_frameworks(env, path): if any( os.path.isfile(os.path.join(path, fname)) for fname in ("library.properties", "keywords.txt") ): return ["arduino"] if os.path.isfile(os.path.join(path, "module.json")): return ["mbed"] include_re = re.compile( r'^#include\s+(<|")(Arduino|mbed)\.h(<|")', flags=re.MULTILINE ) # check source files for root, _, files in os.walk(path, followlinks=True): if "mbed_lib.json" in files: return ["mbed"] for fname in files: if not fs.path_endswith_ext( fname, piobuild.SRC_BUILD_EXT + piobuild.SRC_HEADER_EXT ): continue with io.open( os.path.join(root, fname), encoding="utf8", errors="ignore" ) as fp: content = fp.read() if not content: continue if "Arduino.h" in content and include_re.search(content): return ["arduino"] if "mbed.h" in content and include_re.search(content): return ["mbed"] return [] class LibBuilderBase: CLASSIC_SCANNER = SCons.Scanner.C.CScanner() CCONDITIONAL_SCANNER = SCons.Scanner.C.CConditionalScanner() # Max depth of nested includes: # -1 = unlimited # 0 - disabled nesting # >0 - number of allowed nested includes CCONDITIONAL_SCANNER_DEPTH = 99 PARSE_SRC_BY_H_NAME = True _INCLUDE_DIRS_CACHE = None def __init__(self, env, path, manifest=None, verbose=False): self.env = env.Clone() self.envorigin = env.Clone() self.path = os.path.abspath(env.subst(path)) self.verbose = verbose try: self._manifest = manifest if manifest else self.load_manifest() except ManifestParserError: click.secho( "Warning! Ignoring broken library manifest in " + self.path, fg="yellow" ) self._manifest = {} self.is_dependent = False self.is_built = False self.depbuilders = [] self._deps_are_processed = False self._circular_deps = [] self._processed_search_files = [] # pass a macro to the projenv + libs if "test" in env["BUILD_TYPE"]: self.env.Append(CPPDEFINES=["PIO_UNIT_TESTING"]) # reset source filter, could be overridden with extra script self.env["SRC_FILTER"] = "" # process extra options and append to build environment self.process_extra_options() def __repr__(self): return "%s(%r)" % (self.__class__, self.path) def __contains__(self, child_path): return self.is_common_builder(self.path, child_path) def is_common_builder(self, root_path, child_path): if IS_WINDOWS: root_path = root_path.lower() child_path = child_path.lower() if root_path == child_path: return True if ( os.path.commonprefix([root_path + os.path.sep, child_path]) == root_path + os.path.sep ): return True # try to resolve paths root_path = os.path.realpath(root_path) child_path = os.path.realpath(child_path) return ( os.path.commonprefix([root_path + os.path.sep, child_path]) == root_path + os.path.sep ) @property def name(self): return self._manifest.get("name", os.path.basename(self.path)) @property def version(self): return self._manifest.get("version") @property def dependent(self): """Backward compatibility with ESP-IDF""" return self.is_dependent @property def dependencies(self): return self._manifest.get("dependencies") @property def src_filter(self): return piobuild.SRC_FILTER_DEFAULT + [ "-" % os.sep, "-" % os.sep, "-" % os.sep, "-" % os.sep, ] @property def include_dir(self): for name in ("include", "Include"): d = os.path.join(self.path, name) if os.path.isdir(d): return d return None @property def src_dir(self): for name in ("src", "Src"): d = os.path.join(self.path, name) if os.path.isdir(d): return d return self.path def get_include_dirs(self): items = [] include_dir = self.include_dir if include_dir: items.append(include_dir) items.append(self.src_dir) return items @property def build_dir(self): lib_hash = hashlib.sha1(hashlib_encode_data(self.path)).hexdigest()[:3] return os.path.join( "$BUILD_DIR", "lib%s" % lib_hash, os.path.basename(self.path) ) @property def build_flags(self): return None @property def build_unflags(self): return None @property def extra_script(self): return None @property def lib_archive(self): return self.env.GetProjectOption("lib_archive") @property def lib_ldf_mode(self): return self.env.GetProjectOption("lib_ldf_mode") @staticmethod def validate_ldf_mode(mode): ldf_modes = ProjectOptions["env.lib_ldf_mode"].type.choices if isinstance(mode, string_types): mode = mode.strip().lower() if mode in ldf_modes: return mode try: return ldf_modes[int(mode)] except (IndexError, ValueError): pass return ProjectOptions["env.lib_ldf_mode"].default @property def lib_compat_mode(self): return self.env.GetProjectOption("lib_compat_mode") @staticmethod def validate_compat_mode(mode): compat_modes = ProjectOptions["env.lib_compat_mode"].type.choices if isinstance(mode, string_types): mode = mode.strip().lower() if mode in compat_modes: return mode try: return compat_modes[int(mode)] except (IndexError, ValueError): pass return ProjectOptions["env.lib_compat_mode"].default def is_platforms_compatible(self, platforms): return True def is_frameworks_compatible(self, frameworks): return True def load_manifest(self): return {} def process_extra_options(self): with fs.cd(self.path): self.env.ProcessFlags(self.build_flags) if self.extra_script: self.env.SConscriptChdir(True) self.env.SConscript( os.path.abspath(self.extra_script), exports={"env": self.env, "pio_lib_builder": self}, ) self.env.SConscriptChdir(False) self.env.ProcessUnFlags(self.build_unflags) def process_dependencies(self): if not self.dependencies or self._deps_are_processed: return self._deps_are_processed = True for item in self.dependencies: found = False for lb in self.env.GetLibBuilders(): if item["name"] != lb.name: continue found = True if lb not in self.depbuilders: self.depend_on(lb) break if not found and self.verbose: sys.stderr.write( "Warning: Ignored `%s` dependency for `%s` " "library\n" % (item["name"], self.name) ) def get_search_files(self): return [ os.path.join(self.src_dir, item) for item in self.env.MatchSourceFiles( self.src_dir, self.src_filter, piobuild.SRC_BUILD_EXT ) ] def get_implicit_includes( # pylint: disable=too-many-branches self, search_files=None ): # all include directories if not LibBuilderBase._INCLUDE_DIRS_CACHE: LibBuilderBase._INCLUDE_DIRS_CACHE = [ self.env.Dir(d) for d in ProjectAsLibBuilder( self.envorigin, "$PROJECT_DIR", export_projenv=False ).get_include_dirs() ] for lb in self.env.GetLibBuilders(): LibBuilderBase._INCLUDE_DIRS_CACHE.extend( [self.env.Dir(d) for d in lb.get_include_dirs()] ) # append self include directories include_dirs = [self.env.Dir(d) for d in self.get_include_dirs()] include_dirs.extend(LibBuilderBase._INCLUDE_DIRS_CACHE) result = [] search_files = search_files or [] while search_files: node = self.env.File(search_files.pop(0)) if node.get_abspath() in self._processed_search_files: continue self._processed_search_files.append(node.get_abspath()) try: assert "+" in self.lib_ldf_mode candidates = LibBuilderBase.CCONDITIONAL_SCANNER( node, self.env, tuple(include_dirs), depth=self.CCONDITIONAL_SCANNER_DEPTH, ) except Exception as exc: # pylint: disable=broad-except if self.verbose and "+" in self.lib_ldf_mode: sys.stderr.write( "Warning! Classic Pre Processor is used for `%s`, " "advanced has failed with `%s`\n" % (node.get_abspath(), exc) ) candidates = LibBuilderBase.CLASSIC_SCANNER( node, self.env, tuple(include_dirs) ) # print(node.get_abspath(), [c.get_abspath() for c in candidates]) for item in candidates: item_path = item.get_abspath() # process internal files recursively if ( item_path not in self._processed_search_files and item_path not in search_files and item_path in self ): search_files.append(item_path) if item not in result: result.append(item) if not self.PARSE_SRC_BY_H_NAME: continue if not fs.path_endswith_ext(item_path, piobuild.SRC_HEADER_EXT): continue item_fname = item_path[: item_path.rindex(".")] for ext in piobuild.SRC_C_EXT + piobuild.SRC_CXX_EXT: if not os.path.isfile("%s.%s" % (item_fname, ext)): continue item_c_node = self.env.File("%s.%s" % (item_fname, ext)) if item_c_node not in result: result.append(item_c_node) return result def search_deps_recursive(self, search_files=None): self.process_dependencies() # when LDF is disabled if self.lib_ldf_mode == "off": return if self.lib_ldf_mode.startswith("deep"): search_files = self.get_search_files() lib_inc_map = {} for inc in self.get_implicit_includes(search_files): inc_path = inc.get_abspath() for lb in self.env.GetLibBuilders(): if inc_path in lb: if lb not in lib_inc_map: lib_inc_map[lb] = [] lib_inc_map[lb].append(inc_path) break for lb, lb_search_files in lib_inc_map.items(): self.depend_on(lb, search_files=lb_search_files) def depend_on(self, lb, search_files=None, recursive=True): def _already_depends(_lb): if self in _lb.depbuilders: return True for __lb in _lb.depbuilders: if _already_depends(__lb): return True return False # assert isinstance(lb, LibBuilderBase) if self != lb: if _already_depends(lb): if self.verbose: sys.stderr.write( "Warning! Circular dependencies detected " "between `%s` and `%s`\n" % (self.path, lb.path) ) self._circular_deps.append(lb) elif lb not in self.depbuilders: self.depbuilders.append(lb) lb.is_dependent = True LibBuilderBase._INCLUDE_DIRS_CACHE = None if recursive: lb.search_deps_recursive(search_files) def build(self): libs = [] shared_scopes = ("CPPPATH", "LIBPATH", "LIBS", "LINKFLAGS") for lb in self.depbuilders: libs.extend(lb.build()) # copy shared information to self env self.env.PrependUnique( **{ scope: lb.env.get(scope) for scope in shared_scopes if lb.env.get(scope) } ) for lb in self._circular_deps: self.env.PrependUnique(CPPPATH=lb.get_include_dirs()) if self.is_built: return libs self.is_built = True self.env.PrependUnique(CPPPATH=self.get_include_dirs()) if self.lib_ldf_mode == "off": for lb in self.env.GetLibBuilders(): if self == lb or not lb.is_built: continue self.env.PrependUnique( **{ scope: lb.env.get(scope) for scope in shared_scopes if lb.env.get(scope) } ) do_not_archive = not self.lib_archive if not do_not_archive: nodes = self.env.CollectBuildFiles( self.build_dir, self.src_dir, self.src_filter ) if nodes: libs.append( self.env.BuildLibrary( self.build_dir, self.src_dir, self.src_filter, nodes ) ) else: do_not_archive = True if do_not_archive: self.env.BuildSources(self.build_dir, self.src_dir, self.src_filter) return libs class UnknownLibBuilder(LibBuilderBase): pass class ArduinoLibBuilder(LibBuilderBase): def load_manifest(self): manifest_path = os.path.join(self.path, "library.properties") if not os.path.isfile(manifest_path): return {} return ManifestParserFactory.new_from_file(manifest_path).as_dict() @property def include_dir(self): if not all( os.path.isdir(os.path.join(self.path, d)) for d in ("include", "src") ): return None return os.path.join(self.path, "include") def get_include_dirs(self): include_dirs = super().get_include_dirs() if os.path.isdir(os.path.join(self.path, "src")): return include_dirs if os.path.isdir(os.path.join(self.path, "utility")): include_dirs.append(os.path.join(self.path, "utility")) return include_dirs @property def src_filter(self): src_dir = os.path.join(self.path, "src") if os.path.isdir(src_dir): # pylint: disable=no-member src_filter = LibBuilderBase.src_filter.fget(self) for root, _, files in os.walk(src_dir, followlinks=True): found = False for fname in files: if fname.lower().endswith("asm"): found = True break if not found: continue rel_path = root.replace(src_dir, "") if rel_path.startswith(os.path.sep): rel_path = rel_path[1:] + os.path.sep src_filter.append("-<%s*.[aA][sS][mM]>" % rel_path) return src_filter src_filter = [] is_utility = os.path.isdir(os.path.join(self.path, "utility")) for ext in piobuild.SRC_BUILD_EXT + piobuild.SRC_HEADER_EXT: # arduino ide ignores files with .asm or .ASM extensions if ext.lower() == "asm": continue src_filter.append("+<*.%s>" % ext) if is_utility: src_filter.append("+" % (os.path.sep, ext)) return src_filter @property def dependencies(self): # do not include automatically all libraries for build # chain+ will decide later return None @property def lib_ldf_mode(self): # pylint: disable=no-member if not self._manifest.get("dependencies"): return LibBuilderBase.lib_ldf_mode.fget(self) missing = object() global_value = self.env.GetProjectConfig().getraw( "env:" + self.env["PIOENV"], "lib_ldf_mode", missing ) if global_value != missing: return LibBuilderBase.lib_ldf_mode.fget(self) # automatically enable C++ Preprocessing in runtime # (Arduino IDE has this behavior) return "chain+" def is_frameworks_compatible(self, frameworks): return PackageCompatibility(frameworks=frameworks).is_compatible( PackageCompatibility(frameworks=["arduino", "energia"]) ) def is_platforms_compatible(self, platforms): return PackageCompatibility(platforms=platforms).is_compatible( PackageCompatibility(platforms=self._manifest.get("platforms")) ) @property def build_flags(self): ldflags = [ LibBuilderBase.build_flags.fget(self), # pylint: disable=no-member self._manifest.get("ldflags"), ] if self._manifest.get("precompiled") in ("true", "full"): # add to LDPATH {build.mcu} folder board_config = self.env.BoardConfig() for key in ("build.mcu", "build.cpu"): libpath = os.path.join(self.src_dir, board_config.get(key, "")) if not os.path.isdir(libpath): continue self.env.PrependUnique(LIBPATH=libpath) break ldflags = [flag for flag in ldflags if flag] # remove empty return " ".join(ldflags) if ldflags else None class MbedLibBuilder(LibBuilderBase): def load_manifest(self): manifest_path = os.path.join(self.path, "module.json") if not os.path.isfile(manifest_path): return {} return ManifestParserFactory.new_from_file(manifest_path).as_dict() @property def src_dir(self): if os.path.isdir(os.path.join(self.path, "source")): return os.path.join(self.path, "source") return LibBuilderBase.src_dir.fget(self) # pylint: disable=no-member def get_include_dirs(self): include_dirs = super().get_include_dirs() if self.path not in include_dirs: include_dirs.append(self.path) # library with module.json for p in self._manifest.get("extraIncludes", []): include_dirs.append(os.path.join(self.path, p)) # old mbed library without manifest, add to CPPPATH all folders if not self._manifest: for root, _, __ in os.walk(self.path): part = root.replace(self.path, "").lower() if any(s in part for s in ("%s." % os.path.sep, "test", "example")): continue if root not in include_dirs: include_dirs.append(root) return include_dirs def is_frameworks_compatible(self, frameworks): return PackageCompatibility(frameworks=frameworks).is_compatible( PackageCompatibility(frameworks=["mbed"]) ) def process_extra_options(self): self._process_mbed_lib_confs() return super().process_extra_options() def _process_mbed_lib_confs(self): mbed_lib_paths = [ os.path.join(root, "mbed_lib.json") for root, _, files in os.walk(self.path) if "mbed_lib.json" in files ] if not mbed_lib_paths: return None mbed_config_path = None for p in self.env.get("CPPPATH"): mbed_config_path = os.path.join(self.env.subst(p), "mbed_config.h") if os.path.isfile(mbed_config_path): break mbed_config_path = None if not mbed_config_path: return None macros = {} for mbed_lib_path in mbed_lib_paths: macros.update(self._mbed_lib_conf_parse_macros(mbed_lib_path)) self._mbed_conf_append_macros(mbed_config_path, macros) return True @staticmethod def _mbed_normalize_macro(macro): name = macro value = None if "=" in macro: name, value = macro.split("=", 1) return dict(name=name, value=value) def _mbed_lib_conf_parse_macros(self, mbed_lib_path): macros = {} cppdefines = str(self.env.Flatten(self.env.subst("$CPPDEFINES"))) manifest = fs.load_json(mbed_lib_path) # default macros for macro in manifest.get("macros", []): macro = self._mbed_normalize_macro(macro) macros[macro["name"]] = macro # configuration items for key, options in manifest.get("config", {}).items(): if "value" not in options: continue macros[key] = dict( name=options.get("macro_name"), value=options.get("value") ) # overrode items per target for target, options in manifest.get("target_overrides", {}).items(): if target != "*" and "TARGET_" + target not in cppdefines: continue for macro in options.get("target.macros_add", []): macro = self._mbed_normalize_macro(macro) macros[macro["name"]] = macro for key, value in options.items(): if not key.startswith("target.") and key in macros: macros[key]["value"] = value # normalize macro names for key, macro in macros.items(): if not macro["name"]: macro["name"] = key if "." not in macro["name"]: macro["name"] = "%s.%s" % (manifest.get("name"), macro["name"]) macro["name"] = re.sub( r"[^a-z\d]+", "_", macro["name"], flags=re.I ).upper() macro["name"] = "MBED_CONF_" + macro["name"] if isinstance(macro["value"], bool): macro["value"] = 1 if macro["value"] else 0 return {macro["name"]: macro["value"] for macro in macros.values()} def _mbed_conf_append_macros(self, mbed_config_path, macros): lines = [] with open(mbed_config_path, encoding="utf8") as fp: for line in fp.readlines(): line = line.strip() if line == "#endif": lines.append("// PlatformIO Library Dependency Finder (LDF)") lines.extend( [ "#define %s %s" % (name, value if value is not None else "") for name, value in macros.items() ] ) lines.append("") if not line.startswith("#define"): lines.append(line) continue tokens = line.split() if len(tokens) < 2 or tokens[1] not in macros: lines.append(line) lines.append("") with open(mbed_config_path, mode="w", encoding="utf8") as fp: fp.write("\n".join(lines)) class PlatformIOLibBuilder(LibBuilderBase): def load_manifest(self): manifest_path = os.path.join(self.path, "library.json") if not os.path.isfile(manifest_path): return {} return ManifestParserFactory.new_from_file(manifest_path).as_dict() def _has_arduino_manifest(self): return os.path.isfile(os.path.join(self.path, "library.properties")) @property def include_dir(self): if "includeDir" in self._manifest.get("build", {}): with fs.cd(self.path): return os.path.abspath(self._manifest.get("build").get("includeDir")) return LibBuilderBase.include_dir.fget(self) # pylint: disable=no-member def get_include_dirs(self): include_dirs = super().get_include_dirs() # backwards compatibility with PlatformIO 2.0 if ( "build" not in self._manifest and self._has_arduino_manifest() and not os.path.isdir(os.path.join(self.path, "src")) and os.path.isdir(os.path.join(self.path, "utility")) ): include_dirs.append(os.path.join(self.path, "utility")) for path in self.env.get("CPPPATH", []): if path not in self.envorigin.get("CPPPATH", []): include_dirs.append(self.env.subst(path)) return include_dirs @property def src_dir(self): if "srcDir" in self._manifest.get("build", {}): with fs.cd(self.path): return os.path.abspath(self._manifest.get("build").get("srcDir")) return LibBuilderBase.src_dir.fget(self) # pylint: disable=no-member @property def src_filter(self): # pylint: disable=no-member if "srcFilter" in self._manifest.get("build", {}): return self._manifest.get("build").get("srcFilter") if self.env["SRC_FILTER"]: return self.env["SRC_FILTER"] if self._has_arduino_manifest(): return ArduinoLibBuilder.src_filter.fget(self) return LibBuilderBase.src_filter.fget(self) @property def build_flags(self): if "flags" in self._manifest.get("build", {}): return self._manifest.get("build").get("flags") return LibBuilderBase.build_flags.fget(self) # pylint: disable=no-member @property def build_unflags(self): if "unflags" in self._manifest.get("build", {}): return self._manifest.get("build").get("unflags") return LibBuilderBase.build_unflags.fget(self) # pylint: disable=no-member @property def extra_script(self): if "extraScript" in self._manifest.get("build", {}): return self._manifest.get("build").get("extraScript") return LibBuilderBase.extra_script.fget(self) # pylint: disable=no-member @property def lib_archive(self): missing = object() global_value = self.env.GetProjectConfig().getraw( "env:" + self.env["PIOENV"], "lib_archive", missing ) if global_value != missing: return self.env.GetProjectConfig().get( "env:" + self.env["PIOENV"], "lib_archive" ) # pylint: disable=no-member return self._manifest.get("build", {}).get( "libArchive", LibBuilderBase.lib_archive.fget(self) ) @property def lib_ldf_mode(self): # pylint: disable=no-member return self.validate_ldf_mode( self._manifest.get("build", {}).get( "libLDFMode", LibBuilderBase.lib_ldf_mode.fget(self) ) ) @property def lib_compat_mode(self): # pylint: disable=no-member return self.validate_compat_mode( self._manifest.get("build", {}).get( "libCompatMode", LibBuilderBase.lib_compat_mode.fget(self) ) ) def is_platforms_compatible(self, platforms): return PackageCompatibility(platforms=platforms).is_compatible( PackageCompatibility(platforms=self._manifest.get("platforms")) ) def is_frameworks_compatible(self, frameworks): return PackageCompatibility(frameworks=frameworks).is_compatible( PackageCompatibility(frameworks=self._manifest.get("frameworks")) ) class ProjectAsLibBuilder(LibBuilderBase): def __init__(self, env, *args, **kwargs): export_projenv = kwargs.get("export_projenv", True) if "export_projenv" in kwargs: del kwargs["export_projenv"] # backup original value, will be reset in base.__init__ project_src_filter = env.get("SRC_FILTER") super().__init__(env, *args, **kwargs) self.env["SRC_FILTER"] = project_src_filter if export_projenv: env.Export(dict(projenv=self.env)) def __contains__(self, child_path): for root_path in (self.include_dir, self.src_dir, self.test_dir): if root_path and self.is_common_builder(root_path, child_path): return True return False @property def include_dir(self): include_dir = self.env.subst("$PROJECT_INCLUDE_DIR") return include_dir if os.path.isdir(include_dir) else None @property def src_dir(self): return self.env.subst("$PROJECT_SRC_DIR") @property def test_dir(self): return self.env.subst("$PROJECT_TEST_DIR") def get_search_files(self): items = [] build_type = self.env["BUILD_TYPE"] # project files if "test" not in build_type or self.env.GetProjectOption("test_build_src"): items.extend(super().get_search_files()) # test files if "test" in build_type: items.extend( [ os.path.join("$PROJECT_TEST_DIR", item) for item in self.env.MatchSourceFiles( "$PROJECT_TEST_DIR", "$PIOTEST_SRC_FILTER" ) ] ) return items @property def lib_ldf_mode(self): mode = LibBuilderBase.lib_ldf_mode.fget(self) # pylint: disable=no-member if not mode.startswith("chain"): return mode # parse all project files return "deep+" if "+" in mode else "deep" @property def src_filter(self): # pylint: disable=no-member return self.env.get("SRC_FILTER") or LibBuilderBase.src_filter.fget(self) @property def build_flags(self): # pylint: disable=no-member return self.env.get("SRC_BUILD_FLAGS") or LibBuilderBase.build_flags.fget(self) @property def dependencies(self): return self.env.GetProjectOption("lib_deps", []) def process_extra_options(self): with fs.cd(self.path): self.env.ProcessFlags(self.build_flags) self.env.ProcessUnFlags(self.build_unflags) def install_dependencies(self): def _is_builtin(spec): for lb in self.env.GetLibBuilders(): if lb.name == spec: return True return False not_found_specs = [] for spec in self.dependencies: # check if built-in library if _is_builtin(spec): continue found = False for storage_dir in self.env.GetLibSourceDirs(): lm = LibraryPackageManager(storage_dir) if lm.get_package(spec): found = True break if not found: not_found_specs.append(spec) did_install = False lm = LibraryPackageManager( self.env.subst(os.path.join("$PROJECT_LIBDEPS_DIR", "$PIOENV")) ) for spec in not_found_specs: try: lm.install(spec) did_install = True except ( HTTPClientError, UnknownPackageError, InternetConnectionError, ) as exc: click.secho("Warning! %s" % exc, fg="yellow") # reset cache if did_install: DefaultEnvironment().Replace(__PIO_LIB_BUILDERS=None) def process_dependencies(self): # pylint: disable=too-many-branches found_lbs = [] for spec in self.dependencies: found = False for storage_dir in self.env.GetLibSourceDirs(): if found: break lm = LibraryPackageManager(storage_dir) pkg = lm.get_package(spec) if not pkg: continue for lb in self.env.GetLibBuilders(): if pkg.path != lb.path: continue if lb not in self.depbuilders: self.depend_on(lb, recursive=False) found_lbs.append(lb) found = True break if found: continue # look for built-in libraries by a name # which don't have package manifest for lb in self.env.GetLibBuilders(): if lb.name != spec: continue if lb not in self.depbuilders: self.depend_on(lb) found = True break # process library dependencies for lb in found_lbs: lb.search_deps_recursive() def build(self): self.is_built = True # do not build Project now result = super().build() self.env.PrependUnique(CPPPATH=self.get_include_dirs()) return result def GetLibSourceDirs(env): items = env.GetProjectOption("lib_extra_dirs", []) items.extend(env["LIBSOURCE_DIRS"]) return [ env.subst(fs.expanduser(item) if item.startswith("~") else item) for item in items ] def IsCompatibleLibBuilder(env, lb, verbose=int(ARGUMENTS.get("PIOVERBOSE", 0))): compat_mode = lb.lib_compat_mode if lb.name in env.GetProjectOption("lib_ignore", []): if verbose: sys.stderr.write("Ignored library %s\n" % lb.path) return None if compat_mode == "strict" and not lb.is_platforms_compatible(env["PIOPLATFORM"]): if verbose: sys.stderr.write("Platform incompatible library %s\n" % lb.path) return False if compat_mode in ("soft", "strict") and not lb.is_frameworks_compatible( env.get("PIOFRAMEWORK", "__noframework__") ): if verbose: sys.stderr.write("Framework incompatible library %s\n" % lb.path) return False return True def GetLibBuilders(_): # pylint: disable=too-many-branches env = DefaultEnvironment() if env.get("__PIO_LIB_BUILDERS", None) is not None: return sorted( env["__PIO_LIB_BUILDERS"], key=lambda lb: 0 if lb.is_dependent else 1, ) env.Replace(__PIO_LIB_BUILDERS=[]) verbose = int(ARGUMENTS.get("PIOVERBOSE", 0)) found_incompat = False for storage_dir in env.GetLibSourceDirs(): storage_dir = os.path.abspath(storage_dir) if not os.path.isdir(storage_dir): continue for item in sorted(os.listdir(storage_dir)): lib_dir = os.path.join(storage_dir, item) if item == "__cores__": continue if LibraryPackageManager.is_symlink(lib_dir): lib_dir, _ = LibraryPackageManager.resolve_symlink(lib_dir) if not lib_dir or not os.path.isdir(lib_dir): continue try: lb = LibBuilderFactory.new(env, lib_dir) except exception.InvalidJSONFile: if verbose: sys.stderr.write( "Skip library with broken manifest: %s\n" % lib_dir ) continue if env.IsCompatibleLibBuilder(lb): env.Append(__PIO_LIB_BUILDERS=[lb]) else: found_incompat = True for lb in env.get("EXTRA_LIB_BUILDERS", []): if env.IsCompatibleLibBuilder(lb): env.Append(__PIO_LIB_BUILDERS=[lb]) else: found_incompat = True if verbose and found_incompat: sys.stderr.write( 'More details about "Library Compatibility Mode": ' "https://docs.platformio.org/page/librarymanager/ldf.html#" "ldf-compat-mode\n" ) return env["__PIO_LIB_BUILDERS"] def ConfigureProjectLibBuilder(env): _pm_storage = {} def _get_lib_license(pkg): storage_dir = os.path.dirname(os.path.dirname(pkg.path)) if storage_dir not in _pm_storage: _pm_storage[storage_dir] = LibraryPackageManager(storage_dir) try: return (_pm_storage[storage_dir].load_manifest(pkg) or {}).get("license") except MissingPackageManifestError: pass return None def _correct_found_libs(lib_builders): # build full dependency graph found_lbs = [lb for lb in lib_builders if lb.is_dependent] for lb in lib_builders: if lb in found_lbs: lb.search_deps_recursive(lb.get_search_files()) for lb in lib_builders: for deplb in lb.depbuilders[:]: if deplb not in found_lbs: lb.depbuilders.remove(deplb) def _print_deps_tree(root, level=0): margin = "| " * (level) for lb in root.depbuilders: title = lb.name pkg = PackageItem(lb.path) if pkg.metadata: title += " @ %s" % pkg.metadata.version elif lb.version: title += " @ %s" % lb.version click.echo("%s|-- %s" % (margin, title), nl=False) if int(ARGUMENTS.get("PIOVERBOSE", 0)): click.echo( " (License: %s, " % (_get_lib_license(pkg) or "Unknown"), nl=False ) if pkg.metadata and pkg.metadata.spec.external: click.echo("URI: %s, " % pkg.metadata.spec.uri, nl=False) click.echo("Path: %s" % lb.path, nl=False) click.echo(")", nl=False) click.echo("") if lb.verbose and lb.depbuilders: _print_deps_tree(lb, level + 1) project = ProjectAsLibBuilder(env, "$PROJECT_DIR") if "test" in env["BUILD_TYPE"]: project.env.ConfigureTestTarget() ldf_mode = LibBuilderBase.lib_ldf_mode.fget(project) # pylint: disable=no-member click.echo("LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf") click.echo( "LDF Modes: Finder ~ %s, Compatibility ~ %s" % (ldf_mode, project.lib_compat_mode) ) project.install_dependencies() lib_builders = env.GetLibBuilders() click.echo("Found %d compatible libraries" % len(lib_builders)) click.echo("Scanning dependencies...") project.search_deps_recursive() if ldf_mode.startswith("chain") and project.depbuilders: _correct_found_libs(lib_builders) if project.depbuilders: click.echo("Dependency Graph") _print_deps_tree(project) else: click.echo("No dependencies") return project def exists(_): return True def generate(env): env.AddMethod(GetLibSourceDirs) env.AddMethod(IsCompatibleLibBuilder) env.AddMethod(GetLibBuilders) env.AddMethod(ConfigureProjectLibBuilder) return env