conan-io / conan

Conan - The open-source C and C++ package manager
https://conan.io
MIT License
7.95k stars 951 forks source link

[question] linking header-only libs #16507

Closed Dimitrius-dev closed 1 week ago

Dimitrius-dev commented 1 week ago

What is your question?

Hi, i found out that some case is not working with conan tools.


Example:

I simplified example. There are 4 libs:

The top is lib1: The linking sequence is lib1 <- nolib1 <- lib2 <- nolib2

All of links are PUBLIC or INTERFACE, so theoretically lib1 is able to use functionality of holib1, lib2, holib2

lib1 has includes in its header

...
#include "holib1/holib1.hpp"
#include "holib2/holib2.hpp"
#include "lib2/lib2.h"
...

Without conan the project compiles fine.

With conan lib1 has access only to holib1 and nothing else. Error:

... /lib1/lib1.h:4:10: fatal error: holib2/holib2.hpp: No such file or directory
    4 | #include "holib2/holib2.hpp"
      |          ^~~~~~~~~~~~~~~~~~~

Project files are listed below in linking order.

My settings:

arch: x86_84
os: linux
compiler: gcc 13.2

Questions:

Perhaps there is some conanfile feature for it.


lib1

CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(lib1)

set(CMAKE_CXX_STANDARD 20)

set(all_sources
        source/lib1.cpp
)

add_library(lib1 ${all_sources})
target_include_directories(lib1 PUBLIC include)

find_package(holib1 REQUIRED CONFIG)
target_link_libraries(lib1 PUBLIC
        holib1::holib1
)

install(TARGETS lib1)

conanfile.py

import os.path

from conan import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps
from conan.tools.files import copy
from conan.errors import ConanInvalidConfiguration

class Lib(ConanFile):
    name = "lib1"
    version = "0.0.1"

    settings = "os", "compiler", "build_type", "arch"

    options = {
        "shared": [True, False],
        "fPIC": [True, False]
    }

    default_options = {
        "shared": False,
        "fPIC": True
    }

    def requirements(self):
        self.requires("holib1/0.0.1")
        pass

    def export_sources(self):
        copy(self, "CMakeLists.txt", self.recipe_folder, self.export_sources_folder, keep_path = True)
        copy(self, "include/*", self.recipe_folder, self.export_sources_folder, keep_path = True)
        copy(self, "source/*", self.recipe_folder, self.export_sources_folder, keep_path = True)

    def layout(self):
        cmake_layout(self)
        pass

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

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

    def package(self):
        copy(self, "include/*", self.source_folder, self.package_folder, keep_path=True)
        cmake = CMake(self)
        cmake.install()
        pass

    def package_info(self):
        self.cpp_info.libs = [self.name]
        pass

holib1

CMakeLists.txt

cmake_minimum_required(VERSION 3.12)
project(holib1)

set(CMAKE_CXX_STANDARD 20)

add_library(holib1 INTERFACE)
target_include_directories(holib1 INTERFACE include)

find_package(lib2 REQUIRED CONFIG)
target_link_libraries(holib1 INTERFACE
        lib2::lib2
)

conanfile.py


import os.path

from conan import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps
from conan.tools.files import copy
from conan.errors import ConanInvalidConfiguration

class Lib(ConanFile):
    name = "holib1"
    version = "0.0.1"

    settings = "os", "arch", "compiler", "build_type"

    no_copy_source = True

    def requirements(self):
        self.requires("lib2/0.0.1")
        pass

    def export_sources(self):
        copy(self, "CMakeLists.txt", self.recipe_folder, self.export_sources_folder, keep_path = True)
        copy(self, "include/*", self.recipe_folder, self.export_sources_folder, keep_path = True)
        pass

    def layout(self):
        cmake_layout(self)
        pass

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

    def package(self):
        copy(self, "include/*", self.source_folder, self.package_folder, keep_path=True)
        pass

    def package_info(self):
        pass

    def package_id(self):
        self.info.clear()
        pass

lib2

CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(lib2)

set(CMAKE_CXX_STANDARD 20)

set(all_sources
    source/lib2.cpp
)

add_library(lib2
        ${all_sources}
)

target_include_directories(lib2 PUBLIC include)

find_package(holib2 REQUIRED CONFIG)

target_link_libraries(lib2 PUBLIC
        holib2::holib2
)

install(TARGETS lib2)

conanfile.py

import os.path

from conan import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps
from conan.tools.files import copy
from conan.errors import ConanInvalidConfiguration

class Lib(ConanFile):
    name = "lib2"
    version = "0.0.1"

    settings = "os", "compiler", "build_type", "arch"

    options = {
        "shared": [True, False],
        "fPIC": [True, False]
    }

    default_options = {
        "shared": False,
        "fPIC": True
    }

    def requirements(self):
        self.requires("holib2/0.0.1")
        pass

    def export_sources(self):
        copy(self, "CMakeLists.txt", self.recipe_folder, self.export_sources_folder, keep_path = True)
        copy(self, "include/*", self.recipe_folder, self.export_sources_folder, keep_path = True)
        copy(self, "source/*", self.recipe_folder, self.export_sources_folder, keep_path = True)

    def layout(self):
        cmake_layout(self)
        pass

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

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

    def package(self):
        copy(self, "include/*", self.source_folder, self.package_folder, keep_path=True)
        cmake = CMake(self)
        cmake.install()
        pass

    def package_info(self):
        self.cpp_info.libs = [self.name]
        pass

holib2

CMakeLists.txt

cmake_minimum_required(VERSION 3.12)
project(holib2)

set(CMAKE_CXX_STANDARD 20)

add_library(holib2 INTERFACE)
target_include_directories(holib2 INTERFACE include)

conanfile.py


import os.path

from conan import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps
from conan.tools.files import copy
from conan.errors import ConanInvalidConfiguration

class Lib(ConanFile):
    name = "holib2"
    version = "0.0.1"

    settings = "os", "arch", "compiler", "build_type"

    no_copy_source = True

    def requirements(self):
        pass

    def export_sources(self):
        copy(self, "CMakeLists.txt", self.recipe_folder, self.export_sources_folder, keep_path = True)
        copy(self, "include/*", self.recipe_folder, self.export_sources_folder, keep_path = True)
        pass

    def layout(self):
        cmake_layout(self)
        pass

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

    def package(self):
        copy(self, "include/*", self.source_folder, self.package_folder, keep_path=True)
        pass

    def package_id(self):
        self.info.clear()
        pass

Have you read the CONTRIBUTING guide?

AbrilRBS commented 1 week ago

Hi @Dimitrius-dev thanks a lot for taking the time to report your issue, we appreciate it :)

I'd say that this is not a bug, but expected Conan behaviour. There are two points worth discussing here:

Direct vs transitive dependencies

If your library uses symbols from a library, then it's considered a direct dependency and it's good practice not to rely on transitive dependencies to provide them, but directly specify the direct dependency appropiately where necessary.

Requirement traits

The key here is also to understand how Conan 2 decides what information to propagate downstream in the graph, with the so called traits in each requirement (Docs here)

In your case, if from lib2 you'd need its dependencies' headers/libs to be propagated to lib1 (because lib2 exposes them in its public API for example), then you'd need to set the transitive_headers=True trait, which will expose the include path of that holib1 dependency to its direct lib1 dependant. (So self.requires("holib1/1.0", transitive_headers=True))

The traits can be set in two different ways, by directly adding them in the requirements clause as shown above, or inferred by the package_type of your packages (In this case holib1 and 2 would be package_type = "header-only" and lib1 and 2 package_type = "library". This would automatically infer the traits based on this table)

I go into a bit more detail on all of this for a similar case in https://github.com/conan-io/conan/issues/14600#issuecomment-1696353532 if you want some extra read with some diagrams and all!

Let me know if that helps, I can else provide the practical example if I find a bit of time :)

Dimitrius-dev commented 1 week ago

Thank you a lot. I got some info from this docs topics, so there is no need for detailed explanation.

As you wrote there are two ways to declare transitive requirement. I tried the second one (package_type) and it did not work (i got the the error that i described above. The first approach resolved the problem (transitive_headers).

AbrilRBS commented 1 week ago

I tried the second one (package_type) and it did not work

This is because the package_type is only used to infer the traits of your dependencies, which might not align with your expectations :)

The recommended "full" approach would then be to:

Happy to hear you got this fixed, please feel free to reopen/create a new issue should you have any more questions :)