promise_plugin.py 4.94 KB
##############################################################################
#
# Copyright (c) 2018 Vifib SARL and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################
import re
import logging, os
import zc.buildout.easy_install
from pprint import pformat
from slapos.recipe.librecipe import GenericBaseRecipe

script_template = '''# This script is auto generated by slapgrid, do not edit!
import sys
sys.path[0:0] = %(path)s

extra_config_dict = %(config)s

# We want to cleanup all imported modules from slapos namespace, because
# they will conflict with slapos.core.
# In fact as slapos.grid is already imported, the promise cannot reimport
# his own slapos.grid from an updated sys.path. Then all module imports which
# are not in slapos.core will fail.
#
# call reload(slapos) only solve a part of problem because not all modules
# will be reloaded, and some new modules won't be added.
# The solution is to delete all cached 'slapos' modules as well as all cached
# 'pkg_resources' modules which is responsible of namespace declaration.
# They will be re-imported again using the updated sys.path
for module in sys.modules.keys():
  if 'slapos' in module or 'pkg_resources' in module:
    del sys.modules[module]

%(content)s
'''

class Recipe(GenericBaseRecipe):

  _WORKING_SET_CACHE_NAME = "slapos.cookbook_pplugin_ws_cache"

  def __init__(self, buildout, name, options):
    buildout_section = buildout['buildout']
    options['eggs-directory'] = buildout_section['eggs-directory']
    options['develop-eggs-directory'] = buildout_section['develop-eggs-directory']
    super(Recipe, self).__init__(buildout, name, options)

  def _get_cache_storage(self):
    """Return a mapping where to store generated working sets.
    from https://github.com/buildout/buildout/blob/master/zc.recipe.egg_/src/zc/recipe/egg/egg.py#L170
    """
    try:
      return getattr(self.buildout, self._WORKING_SET_CACHE_NAME)
    except AttributeError:
      cache_storage = {}
      try:
        setattr(
          self.buildout,
          self._WORKING_SET_CACHE_NAME,
          cache_storage
        )
      except AttributeError:
        if not isinstance(self.buildout, dict):
          raise
        # failed to set attribute in test mode, cache not used
    return cache_storage

  def install(self):
    develop_eggs_dir = self.options['develop-eggs-directory']
    eggs_dir = self.options['eggs-directory']
    egg_list = tuple(
      egg.strip()
      for egg in self.options['eggs'].splitlines()
      if egg.strip()
    )

    cache_storage = self._get_cache_storage()
    cache_key = (
      egg_list,
      eggs_dir,
      develop_eggs_dir,
    )
    try:
      working_set = cache_storage[cache_key]
    except KeyError:
      if develop_eggs_dir and eggs_dir:
        working_set = zc.buildout.easy_install.working_set(
          egg_list,
          [develop_eggs_dir, eggs_dir]
        )
        cache_storage[cache_key] = working_set
      else:
        working_set = set()

    regex = r"^[\w_\-\.\s]+$"
    import_path = self.options.get('import', '').strip()
    if import_path:
      if not re.search(regex, import_path):
        raise ValueError("Import path %r is not a valid" % import_path)
      content_string = "from %s import RunPromise" % import_path
    else:
      # old parameter for compatibility
      content_string = self.options['content'].strip()
      if not re.search(regex, content_string):
        raise ValueError("Promise content %r is not valid" % content_string)

    config_dict = {key[7:]: self.options[key]
      for key in self.options
      if key.startswith('config-')}

    return self.createFile(self.options['output'], script_template % {
      'path': pformat([dist.location for dist in working_set], indent=2),
      'content': content_string,
      'config': pformat(config_dict, indent=2),
      }, int(self.options.get('mode', '0644'), 8)),

  update = install