1#!/usr/bin/env python
2
3"""
4Setuptools bootstrapping installer.
5
6Maintained at https://github.com/pypa/setuptools/tree/bootstrap.
7
8Run this script to install or upgrade setuptools.
9
10This method is DEPRECATED. Check https://github.com/pypa/setuptools/issues/581 for more details.
11"""
12
13import os
14import shutil
15import sys
16import tempfile
17import zipfile
18import optparse
19import subprocess
20import platform
21import textwrap
22import contextlib
23
24from distutils import log
25
26try:
27    from urllib.request import urlopen
28except ImportError:
29    from urllib2 import urlopen
30
31try:
32    from site import USER_SITE
33except ImportError:
34    USER_SITE = None
35
36# 33.1.1 is the last version that supports setuptools self upgrade/installation.
37DEFAULT_VERSION = "33.1.1"
38DEFAULT_URL = "https://pypi.io/packages/source/s/setuptools/"
39DEFAULT_SAVE_DIR = os.curdir
40DEFAULT_DEPRECATION_MESSAGE = "ez_setup.py is deprecated and when using it setuptools will be pinned to {0} since it's the last version that supports setuptools self upgrade/installation, check https://github.com/pypa/setuptools/issues/581 for more info; use pip to install setuptools"
41
42MEANINGFUL_INVALID_ZIP_ERR_MSG = 'Maybe {0} is corrupted, delete it and try again.'
43
44log.warn(DEFAULT_DEPRECATION_MESSAGE.format(DEFAULT_VERSION))
45
46
47def _python_cmd(*args):
48    """
49    Execute a command.
50
51    Return True if the command succeeded.
52    """
53    args = (sys.executable,) + args
54    return subprocess.call(args) == 0
55
56
57def _install(archive_filename, install_args=()):
58    """Install Setuptools."""
59    with archive_context(archive_filename):
60        # installing
61        log.warn('Installing Setuptools')
62        if not _python_cmd('setup.py', 'install', *install_args):
63            log.warn('Something went wrong during the installation.')
64            log.warn('See the error message above.')
65            # exitcode will be 2
66            return 2
67
68
69def _build_egg(egg, archive_filename, to_dir):
70    """Build Setuptools egg."""
71    with archive_context(archive_filename):
72        # building an egg
73        log.warn('Building a Setuptools egg in %s', to_dir)
74        _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
75    # returning the result
76    log.warn(egg)
77    if not os.path.exists(egg):
78        raise IOError('Could not build the egg.')
79
80
81class ContextualZipFile(zipfile.ZipFile):
82
83    """Supplement ZipFile class to support context manager for Python 2.6."""
84
85    def __enter__(self):
86        return self
87
88    def __exit__(self, type, value, traceback):
89        self.close()
90
91    def __new__(cls, *args, **kwargs):
92        """Construct a ZipFile or ContextualZipFile as appropriate."""
93        if hasattr(zipfile.ZipFile, '__exit__'):
94            return zipfile.ZipFile(*args, **kwargs)
95        return super(ContextualZipFile, cls).__new__(cls)
96
97
98@contextlib.contextmanager
99def archive_context(filename):
100    """
101    Unzip filename to a temporary directory, set to the cwd.
102
103    The unzipped target is cleaned up after.
104    """
105    tmpdir = tempfile.mkdtemp()
106    log.warn('Extracting in %s', tmpdir)
107    old_wd = os.getcwd()
108    try:
109        os.chdir(tmpdir)
110        try:
111            with ContextualZipFile(filename) as archive:
112                archive.extractall()
113        except zipfile.BadZipfile as err:
114            if not err.args:
115                err.args = ('', )
116            err.args = err.args + (
117                MEANINGFUL_INVALID_ZIP_ERR_MSG.format(filename),
118            )
119            raise
120
121        # going in the directory
122        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
123        os.chdir(subdir)
124        log.warn('Now working in %s', subdir)
125        yield
126
127    finally:
128        os.chdir(old_wd)
129        shutil.rmtree(tmpdir)
130
131
132def _do_download(version, download_base, to_dir, download_delay):
133    """Download Setuptools."""
134    py_desig = 'py{sys.version_info[0]}.{sys.version_info[1]}'.format(sys=sys)
135    tp = 'setuptools-{version}-{py_desig}.egg'
136    egg = os.path.join(to_dir, tp.format(**locals()))
137    if not os.path.exists(egg):
138        archive = download_setuptools(version, download_base,
139            to_dir, download_delay)
140        _build_egg(egg, archive, to_dir)
141    sys.path.insert(0, egg)
142
143    # Remove previously-imported pkg_resources if present (see
144    # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
145    if 'pkg_resources' in sys.modules:
146        _unload_pkg_resources()
147
148    import setuptools
149    setuptools.bootstrap_install_from = egg
150
151
152def use_setuptools(
153        version=DEFAULT_VERSION, download_base=DEFAULT_URL,
154        to_dir=DEFAULT_SAVE_DIR, download_delay=15):
155    """
156    Ensure that a setuptools version is installed.
157
158    Return None. Raise SystemExit if the requested version
159    or later cannot be installed.
160    """
161    to_dir = os.path.abspath(to_dir)
162
163    # prior to importing, capture the module state for
164    # representative modules.
165    rep_modules = 'pkg_resources', 'setuptools'
166    imported = set(sys.modules).intersection(rep_modules)
167
168    try:
169        import pkg_resources
170        pkg_resources.require("setuptools>=" + version)
171        # a suitable version is already installed
172        return
173    except ImportError:
174        # pkg_resources not available; setuptools is not installed; download
175        pass
176    except pkg_resources.DistributionNotFound:
177        # no version of setuptools was found; allow download
178        pass
179    except pkg_resources.VersionConflict as VC_err:
180        if imported:
181            _conflict_bail(VC_err, version)
182
183        # otherwise, unload pkg_resources to allow the downloaded version to
184        #  take precedence.
185        del pkg_resources
186        _unload_pkg_resources()
187
188    return _do_download(version, download_base, to_dir, download_delay)
189
190
191def _conflict_bail(VC_err, version):
192    """
193    Setuptools was imported prior to invocation, so it is
194    unsafe to unload it. Bail out.
195    """
196    conflict_tmpl = textwrap.dedent("""
197        The required version of setuptools (>={version}) is not available,
198        and can't be installed while this script is running. Please
199        install a more recent version first, using
200        'easy_install -U setuptools'.
201
202        (Currently using {VC_err.args[0]!r})
203        """)
204    msg = conflict_tmpl.format(**locals())
205    sys.stderr.write(msg)
206    sys.exit(2)
207
208
209def _unload_pkg_resources():
210    sys.meta_path = [
211        importer
212        for importer in sys.meta_path
213        if importer.__class__.__module__ != 'pkg_resources.extern'
214    ]
215    del_modules = [
216        name for name in sys.modules
217        if name.startswith('pkg_resources')
218    ]
219    for mod_name in del_modules:
220        del sys.modules[mod_name]
221
222
223def _clean_check(cmd, target):
224    """
225    Run the command to download target.
226
227    If the command fails, clean up before re-raising the error.
228    """
229    try:
230        subprocess.check_call(cmd)
231    except subprocess.CalledProcessError:
232        if os.access(target, os.F_OK):
233            os.unlink(target)
234        raise
235
236
237def download_file_powershell(url, target):
238    """
239    Download the file at url to target using Powershell.
240
241    Powershell will validate trust.
242    Raise an exception if the command cannot complete.
243    """
244    target = os.path.abspath(target)
245    ps_cmd = (
246        "[System.Net.WebRequest]::DefaultWebProxy.Credentials = "
247        "[System.Net.CredentialCache]::DefaultCredentials; "
248        '(new-object System.Net.WebClient).DownloadFile("%(url)s", "%(target)s")'
249        % locals()
250    )
251    cmd = [
252        'powershell',
253        '-Command',
254        ps_cmd,
255    ]
256    _clean_check(cmd, target)
257
258
259def has_powershell():
260    """Determine if Powershell is available."""
261    if platform.system() != 'Windows':
262        return False
263    cmd = ['powershell', '-Command', 'echo test']
264    with open(os.path.devnull, 'wb') as devnull:
265        try:
266            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
267        except Exception:
268            return False
269    return True
270download_file_powershell.viable = has_powershell
271
272
273def download_file_curl(url, target):
274    cmd = ['curl', url, '--location', '--silent', '--output', target]
275    _clean_check(cmd, target)
276
277
278def has_curl():
279    cmd = ['curl', '--version']
280    with open(os.path.devnull, 'wb') as devnull:
281        try:
282            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
283        except Exception:
284            return False
285    return True
286download_file_curl.viable = has_curl
287
288
289def download_file_wget(url, target):
290    cmd = ['wget', url, '--quiet', '--output-document', target]
291    _clean_check(cmd, target)
292
293
294def has_wget():
295    cmd = ['wget', '--version']
296    with open(os.path.devnull, 'wb') as devnull:
297        try:
298            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
299        except Exception:
300            return False
301    return True
302download_file_wget.viable = has_wget
303
304
305def download_file_insecure(url, target):
306    """Use Python to download the file, without connection authentication."""
307    src = urlopen(url)
308    try:
309        # Read all the data in one block.
310        data = src.read()
311    finally:
312        src.close()
313
314    # Write all the data in one block to avoid creating a partial file.
315    with open(target, "wb") as dst:
316        dst.write(data)
317download_file_insecure.viable = lambda: True
318
319
320def get_best_downloader():
321    downloaders = (
322        download_file_powershell,
323        download_file_curl,
324        download_file_wget,
325        download_file_insecure,
326    )
327    viable_downloaders = (dl for dl in downloaders if dl.viable())
328    return next(viable_downloaders, None)
329
330
331def download_setuptools(
332        version=DEFAULT_VERSION, download_base=DEFAULT_URL,
333        to_dir=DEFAULT_SAVE_DIR, delay=15,
334        downloader_factory=get_best_downloader):
335    """
336    Download setuptools from a specified location and return its filename.
337
338    `version` should be a valid setuptools version number that is available
339    as an sdist for download under the `download_base` URL (which should end
340    with a '/'). `to_dir` is the directory where the egg will be downloaded.
341    `delay` is the number of seconds to pause before an actual download
342    attempt.
343
344    ``downloader_factory`` should be a function taking no arguments and
345    returning a function for downloading a URL to a target.
346    """
347    # making sure we use the absolute path
348    to_dir = os.path.abspath(to_dir)
349    zip_name = "setuptools-%s.zip" % version
350    url = download_base + zip_name
351    saveto = os.path.join(to_dir, zip_name)
352    if not os.path.exists(saveto):  # Avoid repeated downloads
353        log.warn("Downloading %s", url)
354        downloader = downloader_factory()
355        downloader(url, saveto)
356    return os.path.realpath(saveto)
357
358
359def _build_install_args(options):
360    """
361    Build the arguments to 'python setup.py install' on the setuptools package.
362
363    Returns list of command line arguments.
364    """
365    return ['--user'] if options.user_install else []
366
367
368def _parse_args():
369    """Parse the command line for options."""
370    parser = optparse.OptionParser()
371    parser.add_option(
372        '--user', dest='user_install', action='store_true', default=False,
373        help='install in user site package')
374    parser.add_option(
375        '--download-base', dest='download_base', metavar="URL",
376        default=DEFAULT_URL,
377        help='alternative URL from where to download the setuptools package')
378    parser.add_option(
379        '--insecure', dest='downloader_factory', action='store_const',
380        const=lambda: download_file_insecure, default=get_best_downloader,
381        help='Use internal, non-validating downloader'
382    )
383    parser.add_option(
384        '--version', help="Specify which version to download",
385        default=DEFAULT_VERSION,
386    )
387    parser.add_option(
388        '--to-dir',
389        help="Directory to save (and re-use) package",
390        default=DEFAULT_SAVE_DIR,
391    )
392    options, args = parser.parse_args()
393    # positional arguments are ignored
394    return options
395
396
397def _download_args(options):
398    """Return args for download_setuptools function from cmdline args."""
399    return dict(
400        version=options.version,
401        download_base=options.download_base,
402        downloader_factory=options.downloader_factory,
403        to_dir=options.to_dir,
404    )
405
406
407def main():
408    """Install or upgrade setuptools and EasyInstall."""
409    options = _parse_args()
410    archive = download_setuptools(**_download_args(options))
411    return _install(archive, _build_install_args(options))
412
413if __name__ == '__main__':
414    sys.exit(main())
415