blob: c2d301aebe5166695e7873746565ec1c3a5f0525 [file] [log] [blame]
# Copyright 2020 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.
"""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 of 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 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: 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)
# 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
]
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(constants.CROSUTILS_DIR)
# chromiumos-overlay specifies default settings for every target in
# chromeos/config and so can potentially affect every board.
for package in results:
results[package].append(
os.path.join(constants.CHROOT_SOURCE_ROOT,
constants.CHROMIUMOS_OVERLAY_DIR, 'chromeos', 'config'))
for p in results:
results[p] = path_util.normalize_paths_to_source_root(results[p])
return results