Skip to content

Commit

Permalink
[incubating][CMakeDeps] Adding more CMake paths (#17668)
Browse files Browse the repository at this point in the history
* Refactored and added extra paths

* Added tests

* Added functional test. Fixed missing test requirements

* Added dynamic libs too

* fix test

---------

Co-authored-by: memsharded <[email protected]>
  • Loading branch information
franramirez688 and memsharded authored Feb 3, 2025
1 parent 3ac0416 commit 31d3201
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 80 deletions.
80 changes: 48 additions & 32 deletions conan/tools/cmake/cmakedeps2/cmakedeps.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,30 +165,55 @@ def __init__(self, cmakedeps, conanfile):
self._conanfile = conanfile
self._cmakedeps = cmakedeps

def _get_cmake_paths(self, requirements, dirs_name):
paths = {}
cmake_vars = {
"bindirs": "CMAKE_PROGRAM_PATH",
"libdirs": "CMAKE_LIBRARY_PATH",
"includedirs": "CMAKE_INCLUDE_PATH",
}
for req, dep in requirements:
cppinfo = dep.cpp_info.aggregated_components()
cppinfo_dirs = getattr(cppinfo, dirs_name, [])
if not cppinfo_dirs:
continue
previous = paths.get(req.ref.name)
if previous:
self._conanfile.output.info(f"There is already a '{req.ref}' package contributing"
f" to {cmake_vars[dirs_name]}. Using the one"
f" defined by the context={dep.context}.")
paths[req.ref.name] = cppinfo_dirs
return [d for dirs in paths.values() for d in dirs]

def generate(self):
template = textwrap.dedent("""\
{% for pkg_name, folder in pkg_paths.items() %}
set({{pkg_name}}_DIR "{{folder}}")
{% endfor %}
{% if host_runtime_dirs %}
set(CONAN_RUNTIME_LIB_DIRS {{ host_runtime_dirs }} )
# Only for VS, needs CMake>=3.27
set(CMAKE_VS_DEBUGGER_ENVIRONMENT "PATH=${CONAN_RUNTIME_LIB_DIRS};%PATH%")
{% endif %}
{% if cmake_program_path %}
list(PREPEND CMAKE_PROGRAM_PATH {{ cmake_program_path }})
{% endif %}
""")

{% for pkg_name, folder in pkg_paths.items() %}
set({{pkg_name}}_DIR "{{folder}}")
{% endfor %}
{% if host_runtime_dirs %}
set(CONAN_RUNTIME_LIB_DIRS {{ host_runtime_dirs }} )
# Only for VS, needs CMake>=3.27
set(CMAKE_VS_DEBUGGER_ENVIRONMENT "PATH=${CONAN_RUNTIME_LIB_DIRS};%PATH%")
{% endif %}
{% if cmake_program_path %}
list(PREPEND CMAKE_PROGRAM_PATH {{ cmake_program_path }})
{% endif %}
{% if cmake_library_path %}
list(PREPEND CMAKE_LIBRARY_PATH {{ cmake_library_path }})
{% endif %}
{% if cmake_include_path %}
list(PREPEND CMAKE_INCLUDE_PATH {{ cmake_include_path }})
{% endif %}
""")
host_req = self._conanfile.dependencies.host
build_req = self._conanfile.dependencies.direct_build
test_req = self._conanfile.dependencies.test

all_reqs = list(host_req.items()) + list(test_req.items()) + list(build_req.items())
# gen_folder = self._conanfile.generators_folder.replace("\\", "/")
# if not, test_cmake_add_subdirectory test fails
# content.append('set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON)')
pkg_paths = {}
for req, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()):
for req, dep in all_reqs:
cmake_find_mode = self._cmakedeps.get_property("cmake_find_mode", dep)
cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG
cmake_find_mode = cmake_find_mode.lower()
Expand All @@ -214,26 +239,17 @@ def generate(self):
# content.append(f'set({pkg_name}_ROOT "{gen_folder}")')
pkg_paths[pkg_name] = "${CMAKE_CURRENT_LIST_DIR}"

# CMAKE_PROGRAM_PATH
cmake_program_path = {}
for req, dep in list(host_req.items()) + list(test_req.items()) + list(build_req.items()):
if not req.direct:
continue
cppinfo = dep.cpp_info.aggregated_components()
if not cppinfo.bindirs:
continue
previous = cmake_program_path.get(req.ref.name)
if previous:
self._conanfile.output.info(f"There is already a '{req.ref}' package "
f"contributing to CMAKE_PROGRAM_PATH. The one with "
f"build={req.build} test={req.test} will be used")

cmake_program_path[req.ref.name] = cppinfo.bindirs
cmake_program_path = [d for dirs in cmake_program_path.values() for d in dirs]
# CMAKE_PROGRAM_PATH | CMAKE_LIBRARY_PATH | CMAKE_INCLUDE_PATH
cmake_program_path = self._get_cmake_paths([(req, dep) for req, dep in all_reqs if req.direct], "bindirs")
cmake_library_path = self._get_cmake_paths(list(host_req.items()) + list(test_req.items()), "libdirs")
cmake_include_path = self._get_cmake_paths(list(host_req.items()) + list(test_req.items()), "includedirs")

context = {"host_runtime_dirs": self._get_host_runtime_dirs(),
"pkg_paths": pkg_paths,
"cmake_program_path": _join_paths(self._conanfile, cmake_program_path)}
"cmake_program_path": _join_paths(self._conanfile, cmake_program_path),
"cmake_library_path": _join_paths(self._conanfile, cmake_library_path),
"cmake_include_path": _join_paths(self._conanfile, cmake_include_path),
}
content = Template(template, trim_blocks=True, lstrip_blocks=True).render(context)
save(self._conanfile, self._conan_cmakedeps_paths, content)

Expand Down
148 changes: 100 additions & 48 deletions test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,51 +102,103 @@ def test_runtime_lib_dirs_multiconf(self):


@pytest.mark.tool("cmake")
@pytest.mark.parametrize("requires, tool_requires", [(True, False), (False, True), (True, True)])
def test_cmaketoolchain_path_find_program(requires, tool_requires):
"""Test that executables in bindirs of tool_requires can be found with
find_program() in consumer CMakeLists.
"""
c = TestClient()

conanfile = textwrap.dedent("""
import os
from conan.tools.files import copy
from conan import ConanFile
class TestConan(ConanFile):
name = "tool"
version = "1.0"
exports_sources = "*"
def package(self):
copy(self, "*", self.source_folder, os.path.join(self.package_folder, "bin"))
""")
c.save({"conanfile.py": conanfile, "hello": "", "hello.exe": ""})
c.run("create .")

requires = 'requires = "tool/1.0"' if requires else ""
tool_requires = 'tool_requires = "tool/1.0"' if tool_requires else ""
conanfile = textwrap.dedent(f"""
from conan import ConanFile
from conan.tools.cmake import CMake
class PkgConan(ConanFile):
{requires}
{tool_requires}
settings = "os", "arch", "compiler", "build_type"
generators = "CMakeToolchain", "CMakeDeps"
def build(self):
cmake = CMake(self)
cmake.configure()
""")
consumer = textwrap.dedent("""
cmake_minimum_required(VERSION 3.15)
project(MyHello)
find_program(HELLOPROG hello)
if(HELLOPROG)
message(STATUS "Found hello prog: ${HELLOPROG}")
endif()
""")
c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True)
c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}")
assert "Found hello prog" in c.out
if requires and tool_requires:
assert "There is already a 'tool/1.0' package contributing to CMAKE_PROGRAM_PATH" in c.out
class TestCMakeDepsPaths:

@pytest.mark.parametrize("requires, tool_requires", [(True, False), (False, True), (True, True)])
def test_find_program_path(self, requires, tool_requires):
"""Test that executables in bindirs of tool_requires can be found with
find_program() in consumer CMakeLists.
"""
c = TestClient()

conanfile = textwrap.dedent("""
import os
from conan.tools.files import copy
from conan import ConanFile
class TestConan(ConanFile):
name = "tool"
version = "1.0"
exports_sources = "*"
def package(self):
copy(self, "*", self.source_folder, os.path.join(self.package_folder, "bin"))
""")
c.save({"conanfile.py": conanfile, "hello": "", "hello.exe": ""})
c.run("create .")

requires = 'requires = "tool/1.0"' if requires else ""
tool_requires = 'tool_requires = "tool/1.0"' if tool_requires else ""
conanfile = textwrap.dedent(f"""
from conan import ConanFile
from conan.tools.cmake import CMake
class PkgConan(ConanFile):
{requires}
{tool_requires}
settings = "os", "arch", "compiler", "build_type"
generators = "CMakeToolchain", "CMakeDeps"
def build(self):
cmake = CMake(self)
cmake.configure()
""")
consumer = textwrap.dedent("""
cmake_minimum_required(VERSION 3.15)
project(MyHello)
find_program(HELLOPROG hello)
if(HELLOPROG)
message(STATUS "Found hello prog: ${HELLOPROG}")
endif()
""")
c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True)
c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}")
assert "Found hello prog" in c.out
if requires and tool_requires:
assert "There is already a 'tool/1.0' package contributing to CMAKE_PROGRAM_PATH" in c.out

def test_find_include_and_lib_paths(self):
c = TestClient()
conanfile = textwrap.dedent("""
import os
from conan.tools.files import copy
from conan import ConanFile
class TestConan(ConanFile):
name = "hello"
version = "1.0"
exports_sources = "*"
def package(self):
copy(self, "*.h", self.source_folder, os.path.join(self.package_folder, "include"))
copy(self, "*.lib", self.source_folder, os.path.join(self.package_folder, "lib"))
copy(self, "*.a", self.source_folder, os.path.join(self.package_folder, "lib"))
copy(self, "*.so", self.source_folder, os.path.join(self.package_folder, "lib"))
copy(self, "*.dll", self.source_folder, os.path.join(self.package_folder, "lib"))
""")
c.save({"conanfile.py": conanfile,
"hello.h": "", "hello.lib": "", "libhello.a": "",
"libhello.so": "", "libhello.dll": ""
})
c.run("create .")
conanfile = textwrap.dedent(f"""
from conan import ConanFile
from conan.tools.cmake import CMake
class PkgConan(ConanFile):
requires = "hello/1.0"
settings = "os", "arch", "compiler", "build_type"
generators = "CMakeToolchain", "CMakeDeps"
def build(self):
cmake = CMake(self)
cmake.configure()
""")
consumer = textwrap.dedent("""
cmake_minimum_required(VERSION 3.15)
project(MyHello)
find_file(HELLOINC hello.h)
find_library(HELLOLIB hello)
if(HELLOINC)
message(STATUS "Found hello header: ${HELLOINC}")
endif()
if(HELLOLIB)
message(STATUS "Found hello lib: ${HELLOLIB}")
endif()
""")
c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True)
c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}")
assert "Found hello header" in c.out
assert "Found hello lib" in c.out
92 changes: 92 additions & 0 deletions test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import re
import textwrap

from conan.test.utils.tools import TestClient

new_value = "will_break_next"


def test_cmakedeps_direct_deps_paths():
c = TestClient()
conanfile = textwrap.dedent("""
import os
from conan.tools.files import copy
from conan import ConanFile
class TestConan(ConanFile):
name = "lib"
version = "1.0"
def package_info(self):
self.cpp_info.includedirs = ["myincludes"]
self.cpp_info.libdirs = ["mylib"]
""")
c.save({"conanfile.py": conanfile})
c.run("create .")
conanfile = textwrap.dedent(f"""
from conan import ConanFile
from conan.tools.cmake import CMake
class PkgConan(ConanFile):
requires = "lib/1.0"
settings = "os", "arch", "compiler", "build_type"
generators = "CMakeDeps"
def build(self):
cmake = CMake(self)
cmake.configure()
""")
c.save({"conanfile.py": conanfile}, clean_first=True)
c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}")
cmake_paths = c.load("conan_cmakedeps_paths.cmake")
assert re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH \".*/bin\"", cmake_paths) # default
assert re.search(r"list\(PREPEND CMAKE_LIBRARY_PATH \".*/mylib\"", cmake_paths)
assert re.search(r"list\(PREPEND CMAKE_INCLUDE_PATH \".*/myincludes\"", cmake_paths)


def test_cmakedeps_transitive_paths():
c = TestClient()
conanfile = textwrap.dedent("""
import os
from conan.tools.files import copy
from conan import ConanFile
class TestConan(ConanFile):
name = "liba"
version = "1.0"
def package_info(self):
self.cpp_info.includedirs = ["includea"]
self.cpp_info.libdirs = ["liba"]
self.cpp_info.bindirs = ["bina"]
""")
c.save({"conanfile.py": conanfile})
c.run("create .")
conanfile = textwrap.dedent("""
import os
from conan.tools.files import copy
from conan import ConanFile
class TestConan(ConanFile):
name = "libb"
version = "1.0"
requires = "liba/1.0"
def package_info(self):
self.cpp_info.includedirs = ["includeb"]
self.cpp_info.libdirs = ["libb"]
self.cpp_info.bindirs = ["binb"]
""")
c.save({"conanfile.py": conanfile})
c.run("create .")
conanfile = textwrap.dedent(f"""
from conan import ConanFile
from conan.tools.cmake import CMake
class PkgConan(ConanFile):
requires = "libb/1.0"
settings = "os", "arch", "compiler", "build_type"
generators = "CMakeDeps"
def build(self):
cmake = CMake(self)
cmake.configure()
""")
c.save({"conanfile.py": conanfile}, clean_first=True)
c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}")
cmake_paths = c.load("conan_cmakedeps_paths.cmake")
cmake_paths.replace("\\", "/")
assert re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH \".*/libb.*/p/binb\"\)", cmake_paths)
assert not re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH /bina\"", cmake_paths)
assert re.search(r"list\(PREPEND CMAKE_LIBRARY_PATH \".*/libb.*/p/libb\" \".*/liba.*/p/liba\"\)", cmake_paths)
assert re.search(r"list\(PREPEND CMAKE_INCLUDE_PATH \".*/libb.*/p/includeb\" \".*/liba.*/p/includea\"\)", cmake_paths)

0 comments on commit 31d3201

Please sign in to comment.