| # -*- coding: utf-8 -*- |
| # Copyright 2019 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Deps analysis service.""" |
| |
| from __future__ import print_function |
| |
| import functools |
| import os |
| from pathlib import Path |
| from typing import List, Optional |
| |
| from chromite.lib import build_target_lib |
| from chromite.lib import constants |
| from chromite.lib import cros_build_lib |
| from chromite.lib import dependency_lib |
| from chromite.lib import git |
| from chromite.lib import portage_util |
| from chromite.scripts import cros_extract_deps |
| |
| |
| class Error(Exception): |
| """Base error class for the module.""" |
| |
| |
| def NormalizeSourcePaths(source_paths): |
| """Return the "normalized" form of a list of source paths. |
| |
| Normalizing includes: |
| * Sorting the source paths in alphabetical order. |
| * Remove paths that are sub-path of others in the source paths. |
| * Ensure all the directory path strings are ended with the trailing '/'. |
| * Convert all the path from absolute paths to relative path (relative to |
| the chroot source root). |
| """ |
| return dependency_lib.normalize_source_paths(source_paths) |
| |
| |
| def GenerateSourcePathMapping(packages, sysroot_path, board): |
| """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. |
| i.e: for a given package X, some of its dependency source paths may |
| contain files which doesn't affect the content of X. |
| |
| On the other hands, 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 takes 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 (str): The path to the sysroot. If the packages are board |
| agnostic, then this should be '/'. |
| board (str): The name of the board if packages are dependency of board. If |
| the packages are board agnostic, then this should be None. |
| |
| Returns: |
| Map from each package to the source path (relative to the repo checkout |
| root, i.e: ~/trunk/ in your cros_sdk) it depends on. |
| For each source path which is a directory, the string is ended with a |
| trailing '/'. |
| """ |
| return dependency_lib.get_source_path_mapping(packages, sysroot_path, board) |
| |
| |
| @functools.lru_cache() |
| def GetBuildDependency(sysroot_path, board=None, packages=None): |
| """Return the build dependency and package -> source path map for |board|. |
| |
| Args: |
| sysroot_path (str): The path to the sysroot, or None if no sysroot is being |
| used. |
| board (str): The name of the board whose artifacts are being created, or |
| None if no sysroot is being used. |
| packages (tuple[CPV]): The packages that need to be built, or empty / None |
| to use the default list. |
| |
| Returns: |
| JSON build dependencies report for the given board which includes: |
| - Package level deps graph from portage |
| - Map from each package to the source path |
| (relative to the repo checkout root, i.e: ~/trunk/ in your cros_sdk) it |
| depends on |
| """ |
| if not sysroot_path: |
| sysroot_path = cros_build_lib.GetSysroot(board) |
| |
| results = { |
| 'sysroot_path': sysroot_path, |
| 'target_board': board, |
| 'package_deps': {}, |
| 'source_path_mapping': {}, |
| } |
| |
| sdk_sysroot = cros_build_lib.GetSysroot(None) |
| sdk_results = { |
| 'sysroot_path': sdk_sysroot, |
| 'target_board': 'sdk', |
| 'package_deps': {}, |
| 'source_path_mapping': {}, |
| } |
| |
| if sysroot_path != sdk_sysroot: |
| board_packages = [] |
| if packages: |
| board_packages.extend([cpv.cp for cpv in packages]) |
| else: |
| board_packages.extend([ |
| constants.TARGET_OS_PKG, |
| constants.TARGET_OS_DEV_PKG, |
| constants.TARGET_OS_TEST_PKG, |
| constants.TARGET_OS_FACTORY_PKG, |
| constants.TARGET_OS_FACTORY_SHIM_PKG, |
| ]) |
| # Since we don’t have a clear mapping from autotests to git repos |
| # and/or portage packages, we assume every board run all autotests. |
| board_packages += ['chromeos-base/autotest-all'] |
| |
| board_deps, board_bdeps = cros_extract_deps.ExtractDeps( |
| sysroot=sysroot_path, |
| package_list=board_packages, |
| include_bdepend=False, |
| backtrack=False) |
| |
| results['package_deps'].update(board_deps) |
| results['package_deps'].update(board_bdeps) |
| sdk_results['package_deps'].update(board_bdeps) |
| |
| indep_packages = [ |
| 'virtual/target-sdk', 'virtual/target-sdk-post-cross', |
| ] |
| |
| indep_deps, _ = cros_extract_deps.ExtractDeps( |
| sysroot=sdk_results['sysroot_path'], package_list=indep_packages) |
| |
| indep_map = GenerateSourcePathMapping(list(indep_deps), sdk_sysroot, None) |
| results['package_deps'].update(indep_deps) |
| results['source_path_mapping'].update(indep_map) |
| |
| sdk_results['package_deps'].update(indep_deps) |
| sdk_results['source_path_mapping'].update(indep_map) |
| |
| if sysroot_path != sdk_sysroot: |
| bdep_map = GenerateSourcePathMapping(list(board_bdeps), sdk_sysroot, None) |
| board_map = GenerateSourcePathMapping(list(board_deps), sysroot_path, board) |
| results['source_path_mapping'].update(bdep_map) |
| results['source_path_mapping'].update(board_map) |
| sdk_results['source_path_mapping'].update(bdep_map) |
| |
| return results, sdk_results |
| |
| |
| def determine_package_relevance(dep_src_paths: List[str], |
| src_paths: Optional[List[str]] = None) -> bool: |
| """Determine if the package is relevant to the given source paths. |
| |
| A package is relevant if any of its dependent source paths is in the given |
| list of source paths. If no source paths are given, the default is True. |
| |
| Args: |
| dep_src_paths: List of source paths the package depends on. |
| src_paths: List of source paths of interest. |
| """ |
| if not src_paths: |
| return True |
| for src_path in (Path(x) for x in src_paths): |
| for dep_src_path in (Path(x) for x in dep_src_paths): |
| try: |
| # Will throw an error if src_path isn't under dep_src_path. |
| src_path.relative_to(dep_src_path) |
| return True |
| except ValueError: |
| pass |
| return False |
| |
| |
| def GetDependencies(sysroot_path: str, |
| build_target: build_target_lib.BuildTarget, |
| src_paths: Optional[List[str]] = None, |
| packages: Optional[List[str]] = None) -> List[str]: |
| """Return the packages dependent on the given source paths for |board|. |
| |
| Args: |
| sysroot_path: The path to the sysroot. |
| build_target: The build_target whose dependencies are being calculated. |
| src_paths: List of paths for which to get a list of dependent packages. If |
| empty / None returns all package dependencies. |
| packages: The packages that need to be built, or empty / None to use the |
| default list. |
| |
| Returns: |
| The relevant package dependencies based on the given list of packages and |
| src_paths. |
| """ |
| pkgs = tuple(packages) if packages else None |
| json_deps, _sdk_json_deps = GetBuildDependency( |
| sysroot_path, build_target.name, packages=pkgs) |
| |
| relevant_packages = set() |
| for cpv, dep_src_paths in json_deps['source_path_mapping'].items(): |
| if determine_package_relevance(dep_src_paths, src_paths): |
| relevant_packages.add(cpv) |
| |
| return relevant_packages |
| |
| |
| def DetermineToolchainSourcePaths(): |
| """Returns a list of all source paths relevant to toolchain packages. |
| |
| A package is a 'toolchain package' if it is listed as a direct dependency |
| of virtual/toolchain-packages. This function deliberately does not return |
| deeper transitive dependencies so that only direct changes to toolchain |
| packages trigger the expensive full re-compilation required to test toolchain |
| changes. Eclasses & overlays are not returned as relevant paths for the same |
| reason. |
| |
| Returned paths are relative to the root of the project checkout. |
| |
| Returns: |
| List[str]: A list of paths considered relevant to toolchain packages. |
| """ |
| source_paths = set() |
| toolchain_pkgs = portage_util.GetFlattenedDepsForPackage( |
| 'virtual/toolchain-packages', depth=1) |
| toolchain_pkg_ebuilds = portage_util.FindEbuildsForPackages( |
| toolchain_pkgs, sysroot='/', check=True) |
| |
| # Include the entire directory containing the toolchain ebuild, as the |
| # package's FILESDIR and patches also live there. |
| source_paths.update( |
| os.path.dirname(ebuild_path) |
| for ebuild_path in toolchain_pkg_ebuilds.values()) |
| |
| # Source paths which are cros workon source paths. |
| buildroot = os.path.join(constants.CHROOT_SOURCE_ROOT, 'src') |
| manifest = git.ManifestCheckout.Cached(buildroot) |
| for ebuild_path in toolchain_pkg_ebuilds.values(): |
| attrs = portage_util.EBuild.Classify(ebuild_path) |
| if (not attrs.is_workon or |
| # Blacklisted ebuild is pinned to a specific git sha1, so change in |
| # that repo matter to the ebuild. |
| attrs.is_blacklisted): |
| continue |
| ebuild = portage_util.EBuild(ebuild_path) |
| workon_subtrees = ebuild.GetSourceInfo(buildroot, manifest).subtrees |
| for path in workon_subtrees: |
| source_paths.add(path) |
| |
| return NormalizeSourcePaths(list(source_paths)) |