Repository: conan-io/conan

Issue:
Add InvalidConfigurationException to be raised in Conanfile.configure()
Copy here the issue discussed at conan-package-tools https://github.com/conan-io/conan-package-tools/issues/146:

> Motivation.- Protobuf package has an issue with windows/shared configuration so we have to disable it and prevent CI to run those jobs in order to get a green badge.
>
> What to ask for.- I think that it would be useful to have a conan.errors.InvalidConfigurationException (better name?) to be raised from configure() method in recipes instead of the generic ConanException (as it is suggested here). This way users will get the proper information when running conan, but projects like conan-package-tools can catch this specific exception, handle it and mark the job in CI as successful.
>
> This way we don't need to tweak build.py files to avoid running certain configurations, so we move forwards to a even more standarized (unneeded) file.

Will require to add the exception type and document it ([here](https://docs.conan.io/en/latest/uploading_packages/bintray/conan_center_guide.html#recipe-quality)), I think that Conan's internals won't be affected by it.


Discussion:
The conan application has to return a different return number when that exception is raised, to detect even without running in a python script.

Resolving patch:
diff --git a/conans/client/command.py b/conans/client/command.py
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -9,7 +9,7 @@
 from conans.client.conan_command_output import CommandOutputer
 from conans.client.output import Color
 
-from conans.errors import ConanException, NoRemoteAvailable
+from conans.errors import ConanException, NoRemoteAvailable, ConanInvalidConfiguration
 from conans.model.ref import ConanFileReference
 from conans.util.config_parser import get_bool_from_text
 from conans.util.log import logger
@@ -17,6 +17,16 @@
 from conans.unicode import get_cwd
 
 
+# Exit codes for conan command:
+SUCCESS = 0                         # 0: Success (done)
+ERROR_GENERAL = 1                   # 1: General ConanException error (done)
+ERROR_MIGRATION = 2                 # 2: Migration error
+USER_CTRL_C = 3                     # 3: Ctrl+C
+USER_CTRL_BREAK = 4                 # 4: Ctrl+Break
+ERROR_SIGTERM = 5                   # 5: SIGTERM
+ERROR_INVALID_CONFIGURATION = 6     # 6: Invalid configuration (done)
+
+
 class Extender(argparse.Action):
     """Allows to use the same flag several times in a command and creates a list with the values.
     For example:
@@ -1267,7 +1277,7 @@ def run(self, *args):
         """HIDDEN: entry point for executing commands, dispatcher to class
         methods
         """
-        errors = False
+        ret_code = SUCCESS
         try:
             try:
                 command = args[0][0]
@@ -1287,24 +1297,28 @@ def run(self, *args):
             method(args[0][1:])
         except KeyboardInterrupt as exc:
             logger.error(exc)
-            errors = True
+            ret_code = SUCCESS
         except SystemExit as exc:
             if exc.code != 0:
                 logger.error(exc)
                 self._user_io.out.error("Exiting with code: %d" % exc.code)
-            errors = exc.code
+            ret_code = exc.code
+        except ConanInvalidConfiguration as exc:
+            ret_code = ERROR_INVALID_CONFIGURATION
+            msg = exception_message_safe(exc)
+            self._user_io.out.error(msg)
         except ConanException as exc:
-            errors = True
+            ret_code = ERROR_GENERAL
             msg = exception_message_safe(exc)
             self._user_io.out.error(msg)
         except Exception as exc:
             import traceback
             print(traceback.format_exc())
-            errors = True
+            ret_code = ERROR_GENERAL
             msg = exception_message_safe(exc)
             self._user_io.out.error(msg)
 
-        return errors
+        return ret_code
 
 
 def get_reference_fields(arg_reference):
@@ -1394,11 +1408,13 @@ def main(args):
         2: Migration error
         3: Ctrl+C
         4: Ctrl+Break
+        5: SIGTERM
+        6: Invalid configuration (done)
     """
     try:
         conan_api, client_cache, user_io = Conan.factory()
     except ConanException:  # Error migrating
-        sys.exit(2)
+        sys.exit(ERROR_MIGRATION)
 
     outputer = CommandOutputer(user_io, client_cache)
     command = Command(conan_api, client_cache, user_io, outputer)
@@ -1408,15 +1424,15 @@ def main(args):
 
         def ctrl_c_handler(_, __):
             print('You pressed Ctrl+C!')
-            sys.exit(3)
+            sys.exit(USER_CTRL_C)
 
         def sigterm_handler(_, __):
             print('Received SIGTERM!')
-            sys.exit(5)
+            sys.exit(ERROR_SIGTERM)
 
         def ctrl_break_handler(_, __):
             print('You pressed Ctrl+Break!')
-            sys.exit(4)
+            sys.exit(USER_CTRL_BREAK)
 
         signal.signal(signal.SIGINT, ctrl_c_handler)
         signal.signal(signal.SIGTERM, sigterm_handler)
diff --git a/conans/errors.py b/conans/errors.py
--- a/conans/errors.py
+++ b/conans/errors.py
@@ -22,6 +22,9 @@ def conanfile_exception_formatter(conanfile_name, func_name):
     """
     try:
         yield
+    except ConanInvalidConfiguration as exc:
+        msg = "{}: Invalid configuration: {}".format(conanfile_name, exc)  # TODO: Move from here?
+        raise ConanInvalidConfiguration(msg)
     except Exception as exc:
         msg = _format_conanfile_exception(conanfile_name, func_name, exc)
         raise ConanExceptionInUserConanfileMethod(msg)
@@ -95,6 +98,10 @@ class ConanExceptionInUserConanfileMethod(ConanException):
     pass
 
 
+class ConanInvalidConfiguration(ConanExceptionInUserConanfileMethod):
+    pass
+
+
 # Remote exceptions #
 class InternalErrorException(ConanException):
     """


Test patch:
diff --git a/conans/test/command/invalid_configuration_test.py b/conans/test/command/invalid_configuration_test.py
new file mode 100644
--- /dev/null
+++ b/conans/test/command/invalid_configuration_test.py
@@ -0,0 +1,61 @@
+
+import unittest
+
+from conans.test.utils.tools import TestClient
+from conans.client.command import ERROR_INVALID_CONFIGURATION
+
+
+class InvalidConfigurationTest(unittest.TestCase):
+
+    def setUp(self):
+        self.client = TestClient()
+        self.client.save({"conanfile.py": """
+from conans import ConanFile
+from conans.errors import ConanInvalidConfiguration
+
+class MyPkg(ConanFile):
+    settings = "os", "compiler", "build_type", "arch"
+    
+    def configure(self):
+        if self.settings.compiler.version == "12":
+            raise ConanInvalidConfiguration("user says that compiler.version=12 is invalid")
+
+    """})
+        settings = "-s os=Windows -s compiler='Visual Studio' -s compiler.version={ver}"
+        self.settings_msvc15 = settings.format(ver="15")
+        self.settings_msvc12 = settings.format(ver="12")
+
+    def test_install_method(self):
+        self.client.run("install . %s" % self.settings_msvc15)
+
+        error = self.client.run("install . %s" % self.settings_msvc12, ignore_error=True)
+        self.assertEqual(error, ERROR_INVALID_CONFIGURATION)
+        self.assertIn("Invalid configuration: user says that compiler.version=12 is invalid",
+                      self.client.out)
+
+    def test_create_method(self):
+        self.client.run("create . name/ver@jgsogo/test %s" % self.settings_msvc15)
+
+        error = self.client.run("create . name/ver@jgsogo/test %s" % self.settings_msvc12,
+                                ignore_error=True)
+        self.assertEqual(error, ERROR_INVALID_CONFIGURATION)
+        self.assertIn("name/ver@jgsogo/test: Invalid configuration: user says that "
+                      "compiler.version=12 is invalid", self.client.out)
+
+    def test_as_requirement(self):
+        self.client.run("create . name/ver@jgsogo/test %s" % self.settings_msvc15)
+        self.client.save({"other/conanfile.py": """
+from conans import ConanFile
+from conans.errors import ConanInvalidConfiguration
+
+class MyPkg(ConanFile):
+    requires = "name/ver@jgsogo/test"
+    settings = "os", "compiler", "build_type", "arch"
+    """})
+        self.client.run("create other/ other/ver@jgsogo/test %s" % self.settings_msvc15)
+
+        error = self.client.run("create other/ other/ver@jgsogo/test %s" % self.settings_msvc12,
+                                ignore_error=True)
+        self.assertEqual(error, ERROR_INVALID_CONFIGURATION)
+        self.assertIn("name/ver@jgsogo/test: Invalid configuration: user says that "
+                      "compiler.version=12 is invalid", self.client.out)