1# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md). 2# Licensed under the Apache License, Version 2.0 (see LICENSE). 3 4from __future__ import absolute_import, print_function 5 6import json 7import os 8import warnings 9from collections import namedtuple 10 11from .common import open_zip 12from .compatibility import string as compatibility_string 13from .compatibility import PY2 14from .orderedset import OrderedSet 15from .variables import ENV 16 17PexPlatform = namedtuple('PexPlatform', 'interpreter version strict') 18 19 20# TODO(wickman) Split this into a PexInfoBuilder/PexInfo to ensure immutability. 21# Issue #92. 22class PexInfo(object): 23 """PEX metadata. 24 25 # Build metadata: 26 build_properties: BuildProperties # (key-value information about the build system) 27 code_hash: str # sha1 hash of all names/code in the archive 28 distributions: {dist_name: str} # map from distribution name (i.e. path in 29 # the internal cache) to its cache key (sha1) 30 requirements: list # list of requirements for this environment 31 32 # Environment options 33 pex_root: string # root of all pex-related files eg: ~/.pex 34 entry_point: string # entry point into this pex 35 script: string # script to execute in this pex environment 36 # at most one of script/entry_point can be specified 37 zip_safe: True, default False # is this pex zip safe? 38 inherit_path: True, default False # should this pex inherit site-packages + PYTHONPATH? 39 ignore_errors: True, default False # should we ignore inability to resolve dependencies? 40 always_write_cache: False # should we always write the internal cache to disk first? 41 # this is useful if you have very large dependencies that 42 # do not fit in RAM constrained environments 43 44 .. versionchanged:: 0.8 45 Removed the ``repositories`` and ``indices`` information, as they were never 46 implemented. 47 """ 48 49 PATH = 'PEX-INFO' 50 INTERNAL_CACHE = '.deps' 51 52 @classmethod 53 def make_build_properties(cls): 54 from .interpreter import PythonInterpreter 55 from pkg_resources import get_platform 56 57 pi = PythonInterpreter.get() 58 return { 59 'class': pi.identity.interpreter, 60 'version': pi.identity.version, 61 'platform': get_platform(), 62 } 63 64 @classmethod 65 def default(cls): 66 pex_info = { 67 'requirements': [], 68 'distributions': {}, 69 'build_properties': cls.make_build_properties(), 70 } 71 return cls(info=pex_info) 72 73 @classmethod 74 def from_pex(cls, pex): 75 if os.path.isfile(pex): 76 with open_zip(pex) as zf: 77 pex_info = zf.read(cls.PATH) 78 else: 79 with open(os.path.join(pex, cls.PATH)) as fp: 80 pex_info = fp.read() 81 return cls.from_json(pex_info) 82 83 @classmethod 84 def from_json(cls, content): 85 if isinstance(content, bytes): 86 content = content.decode('utf-8') 87 return cls(info=json.loads(content)) 88 89 @classmethod 90 def from_env(cls, env=ENV): 91 supplied_env = env.strip_defaults() 92 zip_safe = None if supplied_env.PEX_FORCE_LOCAL is None else not supplied_env.PEX_FORCE_LOCAL 93 pex_info = { 94 'pex_root': supplied_env.PEX_ROOT, 95 'entry_point': supplied_env.PEX_MODULE, 96 'script': supplied_env.PEX_SCRIPT, 97 'zip_safe': zip_safe, 98 'inherit_path': supplied_env.PEX_INHERIT_PATH, 99 'ignore_errors': supplied_env.PEX_IGNORE_ERRORS, 100 'always_write_cache': supplied_env.PEX_ALWAYS_CACHE, 101 } 102 # Filter out empty entries not explicitly set in the environment. 103 return cls(info=dict((k, v) for (k, v) in pex_info.items() if v is not None)) 104 105 @classmethod 106 def _parse_requirement_tuple(cls, requirement_tuple): 107 if isinstance(requirement_tuple, (tuple, list)): 108 if len(requirement_tuple) != 3: 109 raise ValueError('Malformed PEX requirement: %r' % (requirement_tuple,)) 110 # pre 0.8.x requirement type: 111 warnings.warn('Attempting to use deprecated PEX feature. Please upgrade past PEX 0.8.x.') 112 return requirement_tuple[0] 113 elif isinstance(requirement_tuple, compatibility_string): 114 return requirement_tuple 115 raise ValueError('Malformed PEX requirement: %r' % (requirement_tuple,)) 116 117 def __init__(self, info=None): 118 """Construct a new PexInfo. This should not be used directly.""" 119 120 if info is not None and not isinstance(info, dict): 121 raise ValueError('PexInfo can only be seeded with a dict, got: ' 122 '%s of type %s' % (info, type(info))) 123 self._pex_info = info or {} 124 self._distributions = self._pex_info.get('distributions', {}) 125 requirements = self._pex_info.get('requirements', []) 126 if not isinstance(requirements, (list, tuple)): 127 raise ValueError('Expected requirements to be a list, got %s' % type(requirements)) 128 self._requirements = OrderedSet(self._parse_requirement_tuple(req) for req in requirements) 129 130 def _get_safe(self, key): 131 if key not in self._pex_info: 132 return None 133 value = self._pex_info[key] 134 return value.encode('utf-8') if PY2 else value 135 136 @property 137 def build_properties(self): 138 """Information about the system on which this PEX was generated. 139 140 :returns: A dictionary containing metadata about the environment used to build this PEX. 141 """ 142 return self._pex_info.get('build_properties', {}) 143 144 @build_properties.setter 145 def build_properties(self, value): 146 if not isinstance(value, dict): 147 raise TypeError('build_properties must be a dictionary!') 148 self._pex_info['build_properties'] = self.make_build_properties() 149 self._pex_info['build_properties'].update(value) 150 151 @property 152 def zip_safe(self): 153 """Whether or not this PEX should be treated as zip-safe. 154 155 If set to false and the PEX is zipped, the contents of the PEX will be unpacked into a 156 directory within the PEX_ROOT prior to execution. This allows code and frameworks depending 157 upon __file__ existing on disk to operate normally. 158 159 By default zip_safe is True. May be overridden at runtime by the $PEX_FORCE_LOCAL environment 160 variable. 161 """ 162 return self._pex_info.get('zip_safe', True) 163 164 @zip_safe.setter 165 def zip_safe(self, value): 166 self._pex_info['zip_safe'] = bool(value) 167 168 @property 169 def inherit_path(self): 170 """Whether or not this PEX should be allowed to inherit system dependencies. 171 172 By default, PEX environments are scrubbed of all system distributions prior to execution. 173 This means that PEX files cannot rely upon preexisting system libraries. 174 175 By default inherit_path is False. This may be overridden at runtime by the $PEX_INHERIT_PATH 176 environment variable. 177 """ 178 return self._pex_info.get('inherit_path', False) 179 180 @inherit_path.setter 181 def inherit_path(self, value): 182 self._pex_info['inherit_path'] = bool(value) 183 184 @property 185 def ignore_errors(self): 186 return self._pex_info.get('ignore_errors', False) 187 188 @ignore_errors.setter 189 def ignore_errors(self, value): 190 self._pex_info['ignore_errors'] = bool(value) 191 192 @property 193 def code_hash(self): 194 return self._pex_info.get('code_hash') 195 196 @code_hash.setter 197 def code_hash(self, value): 198 self._pex_info['code_hash'] = value 199 200 @property 201 def entry_point(self): 202 return self._get_safe('entry_point') 203 204 @entry_point.setter 205 def entry_point(self, value): 206 self._pex_info['entry_point'] = value 207 208 @property 209 def script(self): 210 return self._get_safe('script') 211 212 @script.setter 213 def script(self, value): 214 self._pex_info['script'] = value 215 216 def add_requirement(self, requirement): 217 self._requirements.add(str(requirement)) 218 219 @property 220 def requirements(self): 221 return self._requirements 222 223 def add_distribution(self, location, sha): 224 self._distributions[location] = sha 225 226 @property 227 def distributions(self): 228 return self._distributions 229 230 @property 231 def always_write_cache(self): 232 return self._pex_info.get('always_write_cache', False) 233 234 @always_write_cache.setter 235 def always_write_cache(self, value): 236 self._pex_info['always_write_cache'] = bool(value) 237 238 @property 239 def pex_root(self): 240 return os.path.expanduser(self._pex_info.get('pex_root', os.path.join('~', '.pex'))) 241 242 @pex_root.setter 243 def pex_root(self, value): 244 self._pex_info['pex_root'] = value 245 246 @property 247 def internal_cache(self): 248 return self.INTERNAL_CACHE 249 250 @property 251 def install_cache(self): 252 return os.path.join(self.pex_root, 'install') 253 254 @property 255 def zip_unsafe_cache(self): 256 return os.path.join(self.pex_root, 'code') 257 258 def update(self, other): 259 if not isinstance(other, PexInfo): 260 raise TypeError('Cannot merge a %r with PexInfo' % type(other)) 261 self._pex_info.update(other._pex_info) 262 self._distributions.update(other.distributions) 263 self._requirements.update(other.requirements) 264 265 def dump(self, **kwargs): 266 pex_info_copy = self._pex_info.copy() 267 pex_info_copy['requirements'] = list(self._requirements) 268 pex_info_copy['distributions'] = self._distributions.copy() 269 return json.dumps(pex_info_copy, **kwargs) 270 271 def copy(self): 272 return self.from_json(self.dump()) 273