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 all 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 *
71 changes: 71 additions & 0 deletions conans/client/tools/settings.py
Original file line number Diff line number Diff line change
@@ -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
jgsogo marked this conversation as resolved.
Show resolved Hide resolved
: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
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 the required C++ "
"standard (17)." % cppstd, self.client.out)
158 changes: 158 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,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))

jgsogo marked this conversation as resolved.
Show resolved Hide resolved

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))
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