blob: 38c18a21a450568659420bda82ba6aa6408a2ea4 [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.
"""Utilities for setting up Portage objects for testing."""
from __future__ import division
import itertools
import os
import pathlib
from typing import Dict, Iterable, Tuple, Union
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import osutils
from chromite.lib.parser import package_info
__all__ = ["Overlay", "Package", "Profile", "Sysroot"]
_EXCLUDED_OVERLAYS = ("chromiumos", "portage-stable")
def _dict_to_conf(dictionary):
"""Helper to format a dictionary into a layout.conf file."""
output = []
for key in sorted(dictionary.keys()):
output.append("%s = %s" % (key, dictionary[key]))
output.append("\n")
return "\n".join(output)
def _dict_to_ebuild(dictionary):
"""Helper to format a dictionary into an ebuild file."""
output = []
for key in dictionary.keys():
output.append(f'{key}="{dictionary[key]}"')
output.append("\n")
return "\n".join(output)
class Overlay(object):
"""Portage overlay object, responsible for all writes to its directory."""
HIERARCHY_NAMES = (
"stable",
"project",
"chipset",
"baseboard",
"board",
"board-private",
)
def __init__(self, root_path, name, parent_overlays=None):
self.path = pathlib.Path(root_path)
self.name = str(name)
self.parent_overlays = (
tuple(parent_overlays) if parent_overlays else None
)
self.packages = []
self.profiles = dict()
self.categories = set()
self._write_layout_conf()
def __contains__(
self, item: Union[package_info.CPV, package_info.PackageInfo]
):
if not isinstance(item, (package_info.CPV, package_info.PackageInfo)):
raise TypeError(f"Expected a CPV but received a {type(item)}")
if isinstance(item, package_info.CPV):
ebuild_path = (
self.path / item.category / item.package / f"{item.pv}.ebuild"
)
else:
ebuild_path = self.path / item.relative_path
return ebuild_path.is_file()
def _write_layout_conf(self):
"""Write out the layout.conf as part of this Overlay's initialization."""
layout_conf_path = self.path / "metadata" / "layout.conf"
parent_names = " ".join(m.name for m in self.parent_overlays or [])
conf = {
"masters": "portage-stable chromiumos eclass-overlay"
+ parent_names,
"profile-formats": "portage-2 profile-default-eapi",
"profile_eapi_when_unspecified": "5-progress",
"repo-name": str(self.name),
"thin-manifests": "true",
"use-manifests": "strict",
}
osutils.WriteFile(layout_conf_path, _dict_to_conf(conf), makedirs=True)
def add_package(self, pkg):
"""Add a package to this overlay.
Adds the package to the Overlay object's internal storage and writes the
package metadata to the Overlay's directory.
"""
self.packages.append(pkg)
self._write_ebuild(pkg)
if pkg.category not in self.categories:
self.categories.add(pkg.category)
osutils.WriteFile(
self.path / "profiles" / "categories",
pkg.category + "\n",
mode="a",
makedirs=True,
)
def _write_ebuild(self, pkg: "Package"):
"""Write a Package object out to an ebuild file in this Overlay."""
ebuild_path = (
self.path
/ pkg.category
/ pkg.package
/ (pkg.package + "-" + pkg.version + ".ebuild")
)
# EAPI must be the first thing defined in a ebuild, so write this config
# before anything else.
base_conf = {
"EAPI": pkg.eapi,
"KEYWORDS": pkg.keywords,
"SLOT": pkg.slot,
}
osutils.WriteFile(
ebuild_path, _dict_to_ebuild(base_conf), makedirs=True
)
# Write additional miscellaneous variables declared in the Package object.
for k, v in pkg.variables.items():
osutils.WriteFile(ebuild_path, f'{k}="{v}"\n', mode="a")
# Write an eclass inheritance line, if needed.
if pkg.format_eclass_line():
osutils.WriteFile(ebuild_path, pkg.format_eclass_line(), mode="a")
extra_conf = {
"DEPEND": pkg.depend,
"RDEPEND": pkg.rdepend,
}
osutils.WriteFile(ebuild_path, _dict_to_ebuild(extra_conf), mode="a")
def create_profile(
self, path=None, profile_parents=None, make_defaults=None
):
"""Create a profile in this overlay.
Creates a profile with the given settings and writes the profile with those
settings to the Overlay's directory.
"""
path = pathlib.Path(path) if path else pathlib.Path("base")
if path in self.profiles:
raise KeyError("A profile with that path already exists!")
prof = Profile(
self, path, parents=profile_parents, make_defaults=make_defaults
)
self._write_profile(prof)
self.profiles[path] = prof
return prof
def _write_profile(self, profile):
"""Write a Profile object out to this Overlay's directory."""
osutils.WriteFile(
self.path / "profiles" / profile.path / "make.defaults",
_dict_to_ebuild(profile.make_defaults),
makedirs=True,
)
if profile.parents:
formatted_parents = []
for parent in profile.parents:
formatted_parents.append(
str(parent.overlay) + ":" + str(parent.path)
)
osutils.WriteFile(
self.path / "profiles" / profile.path / "parent",
"\n".join(formatted_parents) + "\n",
)
class Sysroot(object):
"""Sysroot object representing a functional Portage directory for testing."""
# These PORTDIR_OVERLAY entries are necessary for any Portage operations to
# function as the chroot's profile is parsed first, even if that profile is
# not used by the sysroot at all.
# This tuple should effectively be:
# /mnt/host/source/src/third_party/portage-stable
# /mnt/host/source/src/third_party/chromiumos-overlay
# /mnt/host/source/src/third_party/eclass-overlay
_BOOTSTRAP_PORTDIR_OVERLAYS = (
os.path.join(
constants.CHROOT_SOURCE_ROOT, constants.PORTAGE_STABLE_OVERLAY_DIR
),
os.path.join(
constants.CHROOT_SOURCE_ROOT, constants.CHROMIUMOS_OVERLAY_DIR
),
os.path.join(
constants.CHROOT_SOURCE_ROOT, constants.ECLASS_OVERLAY_DIR
),
)
def __init__(self, path, profile, overlays):
self.path = path
osutils.SafeMakedirs(path / "etc" / "portage" / "profile")
osutils.SafeMakedirs(path / "etc" / "portage" / "hooks")
osutils.SafeSymlink(
profile.full_path, path / "etc" / "portage" / "make.profile"
)
sysroot_conf = {
"ACCEPT_KEYWORDS": "amd64",
"ROOT": str(self.path),
"SYSROOT": str(self.path),
"ARCH": "amd64",
"PORTAGE_CONFIGROOT": str(self.path),
"PORTDIR": str(overlays[0].path),
"PORTDIR_OVERLAY": "\n".join(
itertools.chain(
(str(o.path) for o in overlays),
Sysroot._BOOTSTRAP_PORTDIR_OVERLAYS,
)
),
"PORTAGE_BINHOST": "",
"USE": "",
"PKGDIR": str(self.path / "packages"),
"PORTAGE_TMPDIR": str(self.path / "tmp"),
"DISTDIR": str(self.path / "var" / "lib" / "portage" / "distfiles"),
}
osutils.WriteFile(
self.path / "etc" / "portage" / "make.conf",
_dict_to_ebuild(sysroot_conf),
)
osutils.WriteFile(
self.path / "etc" / "portage" / "package.mask",
"".join(f"*/*::{o}\n" for o in _EXCLUDED_OVERLAYS),
)
@property
def _env(self):
"""Return a dict of the environment variables for this sysroot."""
return {
"PORTAGE_CONFIGROOT": str(self.path),
"ROOT": str(self.path),
"SYSROOT": str(self.path),
"BROOT": str(self.path),
}
def run(self, cmd, **kwargs):
"""Run a command against this sysroot.
This method sets up the equivalent calling environment to the `emerge`
wrappers we generate but targeted at this specific sysroot, which has
an arbitrary path in the test environment. This means that Portage commands
such as `equery list '*'` will correctly run against this sysroot.
"""
extra_env = self._env
extra_env.update(kwargs.pop("extra_env", {}))
kwargs.setdefault("encoding", "utf-8")
return cros_build_lib.run(cmd, extra_env=extra_env, **kwargs)
class Profile(object):
"""Portage profile, lives in an overlay."""
def __init__(self, overlay, path, parents=None, make_defaults=None):
self.overlay = overlay.name
self.path = path
self.full_path = overlay.path / "profiles" / path
self.parents = tuple(parents) if parents else None
self.make_defaults = make_defaults if make_defaults else {"USE": ""}
class Package(object):
"""Portage package, lives in an overlay."""
inherit: Tuple[str]
variables: Dict[str, str]
def __init__(
self,
category,
package,
version="1",
eapi="7",
keywords="*",
slot="0",
depend="",
rdepend="",
inherit: Union[Iterable[str], str] = tuple(),
**kwargs,
):
self.category = category
self.package = package
self.version = version
self.eapi = eapi
self.keywords = keywords
self.slot = slot
self.depend = depend
self.rdepend = rdepend
self.inherit = (
(inherit,) if isinstance(inherit, str) else tuple(inherit)
)
self.variables = kwargs
@classmethod
def from_cpv(cls, pkg_str: str):
"""Creates a Package from a CPV string."""
cpv = package_info.parse(pkg_str)
return cls(category=cpv.category, package=cpv.package, version=cpv.vr)
@property
def cpv(self) -> package_info.CPV:
"""Returns a CPV object constructed from this package's metadata.
Deprecated, use package_info instead.
"""
return package_info.SplitCPV(
self.category + "/" + self.package + "-" + self.version
)
@property
def package_info(self) -> package_info.PackageInfo:
"""Returns a PackageInfo object constructed from this package's metadata."""
return package_info.parse(
f"{self.category}/{self.package}-{self.version}"
)
def format_eclass_line(self) -> str:
"""Returns a string containing this package's eclass inheritance line."""
if self.inherit and isinstance(self.inherit, str):
return f"inherit {self.inherit}\n"
elif self.inherit:
return f'inherit {" ".join(self.inherit)}\n'
else:
return ""