Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[question] How to limit CMake components' propagated dependencies? #17368

Open
1 task done
archetypiarz opened this issue Nov 24, 2024 · 5 comments
Open
1 task done
Assignees

Comments

@archetypiarz
Copy link

archetypiarz commented Nov 24, 2024

What is your question?

Recently I've started a thread on stackoverflow, where one responders suggested that the problem I've met with might be an issue in Conan, so I'll paste the problem's description here:

I would like to optimize my CMake project, which uses Conan as dependency management tool. It is an executable, which depends on few Conan libraries. However, the need for sharing some headers and some dependencies arose, so I've split the project, resulting in two new targets additional to the already existing executable target.

I want to link those targets/components to various other projects, depending on the needs. They both share distinct headers and links. The thing is, I can't force CMake or Conan to respect this distinction and I've started to doubt if that's even possible.

To be more precise: I mean that, by linking myproject::myLib in another project, I don't want to include other libs, which would be, let's say, Boost, OpenSSL and MyOtherLib, which are linked to myproject::iface with INTERFACE scope and indirectly to myExecutable (thanks to linking iface component). Is this possible?

I'm using GCC 14.2, CMake 3.26 and Conan 1.65.0.

CMakeLists.txt:

project(myProject)

// ...

// looking for dependencies installed by Conan
find_package(Boost CONFIG REQUIRED)
find_package(OpenSSL CONFIG REQUIRED)
find_package(MyOtherLib CONFIG REQUIRED)

// main executable target
add_executable(myExecutable
  one.cpp
  two.cpp
)

// new static library target
add_library(myLib STATIC)
target_sources(myLib
  PRIVATE
    lib/myLib/threeLib.cpp
  PUBLIC
    FILE_SET libHeaders
    TYPE HEADERS
    BASE_DIRS lib/myLib/include
    FILES
      lib/myLib/threeLib.h
target_link_libraries(myLib PRIVATE boost::boost)

// new header only component target
add_library(iface INTERFACE)
target_sources(iface
  INTERFACE
    FILE_SET ifaceHeaders
    TYPE HEADERS
    BASE_DIRS include
    FILES
      include/fourHeader.h
      include/fiveHeader.h
target_link_libraries(iface
  INTERFACE
    boost::boost
    openssl::openssl
    myotherlib::myotherlib

// executable is using both of these libs/components
target_link_libraries(myExecutable 
  PRIVATE 
    iface
    myLib
)
// ...

conanfile.py

from conan import ConanFile
from conan.tools.cmake import CMakeDeps, CMakeToolchain

class myProject(ConanFile):
    # some attributes and settings
    name = "myproject"
    version = "1.0"
    # version numbers might be randomized
    generators = "CMakeDeps", "CMakeToolchain"
    requires = "boost/1.80.0", "openssl/3.3.5", "myotherlib/1.0@myremote/testing"
    
    def layout(self):
        cmake_layout(self)

    def generate(self):
        toolchain = CMakeToolchain(self)
        toolchain.generate()
        deps = CMakeDeps(self)
        deps.generate()

    def build(self):
        cmake = CMake(self)
        cmake.configure()
        cmake.build()

I've tried many things. At the beginning, I had problems with including headers of both components but I've managed to fix it with CMake's install:

install(
  TARGETS iface
  EXPORT ifaceTargets
  FILE_SET ifaceHeaders
  DESTINATION include)

install(
  EXPORT ifaceTargets
  FILE ifaceTargets.cmake
  NAMESPACE myproject::
  DESTINATION lib/cmake/iface)

install(
  TARGETS myLib
  EXPORT myLibTargets
  FILE_SET myLibHeaders
  DESTINATION myLib_include
  ARCHIVE_DESTINATION lib)

install(
  EXPORT myLibTargets
  FILE myLibTargets.cmake
  NAMESPACE myproject::
  lib/cmake/mylib)

However, the problem was still the fact that linking myproject::myLib effects with linking all other libraries.

I've tried to add the component info in cpp_info section of the Conan file, hoping that, in setting the requires field, I would be able to 'override' some default generated dependencies:

def package(self):
    cmake = CMake(self)
    cmake.install()

def package_info(self):
    self.cpp_info.components["myLib"].libs = ["myLib"]
    self.cpp_info.components["myLib"].set_property("cmake_target_name", "myproject::myLib")
    self.cpp_info.components["myLib"].includedirs = ["myLib_include"]
    self.cpp_info.components["myLib"].requires = []

    self.cpp_info.components["iface"].set_property("cmake_target_name", "myproject::iface")
    self.cpp_info.components["iface"].includedirs = ["include"]
    self.cpp_info.components["iface"].requires = ["boost::boost", "openssl::openssl", "myotherlib::myotherlib"]    

No effect though. Linking myproject::myLib is still linking Boost and other libraries, which I would like to avoid without atomizing the project.

Any ideas how could I achieve this division of propagated links, when all targets/components are defined in one CMake project and one Conan file? Is that even possible without separating targets to distinct project with their own CMakeLists and Conan files?

Have you read the CONTRIBUTING guide?

  • I've read the CONTRIBUTING guide
@memsharded memsharded self-assigned this Nov 24, 2024
@memsharded
Copy link
Member

Hi @archetypiarz

Thanks for your question.

If I understood correctly, the myproject is the class myProject(ConanFile): recipe, as it doesn't have either name or version it seems you are not using it to create a package from it, but you are using it as a "consumer", to conan install + cmake or conan build.

If that is the case the package_info() of that recipe is irrelevant. The package_info() of a recipe is exclusively for the consumers of that package, when some other package is doing a requires="myproject/version". Please let us know if this is the case.

However, the problem was still the fact that linking myproject::myLib effects with linking all other libraries.

How this is happening. Who and how is linking myproject::myLib?

Note that in any case myproject::myLib is a static library, then the boost, openssl and other static libraries are always necessary. This is not a Conan thing, but a build thing. Even if target_link_libraries(myLib PRIVATE boost::boost), this doesn't mean that CMake doesn't propagate the boost library static linkage requirement, it is still necessary to link down consumers linking with myproject::myLib with boost and any other transitive static libraries.

So in your case both components will always propagate linkage requirements to the transitive dependencies.

Please let me know if this helps.

@archetypiarz
Copy link
Author

archetypiarz commented Nov 25, 2024

Thank you @memsharded for quick response,

Not exactly, my bad - I've skipped fragment of code which sets recipe's version and name. I create the package with conan create before I conan install in target/depending project, which has requires = "myproject/version@remote" in it's recipe.

Note that in any case myproject::myLib is a static library, then the boost, openssl and other static libraries are always necessary. This is not a Conan thing, but a build thing.

Could you please elaborate on that? I don't exactly understand how to determine if library is transitive or not. It sounds like they just "stick" to every target declared in the CMake, even if not linked. Does it make the dependencies declared in recipe's requires field always propagated, no matter which component is used? What component's requires field is for then? I thought component system could be used to "atomize" the project, so targets can be freely customizable by their packages, files, but also variables and links.

In my case myproject::myLib is a one header one source lib, which shares just a single functionality implemented by using boost. That's why I don't want to propagate this dependency further, beacuse this library is not intended to share boost's api.

On the contrary is myproject::iface, which is an interface library and is intended to share myproject's include headers and libraries that are needed to implement these, but nothing more. That's the component, which I would like to propagate all linked libraries.

How this is happening. Who and how is linking myproject::myLib?

My other projects. I place myproject in requires field of depending recipe and then link the components to target:

find_package(myproject CONFIG REQUIRED COMPONENTS myLib)
target_link_library(targetproject1 PRIVATE myproject::myLib)

or

find_package(myproject CONFIG REQUIRED COMPONENTS iface)
target_link_library(targetproject2 PRIVATE myproject::iface)

That is, I would like them to be distinct also in propagated linkage - so by linking the myproject::myLib, OpenSSL and MyOtherLib (and preferably Boost too) are not linked to targetproject1. Right now I can't manage to do this and I see theirs includes in compilation flags, preceded with -isystem, when I link just myproject::myLib. On the other hand, linking myproject::iface component to the targetproject2 propagates linkage and that's what I need. I just would like to be able to limit this when the need comes.

Please let me know if that could be done. Maybe I don't understand something fundamental in C++ linkage system or CMake's/Conan's behaviour.

@memsharded
Copy link
Member

Yes, I don't think this is a Conan or CMake issue, it is a linking issue.

When you have:

app -> libc -> libb -> liba

When you are linking app executable, if the 3 libs are static libs, the app need always to link with the 3 libs, like -llibc -llibb -lliba in the compiler arguments. This will always happen, even if you define in CMake a target_link_libraries(libb PRIVATE liba), because CMake knows they are static libraries and they need to propagate linkage requirements. Other aspects like the headers might not be propagated, so app cannot contain direct #includes to liba, but the linkage needs to happen.

So regarding library linkage, there is no way to not link the transitive dependencies, it doesn't depend on Conan 1 or Conan 2 at all, not even depend on Conan or CMake.

Regarding the headers, Conan 1 can't do the headers isolation, but Conan 2 will also hide the #includes unless the requirement is explicitly told to do so with self.requires("liba/version", transitive_headers=True).

@archetypiarz
Copy link
Author

archetypiarz commented Nov 27, 2024

I think I got it sorted. I needed some time to digest the fact that static libraries are not being linked to another ones, thus calling target_link_libraries only performs a propagation of include directories until we link them to executable or linked library. This works correctly, I can exclude include dirs of components by not linking them.
My confusion mostly came from the fact that I saw some unwanted directories in output of cmake --build --preset xxxxx --verbose during compilation and linking phases.
I guess I will see there every lib from CMAKE_MODULE_PATH prepended and appended in conan_toolchain.cmake and there is no way to limit their visibility till dependencies are hierarchically compounded of static libraries.

@memsharded
Copy link
Member

I think I got it sorted. I needed some time to digest the fact that static libraries are not being linked to another ones, thus calling target_link_libraries only performs a propagation of include directories until we link them to executable or linked library. This works correctly, I can exclude include dirs of components by not linking them.

Conan 2 will effectively and automatically hide include directories of transitive dependencies by default

My confusion mostly came from the fact that I saw some unwanted directories in output of cmake --build --preset xxxxx --verbose during compilation and linking phases.
I guess I will see there every lib from CMAKE_MODULE_PATH prepended and appended in conan_toolchain.cmake and there is no way to limit their visibility till dependencies are hierarchically compounded of static libraries.

They don't come from CMAKE_MODULE_PATH in most cases they come from CMAKE_PREFIX_PATH (xxx-config.cmake files), and not all of the directories appended in conan_toolchain.cmake ends up being linked. This really depend on the transitivity and the specific target_link_libraries() used. The generated xxxx-config.cmake files by CMakeDeps will contain the information to link with the necessary libraries, but not all of them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants