conan icon indicating copy to clipboard operation
conan copied to clipboard

[feature] add property to CMakeDeps to source a package's CMake config file(s)

Open CD3 opened this issue 3 years ago • 2 comments

This is related to a topic that was brought up in issue #7118. That issue originally asked about a different feature, so I am opening a new one.

I think it would be very convenient if the CMakeDeps generator had an option to allow the <PKG>Config.cmake file installed in the package to be used. I know that this is not recommended because it means that Conan will not be able to generate files for other build systems, but for a team that uses CMake exclusively for in-house projects and already has a lot of capital invested in specifying build/install configurations in CMake files, this would make adopting Conan very easy.

This is currently possible with the CMakeToolchain generator. All you have to do is add the folder that contains the packages CMake config files to self.cpp_info.builddirs and the toolchain file will add it to CMAKE_PREFIX_PATH and friends. However, you need to make sure that CMakeDeps does not generate files for it, so you do something like this (assume a package has its CMake config files in a folder named cmake/):

def package_info(self):
        self.cpp_info.set_property("cmake_find_mode", "none")
        self.cpp_info.builddirs = os.path.join(self.package_folder,"cmake")

However, this won't work with multi-config builds because the CMake's find_package(...) will only source one file.

I propose adding a property that will tell CMakeDeps to just generate <PKG>Config.cmake file that will just source the package's config file. So you could do something like this:

def package_info(self):
        self.cpp_info.set_property("cmake_file", "MyLib")
        self.cpp_info.set_property("package_cmake_conig_file", os.path.join(self.package_folder,"cmake/MyLibConfig.cmake") )

If this property is set, then the CMakeDeps generator will write three files. MyLibConfig.cmake will just source files matching the pattern MyLibConfig-*.cmake

get_filename_component(_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
file(GLOB CONFIG_FILES "${_DIR}/MyLibConfig-*.cmake")

foreach(f ${CONFIG_FILES})
    include(${f})
endforeach()

A separate MyLibConfig-*.cmake file will be generated for each build type, similar to what is already done with the Targets and data file, and they just source the package_cmake_config_file file.

include("/path/to/the/package/folder/MyLibConfig.cmake")

The MyLibVersion.cmake that is already generated will also be written.

With this, single-config builds that use the package's CMake config file would work straight away (unless they depend on the value MyLib_DIR, which I have found a few of ours that do), and multi-config configs could be supported if the package wanted to. It would need to define Release and Debug versions of its targets and then have the targets that it "exports" link against these with a CMake generator expression. For my projects, I can actually edit the packages cmake config files to do this, and it works.

Anyhow, I think this would be a very clean way for user to just use the CMake config files they have already developed and support multi-config builds, but I may be overlooking something. What do you think?

CD3 avatar Jun 26 '22 22:06 CD3

This is currently possible with the CMakeToolchain generator. All you have to do is add the folder that contains the packages CMake config files to self.cpp_info.builddirs and the toolchain file will add it to CMAKE_PREFIX_PATH and friends. However, you need to make sure that CMakeDeps does not generate files for it, so you do something like this (assume a package has its CMake config files in a folder named cmake/):

def package_info(self):
        self.cpp_info.set_property("cmake_find_mode", "none")
-         self.cpp_info.builddirs = os.path.join(self.package_folder,"cmake")
+         self.cpp_info.builddirs = [os.path.join(self.package_folder,"cmake")]

And actually I think you'd be safe with doing self.cpp_info.builddirs = [self.package_folder] because CMake will automagically search inside the cmake folder for you. See Config Mode Search Procedure

@CD3 , thank you for calling this out!

I had just discovered this discrepancy between the cmake and CMakeToolChain generators.

By default, the cmake generator prepends the CONAN_CMAKE_MODULE_PATH to CMAKE_PREFIX_PATH:

 # Make find_package() to work
 set(CMAKE_PREFIX_PATH ${CONAN_CMAKE_MODULE_PATH} ${CMAKE_PREFIX_PATH})

This lets find_package find xxxx-Config.cmake files shipped alongside a package.

But the latter does not.

Just setting the cmake_find_mode to none isn't enough. Adding the package (or cmake folder) to self.cpp_info.builddirs is key, otherwise find_package won't work.

The doc for self.cpp_info.builddirs mentions FindXXX.cmake files, but not XXXConfig.cmake files, so your comment was very helpful to me!

Thank you!

thorntonryan avatar Jul 20 '22 22:07 thorntonryan

@thorntonryan Thanks for the correction, builddirs needs to be a list. And I think your right about just using self.package_folder, I'll start using that.

CD3 avatar Jul 21 '22 03:07 CD3

Hi @CD3

This is the typical xxxxConfig.cmake generated by CMake install(EXPORT ...:

#----------------------------------------------------------------
# Generated CMake target import file.
#----------------------------------------------------------------

# Commands may need to know the format version.
set(CMAKE_IMPORT_FILE_VERSION 1)

# Protect against multiple inclusion, which would fail when already imported targets are added once more.
set(_targetsDefined)
set(_targetsNotDefined)
set(_expectedTargets)
foreach(_expectedTarget hello::hello)
  list(APPEND _expectedTargets ${_expectedTarget})
  if(NOT TARGET ${_expectedTarget})
    list(APPEND _targetsNotDefined ${_expectedTarget})
  endif()
  if(TARGET ${_expectedTarget})
    list(APPEND _targetsDefined ${_expectedTarget})
  endif()
endforeach()
if("${_targetsDefined}" STREQUAL "${_expectedTargets}")
  unset(_targetsDefined)
  unset(_targetsNotDefined)
  unset(_expectedTargets)
  set(CMAKE_IMPORT_FILE_VERSION)
  cmake_policy(POP)
  return()
endif()
if(NOT "${_targetsDefined}" STREQUAL "")
  message(FATAL_ERROR "Some (but not all) targets in this export set were already defined.\nTargets Defined: ${_targetsDefined}\nTargets not yet defined: ${_targetsNotDefined}\n")
endif()
unset(_targetsDefined)
unset(_targetsNotDefined)
unset(_expectedTargets)


# The installation prefix configured by this project.
set(_IMPORT_PREFIX "T:/tmpcijqls5_conans/path with spaces/data/hello/0.1/_/_/package/3fb49604f9c2f729b85ba3115852006824e72cab")

# Create imported target hello::hello
add_library(hello::hello INTERFACE IMPORTED)

As you can see, this will make your proposed mechanism fail. The different xxxConfig.cmake files for different build types cannot be include().

This is not directly related, but be aware that there are other issues with the packaged xxxConfig.cmake, like the _IMPORT_PREFIX being an absolute path. This will make the package non-relocatable, this will not work if the package is consumed in another computer with a different path (very likely, this is the purpose of packages to be able to use them in other computers). This needs to be workarounded too in the recipe/build scripts. Furthermore, the transitivity can be also problematic. When someone depends on another Conan package, the default generated information in xxxConfig.cmake only contains set_target_properties(consumer::consumer PROPERTIES INTERFACE_LINK_LIBRARIES "dep"), but not a real find that will help locate dep. That means for example that it is easy to link accidentally an old zlib or openssl library in the system, instead of the latest one from the Conan package dependency.

memsharded avatar Aug 29 '22 11:08 memsharded

Hmmm, all good points. Perhaps it is best solved on a case-by-case basis. I am currently using a custom generator that does this, but I don't have a remote cache, so I have not run into the absolute path issue. We are just using Conan to automate our in-house builds.

Thanks for the consideration.

CD3 avatar Oct 13 '22 14:10 CD3

@memsharded One more question: the docs mention that it requires the CMaketoolchain generator for that. Is this also possible with just CMakeDeps?

As mentioned above I'm exporting targets using install() and would like to use them, but can't use the CMaketoolchain as we are using the cmake wrapper.

Tobulus avatar Jan 30 '23 15:01 Tobulus

It might be possible, CMakeToolchain is doing:

  • Adding the right paths to find the files generated by CMakeDeps
  • Defining runtime flags, like MT/MD for Visual
  • Define POSITION_INDEPENDENT_CODE for fPIC option
  • Append libcxx flags like "-stdlib=libc++"
  • Add cppstd flags like CMAKE_CXX_STANDARD
  • Define BUILD_SHARED_LIBS if necessary
  • Define ANDROID_XXX variables as necessary
  • Add several Apple flags like ```VISIBILITY``

All of those things are important to align with the dependencies binaries, otherwise it is possible to have binary mismatches and incompatibilities. If all of those factors are already controlled by your own CMake, including the necessary variability when building for different configurations, then CMakeToolchain might not be completely necessary, and providing the CMAKE_MODULE_PATH and CMAKE_PREFIX_PATH to locate the CMakeDeps could be enough.

Of course, this only works as a consumer, for creating the packages and reusing them, the cmake-wrapper does not work, so using the CMakeToolchain would still be necessary.

memsharded avatar Jan 30 '23 15:01 memsharded