CMake linking static libraries in different subdirectories into one single static library
Asked Answered
C

1

5

I use CMake to build a project that consists of multiple nested static libraries .A similar but simple structure is shown in the figure below:

TestProject:
|-CMakeLists.txt
|-Main.cpp
|-level2
|    | - level2.cpp
|    | - level2.h
|    | - CMakeLists.txt
|    | - level1
|    |     |-level1.cpp
|    |     |-level1.h
|    |     |-CMakeLists.txt

Now, I use CMake to build static libraries for each level separately. According to my test, the static library of each layer only contains the .cpp and .h files of that layer. However, I want to combine it with the library referenced by the previous layer when the static library of each layer is generated. For example, I build the static lib of level 1 first.Then, in the CMakeLists.txt of level 2, I create the static library of level 2 depend on the static library of level 1 ( target_link_libraries(${PROJECT_NAME} LEVEL1) ), and then, I wanted to merge the libraries of level 2 and level 1 together to a new static lib file named as level1_2.lib.

Here is my CMakeLists.txt in Level1:

cmake_minimum_required(VERSION 3.5)

#projcet name
project(LEVEL1 LANGUAGES CXX)

add_library( ${PROJECT_NAME} add.cpp)

# Add the include directories of user-written sources.
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR})

And this is the CMakelists.txt of level2.

cmake_minimum_required(VERSION 3.5)

project(LEVEL2 LANGUAGES CXX)

add_subdirectory(level1)

add_library( ${PROJECT_NAME} addplus.cpp)
target_link_libraries(${PROJECT_NAME} LEVEL1)
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR})

find_program(MSVC_LIB_TOOL lib.exe)
set(LIBNAME "level1_2.lib")
add_custom_command(
    TARGET examplelib POST_BUILD
    COMMAND ${MSVC_LIB_TOOL} /OUT:${LIBNAME} $<TARGET_FILE:LEVEL2> $<TARGET_FILE:LEVEL1>
    DEPENDS LEVEL1 LEVEL2
    COMMENT "Combining libs..."
    )
add_custom_target(combinedLib
    ALL
    DEPENDS ${LIBNAME}
    )

I use the add_custom_command and add_custom_target method trying to generate the mixed lib, referenced to several website below:

CMake linking libraries into one single library

CMake Project Structure: How do I properly merge libraries together and include them in multiple executables

But they can't really solve my needs.Only level1.lib and level2.lib is generated.

Any help would be appreciated.

UPDATE in 08-21 =======================================================

Thanks for everyone's reply.Now I used object library(reference from Alex's answer), and get the merged static library.Here's my new code:

#CMakeLists.txt in level1

cmake_minimum_required(VERSION 3.5)

#projcet name
project(LEVEL1 LANGUAGES CXX)


# Generate lib
add_library( LEVEL1obj OBJECT add.cpp)


# Add the include directories of user-written sources.
target_include_directories(LEVEL1obj PUBLIC ${PROJECT_SOURCE_DIR})

add_library(${PROJECT_NAME})

target_link_libraries(${PROJECT_NAME} PUBLIC LEVEL1obj)

And this is the CMakelists.txt of level2.

#CMakeLists.txt in level2
cmake_minimum_required(VERSION 3.5)

#projcet name
project(LEVEL2 LANGUAGES CXX)


add_subdirectory(level1)

add_library( LEVEL2obj OBJECT addplus.cpp addplus.h)

# Add the include directories of user-written sources.
target_include_directories(LEVEL2obj PUBLIC ${PROJECT_SOURCE_DIR})

target_link_libraries(LEVEL2obj LEVEL1obj)


add_library( ${PROJECT_NAME})

target_link_libraries(${PROJECT_NAME} PUBLIC LEVEL1obj LEVEL2obj)

This is the top level CMakeLists.txt in test project

cmake_minimum_required(VERSION 3.5)

project(TestCppLib)

file(GLOB SRC "${PROJECT_SOURCE_DIR}/*.cpp")

add_subdirectory(level2)

add_executable(${PROJECT_NAME} ${SRC})

target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR})

target_link_libraries(${PROJECT_NAME} PRIVATE LEVEL2)

The slightly different from Alex's answer is : because my level2 library depends on the level1 library, I added a library dependency to it by target_link_libraries(LEVEL2obj LEVEL1obj). The slightly different answer from Alex's is that because my level2 library depends on the level1 library generation, I added a library dependency to it through the following code. At least for now, it works well.

Cathern answered 20/8, 2021 at 15:21 Comment(7)
Why do you use build event variant of the add_custom_command? (And use exampleLib target which is never created, which should give a configuration error.) Instead, use common generating variant of that command: add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${LIBNAME} COMMAND ...).Neurotomy
Use object libraries instead, if you want this behavior.Wageworker
Do you need an actual static library or do you want to use them all together in cmake? I wanted to merge the libraries of level 2 and level 1 together to a new static lib But what for? Just link library level1 with library level2, and the dependencies are transitive. Or do you want to specifically distribute level1_2.lib to the clients? If so, I would suggest instead making one library from the start in cmake.Halima
file(GLOB SRC "${PROJECT_SOURCE_DIR}/*.cpp") -- please do not glob without CONFIGURE_DEPENDS (put it after SRC)Wageworker
Also please remember to always call target_link_libraries with a visibility specifier (one of PRIVATE, PUBLIC, or INTERFACE). There is not an implied default; instead, forgetting to put one triggers legacy behavior with strange edge cases that can bite you later as your code evolves.Wageworker
because my level2 library depends on the level1 library generation - depending on the type of dependency, consider using add_dependencies instead of target_link_libraries.Wageworker
Also, object libraries don't work right prior to version 3.12. You NEED to test your code with the ACTUAL CMake version you declare as your minimum. I really doubt you're running 3.5. Run cmake --version and put that in your cmake_minimum_requiredWageworker
W
6

Here is a minimal example using object libraries to manage sharing object files between static (or shared!) libraries and how to link to them.

In level1/CMakeLists.txt:

add_library(level1_obj OBJECT level1.cpp level1.h)
target_include_directories(level1_obj PUBLIC "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}>")

add_library(level1)
target_link_libraries(level1 PUBLIC level1_obj)

In level2/CMakeLists.txt

add_subdirectory(level1)

add_library(level2_obj OBJECT level2.cpp level2.h)
target_include_directories(level2_obj PUBLIC "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}>")

add_library(level2)
target_link_libraries(level2 PUBLIC level1_obj level2_obj)

Notice that level2 is linked to level1_obj, not level1.

In main/CMakeLists.txt:

cmake_minimum_required(VERSION 3.21)
project(TestProject)

add_subdirectory(level2)

add_executable(app Main.cpp)
target_link_libraries(app PRIVATE level2)

One bug to be aware of is that Xcode does not like libraries without any real source files. If that's a concern, you can add an empty source file to your static library targets.

Please be sure to read the documentation on object libraries:

  1. https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#object-libraries
  2. https://cmake.org/cmake/help/latest/command/target_link_libraries.html#linking-object-libraries
  3. https://cmake.org/cmake/help/latest/command/add_library.html#object-libraries

Also worth noting is that unless you have some need to distribute your level2 library independently from level1, it would be better to keep their objects separate and require linking to both. Normal target linking in CMake handles this automatically via the transitive linking mechanism.

Wageworker answered 20/8, 2021 at 18:9 Comment(6)
Really? we should go change all targets from STATIC to OBJECT (or add new object-targets)? No, adding $<TARGET_OBJECTS:MyOtherTarget> to sources is in most cases more than enough.Raffle
That's the old way of doing things that doesn't track interface propertiesWageworker
This is no old or new matter, your way is just the more right one (to be fair), which may be required in some cases to install headers or set additional linker flags.Raffle
I'll grant you that $<TARGET_OBJECTS> is an acceptable workaround when you aren't in control of the target (e.g. it's from a third party build you got via FetchContent). I also understand there's a new generator expression in development that improves this use case by enabling linking to a library only for its interface properties and not for the actual link line.Wageworker
Thank you for your continued help. But now I have another question: If the level1 static library depends on another external static library (.lib file), how can I merge it with the level1 static library? Can I generate an OBJECT LIBRARY for this lib file?Cathern
"I have another question"... so ask another StackOverflow question :)Wageworker

© 2022 - 2024 — McMap. All rights reserved.