rebootstrap-prepend-paths
-------------------------

Test that sections listed in rebootstrap-prepend-paths are installed
alongside Python during rebootstrap and their locations are prepended
to sys.path in the bootstrap subprocess.

The fakedownload recipe simulates a pre-downloaded compat-eggs section.
It sets options['location'] in __init__ (so slapos.rebootstrap can read
it) and in install() populates that directory with symlinks to the real
setuptools, pip, and wheel egg contents.  It also creates a
_distutils_hack.py shim that raises ImportError, so that the Python3-only
_distutils_hack from setuptools (which may contain syntax incompatible with
the target Python) is shadowed by this harmless shim when the prepend path
comes first in sys.path.

>>> import sys
>>> print(system(buildout, env={'PYTHONWARNINGS':'ignore'}))
Develop: '/sample-buildout/recipes'
<BLANKLINE>

>>> write(sample_buildout, 'recipes', 'setup.py',
... """
... from setuptools import setup
...
... setup(
...   name = "recipes",
...   entry_points = {'zc.buildout':[
...     'pyinstall = pyinstall:Pyinstall',
...     'pyshow = pyshow:Pyshow',
...     'fakedownload = fakedownload:Fakedownload',
...   ]},
...   py_modules = [
...     'pyinstall',
...     'pyshow',
...     'fakedownload',
...   ]
... )
... """)

>>> write(sample_buildout, 'recipes', 'fakedownload.py',
... """
... from __future__ import print_function
... import os, shutil, pkg_resources
...
... class Fakedownload:
...
...   def __init__(self, buildout, name, options):
...     self.location = os.path.join(
...       buildout['buildout']['parts-directory'], name)
...     options['location'] = self.location
...
...   def install(self):
...     print('Installing fake-egg at', self.location)
...     os.makedirs(self.location, exist_ok=True)
...     for dist_name in ('pip', 'setuptools', 'wheel'):
...       dist = pkg_resources.get_distribution(dist_name)
...       egg_info_name = '%s-%s.egg-info' % (dist_name, dist.version)
...       egg_info_src = os.path.join(dist.location, 'EGG-INFO')
...       egg_info_dst = os.path.join(self.location, egg_info_name)
...       if not os.path.exists(egg_info_dst):
...         shutil.copytree(egg_info_src, egg_info_dst)
...       for item in os.listdir(dist.location):
...         if item == 'EGG-INFO':
...           continue
...         target = os.path.join(self.location, item)
...         if not os.path.lexists(target):
...           os.symlink(os.path.join(dist.location, item), target)
...     # Shadow the Python3-only _distutils_hack with a shim that raises
...     # ImportError, which zc.buildout catches.  This prevents SyntaxError
...     # when the bootstrap subprocess runs under an older Python.
...     distutils_hack = os.path.join(self.location, '_distutils_hack.py')
...     if not os.path.exists(distutils_hack):
...       with open(distutils_hack, 'w') as f:
...         print("raise ImportError('_distutils_hack not available')", file=f)
...     return [self.location]
...
...   update = install
... """)

>>> print(system(buildout, env={'PYTHONWARNINGS':'ignore'}))
Develop: '/sample-buildout/recipes'
<BLANKLINE>

>>> write(sample_buildout, 'buildout.cfg',
... """
... [buildout]
... develop = recipes
... extensions = slapos.rebootstrap
... python = installpython
... rebootstrap-prepend-paths = fake-egg
...
... parts =
...   fake-egg
...   realrun
...
... [installpython]
... recipe = recipes:pyinstall
...
... [fake-egg]
... recipe = recipes:fakedownload
...
... [realrun]
... recipe = recipes:pyshow
... """)

The fake-egg section is a normal part that is also listed in
rebootstrap-prepend-paths, so it is installed during rebootstrap alongside
Python and its location is prepended to sys.path in the bootstrap subprocess.

>>> print(system(buildout, env={'PYTHONWARNINGS':'ignore'})) # doctest: +ELLIPSIS
slapos.rebootstrap: Make sure that the section 'installpython' won't be reinstalled after rebootstrap.
Develop: '/sample-buildout/recipes'
Installing installpython.
Installing fake-egg.
Installing fake-egg at ...
slapos.rebootstrap: 
************ REBOOTSTRAP: IMPORTANT NOTICE ************
bin/buildout is being reinstalled right now, as new python:
  /sample_buildout/parts/installpython/bin/python
is available, and buildout is using another python:
  /system_python
Buildout will be restarted automatically to have this change applied.
************ REBOOTSTRAP: IMPORTANT NOTICE ************
<BLANKLINE>
Generated script '/sample-buildout/bin/buildout'.
Develop: '/sample-buildout/recipes'
Updating installpython.
Updating fake-egg.
Installing fake-egg at ...
Installing realrun.
Running with: /sample_buildout/parts/installpython/bin/python
<BLANKLINE>

The fake-egg directory was created on disk by the fakedownload recipe.

>>> import os
>>> os.path.isdir(os.path.join(sample_buildout, 'parts', 'fake-egg'))
True

The system-Python run wrote its installed state to .installed.cfg.prerebootstrap
rather than .installed.cfg, so that system-Python re-runs remain correct and
are not affected by the signature patches applied in the SR-Python run.

>>> os.path.isfile(os.path.join(sample_buildout, '.installed.cfg.prerebootstrap'))
True

On a second run, both sections are updated without triggering a rebootstrap.

>>> print(system(buildout, env={'PYTHONWARNINGS':'ignore'})) # doctest: +ELLIPSIS
Develop: '/sample-buildout/recipes'
Updating installpython.
Updating fake-egg.
Installing fake-egg at ...
Updating realrun.
Running with: /sample_buildout/parts/installpython/bin/python
<BLANKLINE>

A UserError is raised if a section listed in rebootstrap-prepend-paths does not exist.

>>> write(sample_buildout, 'buildout.cfg',
... """
... [buildout]
... develop = recipes
... extensions = slapos.rebootstrap
... python = installpython
... rebootstrap-prepend-paths = nonexistent
...
... parts =
...   realrun
...
... [installpython]
... recipe = recipes:pyinstall
...
... [realrun]
... recipe = recipes:pyshow
... """)
>>> print(system(buildout, env={'PYTHONWARNINGS':'ignore'})) # doctest: +ELLIPSIS
While:
  ...
Error: rebootstrap-prepend-paths: section 'nonexistent' does not exist
<BLANKLINE>

A UserError is raised if a listed section exists but has no 'location' option.

>>> write(sample_buildout, 'buildout.cfg',
... """
... [buildout]
... develop = recipes
... extensions = slapos.rebootstrap
... python = installpython
... rebootstrap-prepend-paths = realrun
...
... parts =
...   realrun
...
... [installpython]
... recipe = recipes:pyinstall
...
... [realrun]
... recipe = recipes:pyshow
... """)
>>> print(system(buildout, env={'PYTHONWARNINGS':'ignore'})) # doctest: +ELLIPSIS
...
While:
  ...
Error: rebootstrap-prepend-paths: section 'realrun' has no 'location' option
<BLANKLINE>
