Repository: conan-io/conan
Pull request #17366 (2024-11-23 20:52:03)

emit a risk warning for options conflicts
Changelog: Fix: UX: adding a "risk" warning for options conflicts, so users can do warn-as-error to raise when they happen.
Docs: Omit

Close https://github.com/conan-io/conan/issues/17190


Patch:
diff --git a/conan/cli/commands/build.py b/conan/cli/commands/build.py
index aa62a740d..4720e6259 100644
--- a/conan/cli/commands/build.py
+++ b/conan/cli/commands/build.py
@@ -61,7 +61,6 @@ def build(conan_api, parser, *args):
     print_graph_packages(deps_graph)
 
     out = ConanOutput()
-    out.title("Installing packages")
     conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes)
 
     out.title("Finalizing install (deploy, generators)")
diff --git a/conan/cli/printers/graph.py b/conan/cli/printers/graph.py
index ddbd95326..931848037 100644
--- a/conan/cli/printers/graph.py
+++ b/conan/cli/printers/graph.py
@@ -91,6 +91,7 @@ def print_graph_basic(graph):
                     output.info(f"        {src_ref}->{option}={conflict_value}", Color.BRIGHT_CYAN)
         output.info("    It is recommended to define options values in profiles, not in recipes",
                     Color.BRIGHT_CYAN)
+        output.warning("There are options conflicts in the dependency graph", warn_tag="risk")
 
 
 def print_graph_packages(graph):
diff --git a/conans/model/build_info.py b/conans/model/build_info.py
index ca4d00e36..e721fab1b 100644
--- a/conans/model/build_info.py
+++ b/conans/model/build_info.py
@@ -445,7 +445,12 @@ class _Component:
 
         if other._properties:
             current_values = self.get_init("_properties", {})
-            current_values.update(other._properties)
+            for k, v in other._properties.items():
+                existing = current_values.get(k)
+                if existing is not None and isinstance(existing, list) and not overwrite:
+                    existing.extend(v)
+                else:
+                    current_values[k] = v
 
     def set_relative_base_folder(self, folder):
         for varname in _DIRS_VAR_NAMES:


Test patch:
diff --git a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_build_modules.py b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_build_modules.py
index 8b7774f24..27981e025 100644
--- a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_build_modules.py
+++ b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_build_modules.py
@@ -204,3 +204,174 @@ def test_build_modules_components_is_not_possible():
     client.save({"CMakeLists.txt": cmakelists.replace("crypto", "#crypto")})
     assert "ROOT MESSAGE:hello!" not in client.out
 
+
+@pytest.mark.tool("cmake")
+@pytest.mark.parametrize("editable", [True, False])
+def test_build_modules_custom_script_editable(editable):
+    c = TestClient()
+    conanfile = textwrap.dedent(r"""
+        import os, glob
+        from conan import ConanFile
+        from conan.tools.cmake import cmake_layout
+        from conan.tools.files import copy, save
+
+        class Conan(ConanFile):
+            name = "myfunctions"
+            version = "1.0"
+            exports_sources = ["src/*.cmake"]
+            settings = "build_type", "arch"
+
+            def build(self):
+                cmake = 'set(MY_CMAKE_PATH ${CMAKE_CURRENT_LIST_DIR})\n'\
+                        'macro(otherfunc)\n'\
+                        'file(READ "${MY_CMAKE_PATH}/my.txt" c)\n'\
+                        'message("Hello ${c}!!!!")\nendmacro()'
+                save(self, "otherfuncs.cmake", cmake)
+                save(self, "my.txt", "contents of text file!!!!")
+
+            def layout(self):
+                cmake_layout(self, src_folder="src")
+                src = glob.glob(os.path.join(self.recipe_folder, self.folders.source, "*.cmake"))
+                build = glob.glob(os.path.join(self.recipe_folder, self.folders.build, "*.cmake"))
+                self.cpp.source.set_property("cmake_build_modules", src)
+                self.cpp.build.set_property("cmake_build_modules", build)
+
+            def package(self):
+                copy(self, "*.cmake", self.source_folder, os.path.join(self.package_folder, "mods"),
+                     keep_path=False)
+                copy(self, "*.cmake", self.build_folder, os.path.join(self.package_folder, "mods"),
+                     keep_path=False)
+                copy(self, "*.txt", self.build_folder, os.path.join(self.package_folder, "mods"),
+                     keep_path=False)
+
+            def package_info(self):
+                self.cpp_info.set_property("cmake_build_modules", glob.glob("mods/*.cmake"))
+        """)
+
+    myfunction = textwrap.dedent("""
+        function(myfunction)
+            message("Hello myfunction!!!!")
+        endfunction()
+        """)
+
+    consumer = textwrap.dedent("""
+        from conan import ConanFile
+        from conan.tools.cmake import CMake
+
+        class Conan(ConanFile):
+            settings = "os", "compiler", "build_type", "arch"
+            generators = "CMakeToolchain", "CMakeDeps"
+            requires = "myfunctions/1.0"
+
+            def build(self):
+                cmake = CMake(self)
+                cmake.configure()
+        """)
+    cmakelists = textwrap.dedent("""
+        cmake_minimum_required(VERSION 3.15)
+        project(test)
+        find_package(myfunctions CONFIG REQUIRED)
+        myfunction()
+        otherfunc()
+        """)
+    c.save({"functions/conanfile.py": conanfile,
+            "functions/src/myfunction.cmake": myfunction,
+            "app/conanfile.py": consumer,
+            "app/CMakeLists.txt": cmakelists})
+
+    if editable:
+        c.run("editable add functions")
+        c.run('build functions -c tools.cmake.cmake_layout:build_folder_vars="[\'settings.arch\']"')
+    else:
+        c.run("create functions")
+    c.run('build app -c tools.cmake.cmake_layout:build_folder_vars="[\'settings.arch\']"')
+    assert "Hello myfunction!!!!" in c.out
+    assert "Hello contents of text file!!!!" in c.out
+
+
+@pytest.mark.tool("cmake")
+@pytest.mark.parametrize("editable", [True, False])
+def test_build_modules_custom_script_editable_package(editable):
+    # same as above, but with files from the package folder
+    c = TestClient()
+    conanfile = textwrap.dedent(r"""
+        import os, glob
+        from conan import ConanFile
+        from conan.tools.cmake import cmake_layout
+        from conan.tools.files import copy, save
+
+        class Conan(ConanFile):
+            name = "myfunctions"
+            version = "1.0"
+            exports_sources = ["src/*.cmake", "src/vis/*.vis"]
+            settings = "build_type", "arch"
+
+            def build(self):
+                config = str(self.settings.build_type).upper()
+                cmake = f'set(MY_CMAKE_PATH ${{{self.name}_RES_DIRS_{config}}})\n'\
+                        'macro(otherfunc)\n'\
+                        'file(READ "${MY_CMAKE_PATH}/my.vis" c)\n'\
+                        'message("Hello ${c}!!!!")\nendmacro()'
+                save(self, "otherfuncs.cmake", cmake)
+
+            def layout(self):
+                cmake_layout(self, src_folder="src")
+                src = glob.glob(os.path.join(self.recipe_folder, self.folders.source, "*.cmake"))
+                build = glob.glob(os.path.join(self.recipe_folder, self.folders.build, "*.cmake"))
+                self.cpp.source.set_property("cmake_build_modules", src)
+                self.cpp.build.set_property("cmake_build_modules", build)
+                self.cpp.source.resdirs = ["vis"]
+
+            def package(self):
+                copy(self, "*.cmake", self.source_folder, os.path.join(self.package_folder, "mods"),
+                     keep_path=False)
+                copy(self, "*.cmake", self.build_folder, os.path.join(self.package_folder, "mods"),
+                     keep_path=False)
+                copy(self, "*.vis", self.source_folder, os.path.join(self.package_folder, "mods"),
+                     keep_path=False)
+
+            def package_info(self):
+                self.cpp_info.resdirs = ["mods"]
+                self.cpp_info.set_property("cmake_build_modules", glob.glob("mods/*.cmake"))
+        """)
+
+    myfunction = textwrap.dedent("""
+        function(myfunction)
+            message("Hello myfunction!!!!")
+        endfunction()
+        """)
+
+    consumer = textwrap.dedent("""
+        from conan import ConanFile
+        from conan.tools.cmake import CMake
+
+        class Conan(ConanFile):
+            settings = "os", "compiler", "build_type", "arch"
+            generators = "CMakeToolchain", "CMakeDeps"
+            requires = "myfunctions/1.0"
+
+            def build(self):
+                cmake = CMake(self)
+                cmake.configure()
+        """)
+    cmakelists = textwrap.dedent("""
+        cmake_minimum_required(VERSION 3.15)
+        project(test)
+        find_package(myfunctions CONFIG REQUIRED)
+        myfunction()
+        otherfunc()
+        """)
+    c.save({"functions/conanfile.py": conanfile,
+            "functions/src/myfunction.cmake": myfunction,
+            "functions/src/vis/my.vis": "contents of text file!!!!",
+            "app/conanfile.py": consumer,
+            "app/CMakeLists.txt": cmakelists})
+
+    if editable:
+        c.run("editable add functions")
+        c.run('build functions -c tools.cmake.cmake_layout:build_folder_vars="[\'settings.arch\']"')
+    else:
+        c.run("create functions -vv")
+    c.run('build app -c tools.cmake.cmake_layout:build_folder_vars="[\'settings.arch\']"')
+    assert "Hello myfunction!!!!" in c.out
+    assert "Hello contents of text file!!!!" in c.out
diff --git a/test/integration/options/options_test.py b/test/integration/options/options_test.py
index 8aedbbfb1..aac9dbc9c 100644
--- a/test/integration/options/options_test.py
+++ b/test/integration/options/options_test.py
@@ -744,11 +744,16 @@ class TestImportantOptions:
 
 class TestConflictOptionsWarnings:
 
-    def test_options_warnings(self):
+    @pytest.mark.parametrize("important", [True, False])
+    def test_options_warnings(self, important):
         c = TestClient()
         liba = GenConanfile("liba", "0.1").with_option("myoption", [1, 2, 3], default=1)
         libb = GenConanfile("libb", "0.1").with_requires("liba/0.1")
-        libc = GenConanfile("libc", "0.1").with_requirement("liba/0.1", options={"myoption": 2})
+        if important:
+            libc = GenConanfile("libc", "0.1").with_requirement("liba/0.1") \
+                                              .with_default_option("liba*:myoption!", 2)
+        else:
+            libc = GenConanfile("libc", "0.1").with_requirement("liba/0.1", options={"myoption": 2})
         app = GenConanfile().with_requires("libb/0.1", "libc/0.1")
 
         c.save({"liba/conanfile.py": liba,
@@ -767,3 +772,4 @@ class TestConflictOptionsWarnings:
                 It is recommended to define options values in profiles, not in recipes
             """)
         assert expected in c.out
+        assert "WARN: risk: There are options conflicts in the dependency graph" in c.out