~ubuntu-branches/debian/jessie/python-pip/jessie

« back to all changes in this revision

Viewing changes to pip/wheel.py

  • Committer: Package Import Robot
  • Author(s): Barry Warsaw
  • Date: 2013-08-19 18:33:23 UTC
  • mfrom: (1.2.5)
  • Revision ID: package-import@ubuntu.com-20130819183323-8xyoldb2798iil6e
Tags: 1.4.1-1
* Team upload.
* New upstream release.
  - d/control: Update Standards-Version to 3.9.4 with no additional
    changes required.
  - d/patches/no-python-specific-scripts.patch: Refreshed.
  - d/patches/format_egg_string.patch: Refreshed.
  - d/patches/system-ca-certificates.patch: Refreshed.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""
 
2
Support for installing and building the "wheel" binary package format.
 
3
"""
 
4
from __future__ import with_statement
 
5
 
 
6
import csv
 
7
import functools
 
8
import hashlib
 
9
import os
 
10
import pkg_resources
 
11
import re
 
12
import shutil
 
13
import sys
 
14
from base64 import urlsafe_b64encode
 
15
 
 
16
from pip.locations import distutils_scheme
 
17
from pip.log import logger
 
18
from pip import pep425tags
 
19
from pip.util import call_subprocess, normalize_path, make_path_relative
 
20
 
 
21
wheel_ext = '.whl'
 
22
# don't use pkg_resources.Requirement.parse, to avoid the override in distribute,
 
23
# that converts 'setuptools' to 'distribute'.
 
24
setuptools_requirement = list(pkg_resources.parse_requirements("setuptools>=0.8"))[0]
 
25
 
 
26
def wheel_setuptools_support():
 
27
    """
 
28
    Return True if we have a setuptools that supports wheel.
 
29
    """
 
30
    fulfilled = False
 
31
    try:
 
32
        installed_setuptools = pkg_resources.get_distribution('setuptools')
 
33
        if installed_setuptools in setuptools_requirement:
 
34
            fulfilled = True
 
35
    except pkg_resources.DistributionNotFound:
 
36
        pass
 
37
    if not fulfilled:
 
38
        logger.warn("%s is required for wheel installs." % setuptools_requirement)
 
39
    return fulfilled
 
40
 
 
41
def rehash(path, algo='sha256', blocksize=1<<20):
 
42
    """Return (hash, length) for path using hashlib.new(algo)"""
 
43
    h = hashlib.new(algo)
 
44
    length = 0
 
45
    with open(path) as f:
 
46
        block = f.read(blocksize)
 
47
        while block:
 
48
            length += len(block)
 
49
            h.update(block)
 
50
            block = f.read(blocksize)
 
51
    digest = 'sha256='+urlsafe_b64encode(h.digest()).decode('latin1').rstrip('=')
 
52
    return (digest, length)
 
53
 
 
54
try:
 
55
    unicode
 
56
    def binary(s):
 
57
        if isinstance(s, unicode):
 
58
            return s.encode('ascii')
 
59
        return s
 
60
except NameError:
 
61
    def binary(s):
 
62
        if isinstance(s, str):
 
63
            return s.encode('ascii')
 
64
 
 
65
def open_for_csv(name, mode):
 
66
    if sys.version_info[0] < 3:
 
67
        nl = {}
 
68
        bin = 'b'
 
69
    else:
 
70
        nl = { 'newline': '' }
 
71
        bin = ''
 
72
    return open(name, mode + bin, **nl)
 
73
 
 
74
def fix_script(path):
 
75
    """Replace #!python with #!/path/to/python
 
76
    Return True if file was changed."""
 
77
    # XXX RECORD hashes will need to be updated
 
78
    if os.path.isfile(path):
 
79
        script = open(path, 'rb')
 
80
        try:
 
81
            firstline = script.readline()
 
82
            if not firstline.startswith(binary('#!python')):
 
83
                return False
 
84
            exename = sys.executable.encode(sys.getfilesystemencoding())
 
85
            firstline = binary('#!') + exename + binary(os.linesep)
 
86
            rest = script.read()
 
87
        finally:
 
88
            script.close()
 
89
        script = open(path, 'wb')
 
90
        try:
 
91
            script.write(firstline)
 
92
            script.write(rest)
 
93
        finally:
 
94
            script.close()
 
95
        return True
 
96
 
 
97
dist_info_re = re.compile(r"""^(?P<namever>(?P<name>.+?)(-(?P<ver>\d.+?))?)
 
98
                                \.dist-info$""", re.VERBOSE)
 
99
 
 
100
def root_is_purelib(name, wheeldir):
 
101
    """
 
102
    Return True if the extracted wheel in wheeldir should go into purelib.
 
103
    """
 
104
    name_folded = name.replace("-", "_")
 
105
    for item in os.listdir(wheeldir):
 
106
        match = dist_info_re.match(item)
 
107
        if match and match.group('name') == name_folded:
 
108
            with open(os.path.join(wheeldir, item, 'WHEEL')) as wheel:
 
109
                for line in wheel:
 
110
                    line = line.lower().rstrip()
 
111
                    if line == "root-is-purelib: true":
 
112
                        return True
 
113
    return False
 
114
 
 
115
def move_wheel_files(name, req, wheeldir, user=False, home=None):
 
116
    """Install a wheel"""
 
117
 
 
118
    scheme = distutils_scheme(name, user=user, home=home)
 
119
 
 
120
    if root_is_purelib(name, wheeldir):
 
121
        lib_dir = scheme['purelib']
 
122
    else:
 
123
        lib_dir = scheme['platlib']
 
124
 
 
125
    info_dir = []
 
126
    data_dirs = []
 
127
    source = wheeldir.rstrip(os.path.sep) + os.path.sep
 
128
    installed = {}
 
129
    changed = set()
 
130
 
 
131
    def normpath(src, p):
 
132
        return make_path_relative(src, p).replace(os.path.sep, '/')
 
133
 
 
134
    def record_installed(srcfile, destfile, modified=False):
 
135
        """Map archive RECORD paths to installation RECORD paths."""
 
136
        oldpath = normpath(srcfile, wheeldir)
 
137
        newpath = normpath(destfile, lib_dir)
 
138
        installed[oldpath] = newpath
 
139
        if modified:
 
140
            changed.add(destfile)
 
141
 
 
142
    def clobber(source, dest, is_base, fixer=None):
 
143
        if not os.path.exists(dest): # common for the 'include' path
 
144
            os.makedirs(dest)
 
145
 
 
146
        for dir, subdirs, files in os.walk(source):
 
147
            basedir = dir[len(source):].lstrip(os.path.sep)
 
148
            if is_base and basedir.split(os.path.sep, 1)[0].endswith('.data'):
 
149
                continue
 
150
            for s in subdirs:
 
151
                destsubdir = os.path.join(dest, basedir, s)
 
152
                if is_base and basedir == '' and destsubdir.endswith('.data'):
 
153
                    data_dirs.append(s)
 
154
                    continue
 
155
                elif (is_base
 
156
                    and s.endswith('.dist-info')
 
157
                    # is self.req.project_name case preserving?
 
158
                    and s.lower().startswith(req.project_name.replace('-', '_').lower())):
 
159
                    assert not info_dir, 'Multiple .dist-info directories'
 
160
                    info_dir.append(destsubdir)
 
161
                if not os.path.exists(destsubdir):
 
162
                    os.makedirs(destsubdir)
 
163
            for f in files:
 
164
                srcfile = os.path.join(dir, f)
 
165
                destfile = os.path.join(dest, basedir, f)
 
166
                shutil.move(srcfile, destfile)
 
167
                changed = False
 
168
                if fixer:
 
169
                    changed = fixer(destfile)
 
170
                record_installed(srcfile, destfile, changed)
 
171
 
 
172
    clobber(source, lib_dir, True)
 
173
 
 
174
    assert info_dir, "%s .dist-info directory not found" % req
 
175
 
 
176
    for datadir in data_dirs:
 
177
        fixer = None
 
178
        for subdir in os.listdir(os.path.join(wheeldir, datadir)):
 
179
            fixer = None
 
180
            if subdir == 'scripts':
 
181
                fixer = fix_script
 
182
            source = os.path.join(wheeldir, datadir, subdir)
 
183
            dest = scheme[subdir]
 
184
            clobber(source, dest, False, fixer=fixer)
 
185
 
 
186
    record = os.path.join(info_dir[0], 'RECORD')
 
187
    temp_record = os.path.join(info_dir[0], 'RECORD.pip')
 
188
    with open_for_csv(record, 'r') as record_in:
 
189
        with open_for_csv(temp_record, 'w+') as record_out:
 
190
            reader = csv.reader(record_in)
 
191
            writer = csv.writer(record_out)
 
192
            for row in reader:
 
193
                row[0] = installed.pop(row[0], row[0])
 
194
                if row[0] in changed:
 
195
                    row[1], row[2] = rehash(row[0])
 
196
                writer.writerow(row)
 
197
            for f in installed:
 
198
                writer.writerow((installed[f], '', ''))
 
199
    shutil.move(temp_record, record)
 
200
 
 
201
def _unique(fn):
 
202
    @functools.wraps(fn)
 
203
    def unique(*args, **kw):
 
204
        seen = set()
 
205
        for item in fn(*args, **kw):
 
206
            if item not in seen:
 
207
                seen.add(item)
 
208
                yield item
 
209
    return unique
 
210
 
 
211
# TODO: this goes somewhere besides the wheel module
 
212
@_unique
 
213
def uninstallation_paths(dist):
 
214
    """
 
215
    Yield all the uninstallation paths for dist based on RECORD-without-.pyc
 
216
 
 
217
    Yield paths to all the files in RECORD. For each .py file in RECORD, add
 
218
    the .pyc in the same directory.
 
219
 
 
220
    UninstallPathSet.add() takes care of the __pycache__ .pyc.
 
221
    """
 
222
    from pip.req import FakeFile # circular import
 
223
    r = csv.reader(FakeFile(dist.get_metadata_lines('RECORD')))
 
224
    for row in r:
 
225
        path = os.path.join(dist.location, row[0])
 
226
        yield path
 
227
        if path.endswith('.py'):
 
228
            dn, fn = os.path.split(path)
 
229
            base = fn[:-3]
 
230
            path = os.path.join(dn, base+'.pyc')
 
231
            yield path
 
232
 
 
233
 
 
234
class Wheel(object):
 
235
    """A wheel file"""
 
236
 
 
237
    # TODO: maybe move the install code into this class
 
238
 
 
239
    wheel_file_re = re.compile(
 
240
                r"""^(?P<namever>(?P<name>.+?)(-(?P<ver>\d.+?))?)
 
241
                ((-(?P<build>\d.*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?)
 
242
                \.whl|\.dist-info)$""",
 
243
                re.VERBOSE)
 
244
 
 
245
    def __init__(self, filename):
 
246
        wheel_info = self.wheel_file_re.match(filename)
 
247
        self.filename = filename
 
248
        self.name = wheel_info.group('name').replace('_', '-')
 
249
        self.version = wheel_info.group('ver')
 
250
        self.pyversions = wheel_info.group('pyver').split('.')
 
251
        self.abis = wheel_info.group('abi').split('.')
 
252
        self.plats = wheel_info.group('plat').split('.')
 
253
 
 
254
        # All the tag combinations from this file
 
255
        self.file_tags = set((x, y, z) for x in self.pyversions for y
 
256
                            in self.abis for z in self.plats)
 
257
 
 
258
    def support_index_min(self, tags=None):
 
259
        """
 
260
        Return the lowest index that a file_tag achieves in the supported_tags list
 
261
        e.g. if there are 8 supported tags, and one of the file tags is first in the
 
262
        list, then return 0.
 
263
        """
 
264
        if tags is None: # for mock
 
265
            tags = pep425tags.supported_tags
 
266
        indexes = [tags.index(c) for c in self.file_tags if c in tags]
 
267
        return min(indexes) if indexes else None
 
268
 
 
269
    def supported(self, tags=None):
 
270
        """Is this wheel supported on this system?"""
 
271
        if tags is None: # for mock
 
272
            tags = pep425tags.supported_tags
 
273
        return bool(set(tags).intersection(self.file_tags))
 
274
 
 
275
 
 
276
class WheelBuilder(object):
 
277
    """Build wheels from a RequirementSet."""
 
278
 
 
279
    def __init__(self, requirement_set, finder, wheel_dir, build_options=[], global_options=[]):
 
280
        self.requirement_set = requirement_set
 
281
        self.finder = finder
 
282
        self.wheel_dir = normalize_path(wheel_dir)
 
283
        self.build_options = build_options
 
284
        self.global_options = global_options
 
285
 
 
286
    def _build_one(self, req):
 
287
        """Build one wheel."""
 
288
 
 
289
        base_args = [
 
290
            sys.executable, '-c',
 
291
            "import setuptools;__file__=%r;"\
 
292
            "exec(compile(open(__file__).read().replace('\\r\\n', '\\n'), __file__, 'exec'))" % req.setup_py] + \
 
293
            list(self.global_options)
 
294
 
 
295
        logger.notify('Running setup.py bdist_wheel for %s' % req.name)
 
296
        logger.notify('Destination directory: %s' % self.wheel_dir)
 
297
        wheel_args = base_args + ['bdist_wheel', '-d', self.wheel_dir] + self.build_options
 
298
        try:
 
299
            call_subprocess(wheel_args, cwd=req.source_dir, show_stdout=False)
 
300
            return True
 
301
        except:
 
302
            logger.error('Failed building wheel for %s' % req.name)
 
303
            return False
 
304
 
 
305
    def build(self):
 
306
        """Build wheels."""
 
307
 
 
308
        #unpack and constructs req set
 
309
        self.requirement_set.prepare_files(self.finder)
 
310
 
 
311
        reqset = self.requirement_set.requirements.values()
 
312
 
 
313
        #make the wheelhouse
 
314
        if not os.path.exists(self.wheel_dir):
 
315
            os.makedirs(self.wheel_dir)
 
316
 
 
317
        #build the wheels
 
318
        logger.notify('Building wheels for collected packages: %s' % ', '.join([req.name for req in reqset]))
 
319
        logger.indent += 2
 
320
        build_success, build_failure = [], []
 
321
        for req in reqset:
 
322
            if req.is_wheel:
 
323
                logger.notify("Skipping building wheel: %s", req.url)
 
324
                continue
 
325
            if self._build_one(req):
 
326
                build_success.append(req)
 
327
            else:
 
328
                build_failure.append(req)
 
329
        logger.indent -= 2
 
330
 
 
331
        #notify sucess/failure
 
332
        if build_success:
 
333
            logger.notify('Successfully built %s' % ' '.join([req.name for req in build_success]))
 
334
        if build_failure:
 
335
            logger.notify('Failed to build %s' % ' '.join([req.name for req in build_failure]))