Introduction
近日某个项目临近结束,书文档,写配置,发现网上的 CMake 教程颇旧,混乱不堪,且缺乏实际作用,难及需求。遂系统读了一些 Modern CMake 资料,撰文记录,以供参考。
实际项目包含上万行代码,依赖三四个第三方库,欲生成支持
find_package()
查找的动态库,并自动传递依赖,以使用户能够直接使用。
下面将其简化为一最小示例,便于演示流程。示例项目结构为:
mylib/
├─ inc/
│ ├─ mylib/
│ │ ├─ lib.h
├─ src/
│ ├─ lib.cc
├─ CMakeLists.txt
// inc/mylib/lib.h
#ifndef LIB_H_
#define LIB_H_
namespace mylib {
void foo();
void bar();
} // namespace mylib
#endif // LIB_H_
// src/lib.cc
#include
#include
#include
#include
namespace mylib {
void foo() {
std::cout <"hello torch\n";
torch::Tensor tensor = torch::rand({2, 3});
std::cout endl;
}
void bar()
{
fmt::print("hello fmtlib\n");
}
} // namespace mylib
该库只包含两个函数,
foo()
和
bar()
,分别依赖
libtorch
和
fmtlib
这两个第三方库。
现在需要编写
CMakeLists.txt
来配置依赖,生成动态库,并传递依赖,以使用户无需重复导入依赖的第三方库。
这可以分为三个步骤进行思考,先确保输入无误(Input),再确保目标信息导出无误 (Process),最后确保输出的传递依赖无误(Output),就是常用的系统分析 IPO 模型。
Input
先看第一步,输入。我们项目的源文件、头文件及第三方库的所有依赖就是这里所说的输入,该步要保证各库路径的正确性和 ABI 的一致性,否则后续可能会产生奇怪的错误。
首先是基本配置,包含项目名称、版本和语言等信息。这样编写即可:
cmake_minimum_required(VERSION 3.20)
project(
mylib
VERSION 1.0.1
DESCRIPTION "First release of mylib"
LANGUAGES CXX
)
其次,导入第三方库。如果库安装在标准路径(
/usr/local/lib
etc.),
find_package()
可以直接找到,而若是自定义路径,则需要手动指定。暂且这样写上:
find_package(Torch REQUIRED)
find_package(fmt REQUIRED)
message(STATUS "Found Torch: ${TORCH_FOUND}")
message(STATUS "Found fmtlib: ${fmt_FOUND}")
写 CMake 要步步为营,我们先运行一下以确保目前不存在任何问题,再继续前进。在
mylib/
目录下,尝试运行以下命令:
cmake -S . -B build/ -DCMAKE_PREFIX_PATH:STRING=/home/lkimuk/libraries/libtorch/
由于
libtorch
并未处于标准目录,故而需要通过
CMAKE_PREFIX_PATH
手动为其指定搜索路径。倘若输出为:
-- Found Torch: TRUE
-- Found fmtlib: 1
-- Configuring done
-- Generating done
即表示当前无误。
libtorch
库的 CMake 配置写法不规范,是老式写法,而
fmtlib
是新式写法,是以输出形式也略有不同。本文是新式写法。
这个搜索路径也可以写在 CMake 中,存在两种方式,一种是
CMAKE_PREFIX_PATH
路径,另一种是
Torch_DIR
变量。
# 1.
list(APPEND CMAKE_PREFIX_PATH "/home/lkimuk/software/libtorch")
# 2.
set(Torch_DIR "/home/lkimuk/software/libtorch/share/cmake/Torch")
但不应写死,而是通过 CMake 命令指定,因为实际运行时,用户的路径不可能与你相同。
# Found all source files
file(GLOB_RECURSE SOURCE_FILES "${PROJECT_SOURCE_DIR}/src/*.cc")
list(LENGTH SOURCE_FILES SRC_FILES_SIZE)
message(STATUS "Found ${SRC_FILES_SIZE} source files of mylib")
# Define a shared library target named `mylib`
add_library(mylib SHARED)
# Specify source files for target named `mylib`
target_sources(mylib PRIVATE ${SOURCE_FILES})
# Specify the include directories for the target named `mylib`
target_include_directories(mylib PUBLIC
$
$
)
# Specify the link directories for the target named `mylib`
target_link_libraries(mylib PUBLIC
fmt::fmt
${TORCH_LIBRARIES}
)
# Request compile features for target named `mylib`
target_compile_features(mylib PUBLIC cxx_std_20)
Modern CMake 是 target-oriented 的, target 就是编译的目标,可以是静态库、动态库和可执行文件,各类目录就是这个 target 的属性,权限就是 target 的依赖传递性。完全可以类比对象,若是想让用户在使用源码时,也可以使用
fmtlib
和
libtorch
的特性而无需再次链接,只需指定
PUBLIC
,对
mylib
所依赖的库进行传递。
但需要注意,
find_package()
本身并不具备传递依赖的能力,依赖传递需要单独处理,否则用户侧不仅需要导入你的
mylib
库,还需要重复导入
fmtlib
和
libtorch
,而这些第三方库其实已经在你提供的 CMake 中导入过了。
cmake -S . -B build/ -DCMAKE_PREFIX_PATH:STRING=/home/lkimuk/libraries/libtorch/
cmake --build ./build
Process
首先,使我们的库能够安装。就是指定一些安装目录、导出头文件之类的操作,
ARCHIVE
是静态库目录,
LIBRARY
是动态库目录,
INCLUDES
是头文件目录。
# Defines the ${CMAKE_INSTALL_INCLUDEDIR} and ${CMAKE_INSTALL_LIBDIR} variable.
include(GNUInstallDirs)
# Make executable target `mylib` installable
install(TARGETS mylib
EXPORT mylib-targets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
# Install the header files
install(
DIRECTORY ${PROJECT_SOURCE_DIR}/inc/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
此处创建了一个导出对象
mylib-targets
,指定了一些导入信息,主要就是某些信息保存的目录,如库存入
${CMAKE_INSTALL_LIBDIR}
(其实就是
lib
)下,头文件存入
${CMAKE_INSTALL_INCLUDEDIR}
(其实就是
include
)下,这两个变量来自
GNUInstallDirs
。但此时尚未实际生成文件,只是指定一些信息而已。
install(DIRECTORY ...)
实际将头文件拷贝至
${CMAKE_INSTALL_INCLUDEDIR}
目录,前面的命令不会自动复制这些内容,需要自己手动写。
接着,根据导出名称,实际导出 targets 文件。
# Generate the required import code for the content in
# into mylib-config.cmake CMake file.
install(EXPORT mylib-targets
NAMESPACE mylib::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/mylib/cmake
)
该部分会实际导出并生成
mylib-targets.cmake
文件,也可以命名为
mylibTargets
,对应会生成
mylibTargets.cmake
文件,这里面包含着
find_packages()
查找库所需重要信息。
如果你的库不依赖第三方库,那么直接将上面的
mylib-targets
改为
mylib-config
文件就可以满足需求,
find_packages()
实际需要的是
mylib-config.cmake
这个名称的文件。这里先生成
mylib-targets
是为了稍后处理依赖传递,后面
mylib-targets.cmake
依旧会包含在
mylib-config.cmake
文件中。
这里暂先不管依赖传递,放到下一节专门讲解。先为库生成版本信息:
# Defines write_basic_package_version_file
include(CMakePackageConfigHelpers)
# Create a package version file for the package.
write_basic_package_version_file(
"mylib-config-version.cmake"
# Package compatibility strategy. SameMajorVersion is essentially `semantic versioning`.
COMPATIBILITY SameMajorVersion
)
# Install command for deploying Config-file package files into the target system.
# It must be present in the same directory as `mylib-config.cmake` file.
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/mylib-config-version.cmake"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/mylib/cmake
)
版本信息通过
write_basic_package_version_file
函数实现,
SameMajorVersion
表示和
project(VERSION)
中指定的主版本相同。这将实际生成
mylib-config-version.cmake
文件,它必须和
mylib-config.cmake
(目前这个文件还未创建,我们将其命名为
mylib-targets.cmake
了)文件处于同一目录,后续可以导入不同版本的库。
cmake -S . -B build/ -DCMAKE_PREFIX_PATH:STRING=/home/lkimuk/libraries/libtorch/
cmake --build ./build
cmake --install ./build --prefix /tmp/install-mylib
/tmp/
├─ install-mylib/
│ ├─ include/
│ │ ├─ mylib/
│ │ │ ├─ lib.h
│ ├─ lib/
│ │ ├─ libmylib.so
│ │ ├─ mylib/
│ │ │ ├─ cmake/
│ │ │ │ ├─ mylib-targets.cmake
│ │ │ │ ├─ mylib-targets-noconfig.cmake
│ │ │ │ ├─ mylib-config-version.cmake
测试之时,指定到临时路径,以防止污染系统目录,待测试完毕,则不用指定目录,将安装到系统标准路径。
此时只剩下一个重要文件,
mylib-config.cmake
,里面需要处理实际的依赖传递。
Output
这节专门讲依赖传递,完成最终输出。此节的内容请添加在
include(CMakePackageConfigHelpers)
(即生成版本导库) 代码的下方。
# Generate the config-file
set(LIB_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR}/mylib)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/mylib-config.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/mylib-config.cmake"
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/mylib/cmake
PATH_VARS LIB_INSTALL_DIR
)
# Found all the required sub-dependencies
file(APPEND "${CMAKE_CURRENT_BINARY_DIR}/mylib-config.cmake" "include(CMakeFindDependencyMacro)\nfind_dependency(fmt)\nfind_package(Torch)")
# Install mylib-config.cmake
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/mylib-config.cmake"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/mylib/cmake
)
这将根据
mylib/mylib-config.cmake.in
模板文件生成
mylib-config.cmake
文件,该模板文件需要手动创建,内容如下:
@PACKAGE_INIT@
set_and_check(MYLIB_LIB_DIR "@PACKAGE_LIB_INSTALL_DIR@")
include("${MYLIB_LIB_DIR}/cmake/mylib-targets.cmake")
check_required_components(mylib)
这模板文件用来自动生成一些安装信息,并检查库所依赖的组件是否已经找到。
所有的子依赖皆需要我们手动处理(多么不可理喻!),即我们还需要手动处理
libtorch
和
fmtlib
的依赖传递,否则用户无法找到
mylib
依赖的这些库。这项工作可以通过
find_dependency
来实现,因此要进行文件读写,在
mylib-config.cmake
的文件末尾追加写入这些依赖,
libtorch
库是老式实现,不支持
find_dependency
,还是写成
find_package(Torch)
。
最终生成的
mylib-config.cmake
文件内容为:
####### Expanded from @PACKAGE_INIT@ by configure_package_config_file() #######
####### Any changes to this file will be overwritten by the next CMake run ####
####### The input file was mylib-config.cmake.in ########
get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../" ABSOLUTE)
macro(set_and_check _var _file)
set(${_var} "${_file}")
if(NOT EXISTS "${_file}")
message(FATAL_ERROR "File or directory ${_file} referenced by variable ${_var} does not exist !")
endif()
endmacro()
macro(check_required_components _NAME)
foreach(comp ${${_NAME}_FIND_COMPONENTS})
if(NOT ${_NAME}_${comp}_FOUND)
if(${_NAME}_FIND_REQUIRED_${comp})
set(${_NAME}_FOUND FALSE)
endif()
endif()
endforeach()
endmacro()
####################################################################################
set_and_check(MYLIB_LIB_DIR "${PACKAGE_PREFIX_DIR}