Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#5958 New tool: cppstd minimum version required #5997

Merged
merged 33 commits into from
Dec 3, 2019
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ebf47b8
#5958 New tool: cppstd minimum version required
uilianries Oct 29, 2019
3933e27
#5958 Fix bad indentation
uilianries Oct 29, 2019
45d1aff
#5958 Add gnu cppstd to the tests
uilianries Nov 5, 2019
cb20aea
#5958 Add gnu extensions
uilianries Nov 5, 2019
b31a875
#5958 Mock distro for Linux
uilianries Nov 5, 2019
bd8cfc9
#5958 Fix tests on MacOS
uilianries Nov 7, 2019
b1cc51c
#5958 Validate cppstd by str
uilianries Nov 8, 2019
421b294
#5958 Add valid_minimum_cppstd
uilianries Nov 11, 2019
5a1880b
Update conans/client/tools/settings.py
uilianries Nov 11, 2019
e56d491
Update conans/client/tools/settings.py
uilianries Nov 11, 2019
e25da13
Update conans/tools.py
uilianries Nov 11, 2019
561d70e
#5958 Apply @jgsogo suggestions
uilianries Nov 11, 2019
774b3af
#5958 Fix broken tests for cppstd
uilianries Nov 11, 2019
6cf41ba
Update conans/tools.py
uilianries Nov 11, 2019
2a9ec44
#5958 Remove duplicated import
uilianries Nov 11, 2019
adb1177
#5958 Fix C++98 comparison
uilianries Nov 11, 2019
f8bf1e1
#5958 Add unit tests for check_min_cppstd
uilianries Nov 11, 2019
3e55d5a
#5958 Fix Conanfile test
uilianries Nov 11, 2019
0a24ed5
#5958 Add unit tests for valid_min_cppstd
uilianries Nov 11, 2019
866c53e
#5958 Remove duplicated functional tests
uilianries Nov 11, 2019
ea7893d
#5958 Test improvements from code review
uilianries Nov 12, 2019
4383c2d
#5958 remove unused import
uilianries Nov 12, 2019
09236a5
#5958 cppstd must be a number
uilianries Nov 18, 2019
f4f2864
#5958 Dont use assert for production code
uilianries Nov 28, 2019
be0427e
#5958 Validate target OS using ConanFile settings
uilianries Nov 28, 2019
59fbfdb
#5958 Use default compiler cppstd when not included in settings
uilianries Dec 2, 2019
06cf9a2
#5958 Fix functional test
uilianries Dec 2, 2019
e12feb1
Review
lasote Dec 3, 2019
d0ab998
Fix bug
lasote Dec 3, 2019
e802b6c
#5958 Detect temporary C++ standard
uilianries Dec 3, 2019
724e805
#5958 Remove temporary C++ standard
uilianries Dec 3, 2019
1d49271
#5958 Do not validate OS for GNU extensions
uilianries Dec 3, 2019
bf53523
#5958 Update GNU extensions description
uilianries Dec 3, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions conans/client/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
63 changes: 63 additions & 0 deletions conans/client/tools/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from conans.errors import ConanInvalidConfiguration
from conans.client.build.cppstd_flags import cppstd_flag, cppstd_from_settings
from conans.client.tools.oss import OSInfo


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.

:param conanfile: ConanFile instance with cppstd to be compared
:param cppstd: Minimal cppstd version required
jgsogo marked this conversation as resolved.
Show resolved Hide resolved
:param gnu_extensions: GNU extension is required (e.g gnu17). This option ONLY works on Linux.
"""
assert (cppstd is not None), "Cannot check invalid cppstd version"
uilianries marked this conversation as resolved.
Show resolved Hide resolved
assert (conanfile is not None), "conanfile must be a ConanFile object"
assert (str(cppstd).isdigit()), "cppstd 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 = cppstd_from_settings(conanfile.settings)
if current_cppstd and gnu_extensions and "gnu" not in current_cppstd and OSInfo().is_linux:
uilianries marked this conversation as resolved.
Show resolved Hide resolved
raise ConanInvalidConfiguration("Current cppstd ({}) does not have GNU extensions, which is"
" required on Linux platform.".format(current_cppstd))
elif current_cppstd and less_than(current_cppstd, cppstd):
raise ConanInvalidConfiguration("Current cppstd ({}) is lower than required C++ standard "
"({}).".format(current_cppstd, cppstd))
else:
if OSInfo().is_linux and gnu_extensions and "gnu" not in cppstd:
cppstd = "gnu" + cppstd
result = cppstd_flag(conanfile.settings.get_safe("compiler"),
conanfile.settings.get_safe("compiler.version"),
cppstd)
if not result:
raise ConanInvalidConfiguration("Current compiler does not support the required "
"C++ standard ({}).".format(cppstd))
elif OSInfo().is_linux and gnu_extensions and "gnu" not in result:
raise ConanInvalidConfiguration("Current compiler does not support GNU extensions.")


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
51 changes: 51 additions & 0 deletions conans/test/functional/tools/cppstd_minimum_version_test.py
Original file line number Diff line number Diff line change
@@ -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 required C++ "
"standard (17)." % cppstd, self.client.out)
192 changes: 192 additions & 0 deletions conans/test/unittests/client/tools/cppstd_required_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
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

from conans.tools import check_min_cppstd, valid_min_cppstd


class UserProneTests(unittest.TestCase):
uilianries marked this conversation as resolved.
Show resolved Hide resolved

def test_check_none_cppstd(self):
""" Cppstd must use a valid number as described in settings.yml
"""
conanfile = MockConanfile(MockSettings({}))
with self.assertRaises(AssertionError) as asserts:
check_min_cppstd(conanfile, None, False)
self.assertEqual("Cannot check invalid cppstd version", str(asserts.exception))

with self.assertRaises(AssertionError) as asserts:
valid_min_cppstd(conanfile, None, False)
self.assertEqual("Cannot check invalid cppstd version", str(asserts.exception))

def test_check_none_conanfile(self):
""" conanfile must be a ConanFile object
"""
with self.assertRaises(AssertionError) as raises:
check_min_cppstd(None, "17", False)
self.assertEqual("conanfile must be a ConanFile object", str(raises.exception))

jgsogo marked this conversation as resolved.
Show resolved Hide resolved
with self.assertRaises(AssertionError) as raises:
valid_min_cppstd(None, "17", False)
self.assertEqual("conanfile must be a ConanFile object", str(raises.exception))

def test_check_cppstd_type(self):
""" cppstd must be a number
"""
conanfile = MockConanfile(MockSettings({}))
with self.assertRaises(AssertionError) as raises:
check_min_cppstd(conanfile, "gnu17", False)
self.assertEqual("cppstd 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 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
"""
with mock.patch("platform.system", mock.MagicMock(return_value="Linux")):
with mock.patch.object(OSInfo, '_get_linux_distro_info'):
conanfile = self._create_conanfile("gcc", "9", "Linux", "gnu17", "libstdc++")
check_min_cppstd(conanfile, cppstd, True)

conanfile.settings.values["compiler.cppstd"] = "17"
with self.assertRaises(ConanInvalidConfiguration) as raises:
check_min_cppstd(conanfile, cppstd, True)
self.assertEqual("Current cppstd (17) does not have GNU extensions, which is "
"required on Linux platform.", str(raises.exception))

@parameterized.expand(["98", "11", "14", "17"])
def test_check_min_cppstd_from_settings_with_extension_windows(self, cppstd):
""" GNU extensions has no effect on Windows for check_min_cppstd
"""
with mock.patch("platform.system", mock.MagicMock(return_value="Windows")):
conanfile = self._create_conanfile("gcc", "9", "Windows", "gnu17", "libstdc++")
check_min_cppstd(conanfile, cppstd, True)

conanfile.settings.values["compiler.cppstd"] = "17"
check_min_cppstd(conanfile, cppstd, True)

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 compiler does not support 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_flag", return_value="17"):
with self.assertRaises(ConanInvalidConfiguration) as raises:
check_min_cppstd(conanfile, "17", True)
self.assertEqual("Current compiler does not support GNU extensions.",
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
"""

with mock.patch("platform.system", mock.MagicMock(return_value="Linux")):
with mock.patch.object(OSInfo, '_get_linux_distro_info'):
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))

@parameterized.expand(["98", "11", "14", "17"])
def test_valid_min_cppstd_from_settings_with_extension_windows(self, cppstd):
""" GNU extensions has no effect on Windows for valid_min_cppstd
"""
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.assertTrue(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_flag", return_value="1z"):
self.assertFalse(valid_min_cppstd(conanfile, "20", True))
1 change: 1 addition & 0 deletions conans/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
uilianries marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down