Source code for rez.package_maker

# SPDX-License-Identifier: Apache-2.0
# Copyright Contributors to the Rez Project


from rez.utils._version import _rez_version
from rez.utils.schema import Required, extensible_schema_dict
from rez.utils.filesystem import retain_cwd
from rez.utils.formatting import PackageRequest
from rez.utils.data_utils import AttrDictWrapper
from rez.utils.logging_ import print_warning
from rez.exceptions import PackageMetadataError
from rez.package_resources import help_schema, _commands_schema, \
    _function_schema, late_bound
from rez.package_repository import create_memory_package_repository
from rez.packages import Package
from rez.package_py_utils import expand_requirement
from rez.vendor.schema.schema import Schema, Optional, Or, Use, And
from rez.vendor.six import six
from rez.vendor.version.version import Version
from contextlib import contextmanager
import os


basestring = six.string_types[0]


# this schema will automatically harden request strings like 'python-*'; see
# the 'expand_requires' function for more info.
#
package_request_schema = Or(And(basestring, Use(expand_requirement)),
                            And(PackageRequest, Use(str)))

tests_schema = Schema({
    Optional(basestring): Or(
        Or(basestring, [basestring]),
        extensible_schema_dict({
            "command": Or(basestring, [basestring]),
            Optional("requires"): [package_request_schema],
            Optional("run_on"): Or(basestring, [basestring]),
            Optional("on_variants"): Or(
                bool,
                {
                    "type": "requires",
                    "value": [package_request_schema]
                }
            )
        })
    )
})


package_schema = Schema({
    Optional("requires_rez_version"):   And(basestring, Use(Version)),

    Required("name"):                   basestring,
    Optional("base"):                   basestring,
    Optional("version"):                Or(basestring,
                                           And(Version, Use(str))),
    Optional('description'):            basestring,
    Optional('authors'):                [basestring],

    Optional('requires'):               late_bound([package_request_schema]),
    Optional('build_requires'):         late_bound([package_request_schema]),
    Optional('private_build_requires'): late_bound([package_request_schema]),

    # deliberately not possible to late bind
    Optional('variants'):               [[package_request_schema]],

    Optional('hashed_variants'):        bool,

    Optional('relocatable'):            late_bound(Or(None, bool)),
    Optional('cachable'):               late_bound(Or(None, bool)),

    Optional('uuid'):                   basestring,
    Optional('config'):                 dict,
    Optional('tools'):                  late_bound([basestring]),
    Optional('help'):                   late_bound(help_schema),

    Optional('tests'):                  late_bound(tests_schema),

    Optional('pre_commands'):           _commands_schema,
    Optional('commands'):               _commands_schema,
    Optional('post_commands'):          _commands_schema,
    Optional('pre_build_commands'):     _commands_schema,
    Optional('pre_test_commands'):      _commands_schema,

    # attributes specific to pre-built packages
    Optional("build_system"):           basestring,
    Optional("build_command"):          Or([basestring], basestring, False),
    Optional("preprocess"):             _function_schema,

    # arbitrary fields
    Optional(basestring):               object
})


[docs]class PackageMaker(AttrDictWrapper): """Utility class for creating packages.""" def __init__(self, name, data=None, package_cls=None): """Create a package maker. Args: name (str): Package name. """ super(PackageMaker, self).__init__(data) self.name = name self.package_cls = package_cls or Package # set by `make_package` self.installed_variants = [] self.skipped_variants = []
[docs] def get_package(self): """Create the analogous package. Returns: `Package` object. """ # get and validate package data package_data = self._get_data() package_data = package_schema.validate(package_data) # check compatibility with rez version if "requires_rez_version" in package_data: ver = package_data.pop("requires_rez_version") if Version(_rez_version) < ver: raise PackageMetadataError( "Failed reading package definition file: rez version >= %s " "needed (current version is %s)" % (ver, _rez_version) ) # create a 'memory' package repository containing just this package version_str = package_data.get("version") or "_NO_VERSION" repo_data = {self.name: {version_str: package_data}} repo = create_memory_package_repository(repo_data) # retrieve the package from the new repository family_resource = repo.get_package_family(self.name) it = repo.iter_packages(family_resource) package_resource = next(it) package = self.package_cls(package_resource) # revalidate the package for extra measure package.validate_data() return package
def _get_data(self): data = self._data.copy() data.pop("installed_variants", None) data.pop("skipped_variants", None) data.pop("package_cls", None) data = dict((k, v) for k, v in data.items() if v is not None) return data
[docs]@contextmanager def make_package(name, path, make_base=None, make_root=None, skip_existing=True, warn_on_skip=True): """Make and install a package. Example: >>> def make_root(variant, path): >>> os.symlink("/foo_payload/misc/python27", "ext") >>> >>> with make_package('foo', '/packages', make_root=make_root) as pkg: >>> pkg.version = '1.0.0' >>> pkg.description = 'does foo things' >>> pkg.requires = ['python-2.7'] Args: name (str): Package name. path (str): Package repository path to install package into. make_base (callable): Function that is used to create the package payload, if applicable. make_root (callable): Function that is used to create the package variant payloads, if applicable. skip_existing (bool): If True, detect if a variant already exists, and skip with a warning message if so. warn_on_skip (bool): If True, print warning when a variant is skipped. Yields: `PackageMaker` object. Note: Both `make_base` and `make_root` are called once per variant install, and have the signature (variant, path). Note: The 'installed_variants' attribute on the `PackageMaker` instance will be appended with variant(s) created by this function, if any. """ maker = PackageMaker(name) yield maker # post-with-block: # package = maker.get_package() src_variants = [] # skip those variants that already exist if skip_existing: for variant in package.iter_variants(): variant_ = variant.install(path, dry_run=True) if variant_ is None: src_variants.append(variant) else: maker.skipped_variants.append(variant_) if warn_on_skip: print_warning("Skipping installation: Package variant already " "exists: %s" % variant_.uri) else: src_variants = package.iter_variants() with retain_cwd(): # install the package variant(s) into the filesystem package repo at `path` for variant in src_variants: variant_ = variant.install(path) base = variant_.base if make_base and base: if not os.path.exists(base): os.makedirs(base) os.chdir(base) make_base(variant_, base) root = variant_.root if make_root and root: if not os.path.exists(root): os.makedirs(root) os.chdir(root) make_root(variant_, root) maker.installed_variants.append(variant_)