# 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