1
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
4
from __future__ import absolute_import, print_function
11
from pkg_resources import Distribution, PathMetadata
13
from .common import safe_mkdtemp, safe_rmtree
14
from .interpreter import PythonInterpreter
15
from .tracer import TRACER
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
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 = """
37
exec(compile(open(__file__).read().replace('\\r\\n', '\\n'), __file__, 'exec'))
40
class Error(Exception): pass
41
class InstallFailure(Error): pass
42
class IncapableInterpreter(Error): pass
44
def __init__(self, source_dir, strict=True, interpreter=None, install_dir=None):
46
Create an installer from an unpacked source distribution in source_dir.
48
If strict=True, fail if any installation dependencies (e.g. distribute)
51
self._source_dir = source_dir
52
self._install_tmp = install_dir or safe_mkdtemp()
53
self._installed = None
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__))
61
"""Return a map from import name to requirement to load into setup script prior to invocation.
68
def install_tmp(self):
69
return self._install_tmp
71
def _setup_command(self):
72
"""the setup command-line to run, to be implemented by subclasses."""
73
raise NotImplementedError
75
def _postprocess(self):
76
"""a post-processing function to run following setup.py invocation."""
80
"""returns the list of requirements for the interpreter to run this installer."""
81
return list(self.mixins().values())
84
def bootstrap_script(self):
85
bootstrap_modules = []
86
for module, requirement in self.mixins().items():
87
path = self._interpreter.get_location(requirement)
89
assert not self._strict # This should be caught by validation
91
bootstrap_modules.append(self.SETUP_BOOTSTRAP_MODULE % {'path': path, 'module': module})
93
[self.SETUP_BOOTSTRAP_HEADER] + bootstrap_modules + [self.SETUP_BOOTSTRAP_FOOTER])
96
if self._installed is not None:
97
return self._installed
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
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
118
return self._installed
121
safe_rmtree(self._install_tmp)
124
class Installer(InstallerBase):
125
"""Install an unpacked distribution with a setup.py."""
127
def __init__(self, source_dir, strict=True, interpreter=None):
129
Create an installer from an unpacked source distribution in source_dir.
131
If strict=True, fail if any installation dependencies (e.g. setuptools)
134
super(Installer, self).__init__(source_dir, strict=strict, interpreter=interpreter)
135
self._egg_info = None
136
fd, self._install_record = tempfile.mkstemp()
139
def _setup_command(self):
141
'--root=%s' % self._install_tmp,
143
'--single-version-externally-managed',
144
'--record', self._install_record]
146
def _postprocess(self):
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!'
158
self._installed = False
159
return self._installed
161
installed_files = [os.path.relpath(fn, egg_info) for fn in installed_files if fn != egg_info]
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))
168
return self._installed
172
return self._egg_info
176
egg_info = self.egg_info()
178
return os.path.realpath(os.path.dirname(egg_info))
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)
188
class DistributionPackager(InstallerBase):
190
mixins = super(DistributionPackager, self).mixins().copy()
191
mixins.update(setuptools='setuptools>=1')
194
def find_distribution(self):
195
dists = os.listdir(self.install_tmp)
197
raise self.InstallFailure('No distributions were produced!')
199
raise self.InstallFailure('Ambiguous source distributions found: %s' % (' '.join(dists)))
201
return os.path.join(self.install_tmp, dists[0])
204
class Packager(DistributionPackager):
206
Create a source distribution from an unpacked setup.py-based project.
209
def _setup_command(self):
210
return ['sdist', '--formats=gztar', '--dist-dir=%s' % self._install_tmp]
214
return self.find_distribution()
217
class EggInstaller(DistributionPackager):
219
Create a source distribution from an unpacked setup.py-based project.
222
def _setup_command(self):
223
return ['bdist_egg', '--dist-dir=%s' % self._install_tmp]
227
return self.find_distribution()
230
class WheelInstaller(DistributionPackager):
232
Create a source distribution from an unpacked setup.py-based project.
235
'setuptools': 'setuptools>=2',
236
'wheel': 'wheel>=0.17',
240
mixins = super(WheelInstaller, self).mixins().copy()
241
mixins.update(self.MIXINS)
244
def _setup_command(self):
245
return ['bdist_wheel', '--dist-dir=%s' % self._install_tmp]
249
return self.find_distribution()