From af371ebccfeadc699699039fa17c60c832e77c3b Mon Sep 17 00:00:00 2001 From: Luis Martinez de Bartolome Izquierdo Date: Tue, 29 May 2018 13:51:35 +0200 Subject: [PATCH] Non-intrusive integration with CMake (#2875) * mac passing * Fixed runtime windows * Tested priority * fixed zlib find test * Fixed test and added interface_compile_definitions * Removed priority of the cmake install folder from the install_folder * Fixed test, added _LIBS var * PENDING WORK TO MAKE IT TRANSITIVE * Transitive targets * Removed msg * Force CI * Automatic module path * Add new priorize test * Fixed test * Replace * Fixed win --- conans/client/build/autotools_environment.py | 2 +- conans/client/build/cmake.py | 4 + conans/client/generators/__init__.py | 6 +- .../client/generators/cmake_find_package.py | 99 ++++++++ conans/client/generators/cmake_paths.py | 22 ++ .../generators/cmake_find_package_test.py | 103 +++++++++ conans/test/generators/cmake_paths_test.py | 216 ++++++++++++++++++ 7 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 conans/client/generators/cmake_find_package.py create mode 100644 conans/client/generators/cmake_paths.py create mode 100644 conans/test/generators/cmake_find_package_test.py create mode 100644 conans/test/generators/cmake_paths_test.py diff --git a/conans/client/build/autotools_environment.py b/conans/client/build/autotools_environment.py index 9bf4bd3e8a2..f43e6dc818f 100644 --- a/conans/client/build/autotools_environment.py +++ b/conans/client/build/autotools_environment.py @@ -138,7 +138,7 @@ def configure(self, configure_dir=None, args=None, build=None, host=None, target else: # If we are using pkg_config generator automate the pcs location, otherwise it could # read wrong files - pkg_env = {"PKG_CONFIG_PATH": self._conanfile.build_folder} \ + pkg_env = {"PKG_CONFIG_PATH": self._conanfile.install_folder} \ if "pkg_config" in self._conanfile.generators else {} if self._conanfile.package_folder is not None: diff --git a/conans/client/build/cmake.py b/conans/client/build/cmake.py index b5f8cb84351..8237a716b7d 100644 --- a/conans/client/build/cmake.py +++ b/conans/client/build/cmake.py @@ -338,6 +338,10 @@ def add_cmake_flag(cmake_flags, name, flag): shared = self._conanfile.options.get_safe("shared") ret["CONAN_CMAKE_POSITION_INDEPENDENT_CODE"] = "ON" if (fpic or shared) else "OFF" + # Adjust automatically the module path in case the conanfile is using the cmake_find_package + if "cmake_find_package" in self._conanfile.generators: + ret["CMAKE_MODULE_PATH"] = self._conanfile.install_folder.replace("\\", "/") + return ret def _get_dirs(self, source_folder, build_folder, source_dir, build_dir, cache_build_folder): diff --git a/conans/client/generators/__init__.py b/conans/client/generators/__init__.py index 817a6e738b7..147dd13ea8a 100644 --- a/conans/client/generators/__init__.py +++ b/conans/client/generators/__init__.py @@ -1,5 +1,6 @@ from os.path import join +from conans.client.generators.cmake_find_package import CMakeFindPackageGenerator from conans.client.generators.compiler_args import CompilerArgsGenerator from conans.client.generators.pkg_config import PkgConfigGenerator from conans.errors import ConanException @@ -9,6 +10,8 @@ from .text import TXTGenerator from .gcc import GCCGenerator from .cmake import CMakeGenerator +from .cmake_paths import CMakePathsGenerator +from .cmake_multi import CMakeMultiGenerator from .qmake import QmakeGenerator from .qbs import QbsGenerator from .scons import SConsGenerator @@ -18,7 +21,6 @@ from .xcode import XCodeGenerator from .ycm import YouCompleteMeGenerator from .virtualenv import VirtualEnvGenerator -from .cmake_multi import CMakeMultiGenerator from .virtualbuildenv import VirtualBuildEnvGenerator from .boostbuild import BoostBuildGenerator from .json_generator import JsonGenerator @@ -52,6 +54,8 @@ def __getitem__(self, key): registered_generators.add("compiler_args", CompilerArgsGenerator) registered_generators.add("cmake", CMakeGenerator) registered_generators.add("cmake_multi", CMakeMultiGenerator) +registered_generators.add("cmake_paths", CMakePathsGenerator) +registered_generators.add("cmake_find_package", CMakeFindPackageGenerator) registered_generators.add("qmake", QmakeGenerator) registered_generators.add("qbs", QbsGenerator) registered_generators.add("scons", SConsGenerator) diff --git a/conans/client/generators/cmake_find_package.py b/conans/client/generators/cmake_find_package.py new file mode 100644 index 00000000000..fec06074db6 --- /dev/null +++ b/conans/client/generators/cmake_find_package.py @@ -0,0 +1,99 @@ +from conans.client.generators.cmake import DepsCppCmake +from conans.model import Generator + + +generic_find_package_template = """ +message(STATUS "Conan: Using autogenerated Find{name}.cmake") +# Global approach +SET({name}_FOUND 1) +SET({name}_INCLUDE_DIRS {deps.include_paths}) +SET({name}_INCLUDES {deps.include_paths}) +SET({name}_DEFINITIONS {deps.defines}) +SET({name}_LIBRARIES "") # Will be filled later +SET({name}_LIBRARIES_TARGETS "") # Will be filled later, if CMake 3 +SET({name}_LIBS "") # Same as {name}_LIBRARIES + +mark_as_advanced({name}_FOUND {name}_INCLUDE_DIRS {name}_INCLUDES + {name}_DEFINITIONS {name}_LIBRARIES {name}_LIBS) + + +# Find the real .lib/.a and add them to {name}_LIBS and {name}_LIBRARY_LIST +SET({name}_LIBRARY_LIST {deps.libs}) +SET({name}_LIB_DIRS {deps.lib_paths}) +foreach(_LIBRARY_NAME ${{{name}_LIBRARY_LIST}}) + unset(CONAN_FOUND_LIBRARY CACHE) + find_library(CONAN_FOUND_LIBRARY NAME ${{_LIBRARY_NAME}} PATHS ${{{name}_LIB_DIRS}} + NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) + if(CONAN_FOUND_LIBRARY) + if(${{CMAKE_VERSION}} VERSION_LESS "3.0") + list(APPEND {name}_LIBRARIES ${{CONAN_FOUND_LIBRARY}}) + else() # Create a micro-target for each lib/a found + set(_LIB_NAME CONAN_LIB::{name}_${{_LIBRARY_NAME}}) + add_library(${{_LIB_NAME}} UNKNOWN IMPORTED) + set_target_properties(${{_LIB_NAME}} PROPERTIES IMPORTED_LOCATION ${{CONAN_FOUND_LIBRARY}}) + list(APPEND {name}_LIBRARIES_TARGETS ${{_LIB_NAME}}) + endif() + message(STATUS "Found: ${{CONAN_FOUND_LIBRARY}}") + else() + message(STATUS "Library ${{_LIBRARY_NAME}} not found in package, might be system one") + endif() +endforeach() +set({name}_LIBS ${{{name}_LIBRARIES}}) + +if(NOT ${{CMAKE_VERSION}} VERSION_LESS "3.0") + # Target approach + if(NOT TARGET {name}::{name}) + add_library({name}::{name} INTERFACE IMPORTED) + if({name}_INCLUDE_DIRS) + set_target_properties({name}::{name} PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${{{name}_INCLUDE_DIRS}}") + endif() + set_property(TARGET {name}::{name} PROPERTY INTERFACE_LINK_LIBRARIES ${{{name}_LIBRARIES_TARGETS}}) + set_property(TARGET {name}::{name} PROPERTY INTERFACE_COMPILE_DEFINITIONS {deps.defines}) + endif() + {find_dependencies} +endif() +""" + + +class CMakeFindPackageGenerator(Generator): + + @property + def filename(self): + pass + + @property + def content(self): + ret = {} + for depname, cpp_info in self.deps_build_info.dependencies: + ret["Find%s.cmake" % depname] = self._single_find_package(depname, cpp_info) + return ret + + @staticmethod + def _single_find_package(name, cpp_info): + deps = DepsCppCmake(cpp_info) + lines = [] + if cpp_info.public_deps: + lines = CMakeFindPackageGenerator._transitive_lines(name, cpp_info) + tmp = generic_find_package_template.format(name=name, deps=deps, + find_dependencies="\n".join(lines)) + return tmp + + @staticmethod + def _transitive_lines(name, cpp_info): + lines = ["# Library dependencies", "include(CMakeFindDependencyMacro)"] + for dep in cpp_info.public_deps: + def property_lines(prop): + lib_t = "%s::%s" % (name, name) + dep_t = "%s::%s" % (dep, dep) + return ["get_target_property(tmp %s %s)" % (dep_t, prop), + "if(tmp)", + " set_property(TARGET %s APPEND PROPERTY %s ${tmp})" % (lib_t, prop), + ' message("${tmp}")', + 'endif()'] + + lines.append("find_dependency(%s REQUIRED)" % dep) + lines.extend(property_lines("INTERFACE_LINK_LIBRARIES")) + lines.extend(property_lines("INTERFACE_COMPILE_DEFINITIONS")) + lines.extend(property_lines("INTERFACE_INCLUDE_DIRECTORIES")) + return lines diff --git a/conans/client/generators/cmake_paths.py b/conans/client/generators/cmake_paths.py new file mode 100644 index 00000000000..003a010b8b9 --- /dev/null +++ b/conans/client/generators/cmake_paths.py @@ -0,0 +1,22 @@ +from conans.client.generators.cmake import DepsCppCmake +from conans.model import Generator + + +class CMakePathsGenerator(Generator): + + @property + def filename(self): + return "conan_paths.cmake" + + @property + def content(self): + deps = DepsCppCmake(self.deps_build_info) + # We want to prioritize the FindXXX.cmake files: + # 1. First the files found in the packages + # 2. The previously set (by default CMAKE_MODULE_PATH is empty) + # 3. The "install_folder" ones, in case there is no FindXXX.cmake, try with the install dir + # if the user used the "cmake_find_package" will find the auto-generated + # 4. The CMake installation dir/Modules ones. + return """set(CMAKE_MODULE_PATH {deps.build_paths} ${{CMAKE_MODULE_PATH}} ${{CMAKE_CURRENT_LIST_DIR}}) +set(CMAKE_PREFIX_PATH {deps.build_paths} ${{CMAKE_PREFIX_PATH}} ${{CMAKE_CURRENT_LIST_DIR}}) +""".format(deps=deps) diff --git a/conans/test/generators/cmake_find_package_test.py b/conans/test/generators/cmake_find_package_test.py new file mode 100644 index 00000000000..56ba9e2ba0a --- /dev/null +++ b/conans/test/generators/cmake_find_package_test.py @@ -0,0 +1,103 @@ +import unittest +from conans.test.utils.cpp_test_files import cpp_hello_conan_files +from conans.test.utils.tools import TestClient +from nose.plugins.attrib import attr + + +@attr('slow') +class CMakeFindPathGeneratorTest(unittest.TestCase): + + def cmake_find_package_test(self): + """First package without custom find_package""" + client = TestClient() + files = cpp_hello_conan_files(name="Hello0", + settings='"os", "compiler", "arch", "build_type"') + client.save(files) + client.run("create . user/channel -s build_type=Release") + + # Consume the previous Hello0 with auto generated FindHello0.cmake + # The module path will point to the "install" folder automatically (CMake helper) + files = cpp_hello_conan_files(name="Hello1", deps=["Hello0/0.1@user/channel"], + settings='"os", "compiler", "arch", "build_type"') + files["conanfile.py"] = files["conanfile.py"].replace( + 'generators = "cmake", "gcc"', + 'generators = "cmake_find_package"') + files["CMakeLists.txt"] = """ +set(CMAKE_CXX_COMPILER_WORKS 1) +set(CMAKE_CXX_ABI_COMPILED 1) +project(MyHello CXX) +cmake_minimum_required(VERSION 2.8) + +find_package(Hello0 REQUIRED) + +add_library(helloHello1 hello.cpp) +target_link_libraries(helloHello1 PUBLIC Hello0::Hello0) +add_executable(say_hello main.cpp) +target_link_libraries(say_hello helloHello1) + +""" + client.save(files, clean_first=True) + client.run("create . user/channel -s build_type=Release") + self.assertIn("Conan: Using autogenerated FindHello0.cmake", client.out) + + # Now link with old cmake + files["CMakeLists.txt"] = """ +set(CMAKE_VERSION "2.8") +set(CMAKE_CXX_COMPILER_WORKS 1) +set(CMAKE_CXX_ABI_COMPILED 1) +project(MyHello CXX) +cmake_minimum_required(VERSION 2.8) +message(${CMAKE_BINARY_DIR}) +set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR} ${CMAKE_MODULE_PATH}) + +find_package(Hello0 REQUIRED) + +add_library(helloHello1 hello.cpp) + +if(NOT DEFINED Hello0_FOUND) + message(FATAL_ERROR "Hello0_FOUND not declared") +endif() +if(NOT DEFINED Hello0_INCLUDE_DIRS) + message(FATAL_ERROR "Hello0_INCLUDE_DIRS not declared") +endif() +if(NOT DEFINED Hello0_INCLUDES) + message(FATAL_ERROR "Hello0_INCLUDES not declared") +endif() +if(NOT DEFINED Hello0_LIBRARIES) + message(FATAL_ERROR "Hello0_LIBRARIES not declared") +endif() + +include_directories(${Hello0_INCLUDE_DIRS}) +target_link_libraries(helloHello1 PUBLIC ${Hello0_LIBS}) +add_executable(say_hello main.cpp) +target_link_libraries(say_hello helloHello1) + +""" + client.save(files, clean_first=True) + client.run("create . user/channel -s build_type=Release") + self.assertIn("Conan: Using autogenerated FindHello0.cmake", client.out) + + # Now a transitive consumer, but the consumer only find_package the first level Hello1 + files = cpp_hello_conan_files(name="Hello2", deps=["Hello1/0.1@user/channel"], + settings='"os", "compiler", "arch", "build_type"') + files["CMakeLists.txt"] = """ +set(CMAKE_CXX_COMPILER_WORKS 1) +set(CMAKE_CXX_ABI_COMPILED 1) +project(MyHello CXX) +cmake_minimum_required(VERSION 2.8) +set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR} ${CMAKE_MODULE_PATH}) +find_package(Hello1 REQUIRED) # We don't need to find Hello0, it is transitive + +add_library(helloHello2 hello.cpp) +target_link_libraries(helloHello2 PUBLIC Hello1::Hello1) + +add_executable(say_hello main.cpp) +target_link_libraries(say_hello helloHello2) + """ + files["conanfile.py"] = files["conanfile.py"].replace( + 'generators = "cmake", "gcc"', + 'generators = "cmake_find_package"') + client.save(files, clean_first=True) + client.run("create . user/channel -s build_type=Release") + self.assertIn("Conan: Using autogenerated FindHello0.cmake", client.out) + self.assertIn("Conan: Using autogenerated FindHello1.cmake", client.out) diff --git a/conans/test/generators/cmake_paths_test.py b/conans/test/generators/cmake_paths_test.py new file mode 100644 index 00000000000..958b582b025 --- /dev/null +++ b/conans/test/generators/cmake_paths_test.py @@ -0,0 +1,216 @@ +import shutil +import unittest +import os +from collections import namedtuple + +from conans.client.generators.cmake_paths import CMakePathsGenerator +from conans.model.build_info import CppInfo +from conans.model.conan_file import ConanFile +from conans.test.utils.test_files import temp_folder +from conans.test.utils.tools import TestClient + + +class CMakePathsGeneratorTest(unittest.TestCase): + + def cmake_vars_unit_test(self): + settings_mock = namedtuple("Settings", "build_type, os, os_build, constraint") + settings = settings_mock("Release", None, None, lambda x, raise_undefined_field: x) + conanfile = ConanFile(None, None, settings, None) + tmp = temp_folder() + cpp_info = CppInfo(tmp) + custom_dir = os.path.join(tmp, "custom_build_dir") + os.mkdir(custom_dir) + cpp_info.builddirs.append(os.path.join(tmp, "custom_build_dir")) + conanfile.deps_cpp_info.update(cpp_info, "MyLib") + + generator = CMakePathsGenerator(conanfile) + cmake_lines = [s.replace("\t\t\t", "").replace('\\', '/') + for s in generator.content.splitlines()] + self.assertEquals('set(CMAKE_MODULE_PATH ' + '"%s/"' % tmp.replace('\\', '/'), cmake_lines[0]) + self.assertEquals('"%s" ${CMAKE_MODULE_PATH} ' + '${CMAKE_CURRENT_LIST_DIR})' % custom_dir.replace('\\', '/'), + cmake_lines[1]) + self.assertEquals('set(CMAKE_PREFIX_PATH ' + '"%s/"' % tmp.replace('\\', '/'), cmake_lines[2]) + self.assertEquals('"%s" ${CMAKE_PREFIX_PATH} ' + '${CMAKE_CURRENT_LIST_DIR})' % custom_dir.replace('\\', '/'), + cmake_lines[3]) + + def cmake_paths_integration_test(self): + """First package with own findHello0.cmake file""" + client = TestClient() + conanfile = """from conans import ConanFile +class TestConan(ConanFile): + name = "Hello0" + version = "0.1" + exports = "*" + + def package(self): + self.copy(pattern="*", keep_path=False) + +""" + files = {"conanfile.py": conanfile, + "FindHello0.cmake": """ +SET(Hello0_FOUND 1) +MESSAGE("HELLO FROM THE Hello0 FIND PACKAGE!") +"""} + client.save(files) + client.run("create . user/channel") + + # Directly using CMake as a consumer we can find it with the "cmake_paths" generator + files = {"CMakeLists.txt": """ +set(CMAKE_CXX_COMPILER_WORKS 1) +set(CMAKE_CXX_ABI_COMPILED 1) +project(MyHello CXX) +cmake_minimum_required(VERSION 2.8) + +find_package(Hello0 REQUIRED) + +"""} + client.save(files, clean_first=True) + client.run("install Hello0/0.1@user/channel -g cmake_paths") + self.assertTrue(os.path.exists(os.path.join(client.current_folder, + "conan_paths.cmake"))) + # Without the toolchain we cannot find the package + build_dir = os.path.join(client.current_folder, "build") + os.mkdir(build_dir) + ret = client.runner("cmake ..", cwd=build_dir) + shutil.rmtree(build_dir) + self.assertNotEqual(ret, 0) + + # With the toolchain everything is ok + os.mkdir(build_dir) + ret = client.runner("cmake .. -DCMAKE_TOOLCHAIN_FILE=../conan_paths.cmake", + cwd=build_dir) + self.assertEqual(ret, 0) + self.assertIn("HELLO FROM THE Hello0 FIND PACKAGE!", client.out) + + # Now try without toolchain but including the file + files = {"CMakeLists.txt": """ +set(CMAKE_CXX_COMPILER_WORKS 1) +set(CMAKE_CXX_ABI_COMPILED 1) +project(MyHello CXX) +cmake_minimum_required(VERSION 2.8) +include(${CMAKE_BINARY_DIR}/../conan_paths.cmake) + +find_package(Hello0 REQUIRED) + +"""} + client.save(files, clean_first=True) + os.mkdir(build_dir) + client.run("install Hello0/0.1@user/channel -g cmake_paths") + ret = client.runner("cmake .. ", cwd=build_dir) + self.assertEqual(ret, 0) + self.assertIn("HELLO FROM THE Hello0 FIND PACKAGE!", client.out) + + def find_package_priority_test(self): + """A package findXXX has priority over the CMake installation one and the curdir one""" + client = TestClient() + conanfile = """from conans import ConanFile +class TestConan(ConanFile): + name = "Zlib" + version = "0.1" + exports = "*" + + def package(self): + self.copy(pattern="*", keep_path=False) + +""" + files = {"conanfile.py": conanfile, + "FindZLIB.cmake": 'MESSAGE("HELLO FROM THE PACKAGE FIND PACKAGE!")'} + client.save(files) + client.run("create . user/channel") + + files = {"CMakeLists.txt": """ +set(CMAKE_CXX_COMPILER_WORKS 1) +set(CMAKE_CXX_ABI_COMPILED 1) +project(MyHello CXX) +cmake_minimum_required(VERSION 2.8) +include(${CMAKE_BINARY_DIR}/../conan_paths.cmake) + +find_package(ZLIB REQUIRED) + +"""} + build_dir = os.path.join(client.current_folder, "build") + files["FindZLIB.cmake"] = 'MESSAGE("HELLO FROM THE INSTALL FOLDER!")' + client.save(files, clean_first=True) + os.mkdir(build_dir) + client.run("install Zlib/0.1@user/channel -g cmake_paths") + ret = client.runner("cmake .. ", cwd=build_dir) + self.assertEquals(ret, 0) + self.assertIn("HELLO FROM THE PACKAGE FIND PACKAGE!", client.out) + + # Now consume the zlib package as a require with the cmake_find_package + # and the "cmake" generator. Should prioritize the one from the package. + conanfile = """from conans import ConanFile, CMake +class TestConan(ConanFile): + name = "Consumer" + version = "0.1" + exports = "*" + settings = "compiler", "arch" + generators = "cmake_find_package", "cmake" + requires = "Zlib/0.1@user/channel" + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + self.copy(pattern="*", keep_path=False) + +""" + files = {"conanfile.py": conanfile, + "CMakeLists.txt": """ +set(CMAKE_CXX_COMPILER_WORKS 1) +set(CMAKE_CXX_ABI_COMPILED 1) +project(MyHello CXX) +cmake_minimum_required(VERSION 2.8) +include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) +conan_basic_setup() + +find_package(ZLIB REQUIRED) + +"""} + client.save(files, clean_first=True) + client.run("create . user/channel") + self.assertIn("HELLO FROM THE PACKAGE FIND PACKAGE!", client.out) + self.assertNotIn("Conan: Using autogenerated FindZlib.cmake", client.out) + + def find_package_priority2_test(self): + """A system findXXX has NOT priority over the install folder one, + the zlib package do not package findZLIB.cmake""" + client = TestClient() + conanfile = """from conans import ConanFile +class TestConan(ConanFile): + name = "Zlib" + version = "0.1" + exports = "*" + + def package(self): + self.copy(pattern="*", keep_path=False) + +""" + files = {"conanfile.py": conanfile} + client.save(files) + client.run("create . user/channel") + + files = {"CMakeLists.txt": """ +set(CMAKE_CXX_COMPILER_WORKS 1) +set(CMAKE_CXX_ABI_COMPILED 1) +project(MyHello CXX) +cmake_minimum_required(VERSION 2.8) +include(${CMAKE_BINARY_DIR}/../conan_paths.cmake) + +find_package(ZLIB REQUIRED) + +"""} + build_dir = os.path.join(client.current_folder, "build") + files["FindZLIB.cmake"] = 'MESSAGE("HELLO FROM THE INSTALL FOLDER!")' + client.save(files, clean_first=True) + os.mkdir(build_dir) + client.run("install Zlib/0.1@user/channel -g cmake_paths") + ret = client.runner("cmake .. ", cwd=build_dir) + self.assertEquals(ret, 0) + self.assertIn("HELLO FROM THE INSTALL FOLDER!", client.out)