Repository: conan-io/conan
Pull request #12547 (2022-11-15 15:25:32)

test to capture behavior of validate()/validate_build() for 2.0
Changelog: Omit
Docs: Omit


Translate the 2.0 test in https://github.com/conan-io/conan/pull/12539, to see the effect and behavior in 1.X.
It works practically the same, except one scenario:

``client.run(f"install pkg/0.1@ {settings} -s compiler.cppstd=11")`` should fail if ``pkg`` contains:

```python
 def validate(self):
      if int(str(self.settings.compiler.cppstd)) < 14:
          raise ConanInvalidConfiguration("I need at least cppstd=14 to be used")
```

But it doesn't fail. It correctly fails in 2.0 https://github.com/conan-io/conan/pull/12539





Patch:
diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py
index eda75b0d6..698e050d2 100644
--- a/conan/tools/cmake/toolchain/blocks.py
+++ b/conan/tools/cmake/toolchain/blocks.py
@@ -12,6 +12,7 @@ from conan.tools.build.cross_building import cross_building
 from conan.tools.cmake.toolchain import CONAN_TOOLCHAIN_FILENAME
 from conan.tools.intel import IntelCC
 from conan.tools.microsoft.visual import is_msvc, msvc_version_to_toolset_version
+from conans.client.subsystems import deduce_subsystem, WINDOWS
 from conans.errors import ConanException
 from conans.util.files import load
 
@@ -153,7 +154,7 @@ class FPicBlock(Block):
     template = textwrap.dedent("""
         {% if fpic %}
         message(STATUS "Conan toolchain: Setting CMAKE_POSITION_INDEPENDENT_CODE={{ fpic }} (options.fPIC)")
-        set(CMAKE_POSITION_INDEPENDENT_CODE {{ fpic }} CACHE BOOL "Position independent code") 
+        set(CMAKE_POSITION_INDEPENDENT_CODE {{ fpic }} CACHE BOOL "Position independent code")
         {% endif %}
         """)
 
@@ -559,6 +560,34 @@ class FindFiles(Block):
         }
 
 
+class PkgConfigBlock(Block):
+    template = textwrap.dedent("""
+        {% if pkg_config %}
+        set(PKG_CONFIG_EXECUTABLE {{ pkg_config }} CACHE FILEPATH "pkg-config executable")
+        {% endif %}
+        {% if pkg_config_path %}
+        if (DEFINED ENV{PKG_CONFIG_PATH})
+        set(ENV{PKG_CONFIG_PATH} "{{ pkg_config_path }}$ENV{PKG_CONFIG_PATH}")
+        else()
+        set(ENV{PKG_CONFIG_PATH} "{{ pkg_config_path }}")
+        endif()
+        {% endif %}
+        """)
+
+    def context(self):
+        pkg_config = self._conanfile.conf.get("tools.gnu:pkg_config", check_type=str)
+        if pkg_config:
+            pkg_config = pkg_config.replace("\\", "/")
+        pkg_config_path = self._conanfile.generators_folder
+        if pkg_config_path:
+            # hardcoding scope as "build"
+            subsystem = deduce_subsystem(self._conanfile, "build")
+            pathsep = ":" if subsystem != WINDOWS else ";"
+            pkg_config_path = pkg_config_path.replace("\\", "/") + pathsep
+        return {"pkg_config": pkg_config,
+                "pkg_config_path": pkg_config_path}
+
+
 class UserToolchain(Block):
     template = textwrap.dedent("""
         {% for user_toolchain in paths %}
diff --git a/conan/tools/cmake/toolchain/toolchain.py b/conan/tools/cmake/toolchain/toolchain.py
index 310648c41..599a9338c 100644
--- a/conan/tools/cmake/toolchain/toolchain.py
+++ b/conan/tools/cmake/toolchain/toolchain.py
@@ -11,8 +11,8 @@ from conan.tools.cmake.presets import write_cmake_presets
 from conan.tools.cmake.toolchain import CONAN_TOOLCHAIN_FILENAME
 from conan.tools.cmake.toolchain.blocks import ToolchainBlocks, UserToolchain, GenericSystemBlock, \
     AndroidSystemBlock, AppleSystemBlock, FPicBlock, ArchitectureBlock, GLibCXXBlock, VSRuntimeBlock, \
-    CppStdBlock, ParallelBlock, CMakeFlagsInitBlock, TryCompileBlock, FindFiles, SkipRPath, \
-    SharedLibBock, OutputDirsBlock, ExtraFlagsBlock
+    CppStdBlock, ParallelBlock, CMakeFlagsInitBlock, TryCompileBlock, FindFiles, PkgConfigBlock, \
+    SkipRPath, SharedLibBock, OutputDirsBlock, ExtraFlagsBlock
 from conan.tools.intel import IntelCC
 from conan.tools.microsoft import VCVars
 from conan.tools.microsoft.visual import vs_ide_version
@@ -139,6 +139,7 @@ class CMakeToolchain(object):
                                        ("cmake_flags_init", CMakeFlagsInitBlock),
                                        ("try_compile", TryCompileBlock),
                                        ("find_paths", FindFiles),
+                                       ("pkg_config", PkgConfigBlock),
                                        ("rpath", SkipRPath),
                                        ("shared", SharedLibBock),
                                        ("output_dirs", OutputDirsBlock)])
diff --git a/conan/tools/gnu/autotools.py b/conan/tools/gnu/autotools.py
index 1992ba20d..a3a7e7449 100644
--- a/conan/tools/gnu/autotools.py
+++ b/conan/tools/gnu/autotools.py
@@ -1,4 +1,5 @@
 import os
+import re
 
 from conan.tools.build import build_jobs
 from conan.tools.files.files import load_toolchain_args
@@ -48,7 +49,8 @@ class Autotools(object):
         str_args = self._make_args
         str_extra_args = " ".join(args) if args is not None else ""
         jobs = ""
-        if "-j" not in str_args and "nmake" not in make_program.lower():
+        jobs_already_passed = re.search(r"(^-j\d+)|(\W-j\d+\s*)", join_arguments([str_args, str_extra_args]))
+        if not jobs_already_passed and "nmake" not in make_program.lower():
             njobs = build_jobs(self._conanfile)
             if njobs:
                 jobs = "-j{}".format(njobs)
@@ -56,8 +58,10 @@ class Autotools(object):
         self._conanfile.run(command)
 
     def install(self, args=None, target="install"):
-        args = args if args is not None else \
-            ["DESTDIR={}".format(unix_path(self._conanfile, self._conanfile.package_folder))]
+        args = args if args else []
+        str_args = " ".join(args)
+        if "DESTDIR=" not in str_args:
+            args.insert(0, "DESTDIR={}".format(unix_path(self._conanfile, self._conanfile.package_folder)))
         self.make(target=target, args=args)
 
     def autoreconf(self, args=None):


Test patch:
diff --git a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
index 397aac643..49f73cf5a 100644
--- a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
+++ b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
@@ -1099,7 +1099,7 @@ def test_cmake_toolchain_vars_when_option_declared():
 
     t.run("new mylib/1.0 --template cmake_lib")
     t.save({"CMakeLists.txt": cmakelists})
-    
+
     # The generated toolchain should set `BUILD_SHARED_LIBS` to `OFF`,
     # and `CMAKE_POSITION_INDEPENDENT_CODE` to `ON` and the calls to
     # `option()` in the CMakeLists.txt should respect the existing values.
@@ -1112,7 +1112,7 @@ def test_cmake_toolchain_vars_when_option_declared():
     assert "mylib target type: STATIC_LIBRARY" in t.out
     assert f"mylib position independent code: {fpic_cmake_value}" in t.out
 
-    # When building manually, ensure the value passed by the toolchain overrides the ones in 
+    # When building manually, ensure the value passed by the toolchain overrides the ones in
     # the CMakeLists
     fpic_option = "-o mylib:fPIC=False" if platform.system() != "Windows" else ""
     t.run(f"install . -o mylib:shared=False {fpic_option}")
@@ -1198,10 +1198,10 @@ def test_find_program_for_tool_requires():
 
             def layout(self):
                 cmake_layout(self)
-            
+
             def requirements(self):
                 self.requires("foobar/1.0")
-            
+
             def build_requirements(self):
                 self.tool_requires("foobar/1.0")
     """)
@@ -1227,7 +1227,63 @@ def test_find_program_for_tool_requires():
 
     with client.chdir("build"):
         client.run_command("cmake .. -DCMAKE_TOOLCHAIN_FILE=generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release")
-        # Verify binary executable is found from build context package, 
+        # Verify binary executable is found from build context package,
         # and library comes from host context package
         assert f"package/{build_context_package_id}/bin/foobin" in client.out
         assert f"package/{host_context_package_id}/include" in client.out
+
+
+@pytest.mark.tool_pkg_config
+def test_cmaketoolchain_and_pkg_config_path():
+    """
+    Lightweight test which is loading a dependency as a *.pc file through
+    CMake thanks to pkg_check_modules macro.
+    It's working because PKG_CONFIG_PATH env variable is being loaded automatically
+    by the CMakeToolchain generator.
+    """
+    client = TestClient()
+    dep = textwrap.dedent("""
+    from conan import ConanFile
+    class DepConan(ConanFile):
+        name = "dep"
+        version = "1.0"
+        def package_info(self):
+            self.cpp_info.libs = ["dep"]
+    """)
+    pkg = textwrap.dedent("""
+    from conan import ConanFile
+    from conan.tools.cmake import CMake, cmake_layout
+    class HelloConan(ConanFile):
+        name = "pkg"
+        version = "1.0"
+        settings = "os", "compiler", "build_type", "arch"
+        generators = "PkgConfigDeps", "CMakeToolchain"
+        exports_sources = "CMakeLists.txt"
+
+        def requirements(self):
+            self.requires("dep/1.0")
+
+        def layout(self):
+            cmake_layout(self)
+
+        def build(self):
+            cmake = CMake(self)
+            cmake.configure()
+            cmake.build()
+    """)
+    cmakelists = textwrap.dedent("""
+    cmake_minimum_required(VERSION 3.15)
+    project(pkg CXX)
+
+    find_package(PkgConfig REQUIRED)
+    # We should have PKG_CONFIG_PATH created in the current environment
+    pkg_check_modules(DEP REQUIRED IMPORTED_TARGET dep)
+    """)
+    client.save({
+        "dep/conanfile.py": dep,
+        "pkg/conanfile.py": pkg,
+        "pkg/CMakeLists.txt": cmakelists
+    })
+    client.run("create dep/conanfile.py")
+    client.run("create pkg/conanfile.py")
+    assert "Found dep, version 1.0" in client.out
diff --git a/conans/test/integration/package_id/test_validate.py b/conans/test/integration/package_id/test_validate.py
index e77a007ce..eeb1268d3 100644
--- a/conans/test/integration/package_id/test_validate.py
+++ b/conans/test/integration/package_id/test_validate.py
@@ -353,3 +353,55 @@ class TestValidate(unittest.TestCase):
         assert "conanfile.py: ERROR: Invalid ID: never ever" in c.out
         assert "Trying to install dependencies, but this configuration will fail to build a package"\
                in c.out
+
+
+class TestValidateCppstd:
+    """ aims to be a very close to real use case of cppstd management and validation in recipes
+    """
+    def test_build_17_consume_14(self):
+        client = TestClient()
+        conanfile = textwrap.dedent("""
+            from conan import ConanFile
+            from conan.errors import ConanInvalidConfiguration
+            class Pkg(ConanFile):
+                name = "pkg"
+                version = "0.1"
+                settings = "compiler"
+
+                def validate_build(self):
+                    # Explicit logic instead of using check_min_cppstd that hides details
+                    if int(str(self.settings.compiler.cppstd)) < 17:
+                        raise ConanInvalidConfiguration("I need at least cppstd=17 to build")
+
+                def validate(self):
+                    if int(str(self.settings.compiler.cppstd)) < 14:
+                        raise ConanInvalidConfiguration("I need at least cppstd=14 to be used")
+
+                def compatibility(conanfile):
+                    return [{"settings": [("compiler.cppstd", v)]} for v in ("11", "14", "17", "20")]
+            """)
+
+        client.save({"conanfile.py": conanfile})
+
+        settings = "-s compiler=gcc -s compiler.version=9 -s compiler.libcxx=libstdc++"
+        client.run(f"create . {settings} -s compiler.cppstd=17")
+        assert "pkg/0.1:7558a15eea8c5762fa0602874a38dde9d36229c1 - Build" in client.out
+
+        # create with cppstd=14 fails, not enough
+        client.run(f"create . {settings} -s compiler.cppstd=14", assert_error=True)
+        assert "pkg/0.1:e480cd97ef8f9dfdc31a7c0710c26d9b8d78375a - Invalid" in client.out
+        assert "pkg/0.1: Cannot build for this configuration: I need at least cppstd=17 to build" \
+               in client.out
+
+        # Install with cppstd=14 can fallback to the previous one
+        client.run(f"install pkg/0.1@ {settings} -s compiler.cppstd=14")
+        assert "Using compatible package '7558a15eea8c5762fa0602874a38dde9d36229c1'" in client.out
+        assert "pkg/0.1:7558a15eea8c5762fa0602874a38dde9d36229c1 - Cache" in client.out
+
+        # FIXME 2.0: install with not enough cppstd should fail, but it doesnt
+        # It is important to know that this only happens with "compatible_packages", otherwise
+        # it will also fail (ConanCenter)
+        client.run(f"install pkg/0.1@ {settings} -s compiler.cppstd=11")
+        # not even trying to fallback to compatibles
+        assert "Using compatible package '7558a15eea8c5762fa0602874a38dde9d36229c1'" in client.out
+        assert "pkg/0.1:7558a15eea8c5762fa0602874a38dde9d36229c1 - Cache" in client.out
diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
index d0bef4d84..1b1a9b76c 100644
--- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -758,3 +758,29 @@ def test_presets_ninja_msvc(arch, arch_toolset):
     presets = json.loads(client.load("build/14/generators/CMakePresets.json"))
     assert "architecture" in presets["configurePresets"][0]
     assert "toolset" not in presets["configurePresets"][0]
+
+
+def test_pkg_config_block():
+    os_ = platform.system()
+    os_ = "Macos" if os_ == "Darwin" else os_
+    profile = textwrap.dedent("""
+        [settings]
+        os=%s
+        arch=x86_64
+
+        [conf]
+        tools.gnu:pkg_config=/usr/local/bin/pkg-config
+        """ % os_)
+
+    client = TestClient(path_with_spaces=False)
+    conanfile = GenConanfile().with_settings("os", "arch")\
+        .with_generator("CMakeToolchain")
+    client.save({"conanfile.py": conanfile,
+                "profile": profile})
+    client.run("install . -pr:b profile -pr:h profile")
+    toolchain = client.load("conan_toolchain.cmake")
+    assert 'set(PKG_CONFIG_EXECUTABLE /usr/local/bin/pkg-config CACHE FILEPATH ' in toolchain
+    pathsep = ":" if os_ != "Windows" else ";"
+    pkg_config_path_set = 'set(ENV{PKG_CONFIG_PATH} "%s$ENV{PKG_CONFIG_PATH}")' % \
+                          (client.current_folder.replace("\\", "/") + pathsep)
+    assert pkg_config_path_set in toolchain
diff --git a/conans/test/unittests/client/toolchain/autotools/autotools_test.py b/conans/test/unittests/client/toolchain/autotools/autotools_test.py
index 50bcf91a4..c310469bd 100644
--- a/conans/test/unittests/client/toolchain/autotools/autotools_test.py
+++ b/conans/test/unittests/client/toolchain/autotools/autotools_test.py
@@ -32,12 +32,47 @@ def test_configure_arguments():
 
     ab = Autotools(conanfile)
     ab.make()
-    assert "my_make my_make_args -j23" in runner.command_called
+    assert "my_make my_make_args -j23" == runner.command_called
 
     # test install target argument
 
     ab.install()
-    assert 'my_make install my_make_args DESTDIR=None -j23' in runner.command_called
+    assert 'my_make install my_make_args DESTDIR=None -j23' == runner.command_called
 
     ab.install(target="install_other")
-    assert 'my_make install_other my_make_args DESTDIR=None -j23' in runner.command_called
+    assert 'my_make install_other my_make_args DESTDIR=None -j23' == runner.command_called
+
+    for make_args in ["my_make_args", ""]:
+
+        save_toolchain_args({
+            "configure_args": "my_configure_args",
+            "make_args": f"{make_args}"}
+        )
+
+        ab = Autotools(conanfile)
+
+        make_args_str = f" {make_args}" if make_args else ""
+
+        ab.make(args=["-j1"])
+        assert "-j23" not in runner.command_called
+        assert f"my_make{make_args_str} -j1" == runner.command_called
+
+        ab.install(args=["-j1"])
+        assert "-j23" not in runner.command_called
+        assert f"my_make install{make_args_str} DESTDIR=None -j1" == runner.command_called
+
+        ab.install(args=["DESTDIR=whatever", "-j1"])
+        assert "-j23" not in runner.command_called
+        assert f"my_make install{make_args_str} DESTDIR=whatever -j1" == runner.command_called
+
+        ab.install(args=["DESTDIR=whatever", "-arg1 -j1 -arg2"])
+        assert "-j23" not in runner.command_called
+        assert f"my_make install{make_args_str} DESTDIR=whatever -arg1 -j1 -arg2" == runner.command_called
+
+        # check that we don't detect -j in an argument as number of jobs
+        ab.install(args=["DESTDIR=/user/smith-john/whatever"])
+        assert f"my_make install{make_args_str} DESTDIR=/user/smith-john/whatever -j23" == runner.command_called
+
+        # check that we don't detect -j in an argument as number of jobs
+        ab.install(args=["DESTDIR=/user/smith-j47/whatever"])
+        assert f"my_make install{make_args_str} DESTDIR=/user/smith-j47/whatever -j23" == runner.command_called