| # Copyright 2020 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Dependency calculation functionality/utilities.""" |
| |
| import logging |
| import os |
| import re |
| from typing import List, Mapping, Union |
| |
| from chromite.lib import constants |
| from chromite.lib import git |
| from chromite.lib import osutils |
| from chromite.lib import path_util |
| from chromite.lib import portage_util |
| |
| |
| class Error(Exception): |
| """Base error class for the module.""" |
| |
| |
| class MissingCacheEntry(Error): |
| """No on-disk cache entry could be found for a package.""" |
| |
| |
| class NoMatchingFileForDigest(Error): |
| """No ebuild or eclass file could be found with the given MD5 digest.""" |
| |
| |
| def _get_eclasses_for_ebuild(ebuild_path, path_cache, overlay_dirs): |
| # Trim '.ebuild' from the tail of the path. |
| ebuild_path_no_ext, _ = os.path.splitext(ebuild_path) |
| |
| # Ebuild paths look like: |
| # {some_dir}/category/package/package-version |
| # but cache entry paths look like: |
| # {some_dir}/category/package-version |
| # So we need to remove the second to last path element from the ebuild path |
| # to construct the path to the matching edb cache entry. |
| path_head, package_name = os.path.split(ebuild_path_no_ext) |
| path_head, _ = os.path.split(path_head) |
| overlay_head, category = os.path.split(path_head) |
| fixed_path = os.path.join(overlay_head, category, package_name) |
| |
| cache_file_relpath = os.path.relpath(fixed_path, "/") |
| |
| edb_cache_file_path = os.path.join("/var/cache/edb/dep", cache_file_relpath) |
| md5_cache_file_path = os.path.join( |
| overlay_head, "metadata", "md5-cache", category, package_name |
| ) |
| |
| if os.path.isfile(edb_cache_file_path): |
| cache_entries = _parse_ebuild_cache_entry(edb_cache_file_path) |
| elif os.path.isfile(md5_cache_file_path): |
| cache_entries = _parse_ebuild_cache_entry(md5_cache_file_path) |
| else: |
| raise MissingCacheEntry( |
| "No cache entry found for package: %s" % package_name |
| ) |
| |
| relevant_eclass_paths = [] |
| for eclass, digest in cache_entries: |
| if digest in path_cache: |
| relevant_eclass_paths.append(path_cache[digest]) |
| else: |
| try: |
| eclass_path = _find_matching_eclass_file( |
| eclass, digest, overlay_dirs |
| ) |
| path_cache[digest] = eclass_path |
| relevant_eclass_paths.append(eclass_path) |
| except NoMatchingFileForDigest: |
| logging.warning( |
| ( |
| "Package %s has a reference to eclass %s with digest " |
| "%s but no matching file could be found." |
| ), |
| package_name, |
| eclass, |
| digest, |
| ) |
| # If we can't find a matching eclass file then we don't know |
| # exactly which overlay the eclass file is coming from, but we |
| # do know that it has to be in one of the overlay_dirs. So as a |
| # fallback we will pretend the eclass could be in any of them |
| # and add all the paths that it could possibly have. |
| relevant_eclass_paths.extend( |
| [ |
| os.path.join(overlay, "eclass", eclass) + ".eclass" |
| for overlay in overlay_dirs |
| ] |
| ) |
| |
| return relevant_eclass_paths |
| |
| |
| def _find_matching_eclass_file(eclass, digest, overlay_dirs): |
| for overlay in overlay_dirs: |
| path = os.path.join(overlay, "eclass", eclass) + ".eclass" |
| if os.path.isfile(path) and digest == osutils.MD5HashFile(path): |
| return path |
| raise NoMatchingFileForDigest( |
| "No matching eclass file found: %s %s" % (eclass, digest) |
| ) |
| |
| |
| def _parse_ebuild_cache_entry(cache_file_path): |
| """Extract the eclasses with their digest from an ebuild's cache file.""" |
| eclass_regex = re.compile(r"_eclasses_=(.*)") |
| eclass_clause_regex = ( |
| # The eclass name, e.g. cros-workon. |
| r"(?P<eclass>[^\s]+)\s+" |
| # The edb cache files contain the overlay path, the md5 cache file does |
| # not, so optionally parse the path. |
| r"((?P<overlay_path>[^\s]+)\s+)?" |
| # The eclass digest followed by a word boundary -- \b prevents parsing |
| # md5 digests as paths when the next class begins with a-f. |
| r"(?P<digest>[\da-fA-F]+)\b(\s+|$)" |
| ) |
| |
| cachefile = osutils.ReadFile(cache_file_path) |
| m = eclass_regex.search(cachefile) |
| if not m: |
| return [] |
| |
| start, end = m.start(1), m.end(1) |
| entries = re.finditer(eclass_clause_regex, cachefile[start:end]) |
| return [(c.group("eclass"), c.group("digest")) for c in entries] |
| |
| |
| def get_source_path_mapping( |
| packages: List[str], |
| sysroot_path: str, |
| board: Union[str, None], |
| include_eclass: bool = True, |
| include_overlay: bool = True, |
| ) -> Mapping[str, List[str]]: |
| """Returns a map from each package to the source paths it depends on. |
| |
| A source path is considered dependency of a package if modifying files in |
| that path might change the content of the resulting package. |
| |
| Notes: |
| 1) This method errs on the side of returning unneeded dependent paths by |
| default. |
| i.e: for a given package X, some of its dependency source paths may |
| contain files which doesn't affect the content of X. By contrast, |
| any missing dependency source paths for package X is considered a |
| bug. |
| 2) This only outputs the direct dependency source paths for a given |
| package and does not include the dependency source paths of |
| dependency packages. |
| e.g: if package A depends on B (DEPEND=B), then results of computing |
| dependency source paths of A doesn't include dependency source paths |
| of B. |
| |
| Args: |
| packages: The list of packages CPV names (str) |
| sysroot_path: The path to the sysroot. If the packages are board |
| agnostic, then this should be '/'. |
| board: The name of the board if packages are dependency of board. If |
| the packages are board agnostic, then this should be None. |
| include_eclass: Whether to include eclass paths. |
| include_overlay: Whether to include overlay paths. |
| |
| Returns: |
| Map from each package to the source path (relative to the repo checkout |
| root, i.e: ~/chromiumos/ in your cros_sdk) it depends on. |
| For each source path which is a directory, the string is ended with a |
| trailing '/'. |
| """ |
| results = {} |
| |
| packages_to_ebuild_paths = portage_util.FindEbuildsForPackages( |
| packages, sysroot=sysroot_path, check=True, include_masked=True |
| ) |
| |
| # Source paths which are the directory of ebuild files. |
| for package, ebuild_path in packages_to_ebuild_paths.items(): |
| # Include the entire directory that contains the ebuild as the package's |
| # FILESDIR probably lives there too. |
| results[package] = [os.path.dirname(ebuild_path)] |
| |
| # Source paths which are cros workon source paths. |
| buildroot = os.path.join(constants.SOURCE_ROOT, "src") |
| manifest = git.ManifestCheckout.Cached(buildroot) |
| for package, ebuild_path in packages_to_ebuild_paths.items(): |
| ebuild = portage_util.EBuild(ebuild_path) |
| if not ebuild.is_workon or ebuild.is_manually_uprevved: |
| # Can only fetch workon source paths from workon ebuilds, and |
| # manually uprevved packages are pinned so changes to the source |
| # repo don't matter. |
| continue |
| |
| workon_subtrees = ebuild.GetSourceInfo(buildroot, manifest).subtrees |
| results[package].extend(workon_subtrees) |
| |
| if board: |
| overlay_directories = portage_util.FindOverlays( |
| overlay_type="both", board=board |
| ) |
| else: |
| # If a board is not specified we assume the package is intended for the |
| # SDK, and so we use the overlays for the SDK builder. |
| overlay_directories = portage_util.FindOverlays( |
| overlay_type="both", board=constants.CHROOT_BUILDER_BOARD |
| ) |
| |
| # Package's inherited eclass paths. |
| if include_eclass: |
| eclass_path_cache = {} |
| for package, ebuild_path in packages_to_ebuild_paths.items(): |
| eclass_paths = _get_eclasses_for_ebuild( |
| ebuild_path, eclass_path_cache, overlay_directories |
| ) |
| results[package].extend(eclass_paths) |
| |
| # Source paths which are the overlay directories for the given board |
| # (packages are board specific). |
| if include_overlay: |
| # The only parts of the overlay that affect every package are the |
| # current profile (which lives somewhere in the profiles/ subdir) and a |
| # top-level make.conf (if it exists). |
| profile_directories = [ |
| os.path.join(x, "profiles") for x in overlay_directories |
| ] |
| make_conf_paths = [ |
| os.path.join(x, "make.conf") for x in overlay_directories |
| ] |
| |
| # These directories *might* affect a build, so we include them for now |
| # to be safe. |
| metadata_directories = [ |
| os.path.join(x, "metadata") for x in overlay_directories |
| ] |
| scripts_directories = [ |
| os.path.join(x, "scripts") for x in overlay_directories |
| ] |
| |
| # TODO(b/236161656): Fix. |
| # pylint: disable-next=consider-using-dict-items |
| for package in results: |
| results[package].extend(profile_directories) |
| results[package].extend(make_conf_paths) |
| results[package].extend(metadata_directories) |
| results[package].extend(scripts_directories) |
| # The 'crosutils' repo potentially affects the build of every |
| # package. |
| results[package].append(str(constants.CROSUTILS_DIR)) |
| |
| # chromiumos-overlay specifies default settings for every target in |
| # chromeos/config and so can potentially affect every board. |
| # TODO(b/236161656): Fix. |
| # pylint: disable-next=consider-using-dict-items |
| for package in results: |
| # TODO(b/236161656): Fix. |
| # pylint: disable-next=modified-iterating-dict |
| results[package].append( |
| os.path.join( |
| constants.CHROOT_SOURCE_ROOT, |
| constants.CHROMIUMOS_OVERLAY_DIR, |
| "chromeos", |
| "config", |
| ) |
| ) |
| |
| for p in results: |
| # TODO(b/236161656): Fix. |
| # pylint: disable-next=modified-iterating-dict |
| results[p] = path_util.normalize_paths_to_source_root(results[p]) |
| |
| return results |