Commit a79d01fc authored by Julien Muchembled's avatar Julien Muchembled

Complete reimplementation

- Work entirely in-place, even to switch to a different version of Python.
- Update bin/buildout to immediately use the wanted Python on subsequent
  buildout runs.

For SlapOS, the second point is required to have the instanciation done
with the built Python.
parent 198edb16
############################################################################## ##############################################################################
# #
# Copyright (c) 2010 ViFiB SARL and Contributors. # Copyright (c) 2010-2017 ViFiB SARL and Contributors.
# All Rights Reserved. # All Rights Reserved.
# #
# This software is subject to the provisions of the Zope Public License, # This software is subject to the provisions of the Zope Public License,
...@@ -11,52 +11,48 @@ ...@@ -11,52 +11,48 @@
# FOR A PARTICULAR PURPOSE. # FOR A PARTICULAR PURPOSE.
# #
############################################################################## ##############################################################################
import os
import zc.buildout
import zc.buildout.buildout
import sys
import logging
import subprocess
def extension(buildout): import logging, os, shutil, subprocess, sys, tempfile
Rebootstrap(buildout)() from zc.buildout import easy_install, UserError
class extension(object):
class Rebootstrap:
def __init__(self, buildout): def __init__(self, buildout):
self.logger = logging.getLogger(__name__)
self.buildout = buildout self.buildout = buildout
buildout_directory = buildout['buildout']['directory'] # fetch section to build python (default value is 'buildout')
# fetch section to build python, obligatory self.python_section = buildout['buildout']['python'].strip()
self.python_section = buildout['buildout'].get('python','').strip()
if not self.python_section:
raise zc.buildout.UserError('buildout:python is not defined.')
if self.python_section not in buildout:
raise zc.buildout.UserError('[%s] is not defined.' % self.python_section)
self.wanted_python = buildout[self.python_section]['executable'] self.wanted_python = buildout[self.python_section]['executable']
rebootstrap_directory = buildout['buildout'].get('rebootstrap-directory') if sys.executable != self.wanted_python:
if rebootstrap_directory: self.hook('_setup_directories')
self.rebootstrap_directory = os.path.join( elif buildout._parts:
buildout_directory, 'rebootstrap' self._frozen = frozenset(buildout._parts)
) self.hook('_compute_part_signatures')
self.wanted_python = self.wanted_python.replace(
buildout_directory, self.rebootstrap_directory, 1 def hook(self, attr):
) buildout = self.buildout
else: getattr(buildout, attr)
self.rebootstrap_directory = buildout_directory def wrapper(*args, **kw):
# query for currently running python delattr(buildout, attr)
self.running_python = sys.executable return getattr(self, attr)(*args, **kw)
setattr(buildout, attr, wrapper)
def __call__(self):
if self.running_python != self.wanted_python: def _setup_directories(self):
self.install_section() logger = logging.getLogger(__name__)
self.reboot() buildout = self.buildout
elif self.python_section: logger.info(
buildout = self.buildout['buildout'] "Make sure that the section %r won't be reinstalled after rebootstrap."
if self.python_section not in buildout['parts'].split(): % self.python_section)
buildout['parts'] = self.python_section + '\n' + buildout['parts'] # We hooked in such a way that all extensions are loaded. Do not reload.
buildout._load_extensions
def reboot(self): buildout._load_extensions = lambda: None
message = """ # workaround for the install command,
# which ignores dependencies when parts are specified
# (the only sections we have accessed so far are those that are required
# to build the wanted Python)
buildout.install(buildout._parts) # [self.python_section]
logger.info("""
************ REBOOTSTRAP: IMPORTANT NOTICE ************ ************ REBOOTSTRAP: IMPORTANT NOTICE ************
bin/buildout is being reinstalled right now, as new python: bin/buildout is being reinstalled right now, as new python:
%(wanted_python)s %(wanted_python)s
...@@ -64,90 +60,69 @@ is available, and buildout is using another python: ...@@ -64,90 +60,69 @@ is available, and buildout is using another python:
%(running_python)s %(running_python)s
Buildout will be restarted automatically to have this change applied. Buildout will be restarted automatically to have this change applied.
************ REBOOTSTRAP: IMPORTANT NOTICE ************ ************ REBOOTSTRAP: IMPORTANT NOTICE ************
""" % dict(wanted_python=self.wanted_python, """ % dict(wanted_python=self.wanted_python, running_python=sys.executable))
running_python=self.running_python)
self.logger.info(message) installed = sys.argv[0]
args = sys.argv[:] new_bin = installed + '-rebootstrap'
env = os.environ if not os.path.exists(new_bin):
if 'ORIG_PYTHON' not in env: from .bootstrap import get_distributions, setup_script
env['ORIG_PYTHON'] = sys.executable if subprocess.call((self.wanted_python, '-c',
os.execve(self.wanted_python, [self.wanted_python] + args, env) 'import sys; sys.exit(sys.version_info[:2] == %r)'
% (sys.version_info[:2],))):
def install_section(self): setup_script(new_bin, self.wanted_python)
if not os.path.exists(self.wanted_python) or \
self.rebootstrap_directory != self.buildout['buildout']['directory']:
self.logger.info('Installing section %r to provide %r' % (
self.python_section, self.wanted_python))
args = sys.argv[:]
if 'install' in args:
args = args[:args.index('install')]
# explicitly specify the config file location by absolute path
if '-c' not in args:
config_file = os.path.abspath(os.path.join(
os.curdir, 'buildout.cfg'))
args.extend(['-c', config_file])
else: else:
config_file = args[args.index('-c') + 1] # With a different version of Python,
if not zc.buildout.buildout._isurl(config_file): # we must reinstall required eggs from source.
config_file = os.path.abspath(config_file) from pkg_resources import resource_string
args[args.index('-c') + 1] = config_file with Cache(buildout['buildout']) as cache:
subprocess.check_call([self.wanted_python, '-c',
# explicitly invoke with the current python interpreter resource_string(__name__, 'bootstrap.py'),
args.insert(0, sys.executable) new_bin, cache._dest, cache.tmp,
] + list(map(cache, get_distributions())))
# remove rebootstrap extension, which is not needed in rebootstrap part
extension_list = self.buildout['buildout']['extensions'].split() shutil.copy(new_bin, installed)
extension_list = [q.strip() for q in extension_list if q.strip() != \ os.execv(self.wanted_python, [self.wanted_python] + sys.argv)
__name__]
bin_directory = self.buildout['buildout']['bin-directory'] def _compute_part_signatures(self, install_parts):
# rerun buildout with only neeeded section to reuse buildout # Skip signature check for parts that were required to build the wanted
# ability to calcuate all dependency # Python. Signatures differ when switching to a different version.
args.extend([ buildout = self.buildout
# chroot to rebootstrap directory buildout._compute_part_signatures(install_parts)
'buildout:directory=%s' % self.rebootstrap_directory, installed_part_options = buildout.installed_part_options
# preserve bin-directory outside the chroot. for part in self._frozen.intersection(install_parts):
'buildout:bin-directory=%s' % bin_directory, buildout[part]['__buildout_signature__'] = \
# install only required section with dependencies installed_part_options[part]['__buildout_signature__']
'buildout:parts=%s' % self.python_section,
# do not load this extension
'buildout:extensions=%s' % ' '.join(extension_list), class Cache(easy_install.Installer):
# more parameters for building slapos package
'buildout:rootdir=%s' % self.rebootstrap_directory, def __init__(self, buildout):
'buildout:destdir=', easy_install.Installer.__init__(self,
]) buildout['eggs-directory'],
self.logger.info('Rerunning buildout to install section %s with ' buildout.get('find-links', '').split())
'arguments %r.'% (self.python_section, args))
process = subprocess.Popen(args) def __enter__(self):
process.wait() self.tmp = self._download_cache or tempfile.mkdtemp('get_dist')
if process.returncode != 0: return self
raise zc.buildout.UserError('Error during installing python '
'provision section.') def __exit__(self, t, v, tb):
if not os.path.exists(self.wanted_python): if self.tmp is not self._download_cache:
raise zc.buildout.UserError('Section %r directed to python executable:\n' shutil.rmtree(self.tmp)
'%r\nUnfortunately even after installing this section executable was' del self.tmp
' not found.\nThis is section responsibility to provide python (eg. '
'by compiling it).' % (self.python_section, self.wanted_python)) def __call__(self, dist):
req = dist.as_requirement()
_uninstall_part_orig = zc.buildout.buildout.Buildout._uninstall_part cache = self._download_cache
def _uninstall_part(self, part, installed_part_options): if cache:
_uninstall_part_orig(self, part, installed_part_options) from pkg_resources import SOURCE_DIST
try: for avail in self._index[dist.project_name]:
location = self[part].get('location') if (avail.version == dist.version and
except zc.buildout.buildout.MissingSection: avail.precedence == SOURCE_DIST and
return cache == os.path.dirname(avail.location)):
if location and sys.executable.startswith(location): return str(req)
message = """ avail = self._obtain(req, True)
************ REBOOTSTRAP: IMPORTANT NOTICE ************ if avail is None:
%r part that provides the running Python is uninstalled. raise UserError("Couldn't find a distribution for %r" % str(req))
Buildout will be restarted automatically with the original Python. if self._fetch(avail, self.tmp, cache) is None:
************ REBOOTSTRAP: IMPORTANT NOTICE ************ raise UserError("Couldn't download distribution %s." % avail)
""" % part return str(req)
self._logger.info(message)
if getattr(self, 'dry_run', False):
sys.exit()
args = sys.argv[:]
env = os.environ
orig_python = env['ORIG_PYTHON']
os.execve(orig_python, [orig_python] + args, env)
zc.buildout.buildout.Buildout._uninstall_part = _uninstall_part
import os, sys
class FakeSysExecutable(object):
def __init__(self, python):
self.executable = python
def __getattr__(self, attr):
return getattr(sys, attr)
def get_distributions():
from pkg_resources import get_distribution
distributions = ['setuptools', 'zc.buildout']
try:
import slapos.libnetworkcache
except ImportError:
pass
else:
distributions.append('slapos.libnetworkcache')
return map(get_distribution, distributions)
def setup_script(path, python=sys.executable):
from zc.buildout import easy_install
try:
if sys.executable != python:
easy_install.sys = FakeSysExecutable(python)
easy_install.scripts(
((os.path.realpath(path), 'zc.buildout.buildout', 'main'),),
get_distributions(),
python)
finally:
easy_install.sys = sys
def main():
import shutil, subprocess, tempfile, zipfile
eggs_dir = sys.argv[2]
cache = sys.argv[3]
# Install setuptools.
src = os.path.join(cache, sys.argv[4].replace('==', '-') + '.zip')
tmp = tempfile.mkdtemp()
try:
with zipfile.ZipFile(src) as zip_file:
zip_file.extractall(tmp)
src, = os.listdir(tmp)
subprocess.check_call((sys.executable, 'setup.py', '-q', 'bdist_egg',
'--dist-dir', tmp), cwd=os.path.join(tmp, src))
egg = os.listdir(tmp)
egg.remove(src)
egg, = egg
dst = os.path.join(eggs_dir, egg)
os.path.exists(dst) or shutil.move(os.path.join(tmp, egg), dst)
finally:
shutil.rmtree(tmp)
sys.path.insert(0, dst)
# Install other requirements given on command line.
from pkg_resources import working_set, require
from setuptools.command import easy_install
reqs = sys.argv[5:]
easy_install.main(['-mZqNxd', eggs_dir, '-f', cache] + reqs)
working_set.add_entry(eggs_dir)
for req in reqs:
require(req)
# Generate bin/buildout-rebootstrap script.
setup_script(sys.argv[1])
if __name__ == '__main__':
sys.exit(main())
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment