Statistics
| Branch: | Tag: | Revision:

root / distribute_setup.py @ 719d8d08

History | View | Annotate | Download (16.9 kB)

1
# Copyright 2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34

    
35
#!python
36
"""Bootstrap distribute installation
37

38
If you want to use setuptools in your package's setup.py, just include this
39
file in the same directory with it, and add this to the top of your setup.py::
40

41
    from distribute_setup import use_setuptools
42
    use_setuptools()
43

44
If you want to require a specific version of setuptools, set a download
45
mirror, or use an alternate download directory, you can do so by supplying
46
the appropriate options to ``use_setuptools()``.
47

48
This file can also be run as a script to install or upgrade setuptools.
49
"""
50
import os
51
import sys
52
import time
53
import fnmatch
54
import tempfile
55
import tarfile
56
from distutils import log
57

    
58
try:
59
    from site import USER_SITE
60
except ImportError:
61
    USER_SITE = None
62

    
63
try:
64
    import subprocess
65

    
66
    def _python_cmd(*args):
67
        args = (sys.executable,) + args
68
        return subprocess.call(args) == 0
69

    
70
except ImportError:
71
    # will be used for python 2.3
72
    def _python_cmd(*args):
73
        args = (sys.executable,) + args
74
        # quoting arguments if windows
75
        if sys.platform == 'win32':
76
            def quote(arg):
77
                if ' ' in arg:
78
                    return '"%s"' % arg
79
                return arg
80
            args = [quote(arg) for arg in args]
81
        return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
82

    
83
DEFAULT_VERSION = "0.6.10"
84
DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
85
SETUPTOOLS_FAKED_VERSION = "0.6c11"
86

    
87
SETUPTOOLS_PKG_INFO = """\
88
Metadata-Version: 1.0
89
Name: setuptools
90
Version: %s
91
Summary: xxxx
92
Home-page: xxx
93
Author: xxx
94
Author-email: xxx
95
License: xxx
96
Description: xxx
97
""" % SETUPTOOLS_FAKED_VERSION
98

    
99

    
100
def _install(tarball):
101
    # extracting the tarball
102
    tmpdir = tempfile.mkdtemp()
103
    log.warn('Extracting in %s', tmpdir)
104
    old_wd = os.getcwd()
105
    try:
106
        os.chdir(tmpdir)
107
        tar = tarfile.open(tarball)
108
        _extractall(tar)
109
        tar.close()
110

    
111
        # going in the directory
112
        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
113
        os.chdir(subdir)
114
        log.warn('Now working in %s', subdir)
115

    
116
        # installing
117
        log.warn('Installing Distribute')
118
        if not _python_cmd('setup.py', 'install'):
119
            log.warn('Something went wrong during the installation.')
120
            log.warn('See the error message above.')
121
    finally:
122
        os.chdir(old_wd)
123

    
124

    
125
def _build_egg(egg, tarball, to_dir):
126
    # extracting the tarball
127
    tmpdir = tempfile.mkdtemp()
128
    log.warn('Extracting in %s', tmpdir)
129
    old_wd = os.getcwd()
130
    try:
131
        os.chdir(tmpdir)
132
        tar = tarfile.open(tarball)
133
        _extractall(tar)
134
        tar.close()
135

    
136
        # going in the directory
137
        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
138
        os.chdir(subdir)
139
        log.warn('Now working in %s', subdir)
140

    
141
        # building an egg
142
        log.warn('Building a Distribute egg in %s', to_dir)
143
        _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
144

    
145
    finally:
146
        os.chdir(old_wd)
147
    # returning the result
148
    log.warn(egg)
149
    if not os.path.exists(egg):
150
        raise IOError('Could not build the egg.')
151

    
152

    
153
def _do_download(version, download_base, to_dir, download_delay):
154
    egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
155
                       % (version, sys.version_info[0], sys.version_info[1]))
156
    if not os.path.exists(egg):
157
        tarball = download_setuptools(version, download_base,
158
                                      to_dir, download_delay)
159
        _build_egg(egg, tarball, to_dir)
160
    sys.path.insert(0, egg)
161
    import setuptools
162
    setuptools.bootstrap_install_from = egg
163

    
164

    
165
def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
166
                   to_dir=os.curdir, download_delay=15, no_fake=True):
167
    # making sure we use the absolute path
168
    to_dir = os.path.abspath(to_dir)
169
    was_imported = 'pkg_resources' in sys.modules or \
170
        'setuptools' in sys.modules
171
    try:
172
        try:
173
            import pkg_resources
174
            if not hasattr(pkg_resources, '_distribute'):
175
                if not no_fake:
176
                    _fake_setuptools()
177
                raise ImportError
178
        except ImportError:
179
            return _do_download(version, download_base, to_dir, download_delay)
180
        try:
181
            pkg_resources.require("distribute>="+version)
182
            return
183
        except pkg_resources.VersionConflict:
184
            e = sys.exc_info()[1]
185
            if was_imported:
186
                sys.stderr.write(
187
                "The required version of distribute (>=%s) is not available,\n"
188
                "and can't be installed while this script is running. Please\n"
189
                "install a more recent version first, using\n"
190
                "'easy_install -U distribute'."
191
                "\n\n(Currently using %r)\n" % (version, e.args[0]))
192
                sys.exit(2)
193
            else:
194
                del pkg_resources, sys.modules['pkg_resources']    # reload ok
195
                return _do_download(version, download_base, to_dir,
196
                                    download_delay)
197
        except pkg_resources.DistributionNotFound:
198
            return _do_download(version, download_base, to_dir,
199
                                download_delay)
200
    finally:
201
        if not no_fake:
202
            _create_fake_setuptools_pkg_info(to_dir)
203

    
204
def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
205
                        to_dir=os.curdir, delay=15):
206
    """Download distribute from a specified location and return its filename
207

208
    `version` should be a valid distribute version number that is available
209
    as an egg for download under the `download_base` URL (which should end
210
    with a '/'). `to_dir` is the directory where the egg will be downloaded.
211
    `delay` is the number of seconds to pause before an actual download
212
    attempt.
213
    """
214
    # making sure we use the absolute path
215
    to_dir = os.path.abspath(to_dir)
216
    try:
217
        from urllib.request import urlopen
218
    except ImportError:
219
        from urllib2 import urlopen
220
    tgz_name = "distribute-%s.tar.gz" % version
221
    url = download_base + tgz_name
222
    saveto = os.path.join(to_dir, tgz_name)
223
    src = dst = None
224
    if not os.path.exists(saveto):  # Avoid repeated downloads
225
        try:
226
            log.warn("Downloading %s", url)
227
            src = urlopen(url)
228
            # Read/write all in one block, so we don't create a corrupt file
229
            # if the download is interrupted.
230
            data = src.read()
231
            dst = open(saveto, "wb")
232
            dst.write(data)
233
        finally:
234
            if src:
235
                src.close()
236
            if dst:
237
                dst.close()
238
    return os.path.realpath(saveto)
239

    
240
def _no_sandbox(function):
241
    def __no_sandbox(*args, **kw):
242
        try:
243
            from setuptools.sandbox import DirectorySandbox
244
            if not hasattr(DirectorySandbox, '_old'):
245
                def violation(*args):
246
                    pass
247
                DirectorySandbox._old = DirectorySandbox._violation
248
                DirectorySandbox._violation = violation
249
                patched = True
250
            else:
251
                patched = False
252
        except ImportError:
253
            patched = False
254

    
255
        try:
256
            return function(*args, **kw)
257
        finally:
258
            if patched:
259
                DirectorySandbox._violation = DirectorySandbox._old
260
                del DirectorySandbox._old
261

    
262
    return __no_sandbox
263

    
264
def _patch_file(path, content):
265
    """Will backup the file then patch it"""
266
    existing_content = open(path).read()
267
    if existing_content == content:
268
        # already patched
269
        log.warn('Already patched.')
270
        return False
271
    log.warn('Patching...')
272
    _rename_path(path)
273
    f = open(path, 'w')
274
    try:
275
        f.write(content)
276
    finally:
277
        f.close()
278
    return True
279

    
280
_patch_file = _no_sandbox(_patch_file)
281

    
282
def _same_content(path, content):
283
    return open(path).read() == content
284

    
285
def _rename_path(path):
286
    new_name = path + '.OLD.%s' % time.time()
287
    log.warn('Renaming %s into %s', path, new_name)
288
    os.rename(path, new_name)
289
    return new_name
290

    
291
def _remove_flat_installation(placeholder):
292
    if not os.path.isdir(placeholder):
293
        log.warn('Unkown installation at %s', placeholder)
294
        return False
295
    found = False
296
    for file in os.listdir(placeholder):
297
        if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
298
            found = True
299
            break
300
    if not found:
301
        log.warn('Could not locate setuptools*.egg-info')
302
        return
303

    
304
    log.warn('Removing elements out of the way...')
305
    pkg_info = os.path.join(placeholder, file)
306
    if os.path.isdir(pkg_info):
307
        patched = _patch_egg_dir(pkg_info)
308
    else:
309
        patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
310

    
311
    if not patched:
312
        log.warn('%s already patched.', pkg_info)
313
        return False
314
    # now let's move the files out of the way
315
    for element in ('setuptools', 'pkg_resources.py', 'site.py'):
316
        element = os.path.join(placeholder, element)
317
        if os.path.exists(element):
318
            _rename_path(element)
319
        else:
320
            log.warn('Could not find the %s element of the '
321
                     'Setuptools distribution', element)
322
    return True
323

    
324
_remove_flat_installation = _no_sandbox(_remove_flat_installation)
325

    
326
def _after_install(dist):
327
    log.warn('After install bootstrap.')
328
    placeholder = dist.get_command_obj('install').install_purelib
329
    _create_fake_setuptools_pkg_info(placeholder)
330

    
331
def _create_fake_setuptools_pkg_info(placeholder):
332
    if not placeholder or not os.path.exists(placeholder):
333
        log.warn('Could not find the install location')
334
        return
335
    pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
336
    setuptools_file = 'setuptools-%s-py%s.egg-info' % \
337
            (SETUPTOOLS_FAKED_VERSION, pyver)
338
    pkg_info = os.path.join(placeholder, setuptools_file)
339
    if os.path.exists(pkg_info):
340
        log.warn('%s already exists', pkg_info)
341
        return
342

    
343
    log.warn('Creating %s', pkg_info)
344
    f = open(pkg_info, 'w')
345
    try:
346
        f.write(SETUPTOOLS_PKG_INFO)
347
    finally:
348
        f.close()
349

    
350
    pth_file = os.path.join(placeholder, 'setuptools.pth')
351
    log.warn('Creating %s', pth_file)
352
    f = open(pth_file, 'w')
353
    try:
354
        f.write(os.path.join(os.curdir, setuptools_file))
355
    finally:
356
        f.close()
357

    
358
_create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info)
359

    
360
def _patch_egg_dir(path):
361
    # let's check if it's already patched
362
    pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
363
    if os.path.exists(pkg_info):
364
        if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
365
            log.warn('%s already patched.', pkg_info)
366
            return False
367
    _rename_path(path)
368
    os.mkdir(path)
369
    os.mkdir(os.path.join(path, 'EGG-INFO'))
370
    pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
371
    f = open(pkg_info, 'w')
372
    try:
373
        f.write(SETUPTOOLS_PKG_INFO)
374
    finally:
375
        f.close()
376
    return True
377

    
378
_patch_egg_dir = _no_sandbox(_patch_egg_dir)
379

    
380
def _before_install():
381
    log.warn('Before install bootstrap.')
382
    _fake_setuptools()
383

    
384

    
385
def _under_prefix(location):
386
    if 'install' not in sys.argv:
387
        return True
388
    args = sys.argv[sys.argv.index('install')+1:]
389
    for index, arg in enumerate(args):
390
        for option in ('--root', '--prefix'):
391
            if arg.startswith('%s=' % option):
392
                top_dir = arg.split('root=')[-1]
393
                return location.startswith(top_dir)
394
            elif arg == option:
395
                if len(args) > index:
396
                    top_dir = args[index+1]
397
                    return location.startswith(top_dir)
398
        if arg == '--user' and USER_SITE is not None:
399
            return location.startswith(USER_SITE)
400
    return True
401

    
402

    
403
def _fake_setuptools():
404
    log.warn('Scanning installed packages')
405
    try:
406
        import pkg_resources
407
    except ImportError:
408
        # we're cool
409
        log.warn('Setuptools or Distribute does not seem to be installed.')
410
        return
411
    ws = pkg_resources.working_set
412
    try:
413
        setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools',
414
                                  replacement=False))
415
    except TypeError:
416
        # old distribute API
417
        setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools'))
418

    
419
    if setuptools_dist is None:
420
        log.warn('No setuptools distribution found')
421
        return
422
    # detecting if it was already faked
423
    setuptools_location = setuptools_dist.location
424
    log.warn('Setuptools installation detected at %s', setuptools_location)
425

    
426
    # if --root or --preix was provided, and if
427
    # setuptools is not located in them, we don't patch it
428
    if not _under_prefix(setuptools_location):
429
        log.warn('Not patching, --root or --prefix is installing Distribute'
430
                 ' in another location')
431
        return
432

    
433
    # let's see if its an egg
434
    if not setuptools_location.endswith('.egg'):
435
        log.warn('Non-egg installation')
436
        res = _remove_flat_installation(setuptools_location)
437
        if not res:
438
            return
439
    else:
440
        log.warn('Egg installation')
441
        pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
442
        if (os.path.exists(pkg_info) and
443
            _same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
444
            log.warn('Already patched.')
445
            return
446
        log.warn('Patching...')
447
        # let's create a fake egg replacing setuptools one
448
        res = _patch_egg_dir(setuptools_location)
449
        if not res:
450
            return
451
    log.warn('Patched done.')
452
    _relaunch()
453

    
454

    
455
def _relaunch():
456
    log.warn('Relaunching...')
457
    # we have to relaunch the process
458
    # pip marker to avoid a relaunch bug
459
    if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']:
460
        sys.argv[0] = 'setup.py'
461
    args = [sys.executable] + sys.argv
462
    sys.exit(subprocess.call(args))
463

    
464

    
465
def _extractall(self, path=".", members=None):
466
    """Extract all members from the archive to the current working
467
       directory and set owner, modification time and permissions on
468
       directories afterwards. `path' specifies a different directory
469
       to extract to. `members' is optional and must be a subset of the
470
       list returned by getmembers().
471
    """
472
    import copy
473
    import operator
474
    from tarfile import ExtractError
475
    directories = []
476

    
477
    if members is None:
478
        members = self
479

    
480
    for tarinfo in members:
481
        if tarinfo.isdir():
482
            # Extract directories with a safe mode.
483
            directories.append(tarinfo)
484
            tarinfo = copy.copy(tarinfo)
485
            tarinfo.mode = 448 # decimal for oct 0700
486
        self.extract(tarinfo, path)
487

    
488
    # Reverse sort directories.
489
    if sys.version_info < (2, 4):
490
        def sorter(dir1, dir2):
491
            return cmp(dir1.name, dir2.name)
492
        directories.sort(sorter)
493
        directories.reverse()
494
    else:
495
        directories.sort(key=operator.attrgetter('name'), reverse=True)
496

    
497
    # Set correct owner, mtime and filemode on directories.
498
    for tarinfo in directories:
499
        dirpath = os.path.join(path, tarinfo.name)
500
        try:
501
            self.chown(tarinfo, dirpath)
502
            self.utime(tarinfo, dirpath)
503
            self.chmod(tarinfo, dirpath)
504
        except ExtractError:
505
            e = sys.exc_info()[1]
506
            if self.errorlevel > 1:
507
                raise
508
            else:
509
                self._dbg(1, "tarfile: %s" % e)
510

    
511

    
512
def main(argv, version=DEFAULT_VERSION):
513
    """Install or upgrade setuptools and EasyInstall"""
514
    tarball = download_setuptools()
515
    _install(tarball)
516

    
517

    
518
if __name__ == '__main__':
519
    main(sys.argv[1:])