Commit 2d9108ff96ab6da032b16cc048df2ea959cd6213

Authored by Myroslav Opyr
2 parents 76c35558 069a757d

Merge pull request #3 from selurvedu/bootstrap_upd

Update bootstrap.py to enable ez_setup.py caching. Add cached ez_setup.py to avoid issues when https://bootstrap.pypa.io/ is down or malfunctioning otherwise.
Showing 2 changed files with 427 additions and 15 deletions
@@ -25,7 +25,10 @@ import tempfile @@ -25,7 +25,10 @@ import tempfile
25 25
26 from optparse import OptionParser 26 from optparse import OptionParser
27 27
28 -tmpeggs = tempfile.mkdtemp() 28 +__version__ = '2015-07-01'
  29 +# See zc.buildout's changelog if this version is up to date.
  30 +
  31 +tmpeggs = tempfile.mkdtemp(prefix='bootstrap-')
29 32
30 usage = '''\ 33 usage = '''\
31 [DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] 34 [DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
@@ -40,8 +43,9 @@ this script from going over the network. @@ -40,8 +43,9 @@ this script from going over the network.
40 ''' 43 '''
41 44
42 parser = OptionParser(usage=usage) 45 parser = OptionParser(usage=usage)
43 -parser.add_option("-v", "--version", help="use a specific zc.buildout version")  
44 - 46 +parser.add_option("--version",
  47 + action="store_true", default=False,
  48 + help=("Return bootstrap.py version."))
45 parser.add_option("-t", "--accept-buildout-test-releases", 49 parser.add_option("-t", "--accept-buildout-test-releases",
46 dest='accept_buildout_test_releases', 50 dest='accept_buildout_test_releases',
47 action="store_true", default=False, 51 action="store_true", default=False,
@@ -59,25 +63,33 @@ parser.add_option("-f", "--find-links", @@ -59,25 +63,33 @@ parser.add_option("-f", "--find-links",
59 parser.add_option("--allow-site-packages", 63 parser.add_option("--allow-site-packages",
60 action="store_true", default=False, 64 action="store_true", default=False,
61 help=("Let bootstrap.py use existing site packages")) 65 help=("Let bootstrap.py use existing site packages"))
  66 +parser.add_option("--buildout-version",
  67 + help="Use a specific zc.buildout version")
62 parser.add_option("--setuptools-version", 68 parser.add_option("--setuptools-version",
63 - help="use a specific setuptools version")  
64 - 69 + help="Use a specific setuptools version")
  70 +parser.add_option("--setuptools-to-dir",
  71 + help=("Allow for re-use of existing directory of "
  72 + "setuptools versions"))
65 73
66 options, args = parser.parse_args() 74 options, args = parser.parse_args()
  75 +if options.version:
  76 + print("bootstrap.py version %s" % __version__)
  77 + sys.exit(0)
  78 +
67 79
68 ###################################################################### 80 ######################################################################
69 # load/install setuptools 81 # load/install setuptools
70 82
71 try: 83 try:
72 - if options.allow_site_packages:  
73 - import setuptools  
74 - import pkg_resources  
75 from urllib.request import urlopen 84 from urllib.request import urlopen
76 except ImportError: 85 except ImportError:
77 from urllib2 import urlopen 86 from urllib2 import urlopen
78 87
79 ez = {} 88 ez = {}
80 -exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez) 89 +if os.path.exists('ez_setup.py'):
  90 + exec(open('ez_setup.py').read(), ez)
  91 +else:
  92 + exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
81 93
82 if not options.allow_site_packages: 94 if not options.allow_site_packages:
83 # ez_setup imports site, which adds site packages 95 # ez_setup imports site, which adds site packages
@@ -88,12 +100,19 @@ if not options.allow_site_packages: @@ -88,12 +100,19 @@ if not options.allow_site_packages:
88 # We can't remove these reliably 100 # We can't remove these reliably
89 if hasattr(site, 'getsitepackages'): 101 if hasattr(site, 'getsitepackages'):
90 for sitepackage_path in site.getsitepackages(): 102 for sitepackage_path in site.getsitepackages():
91 - sys.path[:] = [x for x in sys.path if sitepackage_path not in x] 103 + # Strip all site-packages directories from sys.path that
  104 + # are not sys.prefix; this is because on Windows
  105 + # sys.prefix is a site-package directory.
  106 + if sitepackage_path != sys.prefix:
  107 + sys.path[:] = [x for x in sys.path
  108 + if sitepackage_path not in x]
92 109
93 setup_args = dict(to_dir=tmpeggs, download_delay=0) 110 setup_args = dict(to_dir=tmpeggs, download_delay=0)
94 111
95 if options.setuptools_version is not None: 112 if options.setuptools_version is not None:
96 setup_args['version'] = options.setuptools_version 113 setup_args['version'] = options.setuptools_version
  114 +if options.setuptools_to_dir is not None:
  115 + setup_args['to_dir'] = options.setuptools_to_dir
97 116
98 ez['use_setuptools'](**setup_args) 117 ez['use_setuptools'](**setup_args)
99 import setuptools 118 import setuptools
@@ -110,7 +129,12 @@ for path in sys.path: @@ -110,7 +129,12 @@ for path in sys.path:
110 129
111 ws = pkg_resources.working_set 130 ws = pkg_resources.working_set
112 131
  132 +setuptools_path = ws.find(
  133 + pkg_resources.Requirement.parse('setuptools')).location
  134 +
  135 +# Fix sys.path here as easy_install.pth added before PYTHONPATH
113 cmd = [sys.executable, '-c', 136 cmd = [sys.executable, '-c',
  137 + 'import sys; sys.path[0:0] = [%r]; ' % setuptools_path +
114 'from setuptools.command.easy_install import main; main()', 138 'from setuptools.command.easy_install import main; main()',
115 '-mZqNxd', tmpeggs] 139 '-mZqNxd', tmpeggs]
116 140
@@ -123,11 +147,8 @@ find_links = os.environ.get( @@ -123,11 +147,8 @@ find_links = os.environ.get(
123 if find_links: 147 if find_links:
124 cmd.extend(['-f', find_links]) 148 cmd.extend(['-f', find_links])
125 149
126 -setuptools_path = ws.find(  
127 - pkg_resources.Requirement.parse('setuptools')).location  
128 -  
129 requirement = 'zc.buildout' 150 requirement = 'zc.buildout'
130 -version = options.version 151 +version = options.buildout_version
131 if version is None and not options.accept_buildout_test_releases: 152 if version is None and not options.accept_buildout_test_releases:
132 # Figure out the most recent final version of zc.buildout. 153 # Figure out the most recent final version of zc.buildout.
133 import setuptools.package_index 154 import setuptools.package_index
@@ -167,7 +188,7 @@ if version: @@ -167,7 +188,7 @@ if version:
167 cmd.append(requirement) 188 cmd.append(requirement)
168 189
169 import subprocess 190 import subprocess
170 -if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0: 191 +if subprocess.call(cmd) != 0:
171 raise Exception( 192 raise Exception(
172 "Failed to execute command:\n%s" % repr(cmd)[1:-1]) 193 "Failed to execute command:\n%s" % repr(cmd)[1:-1])
173 194
  1 +#!/usr/bin/env python
  2 +
  3 +"""
  4 +Setuptools bootstrapping installer.
  5 +
  6 +Run this script to install or upgrade setuptools.
  7 +"""
  8 +
  9 +import os
  10 +import shutil
  11 +import sys
  12 +import tempfile
  13 +import zipfile
  14 +import optparse
  15 +import subprocess
  16 +import platform
  17 +import textwrap
  18 +import contextlib
  19 +import warnings
  20 +
  21 +from distutils import log
  22 +
  23 +try:
  24 + from urllib.request import urlopen
  25 +except ImportError:
  26 + from urllib2 import urlopen
  27 +
  28 +try:
  29 + from site import USER_SITE
  30 +except ImportError:
  31 + USER_SITE = None
  32 +
  33 +DEFAULT_VERSION = "18.3.2"
  34 +DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/"
  35 +DEFAULT_SAVE_DIR = os.curdir
  36 +
  37 +
  38 +def _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 +
  48 +def _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 +
  60 +def _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 +
  72 +class 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
  90 +def 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 +
  115 +def _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 +
  134 +def 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 +
  173 +def _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 +
  191 +def _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 +
  200 +def _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 +
  214 +def 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 +
  236 +def 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
  247 +download_file_powershell.viable = has_powershell
  248 +
  249 +
  250 +def download_file_curl(url, target):
  251 + cmd = ['curl', url, '--silent', '--output', target]
  252 + _clean_check(cmd, target)
  253 +
  254 +
  255 +def 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
  263 +download_file_curl.viable = has_curl
  264 +
  265 +
  266 +def download_file_wget(url, target):
  267 + cmd = ['wget', url, '--quiet', '--output-document', target]
  268 + _clean_check(cmd, target)
  269 +
  270 +
  271 +def 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
  279 +download_file_wget.viable = has_wget
  280 +
  281 +
  282 +def 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)
  294 +download_file_insecure.viable = lambda: True
  295 +
  296 +
  297 +def 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 +
  308 +def 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 +
  336 +def _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 +
  345 +def _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 +
  374 +def _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 +
  384 +def 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 +
  390 +if __name__ == '__main__':
  391 + sys.exit(main())
Please register or login to post a comment