Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

context bundles #1029

Merged
merged 8 commits into from
Mar 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions src/rez/bundle_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import os
import os.path

from rez.exceptions import ContextBundleError
from rez.utils.logging_ import print_info, print_warning
from rez.utils.yaml import save_yaml
from rez.package_copy import copy_package


def bundle_context(context, dest_dir, force=False, skip_non_relocatable=False,
quiet=False, verbose=False):
"""Bundle a context and its variants into a relocatable dir.

This creates a copy of a context with its variants retargeted to a local
package repository containing only the variants the context uses. The
generated file structure looks like so:

/dest_dir/
/context.rxt
/packages/
/foo/1.1.1/package.py
/...(payload)...
/bah/4.5.6/package.py
/...(payload)...

Args:
context (`ResolvedContext`): Context to bundle
dest_dir (str): Destination directory. Must not exist.
force (bool): If True, relocate package even if non-relocatable. Use at
your own risk. Overrides `skip_non_relocatable`.
skip_non_relocatable (bool): If True, leave non-relocatable packages
unchanged. Normally this will raise a `PackageCopyError`.
quiet (bool): Suppress all output
verbose (bool): Verbose mode (quiet will override)
"""
if quiet:
verbose = False
if force:
skip_non_relocatable = False

if os.path.exists(dest_dir):
raise ContextBundleError("Dest dir must not exist: %s" % dest_dir)

if not quiet:
label = context.load_path or "context"
print_info("Bundling %s into %s...", label, dest_dir)

os.mkdir(dest_dir)

_init_bundle(dest_dir)

relocated_package_names = _copy_variants(
context=context,
bundle_dir=dest_dir,
force=force,
skip_non_relocatable=skip_non_relocatable,
verbose=verbose
)

rxt_filepath = _write_retargeted_context(
context=context,
bundle_dir=dest_dir,
relocated_package_names=relocated_package_names
)

if verbose:
print_info("Context bundled to %s", rxt_filepath)


def _init_bundle(bundle_dir):
# Create bundle conf file. It doesn't contain anything at time of writing,
# but its presence on disk signifies that this is a context bundle.
#
bundle_filepath = os.path.join(bundle_dir, "bundle.yaml")
save_yaml(bundle_filepath)

# init pkg repo
repo_path = os.path.join(bundle_dir, "packages")
os.mkdir(repo_path)

# Bundled repos are always memcached disabled because they're on local disk
# (so access should be fast); but also, local repo paths written to shared
# memcached instance could easily clash.
#
settings_filepath = os.path.join(bundle_dir, "packages", "settings.yaml")
save_yaml(settings_filepath, disable_memcached=True)


def _copy_variants(context, bundle_dir, force=False, skip_non_relocatable=False,
verbose=False):
relocated_package_names = []
repo_path = os.path.join(bundle_dir, "packages")

for variant in context.resolved_packages:
package = variant.parent

if skip_non_relocatable and not package.is_relocatable:
if verbose:
print_warning(
"Skipped bundling of non-relocatable package %s",
package.qualified_name
)
continue

copy_package(
package=package,
dest_repository=repo_path,
variants=[variant.index],
force=force,
keep_timestamp=True,
verbose=verbose
)

relocated_package_names.append(package.name)

return relocated_package_names


def _write_retargeted_context(context, bundle_dir, relocated_package_names):
repo_path = os.path.join(bundle_dir, "packages")
rxt_filepath = os.path.join(bundle_dir, "context.rxt")

bundled_context = context.retargeted(
package_paths=[repo_path],
package_names=relocated_package_names,
skip_missing=True
)

bundled_context.save(rxt_filepath)
return rxt_filepath
7 changes: 7 additions & 0 deletions src/rez/cli/_entry_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,10 @@ def run_rez_yaml2py():
check_production_install()
from rez.cli._main import run
return run("yaml2py")


@scriptname("rez-bundle")
def run_rez_bundle():
check_production_install()
from rez.cli._main import run
return run("bundle")
4 changes: 2 additions & 2 deletions src/rez/cli/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from argparse import _StoreTrueAction, SUPPRESS
from rez.cli._util import subcommands, LazyArgumentParser, _env_var_true
from rez.utils.logging_ import print_error
from rez.exceptions import RezError, RezSystemError
from rez.exceptions import RezError, RezSystemError, _NeverError
from rez import __version__


Expand Down Expand Up @@ -146,7 +146,7 @@ def run(command=None):
extra_arg_groups = []

if opts.debug or _env_var_true("REZ_DEBUG"):
exc_type = None
exc_type = _NeverError
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small py3 compat issue I ran into

else:
exc_type = RezError

Expand Down
1 change: 1 addition & 0 deletions src/rez/cli/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"test": {},
"view": {},
"yaml2py": {},
"bundle": {}
}


Expand Down
48 changes: 48 additions & 0 deletions src/rez/cli/bundle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'''
Bundle a context and its packages into a relocatable dir.
'''
from __future__ import print_function

import os
import os.path
import sys


def setup_parser(parser, completions=False):
group = parser.add_mutually_exclusive_group()
group.add_argument(
"-s", "--skip-non-relocatable", action="store_true",
help="leave non-relocatable packages non-bundled, rather than raise an error")
group.add_argument(
"-f", "--force", action="store_true",
help="bundle package even if it isn't relocatable (use at your own risk)")
parser.add_argument(
"RXT",
help="context to bundle")
parser.add_argument(
"DEST_DIR",
help="directory to create bundle in; must not exist")


def command(opts, parser, extra_arg_groups=None):
from rez.utils.logging_ import print_error
from rez.bundle_context import bundle_context
from rez.resolved_context import ResolvedContext

rxt_filepath = os.path.abspath(os.path.expanduser(opts.RXT))
dest_dir = os.path.abspath(os.path.expanduser(opts.DEST_DIR))

# sanity checks
if not os.path.exists(rxt_filepath):
print_error("File does not exist: %s", rxt_filepath)
sys.exit(1)

context = ResolvedContext.load(rxt_filepath)

bundle_context(
context=context,
dest_dir=dest_dir,
force=opts.force,
skip_non_relocatable=opts.skip_non_relocatable,
verbose=opts.verbose
)
5 changes: 5 additions & 0 deletions src/rez/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ class PackageCopyError(RezError):
pass


class ContextBundleError(RezError):
"""There was a problem bundling a context."""
pass


class PackageCacheError(RezError):
"""There was an error related to a package cache."""
pass
Expand Down
24 changes: 22 additions & 2 deletions src/rez/package_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,25 @@ def install_variant(self, variant_resource, dry_run=False, overrides=None):
"""
raise NotImplementedError

def get_equivalent_variant(self, variant_resource):
"""Find a variant in this repository that is equivalent to that given.

A variant is equivalent to another if it belongs to a package of the
same name and version, and it has the same definition (ie package
requirements).

Note that even though the implementation is trivial, this function is
provided since using `install_variant` to find an existing variant is
nonintuitive.

Args:
variant_resource (`VariantResource`): Variant to install.

Returns:
`VariantResource` object, or None if the variant was not found.
"""
return self.install_variant(variant_resource, dry_run=True)

def get_parent_package_family(self, package_resource):
"""Get the parent package family of the given package.

Expand Down Expand Up @@ -434,6 +453,7 @@ def get_repository(self, path):
# get possibly cached repo
repository = self.repositories.get(normalised_path)

# create and cache if not already cached
if repository is None:
repository = self._get_repository(normalised_path)
self.repositories[normalised_path] = repository
Expand Down Expand Up @@ -504,10 +524,10 @@ def clear_caches(self):
self.repositories.clear()
self.pool.clear_caches()

def _get_repository(self, path):
def _get_repository(self, path, **repo_args):
repo_type, location = path.split('@', 1)
cls = plugin_manager.get_plugin_class('package_repository', repo_type)
repo = cls(location, self.pool)
repo = cls(location, self.pool, **repo_args)
return repo


Expand Down
Loading