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