plugin.py: Add API to allow plugins to raise deprecation warnings

A plugin's deprecation warning may be silenced by a project by adding
the plugin to the list 'supress-deprecation-warnings' in the project's
project.conf
diff --git a/buildstream/_versions.py b/buildstream/_versions.py
index a9eb86d..5affc78 100644
--- a/buildstream/_versions.py
+++ b/buildstream/_versions.py
@@ -23,7 +23,7 @@
 # This version is bumped whenever enhancements are made
 # to the `project.conf` format or the core element format.
 #
-BST_FORMAT_VERSION = 21
+BST_FORMAT_VERSION = 22
 
 
 # The base BuildStream artifact version
diff --git a/buildstream/plugin.py b/buildstream/plugin.py
index 2f51c88..c21ae93 100644
--- a/buildstream/plugin.py
+++ b/buildstream/plugin.py
@@ -164,6 +164,25 @@
        core format version :ref:`core format version <project_format_version>`.
     """
 
+    BST_PLUGIN_DEPRECATED = False
+    """True if this element plugin has been deprecated.
+
+    If this is set to true, BuildStream will emmit a deprecation
+    warning when this plugin is loaded. This deprecation warning may
+    be suppressed on a plugin by plugin basis by setting
+    ``suppress-deprecation-warnings: true`` in the relevent section of
+    the project's :ref:`plugin configuration overrides <project_overrides>`.
+
+    """
+
+    BST_PLUGIN_DEPRECATION_MESSAGE = ""
+    """ The message printed when this element shows a deprecation warning.
+
+    This should be set if BST_PLUGIN_DEPRECATED is True and should direct the user
+    to the deprecated plug-in's replacement.
+
+    """
+
     def __init__(self, name, context, project, provenance, type_tag):
 
         self.name = name
@@ -188,6 +207,12 @@
         self.__kind = modulename.split('.')[-1]
         self.debug("Created: {}".format(self))
 
+        # If this plugin has been deprecated, emit a warning.
+        if self.BST_PLUGIN_DEPRECATED and not self.__deprecation_warning_silenced():
+            detail = "Using deprecated plugin {}: {}".format(self.__kind,
+                                                             self.BST_PLUGIN_DEPRECATION_MESSAGE)
+            self.__message(MessageType.WARN, detail)
+
     def __del__(self):
         # Dont send anything through the Message() pipeline at destruction time,
         # any subsequent lookup of plugin by unique id would raise KeyError.
@@ -767,6 +792,20 @@
         else:
             return self.name
 
+    def __deprecation_warning_silenced(self):
+        if not self.BST_PLUGIN_DEPRECATED:
+            return False
+        else:
+            silenced_warnings = set()
+            project = self.__project
+            plugin_overrides = {**project.element_overrides, **project.source_overrides}
+
+            for key, value in self.node_items(plugin_overrides):
+                if value.get('suppress-deprecation-warnings', False):
+                    silenced_warnings.add(key)
+
+            return self.get_kind() in silenced_warnings
+
 
 # Hold on to a lookup table by counter of all instantiated plugins.
 # We use this to send the id back from child processes so we can lookup
diff --git a/tests/plugins/deprecationwarnings/deprecationwarnings.py b/tests/plugins/deprecationwarnings/deprecationwarnings.py
new file mode 100644
index 0000000..799f162
--- /dev/null
+++ b/tests/plugins/deprecationwarnings/deprecationwarnings.py
@@ -0,0 +1,41 @@
+import pytest
+import tempfile
+import os
+from buildstream.plugintestutils import cli
+from buildstream import _yaml
+import buildstream.plugins.elements.manual
+
+
+DATA_DIR = os.path.join(
+    os.path.dirname(os.path.realpath(__file__)),
+    "project"
+)
+
+_DEPRECATION_MESSAGE = "Here is some detail."
+_DEPRECATION_WARNING = "Using deprecated plugin deprecated_plugin: {}".format(_DEPRECATION_MESSAGE)
+
+
+@pytest.mark.datafiles(DATA_DIR)
+def test_deprecation_warning_present(cli, datafiles):
+    project = os.path.join(datafiles.dirname, datafiles.basename)
+    result = cli.run(project=project, args=['show', 'deprecated.bst'])
+    result.assert_success()
+    assert _DEPRECATION_WARNING in result.stderr
+
+
+@pytest.mark.datafiles(DATA_DIR)
+def test_suppress_deprecation_warning(cli, datafiles):
+    project = os.path.join(datafiles.dirname, datafiles.basename)
+    result = cli.run(project=project, args=['show', 'manual.bst'])
+
+    element_overrides = "elements:\n" \
+                        "  deprecated_plugin:\n" \
+                        "    suppress-deprecation-warnings : True\n"
+
+    project_conf = os.path.join(project, 'project.conf')
+    with open(project_conf, 'a') as f:
+        f.write(element_overrides)
+
+    result = cli.run(project=project, args=['show', 'deprecated.bst'])
+    result.assert_success()
+    assert _DEPRECATION_WARNING not in result.stderr
diff --git a/tests/plugins/deprecationwarnings/project/elements/deprecated.bst b/tests/plugins/deprecationwarnings/project/elements/deprecated.bst
new file mode 100644
index 0000000..e80bd91
--- /dev/null
+++ b/tests/plugins/deprecationwarnings/project/elements/deprecated.bst
@@ -0,0 +1 @@
+kind: deprecated_plugin
\ No newline at end of file
diff --git a/tests/plugins/deprecationwarnings/project/plugins/elements/deprecated_plugin.py b/tests/plugins/deprecationwarnings/project/plugins/elements/deprecated_plugin.py
new file mode 100644
index 0000000..a8e3956
--- /dev/null
+++ b/tests/plugins/deprecationwarnings/project/plugins/elements/deprecated_plugin.py
@@ -0,0 +1,11 @@
+from buildstream import BuildElement, SandboxFlags
+
+
+class DeprecatedPlugin(BuildElement):
+    BST_PLUGIN_DEPRECATED = True
+    BST_PLUGIN_DEPRECATION_MESSAGE = "Here is some detail."
+
+
+# Plugin entry point
+def setup():
+    return DeprecatedPlugin
diff --git a/tests/plugins/deprecationwarnings/project/plugins/elements/deprecated_plugin.yaml b/tests/plugins/deprecationwarnings/project/plugins/elements/deprecated_plugin.yaml
new file mode 100644
index 0000000..1c07cd8
--- /dev/null
+++ b/tests/plugins/deprecationwarnings/project/plugins/elements/deprecated_plugin.yaml
@@ -0,0 +1,22 @@
+# Deprecated-plugin build element does not provide any default
+# build commands
+config:
+
+  # Commands for configuring the software
+  #
+  configure-commands: []
+
+  # Commands for building the software
+  #
+  build-commands: []
+
+  # Commands for installing the software into a
+  # destination folder
+  #
+  install-commands: []
+
+  # Commands for stripping installed binaries
+  #
+  strip-commands:
+  - |
+    %{strip-binaries}
\ No newline at end of file
diff --git a/tests/plugins/deprecationwarnings/project/project.conf b/tests/plugins/deprecationwarnings/project/project.conf
new file mode 100644
index 0000000..18e368f
--- /dev/null
+++ b/tests/plugins/deprecationwarnings/project/project.conf
@@ -0,0 +1,15 @@
+# Unique project name
+name: deprecation-warnings
+
+# Required BuildStream format version
+format-version: 20
+
+# Subdirectory where elements are stored
+element-path: elements
+
+plugins:
+
+- origin: local
+  path: plugins/elements
+  elements:
+    deprecated_plugin: 0