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 = "15.0" 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