blob: a922a629ce05766dca672d3ed551d484dc7598dd [file] [log] [blame]
#
# Copyright (C) 2016 Codethink Limited
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
#
# Authors:
# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk>
import os
from typing import Tuple, Type, Iterator
from pluginbase import PluginSource
from .. import utils
from .. import _site
from ..plugin import Plugin
from ..source import Source
from ..element import Element
from ..node import Node
from ..utils import UtilError
from .._exceptions import PluginError
from .._messenger import Messenger
from .pluginorigin import PluginOrigin, PluginType
# A Context for loading plugin types
#
# Args:
# plugin_base (PluginBase): The main PluginBase object to work with
# plugin_type (PluginType): The type of plugin to load
#
# Since multiple pipelines can be processed recursively
# within the same interpretor, it's important that we have
# one context associated to the processing of a given pipeline,
# this way sources and element types which are particular to
# a given BuildStream project are isolated to their respective
# Pipelines.
#
class PluginFactory:
def __init__(self, plugin_base, plugin_type):
# For pickling across processes, make sure this context has a unique
# identifier, which we prepend to the identifier of each PluginSource.
# This keeps plugins loaded during the first and second pass distinct
# from eachother.
self._identifier = str(id(self))
self._plugin_type = plugin_type # The kind of plugins this factory loads
self._types = {} # Plugin type lookup table by kind
self._origins = {} # PluginOrigin lookup table by kind
self._allow_deprecated = {} # Lookup table to check if a plugin is allowed to be deprecated
self._plugin_base = plugin_base # The PluginBase object
# The PluginSource objects need to be kept in scope for the lifetime
# of the loaded plugins, otherwise the PluginSources delete the plugin
# modules when they go out of scope.
#
# FIXME: Instead of keeping this table, we can call:
#
# PluginBase.make_plugin_source(..., persist=True)
#
# The persist attribute avoids this behavior. This is not currently viable
# because the BuildStream data model (projects and elements) does not properly
# go out of scope when the CLI completes, causing errors to occur when
# invoking BuildStream multiple times during tests.
#
self._sources = {} # A mapping of (location, kind) -> PluginSource objects
self._init_site_source()
# Initialize the PluginSource object for core plugins
def _init_site_source(self):
if self._plugin_type == PluginType.SOURCE:
self._site_plugins_path = _site.source_plugins
elif self._plugin_type == PluginType.ELEMENT:
self._site_plugins_path = _site.element_plugins
self._site_source = self._plugin_base.make_plugin_source(
searchpath=[self._site_plugins_path], identifier=self._identifier + "site",
)
######################################################
# Public Methods #
######################################################
# register_plugin_origin():
#
# Registers the PluginOrigin to use for the given plugin kind
#
# Args:
# kind (str): The kind identifier of the Plugin
# origin (PluginOrigin): The PluginOrigin providing the plugin
# allow_deprecated (bool): Whether this plugin kind is allowed to be used in a deprecated state
#
def register_plugin_origin(self, kind: str, origin: PluginOrigin, allow_deprecated: bool):
if kind in self._origins:
raise PluginError(
"More than one {} plugin registered as kind '{}'".format(self._plugin_type, kind),
reason="duplicate-plugin",
)
self._origins[kind] = origin
self._allow_deprecated[kind] = allow_deprecated
# lookup():
#
# Fetches a type loaded from a plugin in this plugin context
#
# Args:
# messenger (Messenger): The messenger
# kind (str): The kind of Plugin to create
# provenance_node (Node): The node from where the plugin was referenced
#
# Returns:
# (type): The type associated with the given kind
# (str): A path to the YAML file holding the plugin's defaults, or None
#
# Raises: PluginError
#
def lookup(self, messenger: Messenger, kind: str, provenance_node: Node) -> Tuple[Type[Plugin], str]:
plugin_type, defaults = self._ensure_plugin(kind, provenance_node)
# We can be called with None for the messenger here in the
# case that we've been pickled through the scheduler (see jobpickler.py),
#
# In this case we know that we've already initialized and do not need
# to warn about deprecated plugins a second time.
if messenger is None:
return plugin_type, defaults
# After looking up the type, issue a warning if it's deprecated
#
# We do this here because we want to issue one warning for each time the
# plugin is used.
#
if plugin_type.BST_PLUGIN_DEPRECATED and not self._allow_deprecated[kind]:
messenger.warn(
"{}: Using deprecated plugin '{}'".format(provenance_node.get_provenance(), kind),
detail=plugin_type.BST_PLUGIN_DEPRECATION_MESSAGE,
)
return plugin_type, defaults
# list_plugins():
#
# A generator which yields all of the plugins which have been loaded
#
# Yields:
# (str): The plugin kind
# (type): The loaded plugin type
# (str): The default yaml file, if any
# (str): The display string describing how the plugin was loaded
#
def list_plugins(self) -> Iterator[Tuple[str, Type[Plugin], str, str]]:
for kind, (plugin_type, defaults, display) in self._types.items():
yield kind, plugin_type, defaults, display
# get_plugin_paths():
#
# Gets the directory on disk where the plugin itself is located,
# and a full path to the plugin's accompanying YAML file for
# it's defaults (if any).
#
# Args:
# kind (str): The plugin kind
#
# Returns:
# (str): The full path to the directory containing the plugin
# (str): The full path to the accompanying .yaml file containing
# the plugin's preferred defaults.
# (str): The explanatory display string describing how this plugin was loaded
#
def get_plugin_paths(self, kind: str):
try:
origin = self._origins[kind]
except KeyError:
return None, None, None
return origin.get_plugin_paths(kind, self._plugin_type)
######################################################
# Private Methods #
######################################################
# _ensure_plugin():
#
# Ensures that a plugin is loaded, delegating the work of getting
# the plugin materials from the respective PluginOrigin
#
# Args:
# kind (str): The plugin kind to load
# provenance (str): The provenance of whence the plugin was referred to in the project
#
# Returns:
# (type): The loaded type
# (str): The full path the the yaml file containing defaults, or None
#
# Raises:
# (PluginError): In case something went wrong loading the plugin
#
def _ensure_plugin(self, kind: str, provenance_node: Node) -> Tuple[Type[Plugin], str]:
if kind not in self._types:
# Get the directory on disk where the plugin exists, and
# the optional accompanying .yaml file for the plugin, should
# one have been provided.
#
location, defaults, display = self.get_plugin_paths(kind)
if location:
# Make the PluginSource object
#
source = self._plugin_base.make_plugin_source(
searchpath=[location], identifier=self._identifier + location + kind,
)
# Keep a reference on the PluginSources (see comment in __init__)
#
self._sources[(location, kind)] = source
else:
# Try getting it from the core plugins
if kind not in self._site_source.list_plugins():
raise PluginError(
"{}: No {} plugin registered for kind '{}'".format(
provenance_node.get_provenance(), self._plugin_type, kind
),
reason="plugin-not-found",
)
source = self._site_source
defaults = os.path.join(self._site_plugins_path, "{}.yaml".format(kind))
if not os.path.exists(defaults):
defaults = None
display = "core plugin"
self._types[kind] = (self._load_plugin(source, kind), defaults, display)
type_, defaults, _ = self._types[kind]
return type_, defaults
# _load_plugin():
#
# Loads the actual plugin type from the PluginSource
#
# Args:
# source (PluginSource): The PluginSource
# kind (str): The plugin kind to load
#
# Returns:
# (type): The loaded type
#
# Raises:
# (PluginError): In case something went wrong loading the plugin
#
def _load_plugin(self, source: PluginSource, kind: str) -> Type[Plugin]:
try:
plugin = source.load_plugin(kind)
except ImportError as e:
raise PluginError("Failed to load {} plugin '{}': {}".format(self._plugin_type, kind, e)) from e
try:
plugin_type = plugin.setup()
except AttributeError as e:
raise PluginError(
"{} plugin '{}' did not provide a setup() function".format(self._plugin_type, kind),
reason="missing-setup-function",
) from e
except TypeError as e:
raise PluginError(
"setup symbol in {} plugin '{}' is not a function".format(self._plugin_type, kind),
reason="setup-is-not-function",
) from e
self._assert_plugin(kind, plugin_type)
self._assert_min_version(kind, plugin_type)
return plugin_type
# _assert_plugin():
#
# Performs assertions on the loaded plugin
#
# Args:
# kind (str): The plugin kind to load
# plugin_type (type): The loaded plugin type
#
# Raises:
# (PluginError): In case something went wrong loading the plugin
#
def _assert_plugin(self, kind: str, plugin_type: Type[Plugin]):
if kind in self._types:
raise PluginError(
"Tried to register {} plugin for existing kind '{}' "
"(already registered {})".format(self._plugin_type, kind, self._types[kind].__name__)
)
base_type: Type[Plugin]
if self._plugin_type == PluginType.SOURCE:
base_type = Source
elif self._plugin_type == PluginType.ELEMENT:
base_type = Element
try:
if not issubclass(plugin_type, base_type):
raise PluginError(
"{} plugin '{}' returned type '{}', which is not a subclass of {}".format(
self._plugin_type, kind, plugin_type.__name__, base_type.__name__
),
reason="setup-returns-bad-type",
)
except TypeError as e:
raise PluginError(
"{} plugin '{}' returned something that is not a type (expected subclass of {})".format(
self._plugin_type, kind, self._plugin_type
),
reason="setup-returns-not-type",
) from e
# _assert_min_version():
#
# Performs the version checks on the loaded plugin type,
# ensuring that the loaded plugin is intended to work
# with this version of BuildStream.
#
# Args:
# kind (str): The plugin kind to load
# plugin_type (type): The loaded plugin type
#
# Raises:
# (PluginError): In case something went wrong loading the plugin
#
def _assert_min_version(self, kind, plugin_type):
if plugin_type.BST_MIN_VERSION is None:
raise PluginError(
"{} plugin '{}' did not specify BST_MIN_VERSION".format(self._plugin_type, kind),
reason="missing-min-version",
detail="Are you trying to use a BuildStream 1 plugin with a BuildStream 2 project ?",
)
try:
min_version_major, min_version_minor = utils._parse_version(plugin_type.BST_MIN_VERSION)
except UtilError as e:
raise PluginError(
"{} plugin '{}' specified malformed BST_MIN_VERSION: {}".format(
self._plugin_type, kind, plugin_type.BST_MIN_VERSION
),
reason="malformed-min-version",
detail="BST_MIN_VERSION must be specified as 'MAJOR.MINOR' with "
+ "numeric major and minor minimum required version numbers",
) from e
bst_major, bst_minor = utils._get_bst_api_version()
if min_version_major != bst_major:
raise PluginError(
"{} plugin '{}' requires BuildStream {}, but is being loaded with BuildStream {}".format(
self._plugin_type, kind, min_version_major, bst_major
),
reason="incompatible-major-version",
detail="You will need to find the correct version of this plugin for your project.",
)
if min_version_minor > bst_minor:
raise PluginError(
"{} plugin '{}' requires BuildStream {}, but is being loaded with BuildStream {}.{}".format(
self._plugin_type, kind, plugin_type.BST_MIN_VERSION, bst_major, bst_minor
),
reason="incompatible-minor-version",
detail="Please upgrade to BuildStream {}".format(plugin_type.BST_MIN_VERSION),
)