diff options
-rw-r--r-- | buildstream/_exceptions.py | 4 | ||||
-rw-r--r-- | buildstream/_plugincontext.py | 26 | ||||
-rw-r--r-- | buildstream/_project.py | 31 | ||||
-rw-r--r-- | buildstream/utils.py | 27 | ||||
-rw-r--r-- | setup.cfg | 2 | ||||
-rw-r--r-- | tests/format/project.py | 8 | ||||
-rw-r--r-- | tests/format/project/not-bst-1/project.conf | 4 | ||||
-rw-r--r-- | tests/plugins/bst2.py | 60 | ||||
-rw-r--r-- | tests/plugins/bst2/elements/bst2.py | 19 | ||||
-rw-r--r-- | tests/plugins/bst2/elements/malformed.py | 19 | ||||
-rw-r--r-- | tests/plugins/bst2/sources/bst2.py | 32 | ||||
-rw-r--r-- | tests/plugins/bst2/sources/malformed.py | 32 |
12 files changed, 261 insertions, 3 deletions
diff --git a/buildstream/_exceptions.py b/buildstream/_exceptions.py index d4ab1ea52..8f3f17b9d 100644 --- a/buildstream/_exceptions.py +++ b/buildstream/_exceptions.py @@ -137,8 +137,8 @@ class BstError(Exception): # or by the base :class:`.Plugin` element itself. # class PluginError(BstError): - def __init__(self, message, reason=None, temporary=False): - super().__init__(message, domain=ErrorDomain.PLUGIN, reason=reason, temporary=False) + def __init__(self, message, *, detail=None, reason=None, temporary=False): + super().__init__(message, domain=ErrorDomain.PLUGIN, detail=detail, reason=reason, temporary=False) # LoadErrorReason diff --git a/buildstream/_plugincontext.py b/buildstream/_plugincontext.py index 5a7097485..10e32e58a 100644 --- a/buildstream/_plugincontext.py +++ b/buildstream/_plugincontext.py @@ -22,6 +22,7 @@ import inspect from ._exceptions import PluginError, LoadError, LoadErrorReason from . import utils +from .utils import UtilError # A Context for loading plugin types @@ -223,6 +224,31 @@ class PluginContext(): plugin_type.BST_REQUIRED_VERSION_MAJOR, plugin_type.BST_REQUIRED_VERSION_MINOR)) + # If a BST_MIN_VERSION was specified, then we need to raise an error + # that we are loading a plugin which targets the wrong BuildStream version. + # + try: + min_version = plugin_type.BST_MIN_VERSION + except AttributeError: + return + + # Handle malformed version string specified by plugin + # + try: + major, minor = utils._parse_version(min_version) + except UtilError as e: + raise PluginError( + "Loaded plugin '{}' is not a BuildStream 1 plugin".format(kind), + detail="Error parsing BST_MIN_VERSION: {}".format(e), + reason="plugin-version-mismatch" + ) from e + + raise PluginError( + "Loaded plugin '{}' is a BuildStream {} plugin".format(kind, major), + detail="You need to use BuildStream 1 plugins with BuildStream 1 projects", + reason="plugin-version-mismatch" + ) + # _assert_plugin_format() # # Helper to raise a PluginError if the loaded plugin is of a lesser version then diff --git a/buildstream/_project.py b/buildstream/_project.py index 5530aa23d..0f327c66d 100644 --- a/buildstream/_project.py +++ b/buildstream/_project.py @@ -401,6 +401,37 @@ class Project(): "Project requested format version {}, but BuildStream {}.{} only supports up until format version {}" .format(format_version, major, minor, BST_FORMAT_VERSION)) + # Since BuildStream 2, project.conf is required to specify min-version. + # + # Detect this and raise an error, indicating which major version of BuildStream + # should be used for this project. + # + min_version = _yaml.node_get(pre_config_node, str, 'min-version', default_value=None) + if min_version: + + # Handle case of malformed min-version + # + try: + major, minor = utils._parse_version(min_version) + except UtilError as e: + raise LoadError( + LoadErrorReason.UNSUPPORTED_PROJECT, + "This is not a BuildStream 1 project: {}".format(e) + ) from e + + # Raise a helpful error indicating what the user should do to + # use this project. + # + raise LoadError( + LoadErrorReason.UNSUPPORTED_PROJECT, + "Tried to load a BuildStream {} project with BuildStream 1".format(major), + + # TODO: Include a link to the appropriate documentation for parallel + # installing different BuildStream versions. + # + detail="Please install at least BuildStream {}.{} to use this project".format(major, minor) + ) + # FIXME: # # Performing this check manually in the absense diff --git a/buildstream/utils.py b/buildstream/utils.py index d02777897..f141cb15d 100644 --- a/buildstream/utils.py +++ b/buildstream/utils.py @@ -1207,3 +1207,30 @@ def _deduplicate(iterable, key=None): if k not in seen: seen_add(k) yield element + + +# _parse_version(): +# +# Args: +# version (str): The file name from which to determine compression +# +# Returns: +# A 2-tuple of form (major_version: int, minor_version: int) +# +# Raises: +# UtilError: In the case of a malformed version string +# +def _parse_version(version): + + try: + versions = version.split(".") + except AttributeError as e: + raise UtilError("Malformed version string: {}".format(version),) + + try: + major = int(versions[0]) + minor = int(versions[1]) + except (IndexError, ValueError): + raise UtilError("Malformed version string: {}".format(version),) + + return major, minor @@ -12,7 +12,7 @@ test=pytest [tool:pytest] addopts = --verbose --basetemp ./tmp --pep8 --pylint --pylint-rcfile=.pylintrc --durations=20 -norecursedirs = tests/integration/project integration-cache tmp __pycache__ .eggs +norecursedirs = tests/integration/project tests/plugins/bst2 integration-cache tmp __pycache__ .eggs python_files = tests/*/*.py pep8maxlinelength = 119 pep8ignore = diff --git a/tests/format/project.py b/tests/format/project.py index 83fd38489..e36932eb5 100644 --- a/tests/format/project.py +++ b/tests/format/project.py @@ -117,6 +117,14 @@ def test_project_unsupported(cli, datafiles): result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.UNSUPPORTED_PROJECT) +@pytest.mark.datafiles(os.path.join(DATA_DIR)) +def test_project_unsupported_not_bst1(cli, datafiles): + project = os.path.join(datafiles.dirname, datafiles.basename, "not-bst-1") + + result = cli.run(project=project, args=['workspace', 'list']) + result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.UNSUPPORTED_PROJECT) + + @pytest.mark.datafiles(os.path.join(DATA_DIR, 'element-path')) def test_missing_element_path_directory(cli, datafiles): project = os.path.join(datafiles.dirname, datafiles.basename) diff --git a/tests/format/project/not-bst-1/project.conf b/tests/format/project/not-bst-1/project.conf new file mode 100644 index 000000000..02ad4f504 --- /dev/null +++ b/tests/format/project/not-bst-1/project.conf @@ -0,0 +1,4 @@ +# A BuildStream 2 project +name: foo + +min-version: 2.0 diff --git a/tests/plugins/bst2.py b/tests/plugins/bst2.py new file mode 100644 index 000000000..ca7529dfc --- /dev/null +++ b/tests/plugins/bst2.py @@ -0,0 +1,60 @@ +# Pylint doesn't play well with fixtures and dependency injection from pytest +# pylint: disable=redefined-outer-name + +# +# This test case tests the failure modes of loading a plugin +# after it has already been discovered via it's origin. +# + +import os +import pytest + +from buildstream._exceptions import ErrorDomain +from tests.testutils import cli # pylint: disable=unused-import +from buildstream import _yaml + + +DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "bst2") + + +# Sets up the element.bst file so that it requires a source +# or element plugin. +# +def setup_element(project_path, plugin_type, plugin_name): + element_path = os.path.join(project_path, "element.bst") + + if plugin_type == "elements": + element = {"kind": plugin_name} + else: + element = {"kind": "manual", "sources": [{"kind": plugin_name}]} + + _yaml.dump(element, element_path) + + +#################################################### +# Tests # +#################################################### +@pytest.mark.datafiles(DATA_DIR) +@pytest.mark.parametrize("plugin_type", ["elements", "sources"]) +@pytest.mark.parametrize("plugin", ["bst2", "malformed"]) +def test_plugin_bst2(cli, datafiles, plugin_type, plugin): + project = str(datafiles) + project_conf_path = os.path.join(project, "project.conf") + project_conf = { + "name": "test", + "plugins": [ + { + "origin": "local", + "path": plugin_type, + plugin_type: { + plugin: 0 + } + } + ] + } + _yaml.dump(project_conf, project_conf_path) + + setup_element(project, plugin_type, plugin) + + result = cli.run(project=project, args=["show", "element.bst"]) + result.assert_main_error(ErrorDomain.PLUGIN, "plugin-version-mismatch") diff --git a/tests/plugins/bst2/elements/bst2.py b/tests/plugins/bst2/elements/bst2.py new file mode 100644 index 000000000..34a7e4398 --- /dev/null +++ b/tests/plugins/bst2/elements/bst2.py @@ -0,0 +1,19 @@ +from buildstream import Element + + +class Found(Element): + BST_MIN_VERSION = "2.0" + + def configure(self, node): + pass + + def preflight(self): + pass + + def get_unique_key(self): + return {} + + +# Plugin entry point +def setup(): + return Found diff --git a/tests/plugins/bst2/elements/malformed.py b/tests/plugins/bst2/elements/malformed.py new file mode 100644 index 000000000..d3fe97a6a --- /dev/null +++ b/tests/plugins/bst2/elements/malformed.py @@ -0,0 +1,19 @@ +from buildstream import Element + + +class Found(Element): + BST_MIN_VERSION = 5 + + def configure(self, node): + pass + + def preflight(self): + pass + + def get_unique_key(self): + return {} + + +# Plugin entry point +def setup(): + return Found diff --git a/tests/plugins/bst2/sources/bst2.py b/tests/plugins/bst2/sources/bst2.py new file mode 100644 index 000000000..4ab40f005 --- /dev/null +++ b/tests/plugins/bst2/sources/bst2.py @@ -0,0 +1,32 @@ +from buildstream import Source + + +class Found(Source): + BST_MIN_VERSION = "2.0" + + def configure(self, node): + pass + + def preflight(self): + pass + + def get_unique_key(self): + return {} + + def load_ref(self, node): + pass + + def get_ref(self): + return {} + + def set_ref(self, ref, node): + pass + + def is_cached(self): + return False + + +# Plugin entry point +def setup(): + + return Found diff --git a/tests/plugins/bst2/sources/malformed.py b/tests/plugins/bst2/sources/malformed.py new file mode 100644 index 000000000..786e3d619 --- /dev/null +++ b/tests/plugins/bst2/sources/malformed.py @@ -0,0 +1,32 @@ +from buildstream import Source + + +class Found(Source): + BST_MIN_VERSION = "a pony" + + def configure(self, node): + pass + + def preflight(self): + pass + + def get_unique_key(self): + return {} + + def load_ref(self, node): + pass + + def get_ref(self): + return {} + + def set_ref(self, ref, node): + pass + + def is_cached(self): + return False + + +# Plugin entry point +def setup(): + + return Found |