CMake__0">1. 使用 Modern CMake 开发库
CMake 在 C++社区中非常流行, 可以说是事实上的 C++ 包管理工具. 在Meeting C++ 开发者调查中, 有 75.73%的受访者表示自己使用 CMake 作为构建工具. 选择一个广泛流行的工具来打包库意味着你的项目更容易被别人使用.
本文将从一个简单的库的打包样例开始, 介绍编写 CMake 的最佳实践.
由于 CSDN 的 markdown 不支持 highlight 特定行, 有兴趣的读者可以访问我的个人博客以获取更好体验.
项目配置
当前的目录结构如下(源码在此处):
.
├── CMakeLists.txt
├── foo
│ ├── CMakeLists.txt
│ ├── foo.cpp
│ └── include
│ └── foo
│ └── foo.h
└── foo_usage├── CMakeLists.txt└── main.cpp5 directories, 6 files
不要写这样的 CMakeLists.txt
:
# Specify the minimum required version of CMake
cmake_minimum_required(VERSION 2.8)# Name the project
project(foo)set(CMAKE_CXX_FLAGS "fpic")# Set include directories, if needed
include_directories(${CMAKE_SOURCE_DIR}/foo/include)# Specify the executable target and source files
add_library(foo SHARED foo/foo.cpp)
Modern CMake 提倡使用以 Target 为核心的构建方式. 不提倡使用目录级别(include_directories
/link_directories
)的构建方式.
CMake__49">Modern CMake 写法
-
CMakeLists.txt
# specify the minimum required version of cmake cmake_minimum_required(VERSION 3.28)project(foo)add_subdirectory(foo) add_subdirectory(foo_usage)
-
foo/CMakeLists.txt
add_library(foo)target_sources(fooPUBLICFILE_SET foo_headersTYPE HEADERSBASE_DIRS ./includeFILES include/foo/foo.hPRIVATEfoo.cpp )
这个示例演示了 Target 为核心的写法, 它将 foo.cpp 作为库的源文件, 并且将 foo.h 作为库的头文件.
BASE_DIRS
选项指定了头文件的基目录, 这会影响第三方使用时include
语句的写法. -
foo_usage/CMakeLists.txt
add_executable(foo_usage main.cpp) target_link_libraries(foo_usage PRIVATE foo)
CMake__88">2. 使用 CMake 打包库
C++ 并没有原生的打包机制. CMake 需要收集生成的库文件以及头文件, 另外需要额外的信息, 告诉用户如何使用库.
假定我们的安装目录设置为_install
, 我们一步步介绍如何打包一个库. 安装命令为:
cmake --install . --prefix /path/to/_install
下面的修改是在foo/CMakeLists.txt
中.
-
安装头文件和库文件.
install(TARGETS fooEXPORT fooTargetsFILE_SET foo_headers )
此时安装效果如下:
_install ├── include │ └── foo │ └── foo.h └── lib└── libfoo.a4 directories, 2 files
-
安装 Target
install(EXPORT fooTargetsDESTINATION share/cmake/fooNAMESPACE foo:: )
此时会安装配置文件
_install ├── include │ └── foo │ └── foo.h ├── lib │ └── libfoo.a └── share└── cmake└── foo├── fooTargets.cmake└── fooTargets-debug.cmake
-
安装配置文件
install(FILES./cmake/FooConfig.cmakeDESTINATION share/cmake/foo )
其中
FooConfig.cmake
的内容如下:include(${CMAKE_CURRENT_LIST_DIR}/fooTargets.cmake)
效果如下:
_install ├── include │ └── foo │ └── foo.h ├── lib │ └── libfoo.a └── share└── cmake└── foo├── FooConfig.cmake├── fooTargets.cmake└── fooTargets-debug.cmake
很多项目会允许使用 find_package
或者 git submoudle 来集成第三方的库. 目前的配置中, 使用find_package
集成的话我们的库会在foo::
命名空间下, 而不是foo
.
为了提供一致的体验, 我们做一个 alias. 修改foo/CMakeLists.txt
如下:
add_library(foo)
add_library(foo::foo ALIAS foo)# ...
修改foo_usage/CMakeLists.txt
如下:
add_executable(foo_usage main.cpp)
target_link_libraries(foo_usage PRIVATE foo::foo)
3. 正确处理依赖
不要内置依赖版本
我们的项目不可避免的会使用第三方库, 考虑到我们的库也会被别人使用, 如果我们在项目里写死了依赖的版本, 那么别人使用的时候会出现兼容性问题. 更好的办法是交给集成方去管理, 由集成方去指定依赖的版本.
-
错误写法. 如果使用我们库的人要用不同版本的 Protobuf, 那么他就不得不改写我们的库. 这个非常不友好.
cmake_minimum_required(VERSION 3.28)project(foo)include(FetchContent) FetchContent_Declare(ProtobufGIT_REPOSITORY https://github.com/protocolbuffers/protobuf.gitGIT_TAG v30.1 ) FetchContent_MakeAvailable(Protobuf)add_subdirectory(foo) add_subdirectory(foo_usage)
-
正确写法: 交给用户去设置第三方依赖版本.
cmake_minimum_required(VERSION 3.28)project(foo)find_package(Protobuf REQUIRED)add_subdirectory(foo) add_subdirectory(foo_usage)
使用现代方式的依赖管理
-
不要使用
target_include_directories
/target_link_libraries
find_package(Protobuf REQUIRED)add_library(foo)target_include_directories(foo PUBLIC ${Protobuf_INCLUDE_DIRS}) target_link_libraries(foo PUBLIC ${Protobuf_LIBRARIES})
-
使用
target_link_libraries
find_package(Protobuf REQUIRED)add_library(foo) target_link_libraries(foo PUBLIC Protobuf::libprotobuf)
显示的声明依赖
-
没有明确指明依赖, 编译的结果会依赖于当时的环境. 这存才不确定性.
find_package(JPEG)if (JPEG_FOUND)add_library(foo_jpeg)# ...target_link_libraries(foo_jpeg PRIVATE jpeg::jpeg) endif()
-
更好的写法. 通过一个
option
来明确是否编译某个库. 这样的结果是确定的.option(FOO_HAS_JPEG "Build foo with JPEG" ON) if (FOO_HAS_JPEG)find_package(JPEG REQUIRED)add_library(foo_jpeg)# ...target_link_libraries(foo_jpeg PRIVATE jpeg::jpeg) endif()
CMakeListstxt__285">4. 保持 CMakeLists.txt
整洁
对待 Cmake 代码就像生产代码一样, 以严格的规范来编写.
初始化 Target 属性
在设置一个 Target 属性的时候, 避免使用全局设置. 作为一个库如果别人用add_subdirectory
将你的库添加到它的项目, 那么你的一句set
语句会影响别人的项目. 更好的做法是只声明你的 Target 的属性.
-
不好的写法.
set(CMAKE_CXX_STANDARD 17) add_compile_options(-Wall -Wextra) add_library(foo)
-
更好的做法
target_compile_features(foo PUBLIC cxx_std_17)
从外部注入构建设置(build setting)
一个项目使用的编译选项(比如-Wall
)应该从外部注入, 不要写死在CMakeLists.txt
中. 为什么要这样呢? 因为作为一个库, 不同的人可能会有不同的编译选项, 把这些交给外部处理更灵活.
-
不好的写法
cmake_minimum_required(VERSION 3.28)if(UNIX)message(STATUS "GCC detected - Adding flags")add_compile_options(-Wall -Wextra) endif()project(foo)add_subdirectory(foo) add_subdirectory(foo_usage)
-
更好的做法: CMake Presets
cmake_minimum_required(VERSION 3.28)project(foo)add_subdirectory(foo) add_subdirectory(foo_usage)
CMakePresets.json
{"version": 3,"confgurePresets": [{"name": "unix","displayName": "Default","cacheVariables": {"COMPILE_OPTIONS": "-Wall -Wextra"}}] }
参考资料
- 示例代码
- Clean CMake for C++ (library) developers - Kerstin Keller
相关阅读
- {{< PostRef path=“/posts/column-cmake/2025-01-07-cmake-column-intro.md” >}}