0

This question may be partly duplicate, e.g. 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, i.e. I want to compile all the external libraries of A into my B. Assume A is completely external, i.e. 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, i.e.

  • 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?

Shiwayari
  • 167
  • 2
  • 9
  • Remove all `LIB2_ROOT` `LIB1_ROOT` and use `CMAKE_CURRENT_SOURCE_DIR` to reference current directory where `CMakeLists.txt` is in. I think your post is close to being too broad. – KamilCuk Aug 13 '20 at 22:58
  • @KamilCuk I'm not sure I understand. How would I e.g. refer to `A/include/lib1.hh` from `B/CMakeLists.txt` (which is a different project!) without having some sort of input variable or hard-coding (obviously bad) the path? – Shiwayari Aug 13 '20 at 23:01

1 Answers1

1

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
zaufi
  • 5,946
  • 21
  • 32