diff --git a/conans/client/tools/__init__.py b/conans/client/tools/__init__.py index 4cb4b673d38..07bd328c1a2 100644 --- a/conans/client/tools/__init__.py +++ b/conans/client/tools/__init__.py @@ -15,6 +15,8 @@ # noinspection PyUnresolvedReferences from .scm import * # noinspection PyUnresolvedReferences +from .settings import * +# noinspection PyUnresolvedReferences from .system_pm import * # noinspection PyUnresolvedReferences from .win import * diff --git a/conans/client/tools/settings.py b/conans/client/tools/settings.py new file mode 100644 index 00000000000..f5e70c5cd31 --- /dev/null +++ b/conans/client/tools/settings.py @@ -0,0 +1,71 @@ +from conans.client.build.cppstd_flags import cppstd_default +from conans.errors import ConanInvalidConfiguration, ConanException + + +def check_min_cppstd(conanfile, cppstd, gnu_extensions=False): + """ Check if current cppstd fits the minimal version required. + + In case the current cppstd doesn't fit the minimal version required + by cppstd, a ConanInvalidConfiguration exception will be raised. + + 1. If settings.compiler.cppstd, the tool will use settings.compiler.cppstd to compare + 2. It not settings.compiler.cppstd, the tool will use compiler to compare (reading the + default from cppstd_default) + 3. If not settings.compiler is present (not declared in settings) will raise because it + cannot compare. + + :param conanfile: ConanFile instance with cppstd to be compared + :param cppstd: Minimal cppstd version required + :param gnu_extensions: GNU extension is required (e.g gnu17) + """ + if not str(cppstd).isdigit(): + raise ConanException("cppstd parameter must be a number") + + def less_than(lhs, rhs): + def extract_cpp_version(_cppstd): + return str(_cppstd).replace("gnu", "") + + def add_millennium(_cppstd): + return "19%s" % _cppstd if _cppstd == "98" else "20%s" % _cppstd + + lhs = add_millennium(extract_cpp_version(lhs)) + rhs = add_millennium(extract_cpp_version(rhs)) + return lhs < rhs + + def check_required_gnu_extension(_cppstd): + if gnu_extensions and "gnu" not in _cppstd: + raise ConanInvalidConfiguration("The cppstd GNU extension is required") + + def deduced_cppstd(): + cppstd = conanfile.settings.get_safe("compiler.cppstd") + if cppstd: + return cppstd + + compiler = conanfile.settings.get_safe("compiler") + compiler_version = conanfile.settings.get_safe("compiler.version") + if not compiler or not compiler_version: + raise ConanException("Could not obtain cppstd because there is no declared " + "compiler in the 'settings' field of the recipe.") + return cppstd_default(compiler, compiler_version) + + current_cppstd = deduced_cppstd() + check_required_gnu_extension(current_cppstd) + + if less_than(current_cppstd, cppstd): + raise ConanInvalidConfiguration("Current cppstd ({}) is lower than the required C++ " + "standard ({}).".format(current_cppstd, cppstd)) + + +def valid_min_cppstd(conanfile, cppstd, gnu_extensions=False): + """ Validate if current cppstd fits the minimal version required. + + :param conanfile: ConanFile instance with cppstd to be compared + :param cppstd: Minimal cppstd version required + :param gnu_extensions: GNU extension is required (e.g gnu17). This option ONLY works on Linux. + :return: True, if current cppstd matches the required cppstd version. Otherwise, False. + """ + try: + check_min_cppstd(conanfile, cppstd, gnu_extensions) + except ConanInvalidConfiguration: + return False + return True diff --git a/conans/test/functional/tools/cppstd_minimum_version_test.py b/conans/test/functional/tools/cppstd_minimum_version_test.py new file mode 100644 index 00000000000..632cdf1d9fd --- /dev/null +++ b/conans/test/functional/tools/cppstd_minimum_version_test.py @@ -0,0 +1,51 @@ +import unittest +from parameterized import parameterized +from textwrap import dedent + +from conans.test.utils.tools import TestClient + + +class CppStdMinimumVersionTests(unittest.TestCase): + + CONANFILE = dedent(""" + import os + from conans import ConanFile + from conans.tools import check_min_cppstd, valid_min_cppstd + + class Fake(ConanFile): + name = "fake" + version = "0.1" + settings = "compiler" + + def configure(self): + check_min_cppstd(self, "17", False) + self.output.info("valid standard") + assert valid_min_cppstd(self, "17", False) + """) + + PROFILE = dedent(""" + [settings] + compiler=gcc + compiler.version=9 + compiler.libcxx=libstdc++ + {} + """) + + def setUp(self): + self.client = TestClient() + self.client.save({"conanfile.py": CppStdMinimumVersionTests.CONANFILE}) + + @parameterized.expand(["17", "gnu17"]) + def test_cppstd_from_settings(self, cppstd): + profile = CppStdMinimumVersionTests.PROFILE.replace("{}", "compiler.cppstd=%s" % cppstd) + self.client.save({"myprofile": profile}) + self.client.run("create . user/channel -pr myprofile") + self.assertIn("valid standard", self.client.out) + + @parameterized.expand(["11", "gnu11"]) + def test_invalid_cppstd_from_settings(self, cppstd): + profile = CppStdMinimumVersionTests.PROFILE.replace("{}", "compiler.cppstd=%s" % cppstd) + self.client.save({"myprofile": profile}) + self.client.run("create . user/channel -pr myprofile", assert_error=True) + self.assertIn("Invalid configuration: Current cppstd (%s) is lower than the required C++ " + "standard (17)." % cppstd, self.client.out) diff --git a/conans/test/unittests/client/tools/cppstd_required_test.py b/conans/test/unittests/client/tools/cppstd_required_test.py new file mode 100644 index 00000000000..35d27a4db86 --- /dev/null +++ b/conans/test/unittests/client/tools/cppstd_required_test.py @@ -0,0 +1,158 @@ +import unittest +from mock import mock +from parameterized import parameterized + +from conans.test.utils.conanfile import MockConanfile, MockSettings +from conans.client.tools import OSInfo +from conans.errors import ConanInvalidConfiguration, ConanException + +from conans.tools import check_min_cppstd, valid_min_cppstd + + +class UserInputTests(unittest.TestCase): + + def test_check_cppstd_type(self): + """ cppstd must be a number + """ + conanfile = MockConanfile(MockSettings({})) + with self.assertRaises(ConanException) as raises: + check_min_cppstd(conanfile, "gnu17", False) + self.assertEqual("cppstd parameter must be a number", str(raises.exception)) + + +class CheckMinCppStdTests(unittest.TestCase): + + def _create_conanfile(self, compiler, version, os, cppstd, libcxx=None): + settings = MockSettings({"arch": "x86_64", + "build_type": "Debug", + "os": os, + "compiler": compiler, + "compiler.version": version, + "compiler.cppstd": cppstd}) + if libcxx: + settings.values["compiler.libcxx"] = libcxx + conanfile = MockConanfile(settings) + return conanfile + + @parameterized.expand(["98", "11", "14", "17"]) + def test_check_min_cppstd_from_settings(self, cppstd): + """ check_min_cppstd must accept cppstd less/equal than cppstd in settings + """ + conanfile = self._create_conanfile("gcc", "9", "Linux", "17", "libstdc++") + check_min_cppstd(conanfile, cppstd, False) + + @parameterized.expand(["98", "11", "14"]) + def test_check_min_cppstd_from_outdated_settings(self, cppstd): + """ check_min_cppstd must raise when cppstd is greater when supported on settings + """ + conanfile = self._create_conanfile("gcc", "9", "Linux", cppstd, "libstdc++") + with self.assertRaises(ConanInvalidConfiguration) as raises: + check_min_cppstd(conanfile, "17", False) + self.assertEqual("Current cppstd ({}) is lower than the required C++ standard " + "(17).".format(cppstd), str(raises.exception)) + + @parameterized.expand(["98", "11", "14", "17"]) + def test_check_min_cppstd_from_settings_with_extension(self, cppstd): + """ current cppstd in settings must has GNU extension when extensions is enabled + """ + conanfile = self._create_conanfile("gcc", "9", "Linux", "gnu17", "libstdc++") + check_min_cppstd(conanfile, cppstd, True) + + conanfile.settings.values["compiler.cppstd"] = "17" + with self.assertRaises(ConanException) as raises: + check_min_cppstd(conanfile, cppstd, True) + self.assertEqual("The cppstd GNU extension is required", str(raises.exception)) + + def test_check_min_cppstd_unsupported_standard(self): + """ check_min_cppstd must raise when the compiler does not support a standard + """ + conanfile = self._create_conanfile("gcc", "9", "Linux", None, "libstdc++") + with self.assertRaises(ConanInvalidConfiguration) as raises: + check_min_cppstd(conanfile, "42", False) + self.assertEqual("Current cppstd (gnu14) is lower than the required C++ standard (42).", + str(raises.exception)) + + def test_check_min_cppstd_gnu_compiler_extension(self): + """ Current compiler must support GNU extension on Linux when extensions is required + """ + conanfile = self._create_conanfile("gcc", "9", "Linux", None, "libstdc++") + with mock.patch("platform.system", mock.MagicMock(return_value="Linux")): + with mock.patch.object(OSInfo, '_get_linux_distro_info'): + with mock.patch("conans.client.tools.settings.cppstd_default", return_value="17"): + with self.assertRaises(ConanException) as raises: + check_min_cppstd(conanfile, "17", True) + self.assertEqual("The cppstd GNU extension is required", str(raises.exception)) + + def test_no_compiler_declared(self): + conanfile = self._create_conanfile(None, None, "Linux", None, "libstdc++") + with self.assertRaises(ConanException) as raises: + check_min_cppstd(conanfile, "14", False) + self.assertEqual("Could not obtain cppstd because there is no declared compiler in the " + "'settings' field of the recipe.", str(raises.exception)) + + +class ValidMinCppstdTests(unittest.TestCase): + + def _create_conanfile(self, compiler, version, os, cppstd, libcxx=None): + settings = MockSettings({"arch": "x86_64", + "build_type": "Debug", + "os": os, + "compiler": compiler, + "compiler.version": version, + "compiler.cppstd": cppstd}) + if libcxx: + settings.values["compiler.libcxx"] = libcxx + conanfile = MockConanfile(settings) + return conanfile + + @parameterized.expand(["98", "11", "14", "17"]) + def test_valid_min_cppstd_from_settings(self, cppstd): + """ valid_min_cppstd must accept cppstd less/equal than cppstd in settings + """ + conanfile = self._create_conanfile("gcc", "9", "Linux", "17", "libstdc++") + self.assertTrue(valid_min_cppstd(conanfile, cppstd, False)) + + @parameterized.expand(["98", "11", "14"]) + def test_valid_min_cppstd_from_outdated_settings(self, cppstd): + """ valid_min_cppstd returns False when cppstd is greater when supported on settings + """ + conanfile = self._create_conanfile("gcc", "9", "Linux", cppstd, "libstdc++") + self.assertFalse(valid_min_cppstd(conanfile, "17", False)) + + @parameterized.expand(["98", "11", "14", "17"]) + def test_valid_min_cppstd_from_settings_with_extension(self, cppstd): + """ valid_min_cppstd must returns True when current cppstd in settings has GNU extension and + extensions is enabled + """ + conanfile = self._create_conanfile("gcc", "9", "Linux", "gnu17", "libstdc++") + self.assertTrue(valid_min_cppstd(conanfile, cppstd, True)) + + conanfile.settings.values["compiler.cppstd"] = "17" + self.assertFalse(valid_min_cppstd(conanfile, cppstd, True)) + + def test_valid_min_cppstd_unsupported_standard(self): + """ valid_min_cppstd must returns False when the compiler does not support a standard + """ + conanfile = self._create_conanfile("gcc", "9", "Linux", None, "libstdc++") + self.assertFalse(valid_min_cppstd(conanfile, "42", False)) + + def test_valid_min_cppstd_gnu_compiler_extension(self): + """ valid_min_cppstd must returns False when current compiler does not support GNU extension + on Linux and extensions is required + """ + conanfile = self._create_conanfile("gcc", "9", "Linux", None, "libstdc++") + with mock.patch("platform.system", mock.MagicMock(return_value="Linux")): + with mock.patch.object(OSInfo, '_get_linux_distro_info'): + with mock.patch("conans.client.tools.settings.cppstd_default", return_value="gnu1z"): + self.assertFalse(valid_min_cppstd(conanfile, "20", True)) + + @parameterized.expand(["98", "11", "14", "17"]) + def test_min_cppstd_mingw_windows(self, cppstd): + """ GNU extensions HAS effect on Windows when running a cross-building for Linux + """ + with mock.patch("platform.system", mock.MagicMock(return_value="Windows")): + conanfile = self._create_conanfile("gcc", "9", "Linux", "gnu17", "libstdc++") + self.assertTrue(valid_min_cppstd(conanfile, cppstd, True)) + + conanfile.settings.values["compiler.cppstd"] = "17" + self.assertFalse(valid_min_cppstd(conanfile, cppstd, True)) diff --git a/conans/tools.py b/conans/tools.py index 9aa84fa061c..007c4319fb0 100644 --- a/conans/tools.py +++ b/conans/tools.py @@ -19,6 +19,7 @@ from conans.client.tools.env import * # pylint: disable=unused-import from conans.client.tools.pkg_config import * # pylint: disable=unused-import from conans.client.tools.scm import * # pylint: disable=unused-import +from conans.client.tools.settings import * # pylint: disable=unused-import from conans.client.tools.apple import * from conans.client.tools.android import * # Tools form conans.util