Source code for rez.package_resources

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


from rez.utils.resources import Resource
from rez.utils.schema import Required, schema_keys, extensible_schema_dict
from rez.utils.logging_ import print_warning
from rez.utils.sourcecode import SourceCode
from rez.utils.data_utils import cached_property, AttributeForwardMeta, \
    LazyAttributeMeta
from rez.utils.filesystem import find_matching_symlink
from rez.utils.formatting import PackageRequest
from rez.exceptions import PackageMetadataError, ResourceError
from rez.config import config, Config, create_config
from rez.vendor.version.version import Version
from rez.vendor.schema.schema import Schema, SchemaError, Optional, Or, And, Use
from rez.vendor.six import six

from textwrap import dedent
import os.path
from hashlib import sha1


basestring = six.string_types[0]


# package attributes created at release time
package_release_keys = (
    "timestamp",
    'revision',
    'changelog',
    'release_message',
    'previous_version',
    'previous_revision',
    'vcs')

# package attributes that we don't install
package_build_only_keys = (
    "requires_rez_version",
    "build_system",
    "build_command",
    "preprocess",
    "pre_build_commands"
)

# package attributes that are rex-based functions
package_rex_keys = (
    "pre_commands",
    "commands",
    "post_commands",
    "pre_build_commands",
    "pre_test_commands",
)


# ------------------------------------------------------------------------------
# utility schemas
# ------------------------------------------------------------------------------

help_schema = Or(basestring,  # single help entry
                 [[basestring]])  # multiple help entries

_is_late = And(SourceCode, lambda x: hasattr(x, "_late"))


[docs]def late_bound(schema): return Or(SourceCode, schema)
# used when 'requires' is late bound late_requires_schema = Schema([ Or(PackageRequest, And(basestring, Use(PackageRequest))) ]) # ------------------------------------------------------------------------------ # schema dicts # ------------------------------------------------------------------------------ # requirements of all package-related resources # base_resource_schema_dict = { Required("name"): basestring } # package family # package_family_schema_dict = base_resource_schema_dict.copy() # schema common to both package and variant # tests_schema = Schema({ Optional(basestring): Or( Or(basestring, [basestring]), extensible_schema_dict({ "command": Or(basestring, [basestring]), Optional("requires"): [ Or(PackageRequest, And(basestring, Use(PackageRequest))) ], Optional("run_on"): Or(basestring, [basestring]), Optional("on_variants"): Or( bool, { "type": "requires", "value": [ Or(PackageRequest, And(basestring, Use(PackageRequest))) ] } ) }) ) }) package_base_schema_dict = base_resource_schema_dict.copy() package_base_schema_dict.update({ # basics Optional("base"): basestring, Optional("version"): Version, Optional('description'): basestring, Optional('authors'): [basestring], # dependencies Optional('requires'): late_bound([PackageRequest]), Optional('build_requires'): late_bound([PackageRequest]), Optional('private_build_requires'): late_bound([PackageRequest]), # plugins Optional('has_plugins'): late_bound(bool), Optional('plugin_for'): late_bound([basestring]), # general Optional('uuid'): basestring, Optional('config'): Config, Optional('tools'): late_bound([basestring]), Optional('help'): late_bound(help_schema), # build related Optional('hashed_variants'): bool, # relocatability Optional('relocatable'): late_bound(Or(None, bool)), Optional('cachable'): late_bound(Or(None, bool)), # testing Optional('tests'): late_bound(tests_schema), # commands Optional('pre_commands'): SourceCode, Optional('commands'): SourceCode, Optional('post_commands'): SourceCode, Optional('pre_build_commands'): SourceCode, Optional('pre_test_commands'): SourceCode, # release info Optional("timestamp"): int, Optional('revision'): object, Optional('changelog'): basestring, Optional('release_message'): Or(None, basestring), Optional('previous_version'): Version, Optional('previous_revision'): object, Optional('vcs'): basestring, # arbitrary fields Optional(basestring): late_bound(object) }) # package package_schema_dict = package_base_schema_dict.copy() package_schema_dict.update({ # deliberately not possible to late bind Optional("variants"): [[PackageRequest]] }) # variant variant_schema_dict = package_base_schema_dict.copy() # ------------------------------------------------------------------------------ # resource schemas # ------------------------------------------------------------------------------ package_family_schema = Schema(package_family_schema_dict) package_schema = Schema(package_schema_dict) variant_schema = Schema(variant_schema_dict) # ------------------------------------------------------------------------------ # schemas for converting from POD datatypes # ------------------------------------------------------------------------------ _commands_schema = Or(SourceCode, # commands as converted function callable, # commands as function basestring, # commands in text block [basestring]) # old-style (rez-1) commands _function_schema = Or(SourceCode, callable) _package_request_schema = And(basestring, Use(PackageRequest)) package_pod_schema_dict = base_resource_schema_dict.copy() large_string_dict = And(basestring, Use(lambda x: dedent(x).strip())) package_pod_schema_dict.update({ Optional("base"): basestring, Optional("version"): And(basestring, Use(Version)), Optional('description'): large_string_dict, 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('has_plugins'): late_bound(bool), Optional('plugin_for'): late_bound([basestring]), Optional('uuid'): basestring, Optional('config'): And(dict, Use(lambda x: create_config(overrides=x))), Optional('tools'): late_bound([basestring]), Optional('help'): late_bound(help_schema), Optional('hashed_variants'): bool, Optional('relocatable'): late_bound(Or(None, bool)), Optional('cachable'): late_bound(Or(None, bool)), 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, Optional("timestamp"): int, Optional('revision'): object, Optional('changelog'): large_string_dict, Optional('release_message'): Or(None, basestring), Optional('previous_version'): And(basestring, Use(Version)), Optional('previous_revision'): object, Optional('vcs'): basestring, # arbitrary keys Optional(basestring): late_bound(object) }) package_pod_schema = Schema(package_pod_schema_dict) # ------------------------------------------------------------------------------ # resource classes # ------------------------------------------------------------------------------
[docs]class PackageRepositoryResource(Resource): """Base class for all package-related resources. Attributes: schema_error (`Exception`): Type of exception to throw on bad data. repository_type (str): Type of package repository associated with this resource type. """ schema_error = PackageMetadataError repository_type = None
[docs] @classmethod def normalize_variables(cls, variables): if "repository_type" not in variables or "location" not in \ variables: raise ResourceError("%s resources require a repository_type and " "location" % cls.__name__) return super(PackageRepositoryResource, cls).normalize_variables( variables)
def __init__(self, variables=None): super(PackageRepositoryResource, self).__init__(variables) @cached_property def uri(self): return self._uri() @property def location(self): return self.get("location") @property def name(self): return self.get("name") def _uri(self): """Return a URI. Implement this function to return a short, readable string that uniquely identifies this resource. """ raise NotImplementedError
[docs]class PackageFamilyResource(PackageRepositoryResource): """A package family. A repository implementation's package family resource(s) must derive from this class. It must satisfy the schema `package_family_schema`. """ pass
[docs]class PackageResource(PackageRepositoryResource): """A package. A repository implementation's package resource(s) must derive from this class. It must satisfy the schema `package_schema`. """
[docs] @classmethod def normalize_variables(cls, variables): """Make sure version is treated consistently """ # if the version is False, empty string, etc, throw it out if variables.get('version', True) in ('', False, '_NO_VERSION', None): del variables['version'] return super(PackageResource, cls).normalize_variables(variables)
@cached_property def version(self): ver_str = self.get("version", "") return Version(ver_str)
[docs]class VariantResource(PackageResource): """A package variant. A repository implementation's variant resource(s) must derive from this class. It must satisfy the schema `variant_schema`. Even packages that do not have a 'variants' section contain a variant - in this case it is the 'None' variant (the value of `index` is None). This provides some internal consistency and simplifies the implementation. """ @property def index(self): return self.get("index", None) @cached_property def root(self): """Return the 'root' path of the variant.""" return self._root() @cached_property def subpath(self): """Return the variant's 'subpath' The subpath is the relative path the variant's payload should be stored under, relative to the package base. If None, implies that the variant root matches the package base. """ return self._subpath() def _root(self, ignore_shortlinks=False): raise NotImplementedError def _subpath(self, ignore_shortlinks=False): raise NotImplementedError
# ------------------------------------------------------------------------------ # resource helper classes # # Package repository plugins are not required to use the following classes, but # they may help minimise the amount of code you need to write. # ------------------------------------------------------------------------------
[docs]class PackageResourceHelper(PackageResource): """PackageResource with some common functionality included. """ variant_key = None @cached_property def commands(self): return self._convert_to_rex(self._commands) @cached_property def pre_commands(self): return self._convert_to_rex(self._pre_commands) @cached_property def post_commands(self): return self._convert_to_rex(self._post_commands)
[docs] def iter_variants(self): num_variants = len(self.variants or []) if num_variants == 0: indexes = [None] else: indexes = range(num_variants) for index in indexes: variant = self._repository.get_resource( self.variant_key, location=self.location, name=self.name, version=self.get("version"), index=index) yield variant
def _convert_to_rex(self, commands): if isinstance(commands, list): from rez.utils.backcompat import convert_old_commands msg = "package %r is using old-style commands." % self.uri if config.disable_rez_1_compatibility or config.error_old_commands: raise SchemaError(None, msg) elif config.warn("old_commands"): print_warning(msg) commands = convert_old_commands(commands) if isinstance(commands, basestring): return SourceCode(source=commands) elif callable(commands): return SourceCode(func=commands) else: return commands
class _Metas(AttributeForwardMeta, LazyAttributeMeta): pass
[docs]class VariantResourceHelper(six.with_metaclass(_Metas, VariantResource)): """Helper class for implementing variants that inherit properties from their parent package. Since a variant overlaps so much with a package, here we use the forwarding metaclass to forward our parent package's attributes onto ourself (with some exceptions - eg 'variants', 'requires'). This is a common enough pattern that it's supplied here for other repository plugins to use. """ # Note: lazy key validation doesn't happen in this class, it just fowards on # attributes from the package. But LazyAttributeMeta does still use this # schema to create other class attributes, such as `validate_data`. schema = variant_schema # forward Package attributes onto ourself keys = schema_keys(package_schema) - set(["variants"]) def _uri(self): index = self.index idxstr = '' if index is None else str(index) return "%s[%s]" % (self.parent.uri, idxstr) def _subpath(self, ignore_shortlinks=False): if self.index is None: return None if self.parent.hashed_variants: vars_str = str(list(map(str, self.variant_requires))) h = sha1(vars_str.encode("utf8")) hashdir = h.hexdigest() if (not ignore_shortlinks) and \ config.use_variant_shortlinks and \ self.base is not None: # search for matching shortlink and use that path = os.path.join(self.base, config.variant_shortlinks_dirname) if os.path.exists(path): actual_root = os.path.join(self.base, hashdir) linkname = find_matching_symlink(path, actual_root) if linkname: return os.path.join( config.variant_shortlinks_dirname, linkname) return hashdir else: dirs = [x.safe_str() for x in self.variant_requires] subpath = os.path.join(*dirs) return subpath def _root(self, ignore_shortlinks=False): if self.base is None: return None elif self.index is None: return self.base else: subpath = self._subpath(ignore_shortlinks=ignore_shortlinks) root = os.path.join(self.base, subpath) return root @cached_property def variant_requires(self): index = self.index if index is None: return [] else: try: return self.parent.variants[index] or [] except (IndexError, TypeError): raise ResourceError( "Unexpected error - variant %s cannot be found in its " "parent package %s" % (self.uri, self.parent.uri)) @property def wrapped(self): # forward Package attributes onto ourself return self.parent def _load(self): # doesn't have its own data, forwards on from parent instead return None