Repository: conan-io/conan

Issue:
[feature] Min (max) conan version checks from conan.conf
Companies would like to introduce a check to guarantee that everyone is using the same version of conan. That should be an easy check with a config in conan.conf.

Possibilities?:

- A min-version check
- A max-version check
- An exact version check
- A range


The modes could be raise or warn.


Discussion:

Resolving patch:
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -20,6 +20,7 @@
 from conans.client.cmd.uploader import CmdUpload
 from conans.client.cmd.user import user_set, users_clean, users_list, token_present
 from conans.client.conanfile.package import run_package_method
+from conans.client.conf.required_version import check_required_conan_version
 from conans.client.graph.graph import RECIPE_EDITABLE
 from conans.client.graph.graph_binaries import GraphBinariesAnalyzer
 from conans.client.graph.graph_manager import GraphManager
@@ -233,6 +234,7 @@ def __init__(self, cache_folder=None, output=None, user_io=None, http_requester=
         # Migration system
         migrator = ClientMigrator(self.cache_folder, Version(client_version), self.out)
         migrator.migrate()
+        check_required_conan_version(self.cache_folder, self.out)
         if not get_env(CONAN_V2_MODE_ENVVAR, False):
             # FIXME Remove in Conan 2.0
             sys.path.append(os.path.join(self.cache_folder, "python"))
diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py
--- a/conans/client/conf/__init__.py
+++ b/conans/client/conf/__init__.py
@@ -177,6 +177,7 @@ def get_default_settings_yml(force_v1=False):
     {% endif %}
 
     # config_install_interval = 1h
+    # required_conan_version = >=1.26
 
     [storage]
     # This is the default path, but you can write your own. It must be an absolute path or a
@@ -709,3 +710,10 @@ def config_install_interval(self):
         except Exception as e:
             raise ConanException("Incorrect definition of general.config_install_interval: %s"
                                  % interval)
+
+    @property
+    def required_conan_version(self):
+        try:
+            return self.get_item("general.required_conan_version")
+        except ConanException:
+            return None
diff --git a/conans/client/conf/required_version.py b/conans/client/conf/required_version.py
new file mode 100644
--- /dev/null
+++ b/conans/client/conf/required_version.py
@@ -0,0 +1,30 @@
+from conans.client.cache.cache import ClientCache
+from semver import satisfies, Range
+from conans import __version__ as client_version
+from conans.errors import ConanException
+
+
+def check_required_conan_version(cache_folder, out):
+    """ Check if the required Conan version in config file matches to the current Conan version
+
+            When required_conan_version is not configured, it's skipped
+            When required_conan_version is configured, Conan's version must matches the required
+            version
+            When it doesn't match, an ConanException is raised
+
+        :param cache_folder: Conan cache folder
+        :param out: Output stream
+        :return: None
+    """
+    cache = ClientCache(cache_folder, out)
+    required_version = cache.config.required_conan_version
+    if required_version:
+        try:
+            Range(required_version, False)
+        except ValueError:
+            raise ConanException("The required version expression '{}' is not valid."
+                                 .format(required_version))
+        result = satisfies(client_version, required_version)
+        if not result:
+            raise ConanException("The current Conan version ({}) does not match to the required"
+                                 " version ({}).".format(client_version, required_version))


Test patch:
diff --git a/conans/test/functional/configuration/required_version_test.py b/conans/test/functional/configuration/required_version_test.py
new file mode 100644
--- /dev/null
+++ b/conans/test/functional/configuration/required_version_test.py
@@ -0,0 +1,48 @@
+import unittest
+import mock
+from conans.test.utils.tools import TestClient
+from conans.errors import ConanException
+
+
+class RequiredVersionTest(unittest.TestCase):
+
+    @mock.patch("conans.client.conf.required_version.client_version", "1.26.0")
+    def test_wrong_version(self):
+        required_version = "1.23.0"
+        client = TestClient()
+        client.run("config set general.required_conan_version={}".format(required_version))
+        with self.assertRaises(ConanException) as error:
+            client.run("help")
+        self.assertIn("The current Conan version ({}) "
+                      "does not match to the required version ({})."
+                      .format("1.26.0", required_version), str(error.exception))
+
+    @mock.patch("conans.client.conf.required_version.client_version", "1.22.0")
+    def test_exact_version(self):
+        client = TestClient()
+        client.run("config set general.required_conan_version=1.22.0")
+        client.run("help")
+        self.assertNotIn("ERROR", client.out)
+
+    @mock.patch("conans.client.conf.required_version.client_version", "2.1.0")
+    def test_lesser_version(self):
+        client = TestClient()
+        client.run("config set general.required_conan_version=<3")
+        client.run("help")
+        self.assertNotIn("ERROR", client.out)
+
+    @mock.patch("conans.client.conf.required_version.client_version", "1.0.0")
+    def test_greater_version(self):
+        client = TestClient()
+        client.run("config set general.required_conan_version=>0.1.0")
+        client.run("help")
+        self.assertNotIn("ERROR", client.out)
+
+    def test_bad_format(self):
+        client = TestClient()
+        required_version = "1.0.0.0-foobar"
+        client.run("config set general.required_conan_version={}".format(required_version))
+        with self.assertRaises(ConanException) as error:
+            client.run("help", assert_error=True)
+        self.assertIn("The required version expression '{}' is not valid.".format(required_version),
+                      str(error.exception))