Repository: conan-io/conan
Pull request #11446 (2022-06-13 11:22:59)

cppstd tools ported to develop
Changelog: Feature: Ported cppstd related tools from 2.X branch.
Docs: https://github.com/conan-io/docs/pull/XXXX

IMPORTANT: To merge this to develop2, discard all the changes and keep develop2 ones.


Patch:
diff --git a/conan/tools/build/__init__.py b/conan/tools/build/__init__.py
index f93b3a4dd..c7a2e667b 100644
--- a/conan/tools/build/__init__.py
+++ b/conan/tools/build/__init__.py
@@ -1,2 +1,4 @@
 from conan.tools.build.cpu import build_jobs
 from conan.tools.build.cross_building import cross_building, can_run
+from conan.tools.build.cppstd import check_min_cppstd, valid_min_cppstd, default_cppstd, \
+    supported_cppstd
diff --git a/conan/tools/build/cppstd.py b/conan/tools/build/cppstd.py
new file mode 100644
index 000000000..339a89cc2
--- /dev/null
+++ b/conan/tools/build/cppstd.py
@@ -0,0 +1,189 @@
+from conans.errors import ConanInvalidConfiguration, ConanException
+from conans.model.version import Version
+
+
+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.
+        4. If can not detect the default cppstd for settings.compiler, a exception will be raised.
+
+    :param conanfile: The current recipe object. Always use ``self``.
+    :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
+
+    current_cppstd = conanfile.settings.get_safe("compiler.cppstd")
+    if current_cppstd is None:
+        raise ConanInvalidConfiguration("The compiler.cppstd is not defined for this configuration")
+
+    if gnu_extensions and "gnu" not in current_cppstd:
+        raise ConanInvalidConfiguration("The cppstd GNU extension is required")
+
+    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: The current recipe object. Always use ``self``.
+    :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
+
+
+def default_cppstd(conanfile, compiler=None, compiler_version=None):
+    """
+    Get the default ``compiler.cppstd`` for the "conanfile.settings.compiler" and "conanfile
+    settings.compiler_version" or for the parameters "compiler" and "compiler_version" if specified.
+
+    :param conanfile: The current recipe object. Always use ``self``.
+    :param compiler: Name of the compiler e.g. gcc
+    :param compiler_version: Version of the compiler e.g. 12
+    :return: The default ``compiler.cppstd`` for the specified compiler
+    """
+    from conans.client.conf.detect import _cppstd_default
+    compiler = compiler or conanfile.settings.get_safe("compiler")
+    compiler_version = compiler_version or conanfile.settings.get_safe("compiler.version")
+    if not compiler or not compiler_version:
+        raise ConanException("Called default_cppstd with no compiler or no compiler.version")
+    return _cppstd_default(compiler, Version(compiler_version))
+
+
+def supported_cppstd(conanfile, compiler=None, compiler_version=None):
+    """
+    Get the a list of supported ``compiler.cppstd`` for the "conanfile.settings.compiler" and
+    "conanfile.settings.compiler_version" or for the parameters "compiler" and "compiler_version"
+    if specified.
+
+    :param conanfile: The current recipe object. Always use ``self``.
+    :param compiler: Name of the compiler e.g: gcc
+    :param compiler_version: Version of the compiler e.g: 12
+    :return: a list of supported ``cppstd`` values.
+    """
+    compiler = compiler or conanfile.settings.get_safe("compiler")
+    compiler_version = compiler_version or conanfile.settings.get_safe("compiler.version")
+    if not compiler or not compiler_version:
+        raise ConanException("Called supported_cppstd with no compiler or no compiler.version")
+
+    func = {"apple-clang": _apple_clang_supported_cppstd,
+            "gcc": _gcc_supported_cppstd,
+            "msvc": _msvc_supported_cppstd,
+            "clang": _clang_supported_cppstd,
+            "mcst-lcc": _mcst_lcc_supported_cppstd}.get(compiler)
+    if func:
+        return func(Version(compiler_version))
+    return None
+
+
+def _apple_clang_supported_cppstd(version):
+    """
+    ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20"]
+    """
+    if version < "4.0":
+        return []
+    if version < "5.1":
+        return ["98", "gnu98", "11", "gnu11"]
+    if version < "6.1":
+        return ["98", "gnu98", "11", "gnu11", "14", "gnu14"]
+    if version < "10.0":
+        return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17"]
+
+    return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20"]
+
+
+def _gcc_supported_cppstd(version):
+    """
+    ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20", "23", "gnu23"]
+    """
+    if version < "3.4":
+        return []
+    if version < "4.3":
+        return ["98", "gnu98"]
+    if version < "4.8":
+        return ["98", "gnu98", "11", "gnu11"]
+    if version < "5":
+        return ["98", "gnu98", "11", "gnu11", "14", "gnu14"]
+    if version < "8":
+        return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17"]
+    if version < "11":
+        return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20"]
+
+    return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20", "23", "gnu23"]
+
+
+def _msvc_supported_cppstd(version):
+    """
+    [14, 17, 20, 23]
+    """
+    if version < "190":
+        return []
+    if version < "191":
+        return ["14", "17"]
+    if version < "193":
+        return ["14", "17", "20"]
+
+    return ["14", "17", "20", "23"]
+
+
+def _clang_supported_cppstd(version):
+    """
+    ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20", "23", "gnu23"]
+    """
+    if version < "2.1":
+        return []
+    if version < "3.4":
+        return ["98", "gnu98", "11", "gnu11"]
+    if version < "3.5":
+        return ["98", "gnu98", "11", "gnu11", "14", "gnu14"]
+    if version < "6":
+        return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17"]
+    if version < "12":
+        return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20"]
+
+    return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20", "23", "gnu23"]
+
+
+def _mcst_lcc_supported_cppstd(version):
+    """
+    ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20", "23", "gnu23"]
+    """
+
+    if version < "1.21":
+        return ["98", "gnu98"]
+    if version < "1.24":
+        return ["98", "gnu98", "11", "gnu11", "14", "gnu14"]
+    if version < "1.25":
+        return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17"]
+
+    # FIXME: When cppstd 23 was introduced????
+
+    return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20"]
diff --git a/conans/pylint_plugin.py b/conans/pylint_plugin.py
index 892a51850..3cb8333c8 100644
--- a/conans/pylint_plugin.py
+++ b/conans/pylint_plugin.py
@@ -39,7 +39,7 @@ def transform_conanfile(node):
     }
 
     for f, t in dynamic_fields.items():
-        node.locals[f] = [t]
+        node.locals[f] = [i for i in t]
 
 
 MANAGER.register_transform(


Test patch:
diff --git a/conans/test/unittests/tools/build/test_cppstd.py b/conans/test/unittests/tools/build/test_cppstd.py
new file mode 100644
index 000000000..f81c30465
--- /dev/null
+++ b/conans/test/unittests/tools/build/test_cppstd.py
@@ -0,0 +1,192 @@
+import pytest
+
+from conan.tools.build import supported_cppstd, check_min_cppstd, valid_min_cppstd
+from conans.errors import ConanException, ConanInvalidConfiguration
+from conans.test.utils.mocks import MockSettings, MockConanfile
+
+
+@pytest.mark.parametrize("compiler,compiler_version,values", [
+    ("clang", "2.0", []),
+    ("clang", "2.1", ['98', 'gnu98', '11', 'gnu11']),
+    ("clang", "2.2", ['98', 'gnu98', '11', 'gnu11']),
+    ("clang", "3.1", ['98', 'gnu98', '11', 'gnu11']),
+    ("clang", "3.4", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14"]),
+    ("clang", "3.5", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14", "17", "gnu17"]),
+    ("clang", "4.9", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14", "17", "gnu17"]),
+    ("clang", "5", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14", "17", "gnu17"]),
+    ("clang", "6", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14", "17", "gnu17", "20", "gnu20"]),
+    ("clang", "12", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14", "17", "gnu17", "20",
+                     "gnu20", "23", "gnu23"])
+])
+def test_supported_cppstd_clang(compiler, compiler_version, values):
+    settings = MockSettings({"compiler": compiler, "compiler.version": compiler_version})
+    conanfile = MockConanfile(settings)
+    sot = supported_cppstd(conanfile)
+    assert sot == values
+
+
+def test_supported_cppstd_with_specific_values():
+    settings = MockSettings({})
+    conanfile = MockConanfile(settings)
+    sot = supported_cppstd(conanfile, "clang", "3.1")
+    assert sot == ['98', 'gnu98', '11', 'gnu11']
+
+
+def test_supported_cppstd_error():
+    settings = MockSettings({})
+    conanfile = MockConanfile(settings)
+    with pytest.raises(ConanException) as exc:
+        supported_cppstd(conanfile)
+    assert "Called supported_cppstd with no compiler or no compiler.version" in str(exc)
+
+
+@pytest.mark.parametrize("compiler,compiler_version,values", [
+    ("gcc", "2.0", []),
+    ("gcc", "3.4", ['98', 'gnu98']),
+    ("gcc", "4.2", ['98', 'gnu98']),
+    ("gcc", "4.3", ['98', 'gnu98', '11', 'gnu11']),
+    ("gcc", "4.8", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14"]),
+    ("gcc", "5", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14", "17", "gnu17"]),
+    ("gcc", "8", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14", "17", "gnu17", "20", "gnu20"]),
+    ("gcc", "11", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14", "17", "gnu17", "20", "gnu20",
+                   "23", "gnu23"])
+])
+def test_supported_cppstd_gcc(compiler, compiler_version, values):
+    settings = MockSettings({"compiler": compiler, "compiler.version": compiler_version})
+    conanfile = MockConanfile(settings)
+    sot = supported_cppstd(conanfile)
+    assert sot == values
+
+
+@pytest.mark.parametrize("compiler,compiler_version,values", [
+    ("apple-clang", "3.9", []),
+    ("apple-clang", "4.0", ['98', 'gnu98', '11', 'gnu11']),
+    ("apple-clang", "5.0", ['98', 'gnu98', '11', 'gnu11']),
+    ("apple-clang", "5.1", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14"]),
+    ("apple-clang", "6.1", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14", "17", "gnu17"]),
+    ("apple-clang", "9.5", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14", "17", "gnu17"]),
+    ("apple-clang", "10", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14", "17", "gnu17", "20",
+                           "gnu20"]),
+])
+def test_supported_cppstd_apple_clang(compiler, compiler_version, values):
+    settings = MockSettings({"compiler": compiler, "compiler.version": compiler_version})
+    conanfile = MockConanfile(settings)
+    sot = supported_cppstd(conanfile)
+    assert sot == values
+
+
+@pytest.mark.parametrize("compiler,compiler_version,values", [
+    ("msvc", "180", []),
+    ("msvc", "190", ['14', '17']),
+    ("msvc", "191", ['14', '17', '20']),
+    ("msvc", "193", ['14', '17', '20', '23']),
+])
+def test_supported_cppstd_msvc(compiler, compiler_version, values):
+    settings = MockSettings({"compiler": compiler, "compiler.version": compiler_version})
+    conanfile = MockConanfile(settings)
+    sot = supported_cppstd(conanfile)
+    assert sot == values
+
+
+@pytest.mark.parametrize("compiler,compiler_version,values", [
+    ("mcst-lcc", "1.20", ['98', 'gnu98']),
+    ("mcst-lcc", "1.21", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14"]),
+    ("mcst-lcc", "1.23", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14"]),
+    ("mcst-lcc", "1.24", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14", "17", "gnu17"]),
+    ("mcst-lcc", "1.25", ['98', 'gnu98', '11', 'gnu11', "14", "gnu14", "17", "gnu17", "20", "gnu20"])
+])
+def test_supported_cppstd_mcst(compiler, compiler_version, values):
+    settings = MockSettings({"compiler": compiler, "compiler.version": compiler_version})
+    conanfile = MockConanfile(settings)
+    sot = supported_cppstd(conanfile)
+    assert sot == values
+
+
+def test_check_cppstd_type():
+    """ cppstd must be a number
+    """
+    conanfile = MockConanfile(MockSettings({}))
+    with pytest.raises(ConanException) as exc:
+        check_min_cppstd(conanfile, "gnu17", False)
+
+    assert "cppstd parameter must be a number", str(exc)
+
+
+def _create_conanfile(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
+
+
+@pytest.mark.parametrize("cppstd", ["98", "11", "14", "17"])
+def test_check_min_cppstd_from_settings(cppstd):
+    """ check_min_cppstd must accept cppstd less/equal than cppstd in settings
+    """
+    conanfile = _create_conanfile("gcc", "9", "Linux", "17", "libstdc++")
+    check_min_cppstd(conanfile, cppstd, False)
+
+
+@pytest.mark.parametrize("cppstd", ["98", "11", "14"])
+def test_check_min_cppstd_from_outdated_settings(cppstd):
+    """ check_min_cppstd must raise when cppstd is greater when supported on settings
+    """
+    conanfile = _create_conanfile("gcc", "9", "Linux", cppstd, "libstdc++")
+    with pytest.raises(ConanInvalidConfiguration) as exc:
+        check_min_cppstd(conanfile, "17", False)
+    assert "Current cppstd ({}) is lower than the required C++ standard (17)." \
+           "".format(cppstd) == str(exc.value)
+
+
+@pytest.mark.parametrize("cppstd", ["98", "11", "14", "17"])
+def test_check_min_cppstd_from_settings_with_extension(cppstd):
+    """ current cppstd in settings must has GNU extension when extensions is enabled
+    """
+    conanfile = _create_conanfile("gcc", "9", "Linux", "gnu17", "libstdc++")
+    check_min_cppstd(conanfile, cppstd, True)
+
+    conanfile.settings.values["compiler.cppstd"] = "17"
+    with pytest.raises(ConanException) as raises:
+        check_min_cppstd(conanfile, cppstd, True)
+    assert "The cppstd GNU extension is required" == str(raises.value)
+
+
+@pytest.mark.parametrize("cppstd", ["98", "11", "14", "17"])
+def test_valid_min_cppstd_from_settings(cppstd):
+    """ valid_min_cppstd must accept cppstd less/equal than cppstd in settings
+    """
+    conanfile = _create_conanfile("gcc", "9", "Linux", "17", "libstdc++")
+    assert valid_min_cppstd(conanfile, cppstd, False)
+
+
+@pytest.mark.parametrize("cppstd", ["98", "11", "14"])
+def test_valid_min_cppstd_from_outdated_settings(cppstd):
+    """ valid_min_cppstd returns False when cppstd is greater when supported on settings
+    """
+    conanfile = _create_conanfile("gcc", "9", "Linux", cppstd, "libstdc++")
+    assert not valid_min_cppstd(conanfile, "17", False)
+
+
+@pytest.mark.parametrize("cppstd", ["98", "11", "14", "17"])
+def test_valid_min_cppstd_from_settings_with_extension(cppstd):
+    """ valid_min_cppstd must returns True when current cppstd in settings has GNU extension and
+        extensions is enabled
+    """
+    conanfile = _create_conanfile("gcc", "9", "Linux", "gnu17", "libstdc++")
+    assert valid_min_cppstd(conanfile, cppstd, True)
+
+    conanfile.settings.values["compiler.cppstd"] = "17"
+    assert not valid_min_cppstd(conanfile, cppstd, True)
+
+
+def test_valid_min_cppstd_unsupported_standard():
+    """ valid_min_cppstd must returns False when the compiler does not support a standard
+    """
+    conanfile = _create_conanfile("gcc", "9", "Linux", None, "libstdc++")
+    assert not valid_min_cppstd(conanfile, "42", False)