Conan creates CMake targets inconsistent with everything, including Conan itself
Saturday June 13, 2020 09:32:55

Update: Things in Conan are improving beyond what I document here. I've added an update to the end of this post describing how.

Conan provides multiple “generators” to use for integrating with CMake. Two of them - cmake_paths and cmake_find_package- promise to allow the use of Conan from CMake “unintrusively”, which presumably means if you wanted, you could kick Conan to the curb and still use your CMake scripts with other package managers.

cmake_paths creates a CMake tool chain file which lets your CMake scripts consume the package config files created as part of Conan packaged CMake projects (the thing which the mainstream Conan package sources don't support adding) while “cmake_find_package” creates find modules for every dependency you need.

One major issue: the CMake targets created by using the “find modules” are different from the ones you get from the package config files. This means the generator you use when running Conan will affect the CMake code you have to write.

The BinCrafters SDL 2.0.9 package, used with cmake_find_package, will create a file named Findsdl2.cmake. Your CMake code then needs to call find_package(sdl2) and use the targets sdl2::sdl2. But if you use the cmake_paths generator to get the package files instead you'll need to call find_package(SDL2) and use the targets SDL2::SDL2-static (and, in some cases, SDL2::SDL2).

For other Conan packages, such as BinCrafter's SDL 2 Image, cmake_paths simply doesn't work. There are no CMake package files included with the Conan package, so using find_package simply won't work.

However, it's possible to combine the cmake_paths and cmake_find_package generators like Voltron so you get both the package config files and also the find modules at the same time. If you do this, you can select which version is loaded in CMake by using either the CONFIG (cmake_paths) or MODULE (cmake_find_package) argument when you call find_package.

So, all is well! You get the great, standard CMake targets you crave along with the ability to use a package manager that seems to have near infinite reach and ability to conceptualize a task such as “install Strawberry Perl and then use it to build a single yet critical dependency, but only on Windows”.

Well, not quite.

I created a library called lp3-sdl which used BinCrafter's SDL 2.0.9 and had a CMake script capable of installing a real CMake package config file, but was then turned into a real Conan package as part of CI. I then made a test project in Conan which pulled down lp3-sdl as a dependency and tried to build something simple to make sure that the Conan / CMake packaging process had worked.

What I found was in Windows I got CMake errors in the test project because the CMake target sdl2::sdl2 was not found. After spending hours debugging I finally realized what had happened.

In the test project's CMake file I loaded the lp3-sdl dependency by calling find_package(Lp3_Sdl CONFIG REQUIRED). I specified CONFIG to make sure it would use the CMake package config file I'd hand-crafted and stored inside the Conan package instead of the Conan generated find module files. The package config CMake script then called find_dependency(SDL2) and made the main lp3-sdl library dependent on the target SDL2::SDL2-static / SDL2::SDL2.

However, the test project also had a dependency on BinCrafter's SDL 2 Image package, and because of that _had_ to use the cmake_find_package generator. This meant it got find modules for every dependency Conan knew the test project needed, including transitive dependencies such as SDL2.

Because 1. Findsdl2.cmake was sitting in the build directory, 2. CMake by default uses find modules if it can find them instead of the package config files and 3. Windows has a case insensitive file system, find_dependency(sdl2) in lp3-sdl's package config script caused Conan's cmake_find_package style target, ie sdl2::sdl2, to be introduced instead of SDL2::SDL2-static / SDL2::SDL2, which the package config script still attached as a part of the “INTERFACE_LINK_LIBRARIES” to the main library it exported.

Finally the test project created an executable which depended on lp3-sdl's library which depended on SDL2:: style targets that weren't there. At this point CMake showed me an error stating that it could not find SDL2::SDL2-static when linking the test project executable, which was confusing because by that point I was expecting all of the find_package shannannigans to be finished.

So to summarize: Conan creates CMake targets which are not only inconsistent with CMake's built-in Find Modules (which are supported by at least some C++ package managers) but with alternate runs of Conan itself!

I sort of kind of worked around it with one more hack. In lp3-sdl's CMake file, I now determine which target I need to depend on, and save it as a variable:

if (TARGET SDL2::SDL2)
      set(SDL2_TARGET_NAME "SDL2::SDL2")
      target_link_libraries(lp3_sdl PUBLIC SDL2::SDL2)
elseif(TARGET SDL2::SDL2-static)
    set(SDL2_TARGET_NAME "SDL2::SDL2-static")
    target_link_libraries(lp3_sdl PUBLIC SDL2::SDL2-static)
else()
    message(FATAL_ERROR "Could not find target SDL2::SDL2 or SDL2::SDL2-static!")
endif()

Later, when I'm writing the package config file, I add a ton of gross code after find_dependency(SDL2) to, if necessary, create SDL2::SDL2-static (or SDL2::SDL2) as an alias to sdl2::sdl2, which Conan has created:

file(
    WRITE "${PROJECT_BINARY_DIR}/Lp3_SdlConfig.cmake"
    "
include(CMakeFindDependencyMacro)
if (NOT \"\${CMAKE_SYSTEM_NAME}\" MATCHES \"Emscripten\")
    # On Windows, Conan generated Findsdl2.cmake may get used here instead of
    # the one we want. If that's happened, we can tell because the targets
    # we need will be missing. So make aliases of the Conan targets instead.
    find_dependency(SDL2)
    if (NOT TARGET ${SDL2_TARGET_NAME} AND TARGET \"sdl2::sdl2\")
        add_library(${SDL2_TARGET_NAME} INTERFACE IMPORTED)
        set_target_properties(${SDL2_TARGET_NAME} PROPERTIES INTERFACE_LINK_LIBRARIES \"sdl2::sdl2\")
    elseif (NOT TARGET ${SDL2_TARGET_NAME} AND TARGET \"SDL2::SDL2\")
        add_library(${SDL2_TARGET_NAME} INTERFACE IMPORTED)
        set_target_properties(${SDL2_TARGET_NAME} PROPERTIES INTERFACE_LINK_LIBRARIES \"SDL2::SDL2\")
    endif()
endif()
include(\"\${CMAKE_CURRENT_LIST_DIR}/Lp3_Sdl-targets.cmake\")
")

So, there you have it.

Is this a solution?

Of course it isn't! At this point I have no faith at all that what I'm doing will scale in the future and won't break on some similarly hard to detect issue.

This came out of a three day weekend where I spent hours working around endless problems exactly like this one. Many of the fixes I tried out ended up causing new problems, and often the only way I could ever know for sure if everything was working or if I even had the right conceptual model for what was going on was to spend repeatedly build several projects back to back in one of the slowest feedback loops possible (even after I wrote a series of scripts to automate all the calls to Conan and CMake) or to create entirely new standalone “recreate” projects in Conan and CMake.

So much of this wasted effort is because the Conan community doesn't believe adherring to a standard or at least consistent target naming policy is worth doing, even as they've advertised using Conan with CMake “unintrusively” in the past.

Meanwhile, BinCrafters states very directly that the use of find_package “should be avoided” when consuming their packages. With all due respect to the hard working people behind BinCrafters, I'm going to lay this out there: saying “should be” is nonsense. They need to explicitly state something like “we don't support using find_package. It will not work even if you sometimes think it does. So if you're using our packages don't even think of using CMake like that.”

If find_package is considered a no-no for a player as big as BinCrafters, and Conan Center itself is deleting the package config files created by Boost itself, then who is the cmake_paths generator for? I'm guessing people like Mathieu Ropert are using it in situations where they build everything from scratch themselves.

For others though, be wary: just because the Conan program itself offers flexibility to build software any crazy way you want it doesn't mean the plethora of publicly available packages for Conan are going to support it.

Update: James from Conan.io let me know that Conan has added methods to use the canonical CMake targets when Conan itself is generating CMake scripts, so it's possible to use those names even when the CMake package config files are not being used. He went onto say achieving parity with the canonical CMake target names is a high priority, and work is underway to update all packages in Conan Center to generate the canonical CMake targets when possible.

That's great news. It's still worth keeping in mine though that the issues described above will happen if projects mix CMake package config files with Conan generated find modules while using Conan packages that don't yet utilize these new features.

One last tangent that's worth mentioning: above, you may have noticed I was using SDL 2.0.9. Ironically, the BinCrafter's package for SDL 2.0.12 appears to support the generating the CMake target with the name “SDL2”. I say “appears” to because I hit an issue trying to update my project to use it: it wanted to build about a dozen dependencies, including things I already have on my machine via apt, like pulseaudio, and these would fail to compile for some reason or another, so eventually I gave up / ran out of weekend time. The fact that merely trying to use the SDL2 libraries will sometimes makes me feel like a Gentoo Linux user is a topic for another day.





<---2020-06-19 09:32:55 history 2020-06-07 09:32:55--->



-All material © 2007 Tim Simpson unless otherwise noted-