简体   繁体   中英

“Hiding” transitive external dependencies / combining libraries with CMake

This question may be partly duplicate, eg this question , but is more about what if any are better solutions. Since this question ended up rather long, I marked specific questions with " +Q+ " in bold italic.

I have the situation that I wrote a small library B that depends on some other huge project A split into many libraries A1, A2, ..., An , some of which my library depends on and some on which it doesn't. It was a bit of a pain to do the proper linking. The library is starting to be used by others and I would like to avoid everyone having to go through this awful linking process, ie I want to compile all the external libraries of A into my B . Assume A is completely external, ie that I have no way to recompile A (in this case I do, but it is complicated and I would like to know options for the case that I don't).

I imagine this must be a very standard thing to do, I have used other popular libraries and never did I have to link all the other libraries that they transitively depend on.. ? So I started looking for solutions, and while I found working ones, most solutions seem like a big mess and I wonder if this is really done in practice or if there is some idiomatic way for this.

To avoid more eventual headaches if I ever need a different case, I want to consider all combinations of static/shared libraries, ie

  • A & B are static
  • A is static, B is shared
  • A is shared, B is static
  • A & B are shared

To give some MWE of the code setup (the variables LIB1_ROOT and LIB2_ROOT in the CMakeLists.txt files are A/ and B/ respectively):

A/include/lib1.hh

struct Lib1 { void run() const; };

A/src/lib1.cc

#include <iostream>
#include <lib1.hh>
void Lib1::run() const { std::cout << "Hello from lib1\n"; }

A/CMakeLists.txt

cmake_minimum_required(VERSION 3.14)
project(A)
include_directories(include)
add_library(lib1 src/lib1.cc)
install(TARGETS lib1 DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/lib")

B/include/lib2.hh

class Lib2 {
    class Implementation;
    Implementation* impl;
public:
    Lib2();
    ~Lib2();
    void run() const;
};

B/src/lib2.cc

#include <iostream>
#include <lib1.hh>
#include <lib2.hh>
class Lib2::Implementation {
    const Lib1 m_lib1{};
public:
    void run() const { std::cout << "using lib1 from lib2: "; m_lib1.run(); }
};
Lib2::Lib2() : impl{new Implementation} {}
Lib2::~Lib2() { delete impl; };
void Lib2::run() const { impl->run(); }

App/src/app.cc

#include <lib2.hh>
int main() { Lib2 l; l.run(); }

App/CMakeLists.cc

cmake_minimum_required(VERSION 3.14)
project(App)
include_directories(include "${LIB2_ROOT}/include")
find_library(LIB2 lib2 "${LIB2_ROOT}/lib")
add_executable(app src/main.cc)
target_link_libraries(app "${LIB2}")
install(TARGETS app DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/bin")

I used the pImpl pattern for B since what is the point of hiding link dependencies when I then make the user of my library dig out all the headers anyway.


Finally B/CMakeLists.txt (for my library) depends on the cases I mentioned above:

A & B static

B/CMakeLists.txt

cmake_minimum_required(VERSION 3.14)
project(B)
include_directories(include "${LIB1_ROOT}/include")
find_library(LIB1 lib1 "${LIB1_ROOT}/lib")
add_library(lib2_dependent src/lib2.cc)
add_custom_target(lib2 ALL
    COMMAND ar -x "${LIB1}"
    COMMAND ar -x "$<TARGET_FILE:lib2_dependent>"
    COMMAND ar -qcs "${CMAKE_STATIC_LIBRARY_PREFIX}lib2${CMAKE_STATIC_LIBRARY_SUFFIX}" *.o
    COMMAND rm *.o
    DEPENDS lib2_dependent
    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lib"
)

This solution is from the question I linked at the start. There was also an in my opinion much better version in this answer using

ar -M <<EOM
    CREATE lib2.a
    ADDLIB lib1.a
    ADDLIB lib2_dependent.a
    SAVE
    END
EOM

but I could not get to work the here-document inside CMakeLists.txt... ? There was an additional answer providing what I think was a CMake function to do this, but it was a huge block of code which I found a bit ridiculous for something that should be simple / standard practice / integrated into CMake? The custom_target solution I wrote here also works, but as mentioned in other answers as well, it unpacks object files which lie around and have to be removed again, for each library I want to compile this way. And still in both cases, I can only wonder what is the point of using CMake then if I have to use ar manually anyhow. +Q+ Is there no better / CMake-integrated way to "compile-in" transitive dependencies / combine static libraries?


A static, B shared

As far as I found in this case, I'm out of luck if I cannot recomple A and it is not compiled as position independent code. The next best thing to make it work was to do just that by adding set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") to A/CMakeLists.txt . Again, there seemed to be better alternatives: set(CMAKE_POSITION_INDEPENDENT_CODE ON) or set_property(TARGET lib1 PROPERTY POSITION_INDEPENDENT_CODE ON) from here , which were complete ignored in my case.. (compiling with VERBOSE=1 , there was no -fPIC flag to be seen anywhere and B did not compile)

The B/CMakeLists.txt was easy in this case

cmake_minimum_required(VERSION 3.14)
project(B)
include_directories(include "${LIB1_ROOT}/include")
find_library(LIB1 lib1 "${LIB1_ROOT}/lib")
add_library(lib2 SHARED src/lib2.cc)
target_link_libraries(lib2 "${LIB1}")
install(TARGETS lib2 DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/lib")

and while I don't find anything wrong with this solution, according to answers I found I should need to set additional flags like set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--export-all-symbols") in order for the symbols in the static library to be found. However the above works just fine, compiles, and App runs without issues? +Q+ Am I doing anything wrong here? Or is it maybe due to some update to CMake since these older answers?


A shared, B static or both shared

According to what I found here, this is basically impossible, because shared libraries are "final" in some sense. I find this very strange, surely there are many libraries that do not require a project that uses them to link every single dependency of that library that happens to be a shared library? +Q+ Are there really no options in these cases?

Yep, you're doing wrong:)

The CMake-way is to use packages . Once you've made the LibA package, you just do find_package(LibA) in your B/CMakeLists.txt and also the generated LibBConfig.cmake (the package config file, so your clients would need just find_package(libB) in their App/CMakeLists.txt ) ought to find (that is questionable IMHO, but currently its CMake way) its dependency the same way (using or not the CMake's helper find_dependency ).

The whole process gets much simpler that way:

  • you have/give the full control on how to build (including what version, library type static/dynamic, &etc) and where to install the libA on your developer's host and customer's machine (so dependent projects could find it)
  • same for libB and App
  • you and any customer of your library(ies) use the well known CMake way to find dependencies and have full control over this process
  • and the most important let CMake do complicated things instead of you that keeps your build logic much simpler in CMakeLists.txt of all involved projects

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM