blob: b0ddf19aaba63e0205561235d21a77e509a3ac2e [file] [log] [blame]
# 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