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