Commit 225767ad authored by Kazuhiko Shiozaki's avatar Kazuhiko Shiozaki

Rewrite for zc.buildout >= 2.0.0.

 * Same parts directory as the normal buildout is used so that we can
   build faster and also make the code simpler.
 * Python part is now specified by `python` parameter in `buildout`
   section.
 * Restart automatically with the original python when running python
   part is removed.
parent 26f6d42a
...@@ -4,7 +4,13 @@ Changes ...@@ -4,7 +4,13 @@ Changes
3.2 (unreleased) 3.2 (unreleased)
---------------- ----------------
* No changes yet. * Support zc.buildout >= 2.0.0.
* Same parts directory as the normal buildout is used so that we can
build faster and also make the code simpler.
* Python part is now specified by `python` parameter in `buildout`
section.
* Restart automatically with the original python when running python
part is removed.
3.1 (2011-06-24) 3.1 (2011-06-24)
---------------- ----------------
......
Chicken and egg python zc.buildout extension Rerun buildout with a python provided by buildout
============================================ ===============================================
This extensions for zc.buildout is created to solve chicken and egg problem Buildout 2.0 no-longer supports using multiple versions of Python in a
while working with buildout and when some exact version of python, which is single buildout. This extension makes it possible to use 'yet another
provided by buildout shall be used to *execute* this buildout itself. single' version of python, by building a specified python part and its
dependencies first then reinvoking buildout with the executable.
Usage Usage
----- -----
Part to build python is required. By convention slapos.rebootstrap will try to A part to build python is required. You need to specify it by `python`
find python executable in: parameter in `buildout` section, just same as buildout 1.x. The python
section must provide executable option that gives the path to a Python
executable.
special.parts.directory/partname/bin/partname And add slapos.rebootstrap to `extensions` parameter in `buildout` section.
But when needed python-path parameter can be used to point rebootstrap to find Use whatever python to bootstrap and run buildout. If this extension
python in: detects that sys.executable used to run buildout is different than
executable provided in python section, it will try to find this
special.parts.directory/partname/python-path executable. If it does not exists, it will install this section and
then reinstall buildout using new python executable. Later buildout
Add slapos.rebootstrap to extensions and set rebooter-section to above section. run will continue using new python.
Use whatever python to bootstrap and run buildout. If reboot will detect that
sys.executable used to run buildout is different then executable provided in
python section it will try to find this executable. If it does not exists it
will install this section and then reinstall buildout using new python
executable. Later buildout run will continue using new python.
Because external buildout is used to provide buildout version parameter is
introduced to being able to upgrade not in place python part. This parameter
is required and becomes part of suffix.
Whenever developer-mode is set to true no cleanup will be done in case of
failure. Then user is responsible to cleanup directories.
Example profile and invocation Example profile and invocation
------------------------------ ------------------------------
...@@ -39,14 +29,11 @@ Example profile and invocation ...@@ -39,14 +29,11 @@ Example profile and invocation
[buildout] [buildout]
extensions = slapos.rebootstrap extensions = slapos.rebootstrap
python = slapospython
parts = parts =
realrun realrun
[rebootstrap]
section = slapospython
version = 1
[slapospython] [slapospython]
recipe = plone.recipe.command recipe = plone.recipe.command
stop-on-error = true stop-on-error = true
...@@ -61,7 +48,7 @@ Example profile and invocation ...@@ -61,7 +48,7 @@ Example profile and invocation
After bootstrapping and running this buildout it will print: After bootstrapping and running this buildout it will print:
Running with python /path/to/buildout/parts.rebootstrap.1/slapospython/bin/slapospython Running with python /path/to/buildout/parts/slapospython/bin/slapospython
Running tests Running tests
------------- -------------
......
from setuptools import setup, find_packages from setuptools import setup, find_packages
version = '3.2-dev' version = '3.2'
name = "slapos.rebootstrap" name = "slapos.rebootstrap"
long_description = open("README.txt").read() + '\n\n' long_description = open("README.txt").read() + '\n\n'
...@@ -38,7 +38,7 @@ setup( ...@@ -38,7 +38,7 @@ setup(
zip_safe=True, zip_safe=True,
install_requires=[ install_requires=[
'setuptools', 'setuptools',
'zc.buildout' 'zc.buildout >=2.0.0'
], ],
tests_require=[ tests_require=[
'zope.testing', 'zope.testing',
......
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
# #
############################################################################## ##############################################################################
import os import os
import shutil
import zc.buildout import zc.buildout
import sys import sys
import logging import logging
...@@ -25,53 +24,22 @@ class Rebootstrap: ...@@ -25,53 +24,22 @@ class Rebootstrap:
def __init__(self, buildout): def __init__(self, buildout):
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.buildout = buildout self.buildout = buildout
# fetch section of rebootstrap, obligatory
rebootstrap_section = buildout['rebootstrap']
# leave garbage? only in case of developer mode
self.developer_mode = rebootstrap_section.get('developer-mode',
'false') == 'true'
# fetch section to build python, obligatory # fetch section to build python, obligatory
self.python_section = rebootstrap_section['section'].strip() self.python_section = (
# fetch version of python build, obligatory buildout['buildout'].get('python') or \
version = rebootstrap_section['version'].strip() buildout['rebootstrap']['section']
).strip()
self.wanted_python = buildout[self.python_section]['executable']
# usualy python will be try to found by convention in:
# prefixed-parts/part-name/bin/part-name
# but in case if part is non controlable it will be possible to
# find it as
# prefixed-parts/part-name/python-path
# where python-path is relative path to python interpreter
relative_python = rebootstrap_section.get('python-path', ''
).strip() or os.path.join('bin', self.python_section)
# query for currently running python # query for currently running python
self.running_python = sys.executable self.running_python = sys.executable
# generate rebootstrap.realname.version
prefix = 'rebootstrap.' + version + '.'
self.installed = '.' + prefix + 'installed.cfg'
self.parts_directory = os.path.join(self.buildout['buildout'][
'directory'], prefix + 'parts')
# query for wanted python, which is combination of suffixed parts
# and working recipe
self.wanted_python = os.path.join(self.parts_directory,
self.python_section, relative_python)
# support additional eggs
self.eggs = []
eggs = rebootstrap_section.get('eggs')
if eggs:
eggs = [q.strip() for q in eggs.split() if q.strip()]
self.eggs = eggs
def __call__(self): def __call__(self):
self.install_section() if self.running_python != self.wanted_python:
self.reboot() self.install_section()
self.reboot()
def reboot(self): def reboot(self):
if self.running_python == self.wanted_python:
return
message = """ message = """
************ 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:
...@@ -83,114 +51,61 @@ Buildout will be restarted automatically to have this change applied. ...@@ -83,114 +51,61 @@ Buildout will be restarted automatically to have this change applied.
""" % dict(wanted_python=self.wanted_python, """ % dict(wanted_python=self.wanted_python,
running_python=self.running_python) running_python=self.running_python)
self.logger.info(message) self.logger.info(message)
options = self.buildout['buildout']
self.eggs.append('zc.buildout')
# XXX: A lot of below code is took from zc.buildout.buildout.py:_maybe_upgrade
# which is code duplication issue, but even if newer buildout with needed
# hooks will be released, this extension shall work on older ones
if zc.buildout.easy_install.is_distribute:
self.eggs.append('distribute')
else:
self.eggs.append('setuptools')
ws = zc.buildout.easy_install.install(
[
(spec + ' ' + options.get(spec+'-version', '')).strip()
for spec in self.eggs
],
options['eggs-directory'],
links = options.get('find-links', '').split(),
index = options.get('index'),
path = [options['develop-eggs-directory']],
prefer_final=False,
executable=self.wanted_python
)
# the new dist is different, so we've upgraded.
# Update the scripts and return True
# Ideally the new version of buildout would get a chance to write the
# script. Not sure how to do that.
partsdir = os.path.join(options['parts-directory'], 'buildout')
if os.path.exists(partsdir):
# This is primarily for unit tests, in which .py files change too
# fast for Python to know to regenerate the .pyc/.pyo files.
shutil.rmtree(partsdir)
os.mkdir(partsdir)
script_initialization = ''
# (Honor the relative-paths option.)
relative_paths = options.get('relative-paths', 'false')
if relative_paths == 'true':
relative_paths = options['directory']
else:
assert relative_paths == 'false'
relative_paths = ''
zc.buildout.easy_install.sitepackage_safe_scripts(
options['bin-directory'], ws, self.wanted_python, partsdir,
reqs=['zc.buildout'], relative_paths=relative_paths,
include_site_packages=False,
script_initialization=script_initialization,
)
# Restart
args = map(zc.buildout.easy_install._safe_arg, sys.argv) args = map(zc.buildout.easy_install._safe_arg, sys.argv)
# We want to make sure that our new site.py is used for rerunning env = os.environ
# buildout, so we put the partsdir in PYTHONPATH for our restart. env['ORIG_PYTHON'] = sys.executable
# This overrides any set PYTHONPATH, but since we generally are
# trying to run with a completely "clean" python (only the standard
# library) then that should be fine.
env = os.environ.copy()
env['PYTHONPATH'] = partsdir
sys.stdout.flush()
os.execve(self.wanted_python, [self.wanted_python] + args, env) os.execve(self.wanted_python, [self.wanted_python] + args, env)
def install_section(self): def install_section(self):
try_install = False env = os.environ
try: if not os.path.exists(self.wanted_python):
if not os.path.exists(self.parts_directory) \ self.logger.info('Installing section %r to provide %r' % (
or not os.path.exists(self.wanted_python): self.python_section, self.wanted_python))
try_install = True args = map(zc.buildout.easy_install._safe_arg, sys.argv)
self.logger.info('Installing section %r to provide %r in %r' % ( if 'install' in args:
self.python_section, self.wanted_python, self.parts_directory)) args = args[:args.index('install')]
args = map(zc.buildout.easy_install._safe_arg, sys.argv)
if 'install' in args:
args = args[:args.index('install')]
# remove rebootstrap extension, which is not needed in rebootstrap part # remove rebootstrap extension, which is not needed in rebootstrap part
extension_list = self.buildout['buildout']['extensions'].split() extension_list = self.buildout['buildout']['extensions'].split()
extension_list = [q.strip() for q in extension_list if q.strip() != \ extension_list = [q.strip() for q in extension_list if q.strip() != \
__name__] __name__]
# rerun buildout with only neeeded section to reuse buildout # rerun buildout with only neeeded section to reuse buildout
# ability to calcuate all dependency # ability to calcuate all dependency
args.extend([ args.extend([
# install only required section with dependencies # install only required section with dependencies
'buildout:parts=%s' % self.python_section, 'buildout:parts=%s' % self.python_section,
# do not load extensions (even other ones) # do not load this extension
'buildout:extensions=%s' % ' '.join(extension_list), 'buildout:extensions=%s' % ' '.join(extension_list),
# create virutal python environment ])
'buildout:parts-directory=%s' % self.parts_directory, self.logger.info('Rerunning buildout to install section %s with '
'buildout:installed=%s' % self.installed, 'arguments %r.'% (self.python_section, args))
]) process = subprocess.Popen(args)
self.logger.info('Rerunning buildout to install section %s with ' process.wait()
'arguments %r.'% (self.python_section, args)) if process.returncode != 0:
process = subprocess.Popen(args) raise zc.buildout.UserError('Error during installing python '
process.wait() 'provision section.')
if process.returncode != 0: if not os.path.exists(self.wanted_python):
raise zc.buildout.UserError('Error during installing python ' raise zc.buildout.UserError('Section %r directed to python executable:\n'
'provision section.') '%r\nUnfortunately even after installing this section executable was'
if not os.path.exists(self.wanted_python): ' not found.\nThis is section responsibility to provide python (eg. '
raise zc.buildout.UserError('Section %r directed to python executable:\n' 'by compiling it).' % (self.python_section, self.wanted_python))
'%r\nUnfortunately even after installing this section executable was'
' not found.\nThis is section responsibility to provide python (eg. ' _uninstall_part_orig = zc.buildout.buildout.Buildout._uninstall_part
'by compiling it).' % (self.python_section, self.wanted_python)) def _uninstall_part(self, part, installed_part_options):
except zc.buildout.UserError: _uninstall_part_orig(self, part, installed_part_options)
if try_install: location = self[part].get('location')
if not self.developer_mode: if location and sys.executable.startswith(location):
if os.path.exists(self.parts_directory): message = """
shutil.rmtree(self.parts_directory) ************ REBOOTSTRAP: IMPORTANT NOTICE ************
if os.path.exists(self.installed): %r part that provides the running Python is uninstalled.
os.unlink(self.installed) Buildout will be restarted automatically with the original Python.
else: ************ REBOOTSTRAP: IMPORTANT NOTICE ************
self.logger.warning('Developer mode active.') """ % part
self.logger.warning('Directory %r and file %r are left for further ' self._logger.info(message)
'analysis.' % (self.parts_directory, self.installed)) if getattr(self, 'dry_run', False):
self.logger.warning('Developer is responsible for removing those ' sys.exit()
'directories.') args = map(zc.buildout.easy_install._safe_arg, sys.argv)
raise env = os.environ
orig_python = env['ORIG_PYTHON']
os.execve(orig_python, [orig_python] + args, env)
zc.buildout.buildout.Buildout._uninstall_part = _uninstall_part
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