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