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