c++standardslibrariesexternal-dependencies

Can C++ projects/libraries that have external dependencies be configured to work right off the bat?


I'm a beginner to C++, and I was recently figuring out the process of downloading an external library and linking it to a project I'm working on. Coming from languages where adding new dependencies and installing necessary ones are a command away, it seems that there are several ways to download and link a library in C++, and so I'm struggling to imagine how experienced developers with large scale projects are able to make their scripts and libraries portable and straightforward to setup.

To give an example, what I ended up doing in my project was using a CMakeLists.txt file with a hardcoded path to a library I'm using, libxml2, something like this:

include_directories(/opt/homebrew/Cellar/libxml2/2.10.3_1/include);
link_directories(/opt/homebrew/Cellar/libxml2/2.10.3_1/lib);

But this would require libxml2 to be separately downloaded onto every machine this project is cloned on, and the path would change too depending on the version and environment. So my specific question is, what is considered best practice to package C++ projects with external dependencies?

For example, node.js has a package.json and npm install, python has a requirement.txt and pip install -r requirements.txt. Is there a C++ equivalent?

I'm sorry if this is a silly question, but I'm really struggling finding the right search terms to find a proper answer. Thank you in advance!


Solution

  • I personally just use git modules with the dependencies as part of my repository, so when someone checks it out, he/she can also pull all source code required to build/link the required dependencies.

    Fortunately many popular libraries are themselves built with CMake, so you don't have to do anything fancy to add them into your CMake project. Modules also refer to specific commit of the dependency repo and it helps to maintain consistent dependency version between contributors.

    E.g. here is how file structure looks like in one of my projects (I got rid of some details which are not directly linked to the quesiton):

    |-- CMakeLists.txt
    |-- CMakeSettings.json
    |-- README.md
    |-- assets
    |   |-- assets-files
    |-- lib
    |   |-- CMakeLists.txt
    |   |-- libTwo
    |   |-- libThree
    |   `-- libFour
    `-- src
        |-- source-files
    

    Each subfolder in the lib is a separate submodule in my repo. In the root CMakeLists.txt I merely add this folder with my dependencies with a single line of configuration:

    add_subdirectory(lib)
    

    While the CMakeLists.txt in the lib folder takes care of the rest:

    add_compile_options(-w)
    # Lib One
    find_package(libOne REQUIRED)
    list(APPEND LIB_TARGETS ${LIB_ONE_LIBRARIES})
    list(APPEND LIB_INCLUDE_DIRS ${LIB_ONE_INCLUDE_DIRS})
    
    # Lib Two
    add_subdirectory(libTwo)
    list(APPEND LIB_TARGETS libTwo)
    list(APPEND LIB_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/libTwo/include")
    
    # Lib Three
    add_subdirectory(libThree)
    list(APPEND LIB_TARGETS libThreeProj)
    list(APPEND LIB_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/libThree/include")
    
    # Lib Four
    add_subdirectory(libFour)
    list(APPEND LIB_TARGETS libFour)
    
    if(LIB_INCLUDE_DIRS)
            list(REMOVE_DUPLICATES LIB_INCLUDE_DIRS)
            target_include_directories(${PROJ_NAME} PRIVATE ${LIB_INCLUDE_DIRS})
            if(MSVC)
                    target_compile_options(${PROJ_NAME} PRIVATE
                            "$<$<CXX_COMPILER_ID:Clang>:SHELL:-Xclang -isystem$<JOIN:${LIB_INCLUDE_DIRS}, -Xclang -isystem>>"
                            "$<$<CXX_COMPILER_ID:MSVC>:SHELL:-experimental:external -external:W0 -external:anglebrackets -external:I$<JOIN:${LIB_INCLUDE_DIRS}, -external:I>>")
            else()
                    target_compile_options(${PROJ_NAME} PRIVATE "SHELL:-isystem $<JOIN:${LIB_INCLUDE_DIRS}, -isystem>")
            endif()
    endif()
    
    target_link_libraries(${PROJ_NAME} PRIVATE ${LIB_TARGETS})
    

    You may notice that I also keep other dependencies (i.e. libOne in this case) that can be found with find_package in the same place, and it's neatly compatible with all dependencies I loaded from remote repos.

    Of course it doesn't always work seamlessly, and heavily depends on how the library authors distribute their code, but for me works most of the time, even when it requires some tweaks in the configuration (e.g. libFour is the headers-only library, and doesn't require any linking at all, while libThree has inconsistent target name libThreeProj)