| # |
| # 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), |
| ) |