~ubuntu-branches/ubuntu/vivid/python-pex/vivid

« back to all changes in this revision

Viewing changes to pex/installer.py

  • Committer: Package Import Robot
  • Author(s): Barry Warsaw
  • Date: 2015-02-19 14:13:25 UTC
  • Revision ID: package-import@ubuntu.com-20150219141325-w62bie95l6rawuuv
Tags: upstream-0.8.6
ImportĀ upstreamĀ versionĀ 0.8.6

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
 
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
 
3
 
 
4
from __future__ import absolute_import, print_function
 
5
 
 
6
import os
 
7
import subprocess
 
8
import sys
 
9
import tempfile
 
10
 
 
11
from pkg_resources import Distribution, PathMetadata
 
12
 
 
13
from .common import safe_mkdtemp, safe_rmtree
 
14
from .interpreter import PythonInterpreter
 
15
from .tracer import TRACER
 
16
 
 
17
__all__ = (
 
18
  'Installer',
 
19
  'Packager'
 
20
)
 
21
 
 
22
 
 
23
def after_installation(function):
 
24
  def function_wrapper(self, *args, **kw):
 
25
    self._installed = self.run()
 
26
    if not self._installed:
 
27
      raise Installer.InstallFailure('Failed to install %s' % self._source_dir)
 
28
    return function(self, *args, **kw)
 
29
  return function_wrapper
 
30
 
 
31
 
 
32
class InstallerBase(object):
 
33
  SETUP_BOOTSTRAP_HEADER = "import sys"
 
34
  SETUP_BOOTSTRAP_MODULE = "sys.path.insert(0, %(path)r); import %(module)s"
 
35
  SETUP_BOOTSTRAP_FOOTER = """
 
36
__file__ = 'setup.py'
 
37
exec(compile(open(__file__).read().replace('\\r\\n', '\\n'), __file__, 'exec'))
 
38
"""
 
39
 
 
40
  class Error(Exception): pass
 
41
  class InstallFailure(Error): pass
 
42
  class IncapableInterpreter(Error): pass
 
43
 
 
44
  def __init__(self, source_dir, strict=True, interpreter=None, install_dir=None):
 
45
    """
 
46
      Create an installer from an unpacked source distribution in source_dir.
 
47
 
 
48
      If strict=True, fail if any installation dependencies (e.g. distribute)
 
49
      are missing.
 
50
    """
 
51
    self._source_dir = source_dir
 
52
    self._install_tmp = install_dir or safe_mkdtemp()
 
53
    self._installed = None
 
54
    self._strict = strict
 
55
    self._interpreter = interpreter or PythonInterpreter.get()
 
56
    if not self._interpreter.satisfies(self.capability) and strict:
 
57
      raise self.IncapableInterpreter('Interpreter %s not capable of running %s' % (
 
58
          self._interpreter.binary, self.__class__.__name__))
 
59
 
 
60
  def mixins(self):
 
61
    """Return a map from import name to requirement to load into setup script prior to invocation.
 
62
 
 
63
       May be subclassed.
 
64
    """
 
65
    return {}
 
66
 
 
67
  @property
 
68
  def install_tmp(self):
 
69
    return self._install_tmp
 
70
 
 
71
  def _setup_command(self):
 
72
    """the setup command-line to run, to be implemented by subclasses."""
 
73
    raise NotImplementedError
 
74
 
 
75
  def _postprocess(self):
 
76
    """a post-processing function to run following setup.py invocation."""
 
77
 
 
78
  @property
 
79
  def capability(self):
 
80
    """returns the list of requirements for the interpreter to run this installer."""
 
81
    return list(self.mixins().values())
 
82
 
 
83
  @property
 
84
  def bootstrap_script(self):
 
85
    bootstrap_modules = []
 
86
    for module, requirement in self.mixins().items():
 
87
      path = self._interpreter.get_location(requirement)
 
88
      if not path:
 
89
        assert not self._strict  # This should be caught by validation
 
90
        continue
 
91
      bootstrap_modules.append(self.SETUP_BOOTSTRAP_MODULE % {'path': path, 'module': module})
 
92
    return '\n'.join(
 
93
        [self.SETUP_BOOTSTRAP_HEADER] + bootstrap_modules + [self.SETUP_BOOTSTRAP_FOOTER])
 
94
 
 
95
  def run(self):
 
96
    if self._installed is not None:
 
97
      return self._installed
 
98
 
 
99
    with TRACER.timed('Installing %s' % self._install_tmp, V=2):
 
100
      command = [self._interpreter.binary, '-']
 
101
      command.extend(self._setup_command())
 
102
      po = subprocess.Popen(command,
 
103
          stdin=subprocess.PIPE,
 
104
          stdout=subprocess.PIPE,
 
105
          stderr=subprocess.PIPE,
 
106
          env=self._interpreter.sanitized_environment(),
 
107
          cwd=self._source_dir)
 
108
      so, se = po.communicate(self.bootstrap_script.encode('ascii'))
 
109
      self._installed = po.returncode == 0
 
110
 
 
111
    if not self._installed:
 
112
      name = os.path.basename(self._source_dir)
 
113
      print('**** Failed to install %s. stdout:\n%s' % (name, so.decode('utf-8')), file=sys.stderr)
 
114
      print('**** Failed to install %s. stderr:\n%s' % (name, se.decode('utf-8')), file=sys.stderr)
 
115
      return self._installed
 
116
 
 
117
    self._postprocess()
 
118
    return self._installed
 
119
 
 
120
  def cleanup(self):
 
121
    safe_rmtree(self._install_tmp)
 
122
 
 
123
 
 
124
class Installer(InstallerBase):
 
125
  """Install an unpacked distribution with a setup.py."""
 
126
 
 
127
  def __init__(self, source_dir, strict=True, interpreter=None):
 
128
    """
 
129
      Create an installer from an unpacked source distribution in source_dir.
 
130
 
 
131
      If strict=True, fail if any installation dependencies (e.g. setuptools)
 
132
      are missing.
 
133
    """
 
134
    super(Installer, self).__init__(source_dir, strict=strict, interpreter=interpreter)
 
135
    self._egg_info = None
 
136
    fd, self._install_record = tempfile.mkstemp()
 
137
    os.close(fd)
 
138
 
 
139
  def _setup_command(self):
 
140
    return ['install',
 
141
           '--root=%s' % self._install_tmp,
 
142
           '--prefix=',
 
143
           '--single-version-externally-managed',
 
144
           '--record', self._install_record]
 
145
 
 
146
  def _postprocess(self):
 
147
    installed_files = []
 
148
    egg_info = None
 
149
    with open(self._install_record) as fp:
 
150
      installed_files = fp.read().splitlines()
 
151
      for line in installed_files:
 
152
        if line.endswith('.egg-info'):
 
153
          assert line.startswith('/'), 'Expect .egg-info to be within install_tmp!'
 
154
          egg_info = line
 
155
          break
 
156
 
 
157
    if not egg_info:
 
158
      self._installed = False
 
159
      return self._installed
 
160
 
 
161
    installed_files = [os.path.relpath(fn, egg_info) for fn in installed_files if fn != egg_info]
 
162
 
 
163
    self._egg_info = os.path.join(self._install_tmp, egg_info[1:])
 
164
    with open(os.path.join(self._egg_info, 'installed-files.txt'), 'w') as fp:
 
165
      fp.write('\n'.join(installed_files))
 
166
      fp.write('\n')
 
167
 
 
168
    return self._installed
 
169
 
 
170
  @after_installation
 
171
  def egg_info(self):
 
172
    return self._egg_info
 
173
 
 
174
  @after_installation
 
175
  def root(self):
 
176
    egg_info = self.egg_info()
 
177
    assert egg_info
 
178
    return os.path.realpath(os.path.dirname(egg_info))
 
179
 
 
180
  @after_installation
 
181
  def distribution(self):
 
182
    base_dir = self.root()
 
183
    egg_info = self.egg_info()
 
184
    metadata = PathMetadata(base_dir, egg_info)
 
185
    return Distribution.from_location(base_dir, os.path.basename(egg_info), metadata=metadata)
 
186
 
 
187
 
 
188
class DistributionPackager(InstallerBase):
 
189
  def mixins(self):
 
190
    mixins = super(DistributionPackager, self).mixins().copy()
 
191
    mixins.update(setuptools='setuptools>=1')
 
192
    return mixins
 
193
 
 
194
  def find_distribution(self):
 
195
    dists = os.listdir(self.install_tmp)
 
196
    if len(dists) == 0:
 
197
      raise self.InstallFailure('No distributions were produced!')
 
198
    elif len(dists) > 1:
 
199
      raise self.InstallFailure('Ambiguous source distributions found: %s' % (' '.join(dists)))
 
200
    else:
 
201
      return os.path.join(self.install_tmp, dists[0])
 
202
 
 
203
 
 
204
class Packager(DistributionPackager):
 
205
  """
 
206
    Create a source distribution from an unpacked setup.py-based project.
 
207
  """
 
208
 
 
209
  def _setup_command(self):
 
210
    return ['sdist', '--formats=gztar', '--dist-dir=%s' % self._install_tmp]
 
211
 
 
212
  @after_installation
 
213
  def sdist(self):
 
214
    return self.find_distribution()
 
215
 
 
216
 
 
217
class EggInstaller(DistributionPackager):
 
218
  """
 
219
    Create a source distribution from an unpacked setup.py-based project.
 
220
  """
 
221
 
 
222
  def _setup_command(self):
 
223
    return ['bdist_egg', '--dist-dir=%s' % self._install_tmp]
 
224
 
 
225
  @after_installation
 
226
  def bdist(self):
 
227
    return self.find_distribution()
 
228
 
 
229
 
 
230
class WheelInstaller(DistributionPackager):
 
231
  """
 
232
    Create a source distribution from an unpacked setup.py-based project.
 
233
  """
 
234
  MIXINS = {
 
235
      'setuptools': 'setuptools>=2',
 
236
      'wheel': 'wheel>=0.17',
 
237
  }
 
238
 
 
239
  def mixins(self):
 
240
    mixins = super(WheelInstaller, self).mixins().copy()
 
241
    mixins.update(self.MIXINS)
 
242
    return mixins
 
243
 
 
244
  def _setup_command(self):
 
245
    return ['bdist_wheel', '--dist-dir=%s' % self._install_tmp]
 
246
 
 
247
  @after_installation
 
248
  def bdist(self):
 
249
    return self.find_distribution()