1#!python
2"""Bootstrap distribute installation
3
4If you want to use setuptools in your package's setup.py, just include this
5file in the same directory with it, and add this to the top of your setup.py::
6
7    from distribute_setup import use_setuptools
8    use_setuptools()
9
10If you want to require a specific version of setuptools, set a download
11mirror, or use an alternate download directory, you can do so by supplying
12the appropriate options to ``use_setuptools()``.
13
14This file can also be run as a script to install or upgrade setuptools.
15"""
16import os
17import sys
18import time
19import fnmatch
20import tempfile
21import tarfile
22from distutils import log
23
24try:
25    from site import USER_SITE
26except ImportError:
27    USER_SITE = None
28
29try:
30    import subprocess
31
32    def _python_cmd(*args):
33        args = (sys.executable,) + args
34        return subprocess.call(args) == 0
35
36except ImportError:
37    # will be used for python 2.3
38    def _python_cmd(*args):
39        args = (sys.executable,) + args
40        # quoting arguments if windows
41        if sys.platform == 'win32':
42            def quote(arg):
43                if ' ' in arg:
44                    return '"%s"' % arg
45                return arg
46            args = [quote(arg) for arg in args]
47        return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
48
49DEFAULT_VERSION = "0.6.14"
50DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
51SETUPTOOLS_FAKED_VERSION = "0.6c11"
52
53SETUPTOOLS_PKG_INFO = """\
54Metadata-Version: 1.0
55Name: setuptools
56Version: %s
57Summary: xxxx
58Home-page: xxx
59Author: xxx
60Author-email: xxx
61License: xxx
62Description: xxx
63""" % SETUPTOOLS_FAKED_VERSION
64
65
66def _install(tarball):
67    # extracting the tarball
68    tmpdir = tempfile.mkdtemp()
69    log.warn('Extracting in %s', tmpdir)
70    old_wd = os.getcwd()
71    try:
72        os.chdir(tmpdir)
73        tar = tarfile.open(tarball)
74        _extractall(tar)
75        tar.close()
76
77        # going in the directory
78        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
79        os.chdir(subdir)
80        log.warn('Now working in %s', subdir)
81
82        # installing
83        log.warn('Installing Distribute')
84        if not _python_cmd('setup.py', 'install'):
85            log.warn('Something went wrong during the installation.')
86            log.warn('See the error message above.')
87    finally:
88        os.chdir(old_wd)
89
90
91def _build_egg(egg, tarball, to_dir):
92    # extracting the tarball
93    tmpdir = tempfile.mkdtemp()
94    log.warn('Extracting in %s', tmpdir)
95    old_wd = os.getcwd()
96    try:
97        os.chdir(tmpdir)
98        tar = tarfile.open(tarball)
99        _extractall(tar)
100        tar.close()
101
102        # going in the directory
103        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
104        os.chdir(subdir)
105        log.warn('Now working in %s', subdir)
106
107        # building an egg
108        log.warn('Building a Distribute egg in %s', to_dir)
109        _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
110
111    finally:
112        os.chdir(old_wd)
113    # returning the result
114    log.warn(egg)
115    if not os.path.exists(egg):
116        raise IOError('Could not build the egg.')
117
118
119def _do_download(version, download_base, to_dir, download_delay):
120    egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
121                       % (version, sys.version_info[0], sys.version_info[1]))
122    if not os.path.exists(egg):
123        tarball = download_setuptools(version, download_base,
124                                      to_dir, download_delay)
125        _build_egg(egg, tarball, to_dir)
126    sys.path.insert(0, egg)
127    import setuptools
128    setuptools.bootstrap_install_from = egg
129
130
131def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
132                   to_dir=os.curdir, download_delay=15, no_fake=True):
133    # making sure we use the absolute path
134    to_dir = os.path.abspath(to_dir)
135    was_imported = 'pkg_resources' in sys.modules or \
136        'setuptools' in sys.modules
137    try:
138        try:
139            import pkg_resources
140            if not hasattr(pkg_resources, '_distribute'):
141                if not no_fake:
142                    _fake_setuptools()
143                raise ImportError
144        except ImportError:
145            return _do_download(version, download_base, to_dir, download_delay)
146        try:
147            pkg_resources.require("distribute>="+version)
148            return
149        except pkg_resources.VersionConflict:
150            e = sys.exc_info()[1]
151            if was_imported:
152                sys.stderr.write(
153                "The required version of distribute (>=%s) is not available,\n"
154                "and can't be installed while this script is running. Please\n"
155                "install a more recent version first, using\n"
156                "'easy_install -U distribute'."
157                "\n\n(Currently using %r)\n" % (version, e.args[0]))
158                sys.exit(2)
159            else:
160                del pkg_resources, sys.modules['pkg_resources']    # reload ok
161                return _do_download(version, download_base, to_dir,
162                                    download_delay)
163        except pkg_resources.DistributionNotFound:
164            return _do_download(version, download_base, to_dir,
165                                download_delay)
166    finally:
167        if not no_fake:
168            _create_fake_setuptools_pkg_info(to_dir)
169
170def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
171                        to_dir=os.curdir, delay=15):
172    """Download distribute from a specified location and return its filename
173
174    `version` should be a valid distribute version number that is available
175    as an egg for download under the `download_base` URL (which should end
176    with a '/'). `to_dir` is the directory where the egg will be downloaded.
177    `delay` is the number of seconds to pause before an actual download
178    attempt.
179    """
180    # making sure we use the absolute path
181    to_dir = os.path.abspath(to_dir)
182    try:
183        from urllib.request import urlopen
184    except ImportError:
185        from urllib2 import urlopen
186    tgz_name = "distribute-%s.tar.gz" % version
187    url = download_base + tgz_name
188    saveto = os.path.join(to_dir, tgz_name)
189    src = dst = None
190    if not os.path.exists(saveto):  # Avoid repeated downloads
191        try:
192            log.warn("Downloading %s", url)
193            src = urlopen(url)
194            # Read/write all in one block, so we don't create a corrupt file
195            # if the download is interrupted.
196            data = src.read()
197            dst = open(saveto, "wb")
198            dst.write(data)
199        finally:
200            if src:
201                src.close()
202            if dst:
203                dst.close()
204    return os.path.realpath(saveto)
205
206def _no_sandbox(function):
207    def __no_sandbox(*args, **kw):
208        try:
209            from setuptools.sandbox import DirectorySandbox
210            if not hasattr(DirectorySandbox, '_old'):
211                def violation(*args):
212                    pass
213                DirectorySandbox._old = DirectorySandbox._violation
214                DirectorySandbox._violation = violation
215                patched = True
216            else:
217                patched = False
218        except ImportError:
219            patched = False
220
221        try:
222            return function(*args, **kw)
223        finally:
224            if patched:
225                DirectorySandbox._violation = DirectorySandbox._old
226                del DirectorySandbox._old
227
228    return __no_sandbox
229
230def _patch_file(path, content):
231    """Will backup the file then patch it"""
232    existing_content = open(path).read()
233    if existing_content == content:
234        # already patched
235        log.warn('Already patched.')
236        return False
237    log.warn('Patching...')
238    _rename_path(path)
239    f = open(path, 'w')
240    try:
241        f.write(content)
242    finally:
243        f.close()
244    return True
245
246_patch_file = _no_sandbox(_patch_file)
247
248def _same_content(path, content):
249    return open(path).read() == content
250
251def _rename_path(path):
252    new_name = path + '.OLD.%s' % time.time()
253    log.warn('Renaming %s into %s', path, new_name)
254    os.rename(path, new_name)
255    return new_name
256
257def _remove_flat_installation(placeholder):
258    if not os.path.isdir(placeholder):
259        log.warn('Unkown installation at %s', placeholder)
260        return False
261    found = False
262    for file in os.listdir(placeholder):
263        if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
264            found = True
265            break
266    if not found:
267        log.warn('Could not locate setuptools*.egg-info')
268        return
269
270    log.warn('Removing elements out of the way...')
271    pkg_info = os.path.join(placeholder, file)
272    if os.path.isdir(pkg_info):
273        patched = _patch_egg_dir(pkg_info)
274    else:
275        patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
276
277    if not patched:
278        log.warn('%s already patched.', pkg_info)
279        return False
280    # now let's move the files out of the way
281    for element in ('setuptools', 'pkg_resources.py', 'site.py'):
282        element = os.path.join(placeholder, element)
283        if os.path.exists(element):
284            _rename_path(element)
285        else:
286            log.warn('Could not find the %s element of the '
287                     'Setuptools distribution', element)
288    return True
289
290_remove_flat_installation = _no_sandbox(_remove_flat_installation)
291
292def _after_install(dist):
293    log.warn('After install bootstrap.')
294    placeholder = dist.get_command_obj('install').install_purelib
295    _create_fake_setuptools_pkg_info(placeholder)
296
297def _create_fake_setuptools_pkg_info(placeholder):
298    if not placeholder or not os.path.exists(placeholder):
299        log.warn('Could not find the install location')
300        return
301    pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
302    setuptools_file = 'setuptools-%s-py%s.egg-info' % \
303            (SETUPTOOLS_FAKED_VERSION, pyver)
304    pkg_info = os.path.join(placeholder, setuptools_file)
305    if os.path.exists(pkg_info):
306        log.warn('%s already exists', pkg_info)
307        return
308
309    log.warn('Creating %s', pkg_info)
310    f = open(pkg_info, 'w')
311    try:
312        f.write(SETUPTOOLS_PKG_INFO)
313    finally:
314        f.close()
315
316    pth_file = os.path.join(placeholder, 'setuptools.pth')
317    log.warn('Creating %s', pth_file)
318    f = open(pth_file, 'w')
319    try:
320        f.write(os.path.join(os.curdir, setuptools_file))
321    finally:
322        f.close()
323
324_create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info)
325
326def _patch_egg_dir(path):
327    # let's check if it's already patched
328    pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
329    if os.path.exists(pkg_info):
330        if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
331            log.warn('%s already patched.', pkg_info)
332            return False
333    _rename_path(path)
334    os.mkdir(path)
335    os.mkdir(os.path.join(path, 'EGG-INFO'))
336    pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
337    f = open(pkg_info, 'w')
338    try:
339        f.write(SETUPTOOLS_PKG_INFO)
340    finally:
341        f.close()
342    return True
343
344_patch_egg_dir = _no_sandbox(_patch_egg_dir)
345
346def _before_install():
347    log.warn('Before install bootstrap.')
348    _fake_setuptools()
349
350
351def _under_prefix(location):
352    if 'install' not in sys.argv:
353        return True
354    args = sys.argv[sys.argv.index('install')+1:]
355    for index, arg in enumerate(args):
356        for option in ('--root', '--prefix'):
357            if arg.startswith('%s=' % option):
358                top_dir = arg.split('root=')[-1]
359                return location.startswith(top_dir)
360            elif arg == option:
361                if len(args) > index:
362                    top_dir = args[index+1]
363                    return location.startswith(top_dir)
364        if arg == '--user' and USER_SITE is not None:
365            return location.startswith(USER_SITE)
366    return True
367
368
369def _fake_setuptools():
370    log.warn('Scanning installed packages')
371    try:
372        import pkg_resources
373    except ImportError:
374        # we're cool
375        log.warn('Setuptools or Distribute does not seem to be installed.')
376        return
377    ws = pkg_resources.working_set
378    try:
379        setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools',
380                                  replacement=False))
381    except TypeError:
382        # old distribute API
383        setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools'))
384
385    if setuptools_dist is None:
386        log.warn('No setuptools distribution found')
387        return
388    # detecting if it was already faked
389    setuptools_location = setuptools_dist.location
390    log.warn('Setuptools installation detected at %s', setuptools_location)
391
392    # if --root or --preix was provided, and if
393    # setuptools is not located in them, we don't patch it
394    if not _under_prefix(setuptools_location):
395        log.warn('Not patching, --root or --prefix is installing Distribute'
396                 ' in another location')
397        return
398
399    # let's see if its an egg
400    if not setuptools_location.endswith('.egg'):
401        log.warn('Non-egg installation')
402        res = _remove_flat_installation(setuptools_location)
403        if not res:
404            return
405    else:
406        log.warn('Egg installation')
407        pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
408        if (os.path.exists(pkg_info) and
409            _same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
410            log.warn('Already patched.')
411            return
412        log.warn('Patching...')
413        # let's create a fake egg replacing setuptools one
414        res = _patch_egg_dir(setuptools_location)
415        if not res:
416            return
417    log.warn('Patched done.')
418    _relaunch()
419
420
421def _relaunch():
422    log.warn('Relaunching...')
423    # we have to relaunch the process
424    # pip marker to avoid a relaunch bug
425    if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']:
426        sys.argv[0] = 'setup.py'
427    args = [sys.executable] + sys.argv
428    sys.exit(subprocess.call(args))
429
430
431def _extractall(self, path=".", members=None):
432    """Extract all members from the archive to the current working
433       directory and set owner, modification time and permissions on
434       directories afterwards. `path' specifies a different directory
435       to extract to. `members' is optional and must be a subset of the
436       list returned by getmembers().
437    """
438    import copy
439    import operator
440    from tarfile import ExtractError
441    directories = []
442
443    if members is None:
444        members = self
445
446    for tarinfo in members:
447        if tarinfo.isdir():
448            # Extract directories with a safe mode.
449            directories.append(tarinfo)
450            tarinfo = copy.copy(tarinfo)
451            tarinfo.mode = 448 # decimal for oct 0700
452        self.extract(tarinfo, path)
453
454    # Reverse sort directories.
455    if sys.version_info < (2, 4):
456        def sorter(dir1, dir2):
457            return cmp(dir1.name, dir2.name)
458        directories.sort(sorter)
459        directories.reverse()
460    else:
461        directories.sort(key=operator.attrgetter('name'), reverse=True)
462
463    # Set correct owner, mtime and filemode on directories.
464    for tarinfo in directories:
465        dirpath = os.path.join(path, tarinfo.name)
466        try:
467            self.chown(tarinfo, dirpath)
468            self.utime(tarinfo, dirpath)
469            self.chmod(tarinfo, dirpath)
470        except ExtractError:
471            e = sys.exc_info()[1]
472            if self.errorlevel > 1:
473                raise
474            else:
475                self._dbg(1, "tarfile: %s" % e)
476
477
478def main(argv, version=DEFAULT_VERSION):
479    """Install or upgrade setuptools and EasyInstall"""
480    tarball = download_setuptools()
481    _install(tarball)
482
483
484if __name__ == '__main__':
485    main(sys.argv[1:])
486