blob: 73d64365e5eb11316e7aacf45630081affd44eb8 [file] [log] [blame]
# -*- coding: utf-8 -*-
# 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.
"""Utilities for setting up Portage objects for testing."""
from __future__ import print_function
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, masters=None):
self.path = pathlib.Path(root_path)
self.name = str(name)
self.masters = tuple(masters) if masters 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'
master_names = ' '.join(m.name for m in self.masters or [])
conf = {
'masters': 'portage-stable chromiumos eclass-overlay' + master_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 / 'parents',
'\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 ''