def find_compatible_interpreters(pex_python_path=None, compatibility_constraints=None): """Find all compatible interpreters on the system within the supplied constraints and use PEX_PYTHON_PATH if it is set. If not, fall back to interpreters on $PATH. """ if pex_python_path: interpreters = [] for binary in pex_python_path.split(os.pathsep): try: interpreters.append(PythonInterpreter.from_binary(binary)) except Executor.ExecutionError: print("Python interpreter %s in PEX_PYTHON_PATH failed to load properly." % binary, file=sys.stderr) if not interpreters: die('PEX_PYTHON_PATH was defined, but no valid interpreters could be identified. Exiting.') else: # We may have been invoked with a specific interpreter not on the $PATH, make sure our # sys.executable is included as a candidate in this case. interpreters = OrderedSet([PythonInterpreter.get()]) # Add all qualifying interpreters found in $PATH. interpreters.update(PythonInterpreter.all()) return list( matched_interpreters(interpreters, compatibility_constraints) if compatibility_constraints else interpreters )
def to_requirement(self, dist): req = dist.as_requirement() markers = OrderedSet() # Here we map any wheel python requirement to the equivalent environment marker: # See: # + https://www.python.org/dev/peps/pep-0345/#requires-python # + https://www.python.org/dev/peps/pep-0508/#environment-markers python_requires = dist_metadata.requires_python(dist) if python_requires: markers.update( Marker(python_version) for python_version in sorted( 'python_version {operator} {version!r}'.format( operator=specifier.operator, version=specifier.version) for specifier in python_requires)) markers.update(self._markers_by_requirement_key.get(req.key, ())) if not markers: return req if len(markers) == 1: marker = next(iter(markers)) req.marker = marker return req # We may have resolved with multiple paths to the dependency represented by dist and at least # two of those paths had (different) conditional requirements for dist based on environment # marker predicates. In that case, since the pip resolve succeeded, the implication is that the # environment markers are compatible; i.e.: their intersection selects the target interpreter. # Here we make that intersection explicit. # See: https://www.python.org/dev/peps/pep-0508/#grammar marker = ' and '.join('({})'.format(marker) for marker in markers) return Requirement.parse('{}; {}'.format(req, marker))
def minimum_sys_path(cls, site_libs, inherit_path): scrub_paths = OrderedSet() site_distributions = OrderedSet() user_site_distributions = OrderedSet() def all_distribution_paths(path): locations = set(dist.location for dist in find_distributions(path)) return set([path]) | locations | set(os.path.realpath(path) for path in locations) for path_element in sys.path: if cls._tainted_path(path_element, site_libs): TRACER.log('Tainted path element: %s' % path_element) site_distributions.update(all_distribution_paths(path_element)) else: TRACER.log('Not a tainted path element: %s' % path_element, V=2) user_site_distributions.update(all_distribution_paths(USER_SITE)) if inherit_path == 'false': scrub_paths = site_distributions | user_site_distributions for path in user_site_distributions: TRACER.log('Scrubbing from user site: %s' % path) for path in site_distributions: TRACER.log('Scrubbing from site-packages: %s' % path) scrubbed_sys_path = list(OrderedSet(sys.path) - scrub_paths) pythonpath = cls.unstash_pythonpath() if pythonpath is not None: original_pythonpath = pythonpath.split(os.pathsep) user_pythonpath = list(OrderedSet(original_pythonpath) - set(sys.path)) if original_pythonpath == user_pythonpath: TRACER.log('Unstashed PYTHONPATH of %s' % pythonpath, V=2) else: TRACER.log('Extracted user PYTHONPATH of %s from unstashed PYTHONPATH of %s' % (os.pathsep.join(user_pythonpath), pythonpath), V=2) if inherit_path == 'false': for path in user_pythonpath: TRACER.log('Scrubbing user PYTHONPATH element: %s' % path) elif inherit_path == 'prefer': TRACER.log('Prepending user PYTHONPATH: %s' % os.pathsep.join(user_pythonpath)) scrubbed_sys_path = user_pythonpath + scrubbed_sys_path elif inherit_path == 'fallback': TRACER.log('Appending user PYTHONPATH: %s' % os.pathsep.join(user_pythonpath)) scrubbed_sys_path = scrubbed_sys_path + user_pythonpath scrub_from_importer_cache = filter( lambda key: any(key.startswith(path) for path in scrub_paths), sys.path_importer_cache.keys()) scrubbed_importer_cache = dict((key, value) for (key, value) in sys.path_importer_cache.items() if key not in scrub_from_importer_cache) for importer_cache_entry in scrub_from_importer_cache: TRACER.log('Scrubbing from path_importer_cache: %s' % importer_cache_entry, V=2) return scrubbed_sys_path, scrubbed_importer_cache
def find_compatible_interpreters(path=None, compatibility_constraints=None): """Find all compatible interpreters on the system within the supplied constraints and use path if it is set. If not, fall back to interpreters on $PATH. """ interpreters = OrderedSet() paths = None if path: paths = path.split(os.pathsep) else: # We may have been invoked with a specific interpreter, make sure our sys.executable is included # as a candidate in this case. interpreters.add(PythonInterpreter.get()) interpreters.update(PythonInterpreter.all(paths=paths)) return _filter_compatible_interpreters( interpreters, compatibility_constraints=compatibility_constraints)
def file_deps(console, filedeps_options, transitive_hydrated_targets): uniq_set = OrderedSet() for hydrated_target in transitive_hydrated_targets.closure: if hydrated_target.address.rel_path: uniq_set.add(hydrated_target.address.rel_path) if hasattr(hydrated_target.adaptor, "sources"): uniq_set.update(hydrated_target.adaptor.sources.snapshot.files) with Filedeps.line_oriented(filedeps_options, console) as (print_stdout, print_stderr): for f_path in uniq_set: print_stdout(f_path) return Filedeps(exit_code=0)
def file_deps(console, transitive_hydrated_targets): """List all source and BUILD files a target transitively depends on. Files are listed with relative paths and any BUILD files implied in the transitive closure of targets are also included. """ uniq_set = OrderedSet() for hydrated_target in transitive_hydrated_targets.closure: if hydrated_target.address.rel_path: uniq_set.add(hydrated_target.address.rel_path) if hasattr(hydrated_target.adaptor, "sources"): uniq_set.update(f.path for f in hydrated_target.adaptor.sources.snapshot.files) for f_path in uniq_set: console.print_stdout(f_path)
def minimum_sys_path(cls, site_libs, inherit_path): scrub_paths = OrderedSet() site_distributions = OrderedSet() user_site_distributions = OrderedSet() def all_distribution_paths(path): locations = set(dist.location for dist in find_distributions(path)) return set([path]) | locations | set( os.path.realpath(path) for path in locations) for path_element in sys.path: if cls._tainted_path(path_element, site_libs): TRACER.log('Tainted path element: %s' % path_element) site_distributions.update(all_distribution_paths(path_element)) else: TRACER.log('Not a tainted path element: %s' % path_element, V=2) user_site_distributions.update(all_distribution_paths(USER_SITE)) if inherit_path == 'false': scrub_paths = site_distributions | user_site_distributions for path in user_site_distributions: TRACER.log('Scrubbing from user site: %s' % path) for path in site_distributions: TRACER.log('Scrubbing from site-packages: %s' % path) scrubbed_sys_path = list(OrderedSet(sys.path) - scrub_paths) scrub_from_importer_cache = filter( lambda key: any(key.startswith(path) for path in scrub_paths), sys.path_importer_cache.keys()) scrubbed_importer_cache = dict( (key, value) for (key, value) in sys.path_importer_cache.items() if key not in scrub_from_importer_cache) for importer_cache_entry in scrub_from_importer_cache: TRACER.log('Scrubbing from path_importer_cache: %s' % importer_cache_entry, V=2) return scrubbed_sys_path, scrubbed_importer_cache
def set_script(self, script): """Set the entry point of this PEX environment based upon a distribution script. :param script: The script name as defined either by a console script or ordinary script within the setup.py of one of the distributions added to the PEX. :raises: :class:`PEXBuilder.InvalidExecutableSpecification` if the script is not found in any distribution added to the PEX. """ distributions = OrderedSet(self._distributions.values()) if self._pex_info.pex_path: for pex in self._pex_info.pex_path.split(":"): if os.path.exists(pex): distributions.update( PEX(pex, interpreter=self._interpreter).resolve()) # Check if 'script' is a console_script. dist, entry_point = get_entry_point_from_console_script( script, distributions) if entry_point: self.set_entry_point(entry_point) TRACER.log("Set entrypoint to console_script {!r} in {!r}".format( entry_point, dist)) return # Check if 'script' is an ordinary script. dist_script = get_script_from_distributions(script, distributions) if dist_script: if self._pex_info.entry_point: raise self.InvalidExecutableSpecification( "Cannot set both entry point and script of PEX!") self._pex_info.script = script TRACER.log("Set entrypoint to script {!r} in {!r}".format( script, dist_script.dist)) return raise self.InvalidExecutableSpecification( "Could not find script {!r} in any distribution {} within PEX!". format(script, ", ".join(str(d) for d in distributions)))
def minimum_sys_path(cls, site_libs, inherit_path): scrub_paths = OrderedSet() site_distributions = OrderedSet() user_site_distributions = OrderedSet() def all_distribution_paths(path): locations = set(dist.location for dist in find_distributions(path)) return set([path]) | locations | set(os.path.realpath(path) for path in locations) for path_element in sys.path: if cls._tainted_path(path_element, site_libs): TRACER.log('Tainted path element: %s' % path_element) site_distributions.update(all_distribution_paths(path_element)) else: TRACER.log('Not a tainted path element: %s' % path_element, V=2) user_site_distributions.update(all_distribution_paths(USER_SITE)) if inherit_path == 'false': scrub_paths = site_distributions | user_site_distributions for path in user_site_distributions: TRACER.log('Scrubbing from user site: %s' % path) for path in site_distributions: TRACER.log('Scrubbing from site-packages: %s' % path) scrubbed_sys_path = list(OrderedSet(sys.path) - scrub_paths) scrub_from_importer_cache = filter( lambda key: any(key.startswith(path) for path in scrub_paths), sys.path_importer_cache.keys()) scrubbed_importer_cache = dict((key, value) for (key, value) in sys.path_importer_cache.items() if key not in scrub_from_importer_cache) for importer_cache_entry in scrub_from_importer_cache: TRACER.log('Scrubbing from path_importer_cache: %s' % importer_cache_entry, V=2) return scrubbed_sys_path, scrubbed_importer_cache
class PexInfo(object): """PEX metadata. # Build metadata: build_properties: BuildProperties # (key-value information about the build system) code_hash: str # sha1 hash of all names/code in the archive distributions: {dist_name: str} # map from distribution name (i.e. path in # the internal cache) to its cache key (sha1) requirements: list # list of requirements for this environment # Environment options pex_root: string # root of all pex-related files eg: ~/.pex entry_point: string # entry point into this pex script: string # script to execute in this pex environment # at most one of script/entry_point can be specified zip_safe: bool, default True # is this pex zip safe? unzip: bool, default False # should this pex be unzipped and re-executed from there? inherit_path: false/fallback/prefer # should this pex inherit site-packages + user site-packages # + PYTHONPATH? ignore_errors: True, default False # should we ignore inability to resolve dependencies? always_write_cache: False # should we always write the internal cache to disk first? # this is useful if you have very large dependencies that # do not fit in RAM constrained environments .. versionchanged:: 0.8 Removed the ``repositories`` and ``indices`` information, as they were never implemented. """ PATH = 'PEX-INFO' INSTALL_CACHE = 'installed_wheels' @classmethod def make_build_properties(cls, interpreter=None): from .interpreter import PythonInterpreter from .platforms import Platform pi = interpreter or PythonInterpreter.get() plat = Platform.current() platform_name = plat.platform return { 'pex_version': pex_version, 'class': pi.identity.interpreter, 'version': pi.identity.version, 'platform': platform_name, } @classmethod def default(cls, interpreter=None): pex_info = { 'requirements': [], 'distributions': {}, 'build_properties': cls.make_build_properties(interpreter), } return cls(info=pex_info) @classmethod def from_pex(cls, pex): if os.path.isfile(pex): with open_zip(pex) as zf: pex_info = zf.read(cls.PATH) else: with open(os.path.join(pex, cls.PATH)) as fp: pex_info = fp.read() return cls.from_json(pex_info) @classmethod def from_json(cls, content): if isinstance(content, bytes): content = content.decode('utf-8') return cls(info=json.loads(content)) @classmethod def from_env(cls, env=ENV): supplied_env = env.strip_defaults() zip_safe = None if supplied_env.PEX_FORCE_LOCAL is None else not supplied_env.PEX_FORCE_LOCAL unzip = None if supplied_env.PEX_UNZIP is None else supplied_env.PEX_UNZIP pex_info = { 'pex_root': supplied_env.PEX_ROOT, 'entry_point': supplied_env.PEX_MODULE, 'script': supplied_env.PEX_SCRIPT, 'zip_safe': zip_safe, 'unzip': unzip, 'inherit_path': supplied_env.PEX_INHERIT_PATH, 'ignore_errors': supplied_env.PEX_IGNORE_ERRORS, 'always_write_cache': supplied_env.PEX_ALWAYS_CACHE, } # Filter out empty entries not explicitly set in the environment. return cls(info=dict( (k, v) for (k, v) in pex_info.items() if v is not None)) @classmethod def _parse_requirement_tuple(cls, requirement_tuple): if isinstance(requirement_tuple, (tuple, list)): if len(requirement_tuple) != 3: raise ValueError('Malformed PEX requirement: %r' % (requirement_tuple, )) # pre 0.8.x requirement type: pex_warnings.warn( 'Attempting to use deprecated PEX feature. Please upgrade past PEX 0.8.x.' ) return requirement_tuple[0] elif isinstance(requirement_tuple, compatibility_string): return requirement_tuple raise ValueError('Malformed PEX requirement: %r' % (requirement_tuple, )) def __init__(self, info=None): """Construct a new PexInfo. This should not be used directly.""" if info is not None and not isinstance(info, dict): raise ValueError('PexInfo can only be seeded with a dict, got: ' '%s of type %s' % (info, type(info))) self._pex_info = info or {} if 'inherit_path' in self._pex_info: self.inherit_path = self._pex_info['inherit_path'] self._distributions = self._pex_info.get('distributions', {}) # cast as set because pex info from json must store interpreter_constraints as a list self._interpreter_constraints = set( self._pex_info.get('interpreter_constraints', set())) requirements = self._pex_info.get('requirements', []) if not isinstance(requirements, (list, tuple)): raise ValueError('Expected requirements to be a list, got %s' % type(requirements)) self._requirements = OrderedSet( self._parse_requirement_tuple(req) for req in requirements) def _get_safe(self, key): if key not in self._pex_info: return None value = self._pex_info[key] return value.encode('utf-8') if PY2 else value @property def build_properties(self): """Information about the system on which this PEX was generated. :returns: A dictionary containing metadata about the environment used to build this PEX. """ return self._pex_info.get('build_properties', {}) @build_properties.setter def build_properties(self, value): if not isinstance(value, dict): raise TypeError('build_properties must be a dictionary!') self._pex_info['build_properties'] = self.make_build_properties() self._pex_info['build_properties'].update(value) @property def zip_safe(self): """Whether or not this PEX should be treated as zip-safe. If set to false and the PEX is zipped, the contents of the PEX will be unpacked into a directory within the PEX_ROOT prior to execution. This allows code and frameworks depending upon __file__ existing on disk to operate normally. By default zip_safe is True. May be overridden at runtime by the $PEX_FORCE_LOCAL environment variable. """ return self._pex_info.get('zip_safe', True) @zip_safe.setter def zip_safe(self, value): self._pex_info['zip_safe'] = bool(value) @property def unzip(self): """Whether or not PEX should be unzipped before it's executed. Unzipping a PEX is a operation that can be cached on the 1st run of a given PEX file which can result in lower startup latency in subsequent runs. """ return self._pex_info.get('unzip', False) @unzip.setter def unzip(self, value): self._pex_info['unzip'] = bool(value) @property def strip_pex_env(self): """Whether or not this PEX should strip `PEX_*` env vars before executing its entrypoint. You might want to set this to `False` if this PEX executes other PEXes or the Pex CLI itself and you want the executed PEX to be controlled via PEX environment variables. """ return self._pex_info.get('strip_pex_env', True) @strip_pex_env.setter def strip_pex_env(self, value): self._pex_info['strip_pex_env'] = bool(value) @property def pex_path(self): """A colon separated list of other pex files to merge into the runtime environment. This pex info property is used to persist the PEX_PATH environment variable into the pex info metadata for reuse within a built pex. """ return self._pex_info.get('pex_path') @pex_path.setter def pex_path(self, value): self._pex_info['pex_path'] = value @property def inherit_path(self): """Whether or not this PEX should be allowed to inherit system dependencies. By default, PEX environments are scrubbed of all system distributions prior to execution. This means that PEX files cannot rely upon preexisting system libraries. By default inherit_path is false. This may be overridden at runtime by the $PEX_INHERIT_PATH environment variable. """ return self._pex_info.get('inherit_path', 'false') @inherit_path.setter def inherit_path(self, value): if value is False: value = 'false' elif value is True: value = 'prefer' self._pex_info['inherit_path'] = value @property def interpreter_constraints(self): """A list of constraints that determine the interpreter compatibility for this pex, using the Requirement-style format, e.g. ``'CPython>=3', or just '>=2.7,<3'`` for requirements agnostic to interpreter class. This property will be used at exec time when bootstrapping a pex to search PEX_PYTHON_PATH for a list of compatible interpreters. """ return list(self._interpreter_constraints) def add_interpreter_constraint(self, value): self._interpreter_constraints.add(str(value)) @property def ignore_errors(self): return self._pex_info.get('ignore_errors', False) @ignore_errors.setter def ignore_errors(self, value): self._pex_info['ignore_errors'] = bool(value) @property def emit_warnings(self): return self._pex_info.get('emit_warnings', True) @emit_warnings.setter def emit_warnings(self, value): self._pex_info['emit_warnings'] = bool(value) @property def code_hash(self): return self._pex_info.get('code_hash') @code_hash.setter def code_hash(self, value): self._pex_info['code_hash'] = value @property def entry_point(self): return self._get_safe('entry_point') @entry_point.setter def entry_point(self, value): self._pex_info['entry_point'] = value @property def script(self): return self._get_safe('script') @script.setter def script(self, value): self._pex_info['script'] = value def add_requirement(self, requirement): self._requirements.add(str(requirement)) @property def requirements(self): return self._requirements def add_distribution(self, location, sha): self._distributions[location] = sha @property def distributions(self): return self._distributions @property def always_write_cache(self): return self._pex_info.get('always_write_cache', False) @always_write_cache.setter def always_write_cache(self, value): self._pex_info['always_write_cache'] = bool(value) @property def pex_root(self): pex_root = os.path.expanduser( self._pex_info.get('pex_root', os.path.join('~', '.pex'))) if not can_write_dir(pex_root): tmp_root = safe_mkdtemp() pex_warnings.warn( 'PEX_ROOT is configured as {pex_root} but that path is un-writeable, ' 'falling back to a temporary PEX_ROOT of {tmp_root} which will hurt ' 'performance.'.format(pex_root=pex_root, tmp_root=tmp_root)) pex_root = self._pex_info['pex_root'] = tmp_root return pex_root @pex_root.setter def pex_root(self, value): if value is None: self._pex_info.pop('pex_root', None) else: self._pex_info['pex_root'] = value @property def internal_cache(self): return '.deps' @property def install_cache(self): return os.path.join(self.pex_root, self.INSTALL_CACHE) @property def zip_unsafe_cache(self): return os.path.join(self.pex_root, 'code') def update(self, other): if not isinstance(other, PexInfo): raise TypeError('Cannot merge a %r with PexInfo' % type(other)) self._pex_info.update(other._pex_info) self._distributions.update(other.distributions) self._interpreter_constraints.update(other.interpreter_constraints) self._requirements.update(other.requirements) def dump(self, **kwargs): pex_info_copy = self._pex_info.copy() pex_info_copy['requirements'] = sorted(self._requirements) pex_info_copy['interpreter_constraints'] = sorted( self._interpreter_constraints) pex_info_copy['distributions'] = self._distributions.copy() return json.dumps(pex_info_copy, **kwargs) def copy(self): return self.from_json(self.dump()) @staticmethod def _merge_split(*paths): filtered_paths = filter(None, paths) return [p for p in ':'.join(filtered_paths).split(':') if p] def merge_pex_path(self, pex_path): """Merges a new PEX_PATH definition into the existing one (if any). :param str pex_path: The PEX_PATH to merge. """ if not pex_path: return self.pex_path = ':'.join(self._merge_split(self.pex_path, pex_path)) def __repr__(self): return '{}({!r})'.format(type(self).__name__, self._pex_info)
def _resolve(self, working_set, reqs): environment = self._target_interpreter_env.copy() environment["extra"] = list(set(itertools.chain(*(req.extras for req in reqs)))) reqs_by_key = OrderedDict() for req in reqs: if req.marker and not req.marker.evaluate(environment=environment): TRACER.log( "Skipping activation of `%s` due to environment marker de-selection" % req ) continue reqs_by_key.setdefault(req.key, []).append(req) unresolved_reqs = OrderedDict() resolveds = OrderedSet() # Resolve them one at a time so that we can figure out which ones we need to elide should # there be an interpreter incompatibility. for key, reqs in reqs_by_key.items(): with TRACER.timed("Resolving {} from {}".format(key, reqs), V=2): # N.B.: We resolve the bare requirement with no version specifiers since the resolve process # used to build this pex already did so. There may be multiple distributions satisfying any # particular key (e.g.: a Python 2 specific version and a Python 3 specific version for a # multi-python PEX) and we want the working set to pick the most appropriate one. req = Requirement.parse(key) try: resolveds.update(working_set.resolve([req], env=self)) except DistributionNotFound as e: TRACER.log("Failed to resolve a requirement: %s" % e) requirers = unresolved_reqs.setdefault(e.req, OrderedSet()) if e.requirers: for requirer in e.requirers: requirers.update(reqs_by_key[requirer]) if unresolved_reqs: TRACER.log("Unresolved requirements:") for req in unresolved_reqs: TRACER.log(" - %s" % req) TRACER.log("Distributions contained within this pex:") distributions_by_key = defaultdict(list) if not self._pex_info.distributions: TRACER.log(" None") else: for dist_name, dist_digest in self._pex_info.distributions.items(): TRACER.log(" - %s" % dist_name) distribution = DistributionHelper.distribution_from_path( path=os.path.join(self._pex_info.install_cache, dist_digest, dist_name) ) distributions_by_key[distribution.as_requirement().key].append(distribution) if not self._pex_info.ignore_errors: items = [] for index, (requirement, requirers) in enumerate(unresolved_reqs.items()): rendered_requirers = "" if requirers: rendered_requirers = ("\n Required by:" "\n {requirers}").format( requirers="\n ".join(map(str, requirers)) ) items.append( "{index: 2d}: {requirement}" "{rendered_requirers}" "\n But this pex only contains:" "\n {distributions}".format( index=index + 1, requirement=requirement, rendered_requirers=rendered_requirers, distributions="\n ".join( os.path.basename(d.location) for d in distributions_by_key[requirement.key] ), ) ) die( "Failed to execute PEX file. Needed {platform} compatible dependencies for:\n{items}".format( platform=self._interpreter.platform, items="\n".join(items) ) ) return resolveds
class PexInfo(object): """PEX metadata. # Build metadata: build_properties: BuildProperties # (key-value information about the build system) code_hash: str # sha1 hash of all names/code in the archive distributions: {dist_name: str} # map from distribution name (i.e. path in # the internal cache) to its cache key (sha1) pex_hash: str # sha1 hash of all names/code and distributions in the pex requirements: list # list of requirements for this environment # Environment options pex_root: string # root of all pex-related files eg: ~/.pex entry_point: string # entry point into this pex script: string # script to execute in this pex environment # at most one of script/entry_point can be specified zip_safe: bool, default True # is this pex zip safe? unzip: bool, default False # should this pex be unzipped and re-executed from there? inherit_path: false/fallback/prefer # should this pex inherit site-packages + user site-packages # + PYTHONPATH? ignore_errors: True, default False # should we ignore inability to resolve dependencies? always_write_cache: False # should we always write the internal cache to disk first? # this is useful if you have very large dependencies that # do not fit in RAM constrained environments .. versionchanged:: 0.8 Removed the ``repositories`` and ``indices`` information, as they were never implemented. """ PATH = "PEX-INFO" INSTALL_CACHE = "installed_wheels" @classmethod def make_build_properties(cls, interpreter=None): # This lazy import is currently needed for performance reasons. At PEX runtime PexInfo is # read in the bootstrap to see if the PEX should run in `--unzip` mode. If so, it must # re-exec itself to run against its unzipped contents. Since `make_build_properties` is only # used at PEX buildtime and the transitive imports of PythonInterpreter are large and slow, # we avoid this import cost for runtime-only use. # # See: https://github.com/pantsbuild/pex/issues/1054 from pex.interpreter import PythonInterpreter pi = interpreter or PythonInterpreter.get() plat = pi.platform platform_name = plat.platform return { "pex_version": pex_version, "class": pi.identity.interpreter, "version": pi.identity.version, "platform": platform_name, } @classmethod def default(cls, interpreter=None): # type: (Optional[PythonInterpreter]) -> PexInfo pex_info = { "requirements": [], "distributions": {}, "build_properties": cls.make_build_properties(interpreter), } return cls(info=pex_info) @classmethod def from_pex(cls, pex): # type: (str) -> PexInfo if zipfile.is_zipfile(pex): # Zip App with open_zip(pex) as zf: pex_info = zf.read(cls.PATH) elif os.path.isfile(pex): # Venv with open(os.path.join(os.path.dirname(pex), cls.PATH)) as fp: pex_info = fp.read() else: # Directory (Unzip mode or PEXBuilder.freeze) with open(os.path.join(pex, cls.PATH)) as fp: pex_info = fp.read() return cls.from_json(pex_info) @classmethod def from_json(cls, content): # type: (Union[bytes, Text]) -> PexInfo if isinstance(content, bytes): content = content.decode("utf-8") return cls(info=json.loads(content)) @classmethod def from_env(cls, env=ENV): # type: (Variables) -> PexInfo pex_force_local = Variables.PEX_FORCE_LOCAL.strip_default(env) zip_safe = None if pex_force_local is None else not pex_force_local pex_inherit_path = Variables.PEX_INHERIT_PATH.strip_default(env) inherit_path = None if pex_inherit_path is None else pex_inherit_path.value pex_info = { "pex_root": Variables.PEX_ROOT.strip_default(env), "entry_point": env.PEX_MODULE, "script": env.PEX_SCRIPT, "zip_safe": zip_safe, "unzip": Variables.PEX_UNZIP.strip_default(env), "venv": Variables.PEX_VENV.strip_default(env), "inherit_path": inherit_path, "ignore_errors": Variables.PEX_IGNORE_ERRORS.strip_default(env), "always_write_cache": Variables.PEX_ALWAYS_CACHE.strip_default(env), } # Filter out empty entries not explicitly set in the environment. return cls(info=dict( (k, v) for (k, v) in pex_info.items() if v is not None)) @classmethod def _parse_requirement_tuple(cls, requirement_tuple): if isinstance(requirement_tuple, (tuple, list)): if len(requirement_tuple) != 3: raise ValueError("Malformed PEX requirement: %r" % (requirement_tuple, )) # pre 0.8.x requirement type: pex_warnings.warn( "Attempting to use deprecated PEX feature. Please upgrade past PEX 0.8.x." ) return requirement_tuple[0] elif isinstance(requirement_tuple, compatibility_string): return requirement_tuple raise ValueError("Malformed PEX requirement: %r" % (requirement_tuple, )) def __init__(self, info=None): # type: (Optional[Mapping[str, Any]]) -> None """Construct a new PexInfo. This should not be used directly. """ if info is not None and not isinstance(info, dict): raise ValueError("PexInfo can only be seeded with a dict, got: " "%s of type %s" % (info, type(info))) self._pex_info = dict(info) if info else {} # type Dict[str, Any] self._distributions = self._pex_info.get("distributions", {}) # cast as set because pex info from json must store interpreter_constraints as a list self._interpreter_constraints = set( self._pex_info.get("interpreter_constraints", set())) requirements = self._pex_info.get("requirements", []) if not isinstance(requirements, (list, tuple)): raise ValueError("Expected requirements to be a list, got %s" % type(requirements)) self._requirements = OrderedSet( self._parse_requirement_tuple(req) for req in requirements) def _get_safe(self, key): if key not in self._pex_info: return None value = self._pex_info[key] return value.encode("utf-8") if PY2 else value @property def build_properties(self): """Information about the system on which this PEX was generated. :returns: A dictionary containing metadata about the environment used to build this PEX. """ return self._pex_info.get("build_properties", {}) @build_properties.setter def build_properties(self, value): if not isinstance(value, dict): raise TypeError("build_properties must be a dictionary!") self._pex_info["build_properties"] = self.make_build_properties() self._pex_info["build_properties"].update(value) @property def zip_safe(self): """Whether or not this PEX should be treated as zip-safe. If set to false and the PEX is zipped, the contents of the PEX will be unpacked into a directory within the PEX_ROOT prior to execution. This allows code and frameworks depending upon __file__ existing on disk to operate normally. By default zip_safe is True. May be overridden at runtime by the $PEX_FORCE_LOCAL environment variable. """ return self._pex_info.get("zip_safe", True) @zip_safe.setter def zip_safe(self, value): self._pex_info["zip_safe"] = bool(value) @property def unzip(self): """Whether or not PEX should be unzipped before it's executed. Unzipping a PEX is a operation that can be cached on the 1st run of a given PEX file which can result in lower startup latency in subsequent runs. """ return self._pex_info.get("unzip", False) @unzip.setter def unzip(self, value): self._pex_info["unzip"] = bool(value) @property def unzip_dir(self): # type: () -> Optional[str] if not self.unzip: return None if self.pex_hash is None: raise ValueError( "The unzip_dir was requested but no pex_hash was set.") return variables.unzip_dir(self.pex_root, self.pex_hash) @property def venv(self): # type: () -> bool """Whether or not PEX should be converted to a venv before it's executed. Creating a venv from a PEX is a operation that can be cached on the 1st run of a given PEX file which results in lower startup latency in subsequent runs. """ return self._pex_info.get("venv", False) @venv.setter def venv(self, value): # type: (bool) -> None self._pex_info["venv"] = bool(value) @property def venv_bin_path(self): # type: () -> BinPath.Value """When run as a venv, whether or not to include `bin/` scripts on the PATH.""" return BinPath.for_value( self._pex_info.get("venv_bin_path", BinPath.FALSE.value)) @venv_bin_path.setter def venv_bin_path(self, value): # type: (BinPath.Value) -> None self._pex_info["venv_bin_path"] = str(value) @property def venv_copies(self): # type: () -> bool return self._pex_info.get("venv_copies", False) @venv_copies.setter def venv_copies(self, value): # type: (bool) -> None self._pex_info["venv_copies"] = value @property def venv_dir(self): # type: () -> Optional[str] if not self.venv: return None if self.pex_hash is None: raise ValueError( "The venv_dir was requested but no pex_hash was set.") return variables.venv_dir( pex_root=self.pex_root, pex_hash=self.pex_hash, interpreter_constraints=self.interpreter_constraints, strip_pex_env=self.strip_pex_env, pex_path=self.pex_path, ) @property def strip_pex_env(self): """Whether or not this PEX should strip `PEX_*` env vars before executing its entrypoint. You might want to set this to `False` if this PEX executes other PEXes or the Pex CLI itself and you want the executed PEX to be controlled via PEX environment variables. """ return self._pex_info.get("strip_pex_env", True) @strip_pex_env.setter def strip_pex_env(self, value): self._pex_info["strip_pex_env"] = bool(value) @property def pex_path(self): # type: () -> Optional[str] """A colon separated list of other pex files to merge into the runtime environment. This pex info property is used to persist the PEX_PATH environment variable into the pex info metadata for reuse within a built pex. """ return cast("Optional[str]", self._pex_info.get("pex_path")) @pex_path.setter def pex_path(self, value): # type: (str) -> None self._pex_info["pex_path"] = value @property def inherit_path(self): # type: () -> InheritPath.Value """Whether or not this PEX should be allowed to inherit system dependencies. By default, PEX environments are scrubbed of all system distributions prior to execution. This means that PEX files cannot rely upon preexisting system libraries. By default inherit_path is false. This may be overridden at runtime by the $PEX_INHERIT_PATH environment variable. """ inherit_path = self._pex_info.get("inherit_path") return InheritPath.for_value( inherit_path) if inherit_path else InheritPath.FALSE @inherit_path.setter def inherit_path(self, value): # type: (InheritPath.Value) -> None self._pex_info["inherit_path"] = value.value @property def interpreter_constraints(self): """A list of constraints that determine the interpreter compatibility for this pex, using the Requirement-style format, e.g. ``'CPython>=3', or just '>=2.7,<3'`` for requirements agnostic to interpreter class. This property will be used at exec time when bootstrapping a pex to search PEX_PYTHON_PATH for a list of compatible interpreters. """ return list(self._interpreter_constraints) def add_interpreter_constraint(self, value): self._interpreter_constraints.add(str(value)) @property def ignore_errors(self): return self._pex_info.get("ignore_errors", False) @ignore_errors.setter def ignore_errors(self, value): self._pex_info["ignore_errors"] = bool(value) @property def emit_warnings(self): return self._pex_info.get("emit_warnings", True) @emit_warnings.setter def emit_warnings(self, value): self._pex_info["emit_warnings"] = bool(value) @property def code_hash(self): # type: () -> Optional[str] return self._pex_info.get("code_hash") @code_hash.setter def code_hash(self, value): # type: (str) -> None self._pex_info["code_hash"] = value @property def pex_hash(self): # type: () -> Optional[str] return self._pex_info.get("pex_hash") @pex_hash.setter def pex_hash(self, value): # type: (str) -> None self._pex_info["pex_hash"] = value @property def entry_point(self): return self._get_safe("entry_point") @entry_point.setter def entry_point(self, value): self._pex_info["entry_point"] = value @property def script(self): return self._get_safe("script") @script.setter def script(self, value): self._pex_info["script"] = value def add_requirement(self, requirement): self._requirements.add(str(requirement)) @property def requirements(self): return self._requirements def add_distribution(self, location, sha): self._distributions[location] = sha @property def distributions(self): return self._distributions @property def always_write_cache(self): return self._pex_info.get("always_write_cache", False) @always_write_cache.setter def always_write_cache(self, value): self._pex_info["always_write_cache"] = bool(value) @property def raw_pex_root(self): # type: () -> str return cast(str, self._pex_info.get("pex_root", os.path.join("~", ".pex"))) @property def pex_root(self): # type: () -> str pex_root = os.path.expanduser(self.raw_pex_root) if not can_write_dir(pex_root): tmp_root = safe_mkdtemp() pex_warnings.warn( "PEX_ROOT is configured as {pex_root} but that path is un-writeable, " "falling back to a temporary PEX_ROOT of {tmp_root} which will hurt " "performance.".format(pex_root=pex_root, tmp_root=tmp_root)) pex_root = self._pex_info["pex_root"] = tmp_root return pex_root @pex_root.setter def pex_root(self, value): # type: (Optional[str]) -> None if value is None: self._pex_info.pop("pex_root", None) else: self._pex_info["pex_root"] = value @property def bootstrap(self): return ".bootstrap" @property def internal_cache(self): return ".deps" @property def install_cache(self): return os.path.join(self.pex_root, self.INSTALL_CACHE) @property def zip_unsafe_cache(self): #: type: () -> str return os.path.join(self.pex_root, "code") def update(self, other): # type: (PexInfo) -> None if not isinstance(other, PexInfo): raise TypeError("Cannot merge a %r with PexInfo" % type(other)) self._pex_info.update(other._pex_info) self._distributions.update(other.distributions) self._interpreter_constraints.update(other.interpreter_constraints) self._requirements.update(other.requirements) def as_json_dict(self): # type: () -> Dict[str, Any] data = self._pex_info.copy() data["inherit_path"] = self.inherit_path.value data["requirements"] = list(self._requirements) data["interpreter_constraints"] = list(self._interpreter_constraints) data["distributions"] = self._distributions.copy() return data def dump(self): # type: (...) -> str data = self.as_json_dict() data["requirements"].sort() data["interpreter_constraints"].sort() return json.dumps(data, sort_keys=True) def copy(self): # type: () -> PexInfo return PexInfo(self.as_json_dict()) @staticmethod def _merge_split(*paths): filtered_paths = filter(None, paths) return [p for p in ":".join(filtered_paths).split(":") if p] def merge_pex_path(self, pex_path): """Merges a new PEX_PATH definition into the existing one (if any). :param str pex_path: The PEX_PATH to merge. """ if not pex_path: return self.pex_path = ":".join(self._merge_split(self.pex_path, pex_path)) def __repr__(self): return "{}({!r})".format(type(self).__name__, self._pex_info)
def minimum_sys_path(cls, site_libs, inherit_path): # type: (Iterable[str], InheritPath.Value) -> Tuple[List[str], Mapping[str, Any]] scrub_paths = OrderedSet() # type: OrderedSet[str] site_distributions = OrderedSet() # type: OrderedSet[str] user_site_distributions = OrderedSet() # type: OrderedSet[str] def all_distribution_paths(path): # type: (Optional[str]) -> Iterable[str] if path is None: return () locations = set(dist.location for dist in find_distributions(path)) return {path} | locations | set(os.path.realpath(path) for path in locations) for path_element in sys.path: if cls._tainted_path(path_element, site_libs): TRACER.log("Tainted path element: %s" % path_element) site_distributions.update(all_distribution_paths(path_element)) else: TRACER.log("Not a tainted path element: %s" % path_element, V=2) user_site_distributions.update(all_distribution_paths(USER_SITE)) if inherit_path == InheritPath.FALSE: scrub_paths = OrderedSet(site_distributions) scrub_paths.update(user_site_distributions) for path in user_site_distributions: TRACER.log("Scrubbing from user site: %s" % path) for path in site_distributions: TRACER.log("Scrubbing from site-packages: %s" % path) scrubbed_sys_path = list(OrderedSet(sys.path) - scrub_paths) pythonpath = cls.unstash_pythonpath() if pythonpath is not None: original_pythonpath = pythonpath.split(os.pathsep) user_pythonpath = list(OrderedSet(original_pythonpath) - set(sys.path)) if original_pythonpath == user_pythonpath: TRACER.log("Unstashed PYTHONPATH of %s" % pythonpath, V=2) else: TRACER.log( "Extracted user PYTHONPATH of %s from unstashed PYTHONPATH of %s" % (os.pathsep.join(user_pythonpath), pythonpath), V=2, ) if inherit_path == InheritPath.FALSE: for path in user_pythonpath: TRACER.log("Scrubbing user PYTHONPATH element: %s" % path) elif inherit_path == InheritPath.PREFER: TRACER.log("Prepending user PYTHONPATH: %s" % os.pathsep.join(user_pythonpath)) scrubbed_sys_path = user_pythonpath + scrubbed_sys_path elif inherit_path == InheritPath.FALLBACK: TRACER.log("Appending user PYTHONPATH: %s" % os.pathsep.join(user_pythonpath)) scrubbed_sys_path = scrubbed_sys_path + user_pythonpath scrub_from_importer_cache = filter( lambda key: any(key.startswith(path) for path in scrub_paths), sys.path_importer_cache.keys(), ) scrubbed_importer_cache = dict( (key, value) for (key, value) in sys.path_importer_cache.items() if key not in scrub_from_importer_cache ) for importer_cache_entry in scrub_from_importer_cache: TRACER.log("Scrubbing from path_importer_cache: %s" % importer_cache_entry, V=2) return scrubbed_sys_path, scrubbed_importer_cache
def _resolve(self, working_set, reqs): reqs_by_key = OrderedDict((req.key, req) for req in reqs) unresolved_reqs = OrderedDict() resolveds = OrderedSet() environment = self._target_interpreter_env.copy() environment['extra'] = list( set(itertools.chain(*(req.extras for req in reqs)))) # Resolve them one at a time so that we can figure out which ones we need to elide should # there be an interpreter incompatibility. for req in reqs_by_key.values(): if req.marker and not req.marker.evaluate(environment=environment): TRACER.log( 'Skipping activation of `%s` due to environment marker de-selection' % req) continue with TRACER.timed('Resolving %s' % req, V=2): try: resolveds.update(working_set.resolve([req], env=self)) except DistributionNotFound as e: TRACER.log('Failed to resolve a requirement: %s' % e) requirers = unresolved_reqs.setdefault(e.req, OrderedSet()) if e.requirers: requirers.update(reqs_by_key[requirer] for requirer in e.requirers) if unresolved_reqs: TRACER.log('Unresolved requirements:') for req in unresolved_reqs: TRACER.log(' - %s' % req) TRACER.log('Distributions contained within this pex:') distributions_by_key = defaultdict(list) if not self._pex_info.distributions: TRACER.log(' None') else: for dist_name, dist_digest in self._pex_info.distributions.items( ): TRACER.log(' - %s' % dist_name) distribution = DistributionHelper.distribution_from_path( path=os.path.join(self._pex_info.install_cache, dist_digest, dist_name)) distributions_by_key[ distribution.as_requirement().key].append(distribution) if not self._pex_info.ignore_errors: items = [] for index, (requirement, requirers) in enumerate(unresolved_reqs.items()): rendered_requirers = '' if requirers: rendered_requirers = ( '\n Required by:' '\n {requirers}').format( requirers='\n '.join(map(str, requirers))) items.append('{index: 2d}: {requirement}' '{rendered_requirers}' '\n But this pex only contains:' '\n {distributions}'.format( index=index + 1, requirement=requirement, rendered_requirers=rendered_requirers, distributions='\n '.join( os.path.basename(d.location) for d in distributions_by_key[ requirement.key]))) die('Failed to execute PEX file. Needed {platform} compatible dependencies for:\n{items}' .format(platform=Platform.of_interpreter( self._interpreter), items='\n'.join(items))) return resolveds
class PexInfo(object): """PEX metadata. # Build metadata: build_properties: BuildProperties # (key-value information about the build system) code_hash: str # sha1 hash of all names/code in the archive distributions: {dist_name: str} # map from distribution name (i.e. path in # the internal cache) to its cache key (sha1) requirements: list # list of requirements for this environment # Environment options pex_root: string # root of all pex-related files eg: ~/.pex entry_point: string # entry point into this pex script: string # script to execute in this pex environment # at most one of script/entry_point can be specified zip_safe: True, default False # is this pex zip safe? inherit_path: false/fallback/prefer # should this pex inherit site-packages + PYTHONPATH? ignore_errors: True, default False # should we ignore inability to resolve dependencies? always_write_cache: False # should we always write the internal cache to disk first? # this is useful if you have very large dependencies that # do not fit in RAM constrained environments .. versionchanged:: 0.8 Removed the ``repositories`` and ``indices`` information, as they were never implemented. """ PATH = 'PEX-INFO' INTERNAL_CACHE = '.deps' @classmethod def make_build_properties(cls, interpreter=None): from .interpreter import PythonInterpreter from .platforms import Platform pi = interpreter or PythonInterpreter.get() plat = Platform.current() platform_name = plat.platform return { 'pex_version': pex_version, 'class': pi.identity.interpreter, 'version': pi.identity.version, 'platform': platform_name, } @classmethod def default(cls, interpreter=None): pex_info = { 'requirements': [], 'distributions': {}, 'build_properties': cls.make_build_properties(interpreter), } return cls(info=pex_info) @classmethod def from_pex(cls, pex): if os.path.isfile(pex): with open_zip(pex) as zf: pex_info = zf.read(cls.PATH) else: with open(os.path.join(pex, cls.PATH)) as fp: pex_info = fp.read() return cls.from_json(pex_info) @classmethod def from_json(cls, content): if isinstance(content, bytes): content = content.decode('utf-8') return cls(info=json.loads(content)) @classmethod def from_env(cls, env=ENV): supplied_env = env.strip_defaults() zip_safe = None if supplied_env.PEX_FORCE_LOCAL is None else not supplied_env.PEX_FORCE_LOCAL pex_info = { 'pex_root': supplied_env.PEX_ROOT, 'entry_point': supplied_env.PEX_MODULE, 'script': supplied_env.PEX_SCRIPT, 'zip_safe': zip_safe, 'inherit_path': supplied_env.PEX_INHERIT_PATH, 'ignore_errors': supplied_env.PEX_IGNORE_ERRORS, 'always_write_cache': supplied_env.PEX_ALWAYS_CACHE, } # Filter out empty entries not explicitly set in the environment. return cls(info=dict((k, v) for (k, v) in pex_info.items() if v is not None)) @classmethod def _parse_requirement_tuple(cls, requirement_tuple): if isinstance(requirement_tuple, (tuple, list)): if len(requirement_tuple) != 3: raise ValueError('Malformed PEX requirement: %r' % (requirement_tuple,)) # pre 0.8.x requirement type: pex_warnings.warn('Attempting to use deprecated PEX feature. Please upgrade past PEX 0.8.x.') return requirement_tuple[0] elif isinstance(requirement_tuple, compatibility_string): return requirement_tuple raise ValueError('Malformed PEX requirement: %r' % (requirement_tuple,)) def __init__(self, info=None): """Construct a new PexInfo. This should not be used directly.""" if info is not None and not isinstance(info, dict): raise ValueError('PexInfo can only be seeded with a dict, got: ' '%s of type %s' % (info, type(info))) self._pex_info = info or {} if 'inherit_path' in self._pex_info: self.inherit_path = self._pex_info['inherit_path'] self._distributions = self._pex_info.get('distributions', {}) # cast as set because pex info from json must store interpreter_constraints as a list self._interpreter_constraints = set(self._pex_info.get('interpreter_constraints', set())) requirements = self._pex_info.get('requirements', []) if not isinstance(requirements, (list, tuple)): raise ValueError('Expected requirements to be a list, got %s' % type(requirements)) self._requirements = OrderedSet(self._parse_requirement_tuple(req) for req in requirements) def _get_safe(self, key): if key not in self._pex_info: return None value = self._pex_info[key] return value.encode('utf-8') if PY2 else value @property def build_properties(self): """Information about the system on which this PEX was generated. :returns: A dictionary containing metadata about the environment used to build this PEX. """ return self._pex_info.get('build_properties', {}) @build_properties.setter def build_properties(self, value): if not isinstance(value, dict): raise TypeError('build_properties must be a dictionary!') self._pex_info['build_properties'] = self.make_build_properties() self._pex_info['build_properties'].update(value) @property def zip_safe(self): """Whether or not this PEX should be treated as zip-safe. If set to false and the PEX is zipped, the contents of the PEX will be unpacked into a directory within the PEX_ROOT prior to execution. This allows code and frameworks depending upon __file__ existing on disk to operate normally. By default zip_safe is True. May be overridden at runtime by the $PEX_FORCE_LOCAL environment variable. """ return self._pex_info.get('zip_safe', True) @zip_safe.setter def zip_safe(self, value): self._pex_info['zip_safe'] = bool(value) @property def pex_path(self): """A colon separated list of other pex files to merge into the runtime environment. This pex info property is used to persist the PEX_PATH environment variable into the pex info metadata for reuse within a built pex. """ return self._pex_info.get('pex_path') @pex_path.setter def pex_path(self, value): self._pex_info['pex_path'] = value @property def inherit_path(self): """Whether or not this PEX should be allowed to inherit system dependencies. By default, PEX environments are scrubbed of all system distributions prior to execution. This means that PEX files cannot rely upon preexisting system libraries. By default inherit_path is false. This may be overridden at runtime by the $PEX_INHERIT_PATH environment variable. """ return self._pex_info.get('inherit_path', 'false') @inherit_path.setter def inherit_path(self, value): if value is False: value = 'false' elif value is True: value = 'prefer' self._pex_info['inherit_path'] = value @property def interpreter_constraints(self): """A list of constraints that determine the interpreter compatibility for this pex, using the Requirement-style format, e.g. ``'CPython>=3', or just '>=2.7,<3'`` for requirements agnostic to interpreter class. This property will be used at exec time when bootstrapping a pex to search PEX_PYTHON_PATH for a list of compatible interpreters. """ return list(self._interpreter_constraints) def add_interpreter_constraint(self, value): self._interpreter_constraints.add(str(value)) @property def ignore_errors(self): return self._pex_info.get('ignore_errors', False) @ignore_errors.setter def ignore_errors(self, value): self._pex_info['ignore_errors'] = bool(value) @property def emit_warnings(self): return self._pex_info.get('emit_warnings', True) @emit_warnings.setter def emit_warnings(self, value): self._pex_info['emit_warnings'] = bool(value) @property def code_hash(self): return self._pex_info.get('code_hash') @code_hash.setter def code_hash(self, value): self._pex_info['code_hash'] = value @property def entry_point(self): return self._get_safe('entry_point') @entry_point.setter def entry_point(self, value): self._pex_info['entry_point'] = value @property def script(self): return self._get_safe('script') @script.setter def script(self, value): self._pex_info['script'] = value def add_requirement(self, requirement): self._requirements.add(str(requirement)) @property def requirements(self): return self._requirements def add_distribution(self, location, sha): self._distributions[location] = sha @property def distributions(self): return self._distributions @property def always_write_cache(self): return self._pex_info.get('always_write_cache', False) @always_write_cache.setter def always_write_cache(self, value): self._pex_info['always_write_cache'] = bool(value) @property def pex_root(self): return os.path.expanduser(self._pex_info.get('pex_root', os.path.join('~', '.pex'))) @pex_root.setter def pex_root(self, value): self._pex_info['pex_root'] = value @property def internal_cache(self): return self.INTERNAL_CACHE @property def install_cache(self): return os.path.join(self.pex_root, 'install') @property def zip_unsafe_cache(self): return os.path.join(self.pex_root, 'code') def update(self, other): if not isinstance(other, PexInfo): raise TypeError('Cannot merge a %r with PexInfo' % type(other)) self._pex_info.update(other._pex_info) self._distributions.update(other.distributions) self._interpreter_constraints.update(other.interpreter_constraints) self._requirements.update(other.requirements) def dump(self, **kwargs): pex_info_copy = self._pex_info.copy() pex_info_copy['requirements'] = sorted(self._requirements) pex_info_copy['interpreter_constraints'] = sorted(self._interpreter_constraints) pex_info_copy['distributions'] = self._distributions.copy() return json.dumps(pex_info_copy, **kwargs) def copy(self): return self.from_json(self.dump()) def merge_pex_path(self, pex_path): """Merges a new PEX_PATH definition into the existing one (if any). :param string pex_path: The PEX_PATH to merge. """ if not pex_path: return self.pex_path = ':'.join(merge_split(self.pex_path, pex_path)) def __repr__(self): return '{}({!r})'.format(type(self).__name__, self._pex_info)