cmake杂记

sheep_cpp中使用cmake的find_package来提供对外使用,并且第三方库依赖了vcpkg,感觉这种方式还是太重了

因此研究了一下去除了对vcpkg的依赖

最终安装后所有头文件(包括依赖的项目头文件)在安装目录的include下,

静态库全部打包在了一起(包括依赖的项目静态库)在安装目录的lib下,通过-lsheep_cpp -lpthread -ldl即可链接

在这个过程中遇到很多问题,对一些进阶知识或者常用用法记录一下

主项目常用的目录结构

在根目录下存在一个CMakeLists.txt,存在一个cmake文件夹放一些common的cmake代码

  • 源码文件放在src下
    • 只存在源码文件
  • 测试代码放在test下
    • test下有一个CMakeLists.txt,进行测试代码的编译,在根目录的CMakeLists.txt通过add_subdirectory(test)来引入他
  • 第三方依赖放在thirdparty下
    • 第三方库使用git submodule来完成依赖关系
    • 每个第三方库xxx有一个xxx.cmake文件

查找文件或者文件夹

主要依赖GLOB_RECURSE实现,由于cmake的GLOB_RECURSE性能很差

因此最好是通过file(READ...)file(WRITE...)将查找到的结果缓存起来

1
2
3
4
5
6
7
8
if (EXISTS ${xxx})
file(READ ${xxx} SOURCE_FILES)
message(STATUS "read source files from cache ${xxx}")
else()
do something GLOB_RECURSE...
file(WRITE ${xxx} "${SOURCE_FILES}")
message(STATUS "write source files to cache ${xxx}")
end()

下面的全部场景都进行了缓存

  • 要把所有源文件找到进行add_library

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #init Dirs
    list(APPEND Dirs ${BasePath}/small_server)
    list(APPEND Dirs ${BasePath}/small_client)
    list(APPEND Dirs ${BasePath}/net)
    list(APPEND Dirs ${BasePath}/log)
    list(APPEND Dirs ${BasePath}/extends)

    #get all source files
    if (EXISTS ${SourceFilesCachePath})
    file(READ ${SourceFilesCachePath} SOURCE_FILES)
    message(STATUS "read source files from cache ${SourceFilesCachePath}")
    else()
    foreach(Dir ${Dirs})
    file(GLOB_RECURSE SOURCE_FILES_TEMP ${Dir} ${Dir}/*.cpp)
    IGNORE_DIR(SOURCE_FILES_TEMP)
    list(APPEND SOURCE_FILES ${SOURCE_FILES_TEMP})
    endforeach()
    file(WRITE ${SourceFilesCachePath} "${SOURCE_FILES}")
    message(STATUS "write source files to cache ${SourceFilesCachePath}")
    endif()

    使用GLOB_RECURSE来找到希望遍历的子目录,并且将结果缓存在SourceFilesCachePath文件里面

  • 要把所有头文件找到进行target_include_directories

    • 首先实现了一个查找子目录的函数

      默认的GLOB_RECURSE只能使用LIST_DIRECTORIES true查找包含子目录的某些文件,因此需要使用IS_DIRECTORY来将子目录筛选出来

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      function(GET_SUB_DIRS result curdir cache_file_prefix)
      # 检查缓存文件是否存在
      set(cache_file ${cache_file_prefix}/${curdir}.txt)
      if(EXISTS ${cache_file})
      # 从缓存文件读取子目录
      file(READ ${cache_file} subdirs)
      message(STATUS "reading from cache file: ${cache_file}")
      else()
      file(GLOB_RECURSE all_content LIST_DIRECTORIES true ${curdir} ${curdir}/*)

      set(subdirs ${curdir})
      foreach(item IN LISTS all_content)
      if(IS_DIRECTORY ${item})
      list(APPEND subdirs ${item})
      endif()
      endforeach()

      # 将子目录写入缓存文件
      file(WRITE ${cache_file} "${subdirs}")
      message(STATUS "writing to cache file: ${cache_file}")
      endif()

      set(${result} ${subdirs} PARENT_SCOPE)
      #message(STATUS "${curdir} temp dir: ${subdirs}")
      endfunction()

    • 然后实现了一个排除某些文件夹的函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      list(APPEND IgnoreDirs "test")
      list(APPEND IgnoreDirs "build")
      function(IGNORE_DIR INPUT_SOURCE_FILES_VAR)
      set(SOURCE_FILES_TEMP ${${INPUT_SOURCE_FILES_VAR}}) # 获取传入变量的值
      foreach(Dir ${IgnoreDirs})
      list(FILTER SOURCE_FILES_TEMP EXCLUDE REGEX ${Dir})
      endforeach()
      set(${INPUT_SOURCE_FILES_VAR} ${SOURCE_FILES_TEMP} PARENT_SCOPE) # 将修改的列表传回父作用域
      endfunction()
    • 最终使用这两个函数

      1
      2
      3
      4
      5
      6
      7
      8
      target_include_directories(${PROJECT_NAME} PRIVATE ${BasePath})
      foreach(Dir ${Dirs})
      GET_SUB_DIRS(SUBDIRS ${Dir} ${SubDirsCachePath})
      IGNORE_DIR(SUBDIRS)
      foreach(SUBDIR IN LISTS SUBDIRS)
      target_include_directories(${PROJECT_NAME} PRIVATE ${SUBDIR})
      endforeach()
      endforeach()
  • 最后还需要把所有的头文件按照原本的目录结构install到安装目录去

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    if (EXISTS ${HeadFilesCachePath}) 
    file(READ ${HeadFilesCachePath} HEADER_FILES)
    message(STATUS "read header files from cache ${HeadFilesCachePath}")
    else()
    file(GLOB_RECURSE HEADER_FILES ${BasePath} ${BasePath}/*.h)
    file(WRITE ${HeadFilesCachePath} "${HEADER_FILES}")
    message(STATUS "write header files to cache ${HeadFilesCachePath}")
    endif()

    foreach(HEADER_FILE ${HEADER_FILES})
    get_filename_component(FILE_PATH "${HEADER_FILE}" PATH)
    string(REPLACE ${BasePath} "include" TARGET_PATH "${FILE_PATH}")
    install(FILES "${HEADER_FILE}" DESTINATION "${TARGET_PATH}")
    endforeach()

    在使用GLOB_RECURSE找到所有的头文件以后,为了保持目录结构,需要先用get_filename_component找到绝对路径

    然后使用REPLACE替换出相对路径,来进行最终的install

编写依赖的第三方库cmake

在主项目中通过include依赖第三方库的cmake,

随后通过add_dependencies指定依赖的第三方库,如下

1
2
3
4
5
6
7
include(${CMAKE_SOURCE_DIR}/thirdparty/gflags.cmake)
include(${CMAKE_SOURCE_DIR}/thirdparty/glog.cmake)

add_dependencies(${PROJECT_NAME}
gflags_external_project
glog_external_project
)

随后在thirdparty/glog.cmake中编写glog_external_project相关语句,主要就是编译安装第三方库

使用cmake编译的第三方库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
include(ExternalProject) #ExternalProject_Add依赖这个系统cmake库

#声明编译后要安装到哪个位置,CMAKE_BINARY_DIR就是当前执行cmake命令的路径
set(GLOG_INSTALL_DIR ${CMAKE_BINARY_DIR}/thirdparty/glog)
set(GLOG_INCLUDE_DIR ${GLOG_INSTALL_DIR}/include)
set(GLOG_LIB_DIR ${GLOG_INSTALL_DIR}/lib)

#使用ExternalProject_Add声明glog_external_project,指定PREFIX和INSTALL_DIR等
ExternalProject_Add(glog_external_project
SOURCE_DIR ${CMAKE_SOURCE_DIR}/thirdparty/glog
PREFIX ${GLOG_INSTALL_DIR}
INSTALL_DIR ${GLOG_INSTALL_DIR}
CMAKE_ARGS -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
-DCMAKE_CXX_FLAGS=${EXTERNAL_PROJECT_CMAKE_CXX_FLAGS}
-DCMAKE_INSTALL_PREFIX:PATH=${GLOG_INSTALL_DIR}
-DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS}
DEPENDS gflags_external_project #glog还依赖了gflags,这个ExternalProject在gflags.cmake中编写
)
include_directories(BEFORE SYSTEM ${GLOG_INCLUDE_DIR}) #编译时,在系统库前声明第三方库
link_directories(${GLOG_LIB_DIR}) #指定链接路径
install(DIRECTORY ${GLOG_INCLUDE_DIR} DESTINATION ${CMAKE_INSTALL_PREFIX}) #将头文件安装到指定的CMAKE_INSTALL_PREFIX下

使用configure和make编译的第三方库

1
2
3
4
5
6
7
8
9
10
11
12
ExternalProject_Add(libunwind_external_project
SOURCE_DIR ${CMAKE_SOURCE_DIR}/thirdparty/libunwind/
PREFIX ${LIBUNWIND_INSTALL_DIR}
CONFIGURE_COMMAND ./autogen.sh && ./configure
--prefix=${LIBUNWIND_INSTALL_DIR}
--enable-shared=no
--disable-minidebuginfo
"CFLAGS=-fPIC -g -O2"
BUILD_COMMAND $(MAKE) -j12
BUILD_IN_SOURCE 1
INSTALL_COMMAND $(MAKE) install
)

不再给CMAKE_ARGS填充值

CONFIGURE_COMMAND中填充configure相关语句,在BUILD_COMMANDINSTALL_COMMAND中填充值

如果可以直接make,不需要configure,那么CONFIGURE_COMMAND需要设置为空,如下

1
2
3
4
5
6
7
ExternalProject_Add(grpc_external_project
SOURCE_DIR ${CMAKE_SOURCE_DIR}/thirdparty/grpc
CONFIGURE_COMMAND ""
BUILD_COMMAND $(MAKE) -j12
BUILD_IN_SOURCE 1
INSTALL_COMMAND $(MAKE) install prefix=${GRPC_INSTALL_DIR}
)

如果有些headonly的项目完全不需要编译,那么连BUILD_COMMAND都可以设置为空

1
2
3
4
5
6
7
ExternalProject_Add(tinytoml_external_project
SOURCE_DIR "${CMAKE_SOURCE_DIR}/thirdparty/tinytoml"
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ${CMAKE_COMMAND} -E copy_directory
<SOURCE_DIR>/include ${TINY_TOML_INCLUDE_DIR}
)

复杂的XXX_Command

如果想在XXX_Command写多个命令,那么直接换行Command即可,例如

1
2
3
4
5
ExternalProject_Add(grpc_external_project
...
INSTALL_COMMAND $(MAKE) install prefix=${GRPC_INSTALL_DIR}
COMMAND ${CMAKE_COMMAND} -E copy ${GRPC_LIB_DIR}/libgrpc++.a ${GRPC_LIB_DIR}/libgrpcpp.a
)

如果XXX_Command比较复杂,就需要使用ExternalProject_Add_Step来完成了

例如想在protobuf的cmake的install以后再运行chmod -R +x ${PROTOBUF_INSTALL_DIR}/bin

1
2
3
4
5
6
7
8
9
10
11
12
ExternalProject_Add(protobuf_external_project
SOURCE_DIR ${CMAKE_SOURCE_DIR}/thirdparty/grpc/third_party/protobuf
CONFIGURE_COMMAND ""
BUILD_COMMAND $(MAKE) -j12
BUILD_IN_SOURCE 1
INSTALL_COMMAND $(MAKE) install prefix=${PROTOBUF_INSTALL_DIR}
DEPENDS grpc_external_project
)
ExternalProject_Add_Step(protobuf_external_project SetExecutablePermissions
COMMAND chmod -R +x ${PROTOBUF_INSTALL_DIR}/bin
DEPENDEES install
)
1
2
3
4
5
6
7
8
9
10
11
ExternalProject_Add_Step(<external_project_name> <step_name>
[COMMAND command1 [ARGS]
[ COMMAND command2 [ARGS]...]]
[COMMENT comment]
[DEPENDEES step1 [step2...]]
[DEPENDERS step1 [step2...]]
[ALWAYS [1]]
)
DEPENDEES:此步骤所依赖的其他步骤,这些步骤将在此步骤之前执行。可选值包括:configure, build, install, test 或其他自定义步骤。
DEPENDERS:依赖于此步骤的其他步骤,它们将在此步骤之后执行。可选值与 DEPENDEES 相同。
ALWAYS:设置为 1 时,表示在每次构建时都执行此步骤,而不仅仅是在首次构建时。

主项目静态库合并依赖的第三方静态库

需求是将一个THIRDPARTY_LIBS的列表内的静态库合并起来,这个列表是各个第三库相关的cmake中编写的

1
2
3
4
5
6
7
8
9
#in libunwind.a
list(APPEND THIRDPARTY_LIBS ${LIBUNWIND_LIB_DIR}/libunwind.a)
#in grpc.a
list(APPEND THIRDPARTY_LIBS ${GRPC_LIB_DIR}/libgrpcpp.a)
list(APPEND THIRDPARTY_LIBS ${GRPC_LIB_DIR}/libgrpc.a)
list(APPEND THIRDPARTY_LIBS /usr/lib/x86_64-linux-gnu/librt.a)
list(APPEND THIRDPARTY_LIBS /usr/lib/x86_64-linux-gnu/libz.a)
list(APPEND THIRDPARTY_LIBS /usr/lib/x86_64-linux-gnu/libssl.a)
list(APPEND THIRDPARTY_LIBS /usr/lib/x86_64-linux-gnu/libcrypto.a)

听起来很简单,使用find_library然后COMMAND ar qc即可

但是最后我发现这些指令都是在运行cmake指令阶段就运行了,而不是在运行make构建指令时运行的

很显然就能想到使用add_custom_command+add_custom_target来解决这个问题,但是add_custom_command的无法运行foreach等复杂命令,因此只能寻求在add_custom_command+第三方脚本来解决这个问题

我根据chatgpt的提示,使用add_custom_command+cmake脚本来解决这个问题,最终失败了

类似于这样

失败方案

1
2
3
4
5
6
7
8
9
10
11
12
13
set(MERGED_LIB_OUTPUT "merged_lib.a")
add_custom_command(
OUTPUT ${MERGED_LIB_OUTPUT}
COMMAND
${CMAKE_COMMAND} "-DTHIRDPARTY_LIBS=${THIRDPARTY_LIBS}" "-DCMAKE_BINARY_DIR=${CMAKE_BINARY_DIR}" "-DMERGED_LIB_OUTPUT=${MERGED_LIB_OUTPUT}" -P "${CMAKE_CURRENT_LIST_DIR}/merge_libraries.cmake"
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
DEPENDS ${PROJECT_NAME}
COMMENT "Merging all static libraries into: ${MERGED_LIB_OUTPUT}"
VERBATIM
)

# 添加 ALL 以将此目标包含在默认构建中
add_custom_target(merged_lib ALL DEPENDS ${MERGED_LIB_OUTPUT})

其中merge_libraries.cmake是一个独立的脚本,作用是将提供的THIRDPARTY_LIBS全部打包成MERGED_LIB_OUTPUT

内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
set(THIRDPARTY_LIBS_PATHS "")
foreach(LIB_NAME IN LISTS THIRDPARTY_LIBS)
find_library(LIB_${LIB_NAME}_PATH ${LIB_NAME} PATHS ${CMAKE_BINARY_DIR}/thirdparty)
list(APPEND THIRDPARTY_LIBS_PATHS ${LIB_${LIB_NAME}_PATH})
endforeach()

set(IMPORTED_LIBS "")
foreach(LIB_PATH IN LISTS THIRDPARTY_LIBS_PATHS)
get_filename_component(LIB_NAME ${LIB_PATH} NAME_WE)
add_library(${LIB_NAME} STATIC IMPORTED)
set_target_properties(${LIB_NAME} PROPERTIES IMPORTED_LOCATION ${LIB_PATH})
list(APPEND IMPORTED_LIBS ${LIB_NAME})
endforeach()

execute_process(
COMMAND ar qc ${MERGED_LIB_OUTPUT} ${IMPORTED_LIBS}
COMMAND ranlib ${MERGED_LIB_OUTPUT}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)

这个方案坑爹的点,在于${CMAKE_COMMAND} -P "${CMAKE_CURRENT_LIST_DIR}/merge_libraries.cmake"执行的merge_libraries.cmake脚本,不带有任何的系统环境变量,因为系统环境变量是通过Project(xxx)的时候初始化的,通过这种方式编写的脚本,不允许声明Project(xxx)

在不存在系统环境变量的时候find_library等等指令都是无法运行的

成功方案

最终在Combining several static libraries into one using CMake中找到了可行的办法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function(combine_archives output_archive list_of_input_archives dependencies)
set(mri_file ${CMAKE_BINARY_DIR}/${output_archive}.mri)
set(FULL_OUTPUT_PATH ${output_archive})
file(WRITE ${mri_file} "create lib${FULL_OUTPUT_PATH}.a\n")
FOREACH(in_archive ${list_of_input_archives})
file(APPEND ${mri_file} "addlib ${in_archive}\n")
ENDFOREACH()
file(APPEND ${mri_file} "save\n")
file(APPEND ${mri_file} "end\n")

set(output_archive_dummy_file ${CMAKE_BINARY_DIR}/${output_archive}.dummy.cpp)
add_custom_command(
OUTPUT ${output_archive_dummy_file}
COMMAND touch ${output_archive_dummy_file}
DEPENDS ${dependencies}
)

add_library(${output_archive} STATIC ${output_archive_dummy_file})
add_custom_command(
TARGET ${output_archive}
POST_BUILD
COMMAND ar -M < ${mri_file}
DEPENDS ${dependencies}
COMMENT "Merging all static libraries into: ${output_archive}"
)
endfunction(combine_archives)

使用方法

1
2
3
4
5
6
7
#merge all static libraries
list(APPEND FINAL_PATHS ${CMAKE_BINARY_DIR}/lib${PROJECT_NAME}.a)
list(APPEND FINAL_PATHS ${THIRDPARTY_LIBS})
combine_archives(${FINAL_OUTPUT} "${FINAL_PATHS}" ${PROJECT_NAME})
add_custom_target(final_target DEPENDS ${FINAL_OUTPUT})
# 不能install targets,因为custom_target不会产生实际文件
install(FILES ${CMAKE_BINARY_DIR}/lib${FINAL_OUTPUT}.a DESTINATION "lib")

这里使用首先把需要合并的库写入了一个mri脚本中,随后在add_custom_command中使用ar -M < ${mri_file}的方式合并了所有静态库

mri脚本的文件内容大致如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
create libsheep_cpp.a
addlib /root/sheep_cpp/build/libsheep_cpp_core.a
addlib /root/sheep_cpp/thirdparty/http-parser/libhttp_parser.a
addlib /root/sheep_cpp/build/thirdparty/hiredis/lib/libhiredis.a
addlib /root/sheep_cpp/build/thirdparty/gflags/lib/libgflags.a
addlib /root/sheep_cpp/build/thirdparty/glog/lib/libglog.a
addlib /root/sheep_cpp/build/thirdparty/grpc/lib/libgrpcpp.a
addlib /root/sheep_cpp/build/thirdparty/grpc/lib/libgrpc.a
addlib /root/sheep_cpp/build/thirdparty/protobuf/lib/libprotobuf.a
addlib /root/sheep_cpp/build/thirdparty/libunwind/lib/libunwind.a
addlib /root/sheep_cpp/build/thirdparty/gperftools/lib/libtcmalloc_and_profiler.a
addlib /root/sheep_cpp/build/thirdparty/gperftools-httpd/lib/libghttpd.a
addlib /usr/lib/x86_64-linux-gnu/librt.a
addlib /usr/lib/x86_64-linux-gnu/libz.a
addlib /usr/lib/x86_64-linux-gnu/libssl.a
addlib /usr/lib/x86_64-linux-gnu/libcrypto.a
save
end

但是这里有一个很坑的点,mri脚本作为makefile的扩展,语法支持非常的烂

例如libgrpc++他无法识别会报错,必须安装的时候改名成libgrpcpp才能继续使用

然后mri脚本的相关资料又非常的少,因此如果下次遇到这种问题,我大概会直接使用第三方的python脚本来解决问题,毕竟都依赖这么多库了,也不差python了

cmake install的小坑

安装目录文件的时候默认是不会继承文件的属性的,因此在安装二进制文件的时候需要使用USE_SOURCE_PERMISSIONS显式指定继承

1
install(DIRECTORY ${GRPC_BIN_DIR} DESTINATION ${CMAKE_INSTALL_PREFIX} USE_SOURCE_PERMISSIONS)