CMake 开发库(Library)的最佳实践

devtools/2025/3/18 11:40:59/

CMake__0">1. 使用 Modern CMake 开发库

CMake 在 C++社区中非常流行, 可以说是事实上的 C++ 包管理工具. 在Meeting C++ 开发者调查中, 有 75.73%的受访者表示自己使用 CMake 作为构建工具. 选择一个广泛流行的工具来打包库意味着你的项目更容易被别人使用.

本文将从一个简单的库的打包样例开始, 介绍编写 CMake最佳实践.
由于 CSDN 的 markdown 不支持 highlight 特定行, 有兴趣的读者可以访问我的个人博客以获取更好体验.
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 写法

  1. CMakeLists.txt

    # specify the minimum required version of cmake
    cmake_minimum_required(VERSION 3.28)project(foo)add_subdirectory(foo)
    add_subdirectory(foo_usage)
    
  2. 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语句的写法.

  3. 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中.

  1. 安装头文件和库文件.

    install(TARGETS fooEXPORT fooTargetsFILE_SET foo_headers
    )
    

    此时安装效果如下:

    _install
    ├── include
    │   └── foo
    │       └── foo.h
    └── lib└── libfoo.a4 directories, 2 files
    
  2. 安装 Target

    install(EXPORT fooTargetsDESTINATION share/cmake/fooNAMESPACE foo::
    )
    

    此时会安装配置文件

    _install
    ├── include
    │   └── foo
    │       └── foo.h
    ├── lib
    │   └── libfoo.a
    └── share└── cmake└── foo├── fooTargets.cmake└── fooTargets-debug.cmake
    
  3. 安装配置文件

    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. 正确处理依赖

不要内置依赖版本

我们的项目不可避免的会使用第三方库, 考虑到我们的库也会被别人使用, 如果我们在项目里写死了依赖的版本, 那么别人使用的时候会出现兼容性问题. 更好的办法是交给集成方去管理, 由集成方去指定依赖的版本.

  1. 错误写法. 如果使用我们库的人要用不同版本的 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)
    
  2. 正确写法: 交给用户去设置第三方依赖版本.

    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” >}}

http://www.ppmy.cn/devtools/168051.html

相关文章

考研专业课复习方法:如何高效记忆和理解?

高效记忆与理解指南 考研专业课是每个考生面临的重大挑战之一&#xff0c;它不仅要求我们掌握大量的知识点&#xff0c;还考验我们的理解和应用能力&#xff0c;为了在有限的时间内取得最佳效果&#xff0c;我们需要制定一套高效的复习策略,以下是关于如何在考研中高效记忆和理…

Linux 操作系统简介

Linux 操作系统 Linux 是一种自由和开源的操作系统&#xff0c;最初由芬兰的 Linus Torvalds 在1991年创建。它是一个类 Unix 操作系统&#xff0c;广泛用于服务器、个人电脑和嵌入式设备。Linux 操作系统的核心是 Linux 内核&#xff0c;其周围构建了各种工具和应用程序&…

【图像分类】ImageNet32 数据集下载指南

【图像分类】ImageNet32 数据集下载指南 写在最前面1. 介绍2. 访问 ImageNet 官网3. 申请下载权限**申请流程&#xff1a;** 4. 下载 ImageNet 数据集5. 注意事项6. 结论 &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f680; 感谢你的陪伴与支持~ 欢迎添加文末好友 &am…

QT编程之HTTP服务端与客户端技术

一、HTTP 服务器实现方案 ‌QtWebApp 集成‌ 将QtWebApp源码的 httpserver 目录导入项目&#xff0c;并在 .pro 文件中添加 include ($$PWD/httpserver/httpserver.pri)‌。配置 WebApp.ini 文件定义服务参数&#xff08;IP、端口、线程池等&#xff09;&#xff0c;通过 HttpL…

关闭Windows更新

win R 进入 services.msc 找到windows更新 更新一个

ubuntu20.04装nv驱动的一些坑

**1.一定要去bios里面关闭secure boot&#xff0c;否则驱动程序需要签名&#xff0c;安装了的驱动无法被识别加载 2.假如没有关闭secure boot然后装了驱动&#xff0c;然后再去关闭secure boot&#xff0c;可能会导致进入不了ubuntu的情况 此时&#xff0c;先恢复secure boot&…

VSCode + CMake

参考文献&#xff1a; 如何用 GCC, CMake 和 Make 编译C/C代码Windows 上的 Linux 子系统&#xff1a;WSLWSL&#xff1a;桌面 UI 远程连接 RDP 配置 VScode 文章目录 CMake 配置VSCode 配置launch.jsontask.jsonc_cpp_properties.json CMake 配置 编写如下的 CmakeLists.t…

DeepSeek大模型在政务服务领域的应用

DeepSeek大模型作为国产人工智能技术的代表&#xff0c;近年来在政务服务领域的应用呈现多点开花的态势。通过多地实践&#xff0c;该技术不仅显著提升了政务服务的效率与智能化水平&#xff0c;还推动了政府治理模式的创新。以下从技术应用场景、典型案例及发展趋势三个维度进…