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 (KeyboardInterrupt, SystemExit):
188            raise
189        except Exception:
190            return False
191    finally:
192        devnull.close()
193    return True
194
195download_file_powershell.viable = has_powershell
196
197def download_file_curl(url, target):
198    cmd = ['curl', url, '--silent', '--output', target]
199    _clean_check(cmd, target)
200
201def has_curl():
202    cmd = ['curl', '--version']
203    devnull = open(os.path.devnull, 'wb')
204    try:
205        try:
206            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
207        except (KeyboardInterrupt, SystemExit):
208            raise
209        except Exception:
210            return False
211    finally:
212        devnull.close()
213    return True
214
215download_file_curl.viable = has_curl
216
217def download_file_wget(url, target):
218    cmd = ['wget', url, '--quiet', '--output-document', target]
219    _clean_check(cmd, target)
220
221def has_wget():
222    cmd = ['wget', '--version']
223    devnull = open(os.path.devnull, 'wb')
224    try:
225        try:
226            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
227        except (KeyboardInterrupt, SystemExit):
228            raise
229        except Exception:
230            return False
231    finally:
232        devnull.close()
233    return True
234
235download_file_wget.viable = has_wget
236
237def download_file_insecure(url, target):
238    """
239    Use Python to download the file, even though it cannot authenticate the
240    connection.
241    """
242    try:
243        from urllib.request import urlopen
244    except ImportError:
245        from urllib2 import urlopen
246    src = dst = None
247    try:
248        src = urlopen(url)
249        # Read/write all in one block, so we don't create a corrupt file
250        # if the download is interrupted.
251        data = src.read()
252        dst = open(target, "wb")
253        dst.write(data)
254    finally:
255        if src:
256            src.close()
257        if dst:
258            dst.close()
259
260download_file_insecure.viable = lambda: True
261
262def get_best_downloader():
263    downloaders = [
264        download_file_powershell,
265        download_file_curl,
266        download_file_wget,
267        download_file_insecure,
268    ]
269
270    for dl in downloaders:
271        if dl.viable():
272            return dl
273
274def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
275                        to_dir=os.curdir, delay=15,
276                        downloader_factory=get_best_downloader):
277    """Download setuptools from a specified location and return its filename
278
279    `version` should be a valid setuptools version number that is available
280    as an egg for download under the `download_base` URL (which should end
281    with a '/'). `to_dir` is the directory where the egg will be downloaded.
282    `delay` is the number of seconds to pause before an actual download
283    attempt.
284
285    ``downloader_factory`` should be a function taking no arguments and
286    returning a function for downloading a URL to a target.
287    """
288    # making sure we use the absolute path
289    to_dir = os.path.abspath(to_dir)
290    tgz_name = "setuptools-%s.tar.gz" % version
291    url = download_base + tgz_name
292    saveto = os.path.join(to_dir, tgz_name)
293    if not os.path.exists(saveto):  # Avoid repeated downloads
294        log.warn("Downloading %s", url)
295        downloader = downloader_factory()
296        downloader(url, saveto)
297    return os.path.realpath(saveto)
298
299
300def _extractall(self, path=".", members=None):
301    """Extract all members from the archive to the current working
302       directory and set owner, modification time and permissions on
303       directories afterwards. `path' specifies a different directory
304       to extract to. `members' is optional and must be a subset of the
305       list returned by getmembers().
306    """
307    import copy
308    import operator
309    from tarfile import ExtractError
310    directories = []
311
312    if members is None:
313        members = self
314
315    for tarinfo in members:
316        if tarinfo.isdir():
317            # Extract directories with a safe mode.
318            directories.append(tarinfo)
319            tarinfo = copy.copy(tarinfo)
320            tarinfo.mode = 448  # decimal for oct 0700
321        self.extract(tarinfo, path)
322
323    # Reverse sort directories.
324    if sys.version_info < (2, 4):
325        def sorter(dir1, dir2):
326            return cmp(dir1.name, dir2.name)
327        directories.sort(sorter)
328        directories.reverse()
329    else:
330        directories.sort(key=operator.attrgetter('name'), reverse=True)
331
332    # Set correct owner, mtime and filemode on directories.
333    for tarinfo in directories:
334        dirpath = os.path.join(path, tarinfo.name)
335        try:
336            self.chown(tarinfo, dirpath)
337            self.utime(tarinfo, dirpath)
338            self.chmod(tarinfo, dirpath)
339        except ExtractError:
340            e = sys.exc_info()[1]
341            if self.errorlevel > 1:
342                raise
343            else:
344                self._dbg(1, "tarfile: %s" % e)
345
346
347def _build_install_args(options):
348    """
349    Build the arguments to 'python setup.py install' on the setuptools package
350    """
351    install_args = []
352    if options.user_install:
353        if sys.version_info < (2, 6):
354            log.warn("--user requires Python 2.6 or later")
355            raise SystemExit(1)
356        install_args.append('--user')
357    return install_args
358
359def _parse_args():
360    """
361    Parse the command line for options
362    """
363    parser = optparse.OptionParser()
364    parser.add_option(
365        '--user', dest='user_install', action='store_true', default=False,
366        help='install in user site package (requires Python 2.6 or later)')
367    parser.add_option(
368        '--download-base', dest='download_base', metavar="URL",
369        default=DEFAULT_URL,
370        help='alternative URL from where to download the setuptools package')
371    parser.add_option(
372        '--insecure', dest='downloader_factory', action='store_const',
373        const=lambda: download_file_insecure, default=get_best_downloader,
374        help='Use internal, non-validating downloader'
375    )
376    options, args = parser.parse_args()
377    # positional arguments are ignored
378    return options
379
380def main(version=DEFAULT_VERSION):
381    """Install or upgrade setuptools and EasyInstall"""
382    options = _parse_args()
383    tarball = download_setuptools(download_base=options.download_base,
384        downloader_factory=options.downloader_factory)
385    return _install(tarball, _build_install_args(options))
386
387if __name__ == '__main__':
388    sys.exit(main())
389