]> git.phdru.name Git - mimedecode.git/blob - ez_setup.py
Upgrade ez_setup.py
[mimedecode.git] / ez_setup.py
1 #!/usr/bin/env python
2
3 """
4 Setuptools bootstrapping installer.
5
6 Maintained at https://github.com/pypa/setuptools/tree/bootstrap.
7
8 Run this script to install or upgrade setuptools.
9 """
10
11 import os
12 import shutil
13 import sys
14 import tempfile
15 import zipfile
16 import optparse
17 import subprocess
18 import platform
19 import textwrap
20 import contextlib
21 import json
22 import codecs
23
24 from distutils import log
25
26 try:
27     from urllib.request import urlopen
28     from urllib.parse import urljoin
29 except ImportError:
30     from urllib2 import urlopen
31     from urlparse import urljoin
32
33 try:
34     from site import USER_SITE
35 except ImportError:
36     USER_SITE = None
37
38 LATEST = object()
39 DEFAULT_VERSION = LATEST
40 DEFAULT_URL = "https://pypi.io/packages/source/s/setuptools/"
41 DEFAULT_SAVE_DIR = os.curdir
42
43 MEANINGFUL_INVALID_ZIP_ERR_MSG = 'Maybe {0} is corrupted, delete it and try again.'
44
45
46 def _python_cmd(*args):
47     """
48     Execute a command.
49
50     Return True if the command succeeded.
51     """
52     args = (sys.executable,) + args
53     return subprocess.call(args) == 0
54
55
56 def _install(archive_filename, install_args=()):
57     """Install Setuptools."""
58     with archive_context(archive_filename):
59         # installing
60         log.warn('Installing Setuptools')
61         if not _python_cmd('setup.py', 'install', *install_args):
62             log.warn('Something went wrong during the installation.')
63             log.warn('See the error message above.')
64             # exitcode will be 2
65             return 2
66
67
68 def _build_egg(egg, archive_filename, to_dir):
69     """Build Setuptools egg."""
70     with archive_context(archive_filename):
71         # building an egg
72         log.warn('Building a Setuptools egg in %s', to_dir)
73         _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
74     # returning the result
75     log.warn(egg)
76     if not os.path.exists(egg):
77         raise IOError('Could not build the egg.')
78
79
80 class ContextualZipFile(zipfile.ZipFile):
81
82     """Supplement ZipFile class to support context manager for Python 2.6."""
83
84     def __enter__(self):
85         return self
86
87     def __exit__(self, type, value, traceback):
88         self.close()
89
90     def __new__(cls, *args, **kwargs):
91         """Construct a ZipFile or ContextualZipFile as appropriate."""
92         if hasattr(zipfile.ZipFile, '__exit__'):
93             return zipfile.ZipFile(*args, **kwargs)
94         return super(ContextualZipFile, cls).__new__(cls)
95
96
97 @contextlib.contextmanager
98 def archive_context(filename):
99     """
100     Unzip filename to a temporary directory, set to the cwd.
101
102     The unzipped target is cleaned up after.
103     """
104     tmpdir = tempfile.mkdtemp()
105     log.warn('Extracting in %s', tmpdir)
106     old_wd = os.getcwd()
107     try:
108         os.chdir(tmpdir)
109         try:
110             with ContextualZipFile(filename) as archive:
111                 archive.extractall()
112         except zipfile.BadZipfile as err:
113             if not err.args:
114                 err.args = ('', )
115             err.args = err.args + (
116                 MEANINGFUL_INVALID_ZIP_ERR_MSG.format(filename),
117             )
118             raise
119
120         # going in the directory
121         subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
122         os.chdir(subdir)
123         log.warn('Now working in %s', subdir)
124         yield
125
126     finally:
127         os.chdir(old_wd)
128         shutil.rmtree(tmpdir)
129
130
131 def _do_download(version, download_base, to_dir, download_delay):
132     """Download Setuptools."""
133     py_desig = 'py{sys.version_info[0]}.{sys.version_info[1]}'.format(sys=sys)
134     tp = 'setuptools-{version}-{py_desig}.egg'
135     egg = os.path.join(to_dir, tp.format(**locals()))
136     if not os.path.exists(egg):
137         archive = download_setuptools(version, download_base,
138             to_dir, download_delay)
139         _build_egg(egg, archive, to_dir)
140     sys.path.insert(0, egg)
141
142     # Remove previously-imported pkg_resources if present (see
143     # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
144     if 'pkg_resources' in sys.modules:
145         _unload_pkg_resources()
146
147     import setuptools
148     setuptools.bootstrap_install_from = egg
149
150
151 def use_setuptools(
152         version=DEFAULT_VERSION, download_base=DEFAULT_URL,
153         to_dir=DEFAULT_SAVE_DIR, download_delay=15):
154     """
155     Ensure that a setuptools version is installed.
156
157     Return None. Raise SystemExit if the requested version
158     or later cannot be installed.
159     """
160     version = _resolve_version(version)
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
191 def _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
209 def _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
223 def _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
237 def 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
259 def 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
270 download_file_powershell.viable = has_powershell
271
272
273 def download_file_curl(url, target):
274     cmd = ['curl', url, '--location', '--silent', '--output', target]
275     _clean_check(cmd, target)
276
277
278 def 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
286 download_file_curl.viable = has_curl
287
288
289 def download_file_wget(url, target):
290     cmd = ['wget', url, '--quiet', '--output-document', target]
291     _clean_check(cmd, target)
292
293
294 def 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
302 download_file_wget.viable = has_wget
303
304
305 def 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)
317 download_file_insecure.viable = lambda: True
318
319
320 def 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
331 def 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     version = _resolve_version(version)
348     # making sure we use the absolute path
349     to_dir = os.path.abspath(to_dir)
350     zip_name = "setuptools-%s.zip" % version
351     url = download_base + zip_name
352     saveto = os.path.join(to_dir, zip_name)
353     if not os.path.exists(saveto):  # Avoid repeated downloads
354         log.warn("Downloading %s", url)
355         downloader = downloader_factory()
356         downloader(url, saveto)
357     return os.path.realpath(saveto)
358
359
360 def _resolve_version(version):
361     """
362     Resolve LATEST version
363     """
364     if version is not LATEST:
365         return version
366
367     meta_url = urljoin(DEFAULT_URL, '/pypi/setuptools/json')
368     resp = urlopen(meta_url)
369     with contextlib.closing(resp):
370         try:
371             charset = resp.info().get_content_charset()
372         except Exception:
373             # Python 2 compat; assume UTF-8
374             charset = 'UTF-8'
375         reader = codecs.getreader(charset)
376         doc = json.load(reader(resp))
377
378     return str(doc['info']['version'])
379
380
381 def _build_install_args(options):
382     """
383     Build the arguments to 'python setup.py install' on the setuptools package.
384
385     Returns list of command line arguments.
386     """
387     return ['--user'] if options.user_install else []
388
389
390 def _parse_args():
391     """Parse the command line for options."""
392     parser = optparse.OptionParser()
393     parser.add_option(
394         '--user', dest='user_install', action='store_true', default=False,
395         help='install in user site package')
396     parser.add_option(
397         '--download-base', dest='download_base', metavar="URL",
398         default=DEFAULT_URL,
399         help='alternative URL from where to download the setuptools package')
400     parser.add_option(
401         '--insecure', dest='downloader_factory', action='store_const',
402         const=lambda: download_file_insecure, default=get_best_downloader,
403         help='Use internal, non-validating downloader'
404     )
405     parser.add_option(
406         '--version', help="Specify which version to download",
407         default=DEFAULT_VERSION,
408     )
409     parser.add_option(
410         '--to-dir',
411         help="Directory to save (and re-use) package",
412         default=DEFAULT_SAVE_DIR,
413     )
414     options, args = parser.parse_args()
415     # positional arguments are ignored
416     return options
417
418
419 def _download_args(options):
420     """Return args for download_setuptools function from cmdline args."""
421     return dict(
422         version=options.version,
423         download_base=options.download_base,
424         downloader_factory=options.downloader_factory,
425         to_dir=options.to_dir,
426     )
427
428
429 def main():
430     """Install or upgrade setuptools and EasyInstall."""
431     options = _parse_args()
432     archive = download_setuptools(**_download_args(options))
433     return _install(archive, _build_install_args(options))
434
435 if __name__ == '__main__':
436     sys.exit(main())