Repository: conan-io/conan
Pull request #17927 (2025-03-10 17:37:33)

[GH actions] Adding CMake 4.0 version to CI
_Waiting for CMake 4.0 release (4.0.0-rc4 for now)_

Summary:

* Conan seems compatible with CMake 4.0.
* 99% of failures are due to _cmake_minimum_required < 3.5_
```
  Compatibility with CMake < 3.5 has been removed from CMake.

  Update the VERSION argument <min> value.  Or, use the <min>...<max> syntax
  to tell CMake that the project requires at least <min> but has been updated
  to work with policies introduced by <max> or earlier.

  Or, add -DCMAKE_POLICY_VERSION_MINIMUM=3.5 to try configuring anyway.
```
* Another failure was related to `--preset` usage:
**cmake < 4.x**
```
$ cmake . --preset conan-release
CMake Warning:
  Ignoring extra path from command line:

   "."

Preset CMake variables:

  CMAKE_BUILD_TYPE="Release"
  CMAKE_POLICY_DEFAULT_CMP0091="NEW"
  CMAKE_TOOLCHAIN_FILE:FILEPATH="generators/conan_toolchain.cmake"

# ............
```
**cmake >= 4.x**
```
$ cmake . --preset conan-release --log-level VERBOSE
CMake Warning:
  Ignoring extra path from command line:

   "."

Preset CMake variables:

  CMAKE_BUILD_TYPE="Release"
  CMAKE_POLICY_DEFAULT_CMP0091="NEW"
  CMAKE_TOOLCHAIN_FILE:FILEPATH="generators/conan_toolchain.cmake"

# ............
```
Then, CMake 4.x needs to add `--log-level VERBOSE` to show those variables.
The test that checks this is `FAILED test/functional/toolchains/cmake/test_cmake_toolchain.py::test_cmake_user_presets_load[None]` 


Patch:
diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py
index 335d7c08c..3e3e95959 100644
--- a/conan/internal/model/conf.py
+++ b/conan/internal/model/conf.py
@@ -610,18 +610,16 @@ class ConfDefinition:
         else:
             self._pattern_confs[pattern] = conf
 
-    def rebase_conf_definition(self, other):
+    def rebase_conf_definition(self, global_conf):
         """
         for taking the new global.conf and composing with the profile [conf]
-        :type other: ConfDefinition
+        :type global_conf: ConfDefinition
         """
-        for pattern, conf in other._pattern_confs.items():
-            new_conf = conf.filter_user_modules()  # Creates a copy, filtered
-            existing = self._pattern_confs.get(pattern)
-            if existing:
-                existing.compose_conf(new_conf)
-            else:
-                self._pattern_confs[pattern] = new_conf
+        result = ConfDefinition()
+        result._pattern_confs = global_conf._pattern_confs.copy()
+        result.update_conf_definition(self)
+        self._pattern_confs = result._pattern_confs
+        return
 
     def update(self, key, value, profile=False, method="define"):
         """


Test patch:
diff --git a/.ci/docker/conan-tests b/.ci/docker/conan-tests
index 88b979455..ef26a5530 100644
--- a/.ci/docker/conan-tests
+++ b/.ci/docker/conan-tests
@@ -16,6 +16,7 @@ ENV PY36=3.6.15 \
     CMAKE_3_19=/usr/share/cmake-3.19.7/bin/cmake \
     CMAKE_3_23=/usr/share/cmake-3.23.5/bin/cmake \
     CMAKE_3_27=/usr/share/cmake-3.27.9/bin/cmake \
+    CMAKE_4_0=/usr/share/cmake-4.0.0-rc3/bin/cmake \
     GCC_9=/usr/bin/gcc-9 \
     GXX_9=/usr/bin/g++-9 \
     GCC_11=/usr/bin/gcc-11 \
@@ -120,12 +121,17 @@ RUN wget https://github.com/Kitware/CMake/releases/download/v3.15.7/cmake-3.15.7
     tar -xvzf cmake-3.23.5-Linux-x86_64.tar.gz && mv cmake-3.23.5-linux-x86_64/ /usr/share/cmake-3.23.5 && \
     wget https://github.com/Kitware/CMake/releases/download/v3.27.9/cmake-3.27.9-Linux-x86_64.tar.gz   && \
     tar -xvzf cmake-3.27.9-Linux-x86_64.tar.gz && mv cmake-3.27.9-linux-x86_64/ /usr/share/cmake-3.27.9 && \
+    wget https://cmake.org/files/v4.0/cmake-4.0.0-rc3-linux-x86_64.tar.gz   && \
+    tar -xvzf cmake-4.0.0-rc3-linux-x86_64.tar.gz && mv cmake-4.0.0-rc3-linux-x86_64/ /usr/share/cmake-4.0.0-rc3 && \
     update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_15 10 && \
     update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_16 20 && \
     update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_17 30 && \
     update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_19 40 && \
     update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_23 50 && \
-    update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_27 60
+    update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_27 60 && \
+    update-alternatives --install /usr/bin/cmake cmake $CMAKE_4_0 70 && \
+    # set CMake 3.15 as default
+    update-alternatives --set cmake $CMAKE_3_15
 
 RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 10 && \
     update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 30 && \
diff --git a/.github/workflows/osx-tests.yml b/.github/workflows/osx-tests.yml
index 1942d955c..c02a16596 100644
--- a/.github/workflows/osx-tests.yml
+++ b/.github/workflows/osx-tests.yml
@@ -58,6 +58,7 @@ jobs:
             ~/Applications/CMake/3.19.7
             ~/Applications/CMake/3.23.5
             ~/Applications/CMake/3.27.9
+            ~/Applications/CMake/4.0.0-rc3
             ~/Applications/bazel/6.5.0
             ~/Applications/bazel/7.4.1
             ~/Applications/bazel/8.0.0
@@ -86,7 +87,7 @@ jobs:
         if: steps.cache-tools.outputs.cache-hit != 'true'
         run: |
           set -e
-          CMAKE_PRECOMP_VERSIONS=("3.19.7" "3.23.5" "3.27.9")
+          CMAKE_PRECOMP_VERSIONS=("3.19.7" "3.23.5" "3.27.9" "4.0.0-rc3")
           for version in "${CMAKE_PRECOMP_VERSIONS[@]}"; do
             echo "Downloading and installing precompiled universal CMake version ${version}..."
             wget -q --no-check-certificate https://cmake.org/files/v${version%.*}/cmake-${version}-macos-universal.tar.gz
@@ -146,6 +147,7 @@ jobs:
             ~/Applications/CMake/3.19.7
             ~/Applications/CMake/3.23.5
             ~/Applications/CMake/3.27.9
+            ~/Applications/CMake/4.0.0-rc3
             ~/Applications/bazel/6.5.0
             ~/Applications/bazel/7.4.1
             ~/Applications/bazel/8.0.0
diff --git a/.github/workflows/win-tests.yml b/.github/workflows/win-tests.yml
index 75e21a273..ee3d55f0c 100644
--- a/.github/workflows/win-tests.yml
+++ b/.github/workflows/win-tests.yml
@@ -161,6 +161,7 @@ jobs:
             C:\tools\cmake\3.19.7
             C:\tools\cmake\3.23.5
             C:\tools\cmake\3.27.9
+            C:\tools\cmake\4.0.0-rc3
             C:\tools\bazel\6.5.0
             C:\tools\bazel\7.4.1
             C:\tools\bazel\8.0.0
@@ -169,7 +170,7 @@ jobs:
       - name: Install CMake versions
         if: steps.cache-tools.outputs.cache-hit != 'true'
         run: |
-          $CMAKE_BUILD_VERSIONS = "3.15.7", "3.19.7", "3.23.5", "3.27.9"
+          $CMAKE_BUILD_VERSIONS = "3.15.7", "3.19.7", "3.23.5", "3.27.9", "4.0.0-rc3"
           foreach ($version in $CMAKE_BUILD_VERSIONS) {
             Write-Host "Downloading CMake version $version for Windows..."
             $destination = "C:\tools\cmake\$version"
diff --git a/test/conftest.py b/test/conftest.py
index 4583c44fc..a0f6ead5a 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -86,6 +86,11 @@ tools_locations = {
             "path": {'Windows': 'C:/tools/cmake/3.27.9/cmake-3.27.9-windows-x86_64/bin',
                      'Darwin': '/Users/runner/Applications/CMake/3.27.9/bin',
                      'Linux': "/usr/share/cmake-3.27.9/bin"}
+        },
+        "4.0": {
+            "path": {'Windows': 'C:/tools/cmake/4.0.0-rc3/cmake-4.0.0-rc3-windows-x86_64/bin',
+                     'Darwin': '/Users/runner/Applications/CMake/4.0.0-rc3/bin',
+                     'Linux': "/usr/share/cmake-4.0.0-rc3/bin"}
         }
     },
     'ninja': {
diff --git a/test/functional/layout/test_editable_cmake_components.py b/test/functional/layout/test_editable_cmake_components.py
index db407274d..70ffe4cec 100644
--- a/test/functional/layout/test_editable_cmake_components.py
+++ b/test/functional/layout/test_editable_cmake_components.py
@@ -2,10 +2,13 @@ import os
 import shutil
 import textwrap
 
+import pytest
+
 from conan.test.assets.sources import gen_function_cpp, gen_function_h
 from conan.test.utils.tools import TestClient
 
 
+@pytest.mark.tool("cmake")
 def test_editable_cmake_components():
     hello_h = gen_function_h(name="hello")
     hello_cpp = gen_function_cpp(name="hello", includes=["hello"])
diff --git a/test/functional/toolchains/cmake/test_cmake_toolchain.py b/test/functional/toolchains/cmake/test_cmake_toolchain.py
index 14ed165c6..76101e05c 100644
--- a/test/functional/toolchains/cmake/test_cmake_toolchain.py
+++ b/test/functional/toolchains/cmake/test_cmake_toolchain.py
@@ -1119,6 +1119,7 @@ def test_resdirs_none_cmake_install():
     assert "Cannot install stuff" in client.out
 
 
+@pytest.mark.tool("cmake")
 def test_cmake_toolchain_vars_when_option_declared():
     t = TestClient()
 
diff --git a/test/functional/toolchains/meson/test_install.py b/test/functional/toolchains/meson/test_install.py
index c839d02f3..2d1aa78ce 100644
--- a/test/functional/toolchains/meson/test_install.py
+++ b/test/functional/toolchains/meson/test_install.py
@@ -97,6 +97,7 @@ class MesonInstall(TestMesonBase):
         """)
 
     @pytest.mark.tool("meson")
+    @pytest.mark.tool("cmake")
     def test_install(self):
         hello_cpp = gen_function_cpp(name="hello")
         hello_h = gen_function_h(name="hello")
diff --git a/test/integration/graph/test_skip_build.py b/test/integration/graph/test_skip_build.py
index 44e74e0fb..3eb419a50 100644
--- a/test/integration/graph/test_skip_build.py
+++ b/test/integration/graph/test_skip_build.py
@@ -1,3 +1,4 @@
+import re
 import textwrap
 
 from conan.test.assets.genconanfile import GenConanfile
@@ -46,3 +47,38 @@ def test_graph_skip_build_test():
     c.run("install app -c tools.graph:skip_build=True --build=pkg/*", assert_error=True)
     assert "ERROR: Package pkg/1.0 skipped its test/tool requires with tools.graph:skip_build, " \
            "but was marked to be built " in c.out
+
+
+def test_skip():
+    # https://github.com/conan-io/conan/issues/13439
+    c = TestClient()
+    global_conf = textwrap.dedent("""
+        tools.graph:skip_test=True
+        tools.build:skip_test=True
+        """)
+    c.save_home({"global.conf": global_conf})
+    c.save({"pkga/conanfile.py": GenConanfile("pkga", "1.0.0"),
+            "pkgb/conanfile.py": GenConanfile("pkgb", "1.0.0").with_test_requires("pkga/1.0.0"),
+            "pkgc/conanfile.py": GenConanfile("pkgc", "1.0.0").with_test_requires("pkgb/1.0.0")})
+    c.run("create pkga")
+    c.run("create pkgb")
+
+    # Always skipped
+    c.run("install pkgc")
+    # correct, pkga and pkgb are not in the output at all, they have been skipped
+    assert "pkga" not in c.out
+    assert "pkgb" not in c.out
+
+    # not skipping test-requires
+    c.run("install pkgc -c tools.graph:skip_test=False")
+    # correct, pkga and pkgb are not skipped now
+    assert "pkga" in c.out
+    assert "pkgb" in c.out
+    # but pkga binary is not really necessary
+    assert re.search(r"Skipped binaries(\s*)pkga/1.0.0", c.out)
+
+    # skipping all but the current one
+    c.run("install pkgc -c &:tools.graph:skip_test=False")
+    # correct, only pkga  is skipped now
+    assert "pkga" not in c.out
+    assert "pkgb" in c.out
diff --git a/test/unittests/model/test_conf.py b/test/unittests/model/test_conf.py
index 25dfbdedf..a20b132b0 100644
--- a/test/unittests/model/test_conf.py
+++ b/test/unittests/model/test_conf.py
@@ -76,6 +76,71 @@ def test_conf_rebase(conf_definition):
     assert c.dumps() == result
 
 
+def test_conf_rebase_patterns():
+    text = textwrap.dedent("""\
+        user.company:conf1=value1
+        user.company:conf2=[1, 2, 3]
+        &:user.company:conf1=value2
+        &:user.company:conf2+=[4, 5]
+        """)
+    global_conf = ConfDefinition()
+    global_conf.loads(text)
+    text = textwrap.dedent("""\
+        user.company:conf1=newvalue1
+        user.company:conf2=[6, 7]
+        """)
+    profile = ConfDefinition()
+    profile.loads(text)
+    profile.rebase_conf_definition(global_conf)
+    # TODO: at the moment dumps() doesn't print += append
+    result = textwrap.dedent("""\
+        user.company:conf1=newvalue1
+        user.company:conf2=[6, 7]
+        &:user.company:conf1=value2
+        &:user.company:conf2=[4, 5]
+    """)
+    assert profile.dumps() == result
+
+    text = textwrap.dedent("""\
+        &:user.company:conf1=newvalue1
+        &:user.company:conf2=[6, 7]
+        """)
+    profile = ConfDefinition()
+    profile.loads(text)
+    profile.rebase_conf_definition(global_conf)
+    result = textwrap.dedent("""\
+        user.company:conf1=value1
+        user.company:conf2=[1, 2, 3]
+        &:user.company:conf1=newvalue1
+        &:user.company:conf2=[6, 7]
+       """)
+    assert profile.dumps() == result
+
+    text = "user.company:conf2+=[6, 7]"
+    profile = ConfDefinition()
+    profile.loads(text)
+    profile.rebase_conf_definition(global_conf)
+    result = textwrap.dedent("""\
+        user.company:conf1=value1
+        user.company:conf2=[1, 2, 3, 6, 7]
+        &:user.company:conf1=value2
+        &:user.company:conf2=[4, 5]
+        """)
+    assert profile.dumps() == result
+
+    text = "&:user.company:conf2=+[6, 7]"
+    profile = ConfDefinition()
+    profile.loads(text)
+    profile.rebase_conf_definition(global_conf)
+    result = textwrap.dedent("""\
+        user.company:conf1=value1
+        user.company:conf2=[1, 2, 3]
+        &:user.company:conf1=value2
+        &:user.company:conf2=[6, 7, 4, 5]
+        """)
+    assert profile.dumps() == result
+
+
 def test_conf_error_per_package():
     text = "*:core:non_interactive=minimal"
     c = ConfDefinition()