1#!python
2"""Bootstrap setuptools 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 ez_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 shutil
18import sys
19import tempfile
20import tarfile
21import optparse
22import subprocess
23import platform
24
25from distutils import log
26
27try:
28    from site import USER_SITE
29except ImportError:
30    USER_SITE = None
31
32DEFAULT_VERSION = "1.4.2"
33DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/"
34
35def _python_cmd(*args):
36    args = (sys.executable,) + args
37    return subprocess.call(args) == 0
38
39def _check_call_py24(cmd, *args, **kwargs):
40    res = subprocess.call(cmd, *args, **kwargs)
41    class CalledProcessError(Exception):
42        pass
43    if not res == 0:
44        msg = "Command '%s' return non-zero exit status %d" % (cmd, res)
45        raise CalledProcessError(msg)
46vars(subprocess).setdefault('check_call', _check_call_py24)
47
48def _install(tarball, install_args=()):
49    # extracting the tarball
50    tmpdir = tempfile.mkdtemp()
51    log.warn('Extracting in %s', tmpdir)
52    old_wd = os.getcwd()
53    try:
54        os.chdir(tmpdir)
55        tar = tarfile.open(tarball)
56        _extractall(tar)
57        tar.close()
58
59        # going in the directory
60        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
61        os.chdir(subdir)
62        log.warn('Now working in %s', subdir)
63
64        # installing
65        log.warn('Installing Setuptools')
66        if not _python_cmd('setup.py', 'install', *install_args):
67            log.warn('Something went wrong during the installation.')
68            log.warn('See the error message above.')
69            # exitcode will be 2
70            return 2
71    finally:
72        os.chdir(old_wd)
73        shutil.rmtree(tmpdir)
74
75
76def _build_egg(egg, tarball, to_dir):
77    # extracting the tarball
78    tmpdir = tempfile.mkdtemp()
79    log.warn('Extracting in %s', tmpdir)
80    old_wd = os.getcwd()
81    try:
82        os.chdir(tmpdir)
83        tar = tarfile.open(tarball)
84        _extractall(tar)
85        tar.close()
86
87        # going in the directory
88        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
89        os.chdir(subdir)
90        log.warn('Now working in %s', subdir)
91
92        # building an egg
93        log.warn('Building a Setuptools egg in %s', to_dir)
94        _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
95
96    finally:
97        os.chdir(old_wd)
98        shutil.rmtree(tmpdir)
99    # returning the result
100    log.warn(egg)
101    if not os.path.exists(egg):
102        raise IOError('Could not build the egg.')
103
104
105def _do_download(version, download_base, to_dir, download_delay):
106    egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg'
107                       % (version, sys.version_info[0], sys.version_info[1]))
108    if not os.path.exists(egg):
109        tarball = download_setuptools(version, download_base,
110                                      to_dir, download_delay)
111        _build_egg(egg, tarball, to_dir)
112    sys.path.insert(0, egg)
113
114    # Remove previously-imported pkg_resources if present (see
115    # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
116    if 'pkg_resources' in sys.modules:
117        del sys.modules['pkg_resources']
118
119    import setuptools
120    setuptools.bootstrap_install_from = egg
121
122
123def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
124                   to_dir=os.curdir, download_delay=15):
125    # making sure we use the absolute path
126    to_dir = os.path.abspath(to_dir)
127    was_imported = 'pkg_resources' in sys.modules or \
128        'setuptools' in sys.modules
129    try:
130        import pkg_resources
131    except ImportError:
132        return _do_download(version, download_base, to_dir, download_delay)
133    try:
134        pkg_resources.require("setuptools>=" + version)
135        return
136    except pkg_resources.VersionConflict:
137        e = sys.exc_info()[1]
138        if was_imported:
139            sys.stderr.write(
140            "The required version of setuptools (>=%s) is not available,\n"
141            "and can't be installed while this script is running. Please\n"
142            "install a more recent version first, using\n"
143            "'easy_install -U setuptools'."
144            "\n\n(Currently using %r)\n" % (version, e.args[0]))
145            sys.exit(2)
146        else:
147            del pkg_resources, sys.modules['pkg_resources']    # reload ok
148            return _do_download(version, download_base, to_dir,
149                                download_delay)
150    except pkg_resources.DistributionNotFound:
151        return _do_download(version, download_base, to_dir,
152                            download_delay)
153
154def _clean_check(cmd, target):
155    """
156    Run the command to download target. If the command fails, clean up before
157    re-raising the error.
158    """
159    try:
160        subprocess.check_call(cmd)
161    except subprocess.CalledProcessError:
162        if os.access(target, os.F_OK):
163            os.unlink(target)
164        raise
165
166def download_file_powershell(url, target):
167    """
168    Download the file at url to target using Powershell (which will validate
169    trust). Raise an exception if the command cannot complete.
170    """
171    target = os.path.abspath(target)
172    cmd = [
173        'powershell',
174        '-Command',
175        "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(),
176    ]
177    _clean_check(cmd, target)
178
179def has_powershell():
180    if platform.system() != 'Windows':
181        return False
182    cmd = ['powershell', '-Command', 'echo test']
183    devnull = open(os.path.devnull, 'wb')
184    try:
185        try:
186            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
187        except:
188            return False
189    finally:
190        devnull.close()
191    return True
192
193download_file_powershell.viable = has_powershell
194
195def download_file_curl(url, target):
196    cmd = ['curl', url, '--silent', '--output', target]
197    _clean_check(cmd, target)
198
199def has_curl():
200    cmd = ['curl', '--version']
201    devnull = open(os.path.devnull, 'wb')
202    try:
203        try:
204            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
205        except:
206            return False
207    finally:
208        devnull.close()
209    return True
210
211download_file_curl.viable = has_curl
212
213def download_file_wget(url, target):
214    cmd = ['wget', url, '--quiet', '--output-document', target]
215    _clean_check(cmd, target)
216
217def has_wget():
218    cmd = ['wget', '--version']
219    devnull = open(os.path.devnull, 'wb')
220    try:
221        try:
222            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
223        except:
224            return False
225    finally:
226        devnull.close()
227    return True
228
229download_file_wget.viable = has_wget
230
231def download_file_insecure(url, target):
232    """
233    Use Python to download the file, even though it cannot authenticate the
234    connection.
235    """
236    try:
237        from urllib.request import urlopen
238    except ImportError:
239        from urllib2 import urlopen
240    src = dst = None
241    try:
242        src = urlopen(url)
243        # Read/write all in one block, so we don't create a corrupt file
244        # if the download is interrupted.
245        data = src.read()
246        dst = open(target, "wb")
247        dst.write(data)
248    finally:
249        if src:
250            src.close()
251        if dst:
252            dst.close()
253
254download_file_insecure.viable = lambda: True
255
256def get_best_downloader():
257    downloaders = [
258        download_file_powershell,
259        download_file_curl,
260        download_file_wget,
261        download_file_insecure,
262    ]
263
264    for dl in downloaders:
265        if dl.viable():
266            return dl
267
268def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
269                        to_dir=os.curdir, delay=15,
270                        downloader_factory=get_best_downloader):
271    """Download setuptools from a specified location and return its filename
272
273    `version` should be a valid setuptools version number that is available
274    as an egg for download under the `download_base` URL (which should end
275    with a '/'). `to_dir` is the directory where the egg will be downloaded.
276    `delay` is the number of seconds to pause before an actual download
277    attempt.
278
279    ``downloader_factory`` should be a function taking no arguments and
280    returning a function for downloading a URL to a target.
281    """
282    # making sure we use the absolute path
283    to_dir = os.path.abspath(to_dir)
284    tgz_name = "setuptools-%s.tar.gz" % version
285    url = download_base + tgz_name
286    saveto = os.path.join(to_dir, tgz_name)
287    if not os.path.exists(saveto):  # Avoid repeated downloads
288        log.warn("Downloading %s", url)
289        downloader = downloader_factory()
290        downloader(url, saveto)
291    return os.path.realpath(saveto)
292
293
294def _extractall(self, path=".", members=None):
295    """Extract all members from the archive to the current working
296       directory and set owner, modification time and permissions on
297       directories afterwards. `path' specifies a different directory
298       to extract to. `members' is optional and must be a subset of the
299       list returned by getmembers().
300    """
301    import copy
302    import operator
303    from tarfile import ExtractError
304    directories = []
305
306    if members is None:
307        members = self
308
309    for tarinfo in members:
310        if tarinfo.isdir():
311            # Extract directories with a safe mode.
312            directories.append(tarinfo)
313            tarinfo = copy.copy(tarinfo)
314            tarinfo.mode = 448  # decimal for oct 0700
315        self.extract(tarinfo, path)
316
317    # Reverse sort directories.
318    if sys.version_info < (2, 4):
319        def sorter(dir1, dir2):
320            return cmp(dir1.name, dir2.name)
321        directories.sort(sorter)
322        directories.reverse()
323    else:
324        directories.sort(key=operator.attrgetter('name'), reverse=True)
325
326    # Set correct owner, mtime and filemode on directories.
327    for tarinfo in directories:
328        dirpath = os.path.join(path, tarinfo.name)
329        try:
330            self.chown(tarinfo, dirpath)
331            self.utime(tarinfo, dirpath)
332            self.chmod(tarinfo, dirpath)
333        except ExtractError:
334            e = sys.exc_info()[1]
335            if self.errorlevel > 1:
336                raise
337            else:
338                self._dbg(1, "tarfile: %s" % e)
339
340
341def _build_install_args(options):
342    """
343    Build the arguments to 'python setup.py install' on the setuptools package
344    """
345    install_args = []
346    if options.user_install:
347        if sys.version_info < (2, 6):
348            log.warn("--user requires Python 2.6 or later")
349            raise SystemExit(1)
350        install_args.append('--user')
351    return install_args
352
353def _parse_args():
354    """
355    Parse the command line for options
356    """
357    parser = optparse.OptionParser()
358    parser.add_option(
359        '--user', dest='user_install', action='store_true', default=False,
360        help='install in user site package (requires Python 2.6 or later)')
361    parser.add_option(
362        '--download-base', dest='download_base', metavar="URL",
363        default=DEFAULT_URL,
364        help='alternative URL from where to download the setuptools package')
365    parser.add_option(
366        '--insecure', dest='downloader_factory', action='store_const',
367        const=lambda: download_file_insecure, default=get_best_downloader,
368        help='Use internal, non-validating downloader'
369    )
370    options, args = parser.parse_args()
371    # positional arguments are ignored
372    return options
373
374def main(version=DEFAULT_VERSION):
375    """Install or upgrade setuptools and EasyInstall"""
376    options = _parse_args()
377    tarball = download_setuptools(download_base=options.download_base,
378        downloader_factory=options.downloader_factory)
379    return _install(tarball, _build_install_args(options))
380
381if __name__ == '__main__':
382    sys.exit(main())
383