引言

CMake是一个开源、跨平台的构建自动化工具,它使用平台无关的配置文件(CMakeLists.txt)来生成标准的构建文件(如Unix的Makefile或Windows的Visual Studio项目)。CMake支持复杂的构建需求,包括项目依赖管理、测试打包和安装等,使其成为C/C++项目中最受欢迎的构建系统之一。

对于C语言开发者来说,掌握CMake不仅能够简化项目构建过程,还能有效地管理项目依赖,实现跨平台开发。本文将从CMake的基础概念开始,逐步深入到高级应用,帮助读者全面掌握CMake在C语言项目中的使用技巧。

CMake基础

CMake简介和安装

CMake(Cross-platform Make)是一个构建系统生成器,它不直接构建软件,而是生成标准的构建文件(如Makefile、Visual Studio项目等),然后使用这些构建文件来编译和链接代码。CMake的主要优势在于其跨平台特性和处理复杂项目的能力。

安装CMake非常简单,可以根据不同操作系统选择相应的安装方式:

在Linux上(以Ubuntu为例):

sudo apt update sudo apt install cmake 

在macOS上(使用Homebrew):

brew install cmake 

在Windows上: 可以从CMake官方网站(https://cmake.org/download/)下载安装程序,按照向导进行安装。

安装完成后,可以通过以下命令验证安装:

cmake --version 

基本项目结构

一个基本的CMake项目通常包含以下结构:

my_project/ ├── CMakeLists.txt ├── src/ │ ├── main.c │ └── utils.c ├── include/ │ └── utils.h └── build/ 
  • CMakeLists.txt:CMake的配置文件,定义项目的构建规则。
  • src/:源代码目录。
  • include/:头文件目录。
  • build/:构建目录,用于存放生成的构建文件和编译结果。

CMakeLists.txt基础语法

CMakeLists.txt是CMake的核心配置文件,它使用简单的脚本语言来定义项目的构建规则。下面是一个基本的CMakeLists.txt示例:

# 设置最低CMake版本要求 cmake_minimum_required(VERSION 3.10) # 设置项目名称和版本 project(MyProject VERSION 1.0) # 设置C标准 set(CMAKE_C_STANDARD 99) # 添加可执行文件 add_executable(my_app src/main.c src/utils.c) # 包含头文件目录 target_include_directories(my_app PRIVATE include) 

这个简单的CMakeLists.txt文件做了以下几件事:

  1. 指定所需的最低CMake版本。
  2. 定义项目名称和版本。
  3. 设置C语言标准为C99。
  4. 添加一个名为my_app的可执行文件,由src/main.csrc/utils.c编译而成。
  5. 指定include目录作为头文件搜索路径。

项目配置基础

设置项目名称和版本

在CMake中,使用project()命令来设置项目的基本信息:

project(MyProject VERSION 1.0.2 DESCRIPTION "My awesome C project" LANGUAGES C ) 

这个命令不仅设置了项目名称和版本,还指定了项目使用的编程语言(这里是C)。设置项目后,CMake会自动定义一些变量,如PROJECT_NAMEPROJECT_VERSION等,可以在CMakeLists.txt的其他部分使用。

添加可执行文件和库

在CMake中,可以使用add_executable()add_library()命令来定义构建目标。

添加可执行文件:

add_executable(my_app src/main.c) 

添加静态库:

add_library(my_static_lib STATIC src/utils.c) 

添加动态库:

add_library(my_shared_lib SHARED src/utils.c) 

添加对象库(不生成库文件,只生成目标文件):

add_library(my_object_lib OBJECT src/utils.c) 

设置编译选项

CMake允许设置各种编译选项,以控制代码的编译过程。

设置编译器标志:

# 设置C编译器标志 set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra") # 设置特定构建类型的编译器标志 set(CMAKE_C_FLAGS_DEBUG "-g -O0") set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG") 

添加编译定义:

add_compile_definitions(ENABLE_FEATURE_X) 

添加特定目标的编译定义:

target_compile_definitions(my_app PRIVATE DEBUG_MODE) 

构建系统

生成构建文件

使用CMake构建项目通常包括两个步骤:配置和生成。

  1. 创建构建目录并进入:
mkdir build cd build 
  1. 运行CMake配置项目:
cmake .. 
  1. 构建项目:
cmake --build . 

或者,如果生成了Makefile,可以直接使用make:

make 

构建类型(Debug/Release等)

CMake支持多种构建类型,常见的有Debug、Release、RelWithDebInfo和MinSizeRel。

指定构建类型:

cmake -DCMAKE_BUILD_TYPE=Debug .. 

在CMakeLists.txt中设置默认构建类型:

if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) endif() 

根据构建类型设置不同的编译选项:

set(CMAKE_C_FLAGS_DEBUG "-g -O0 -DDEBUG") set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG") 

跨平台构建考虑

CMake的一个主要优势是其跨平台能力。为了确保项目在不同平台上都能正常构建,需要考虑一些平台特定的问题。

检测操作系统:

if(WIN32) # Windows特定的配置 add_definitions(-DWINDOWS_PLATFORM) elseif(UNIX AND NOT APPLE) # Linux特定的配置 add_definitions(-DLINUX_PLATFORM) elseif(APPLE) # macOS特定的配置 add_definitions(-DMACOS_PLATFORM) endif() 

处理路径分隔符:

# 使用正斜杠作为路径分隔符,CMake会自动转换为平台特定的格式 set(SRC_FILES src/main.c src/utils.c) 

处理库文件扩展名:

# CMake会自动处理库文件扩展名 target_link_libraries(my_app my_library) 

依赖管理基础

查找依赖包

CMake提供了多种方式来查找和使用外部依赖包。最常用的方法是使用find_package()命令。

基本用法:

find_package(ZLIB REQUIRED) 

这个命令会查找ZLIB库,如果找不到,CMake会报错。找到后,CMake会设置一些变量,如ZLIB_FOUNDZLIB_INCLUDE_DIRSZLIB_LIBRARIES,可以在后续的构建过程中使用。

使用find_package

find_package()有两种模式:Module模式和Config模式。在Module模式下,CMake会查找名为Find<PackageName>.cmake的模块文件;在Config模式下,CMake会查找<PackageName>Config.cmake<lower-case-package-name>-config.cmake文件。

使用找到的包:

find_package(Threads REQUIRED) # 添加可执行文件 add_executable(my_app src/main.c) # 链接线程库 target_link_libraries(my_app PRIVATE Threads::Threads) 

使用包含目录和库文件:

find_package(ZLIB REQUIRED) add_executable(my_app src/main.c) # 包含头文件 target_include_directories(my_app PRIVATE ${ZLIB_INCLUDE_DIRS}) # 链接库文件 target_link_libraries(my_app PRIVATE ${ZLIB_LIBRARIES}) 

链接外部库

除了使用find_package()找到的库,还可以直接链接外部库。

链接系统库:

target_link_libraries(my_app PRIVATE m) # 链接数学库 

链接特定路径的库:

# 查找库文件 find_library(MY_LIB_PATH libmylib.a PATHS /path/to/libs) # 链接库 target_link_libraries(my_app PRIVATE ${MY_LIB_PATH}) 

链接导入的目标:

# 导入已预编译的库 add_library(my_imported_lib SHARED IMPORTED) set_property(TARGET my_imported_lib PROPERTY IMPORTED_LOCATION /path/to/libmylib.so) # 链接导入的库 target_link_libraries(my_app PRIVATE my_imported_lib) 

高级CMake技术

自定义函数和宏

CMake允许定义自定义函数和宏,以复用代码和简化CMakeLists.txt文件。

定义函数:

function(add_my_library name) add_library(${name} ${ARGN}) target_include_directories(${name} PUBLIC include) set_target_properties(${name} PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON ) endfunction() # 使用自定义函数 add_my_library(my_lib src/utils.c) 

定义宏:

macro(configure_common_target target) target_compile_features(${target} PRIVATE c_std_99) target_compile_options(${target} PRIVATE -Wall -Wextra) if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_definitions(${target} PRIVATE DEBUG) endif() endmacro() # 使用自定义宏 add_executable(my_app src/main.c) configure_common_target(my_app) 

函数和宏的主要区别在于作用域:函数有自己的变量作用域,而宏则没有,宏类似于文本替换。

配置文件和模板

CMake可以生成配置文件,这些文件可以在编译时或运行时使用。

配置头文件:

# 配置头文件模板 configure_file( "${PROJECT_SOURCE_DIR}/include/config.h.in" "${PROJECT_BINARY_DIR}/include/config.h" ) # 添加生成的头文件到包含目录 target_include_directories(my_app PRIVATE ${PROJECT_BINARY_DIR}/include) 

config.h.in模板文件示例:

#ifndef CONFIG_H #define CONFIG_H #cmakedefine VERSION_MAJOR @VERSION_MAJOR@ #cmakedefine VERSION_MINOR @VERSION_MINOR@ #cmakedefine ENABLE_FEATURE_X #endif // CONFIG_H 

使用CMake的configure_file生成源文件:

# 生成版本信息源文件 configure_file( "${PROJECT_SOURCE_DIR}/src/version.c.in" "${PROJECT_BINARY_DIR}/src/version.c" ) # 添加生成的源文件到可执行文件 target_sources(my_app PRIVATE ${PROJECT_BINARY_DIR}/src/version.c) 

测试集成

CMake支持与CTest测试框架集成,方便进行单元测试和回归测试。

启用测试:

# 启用测试 enable_testing() # 添加测试 add_executable(my_test test/test_main.c test/test_utils.c) target_link_libraries(my_test PRIVATE my_lib) # 添加测试用例 add_test(NAME MyTest COMMAND my_test) 

定义测试属性:

# 设置测试超时时间 set_tests_properties(MyTest PROPERTIES TIMEOUT 10) # 设置测试依赖关系 add_test(NAME MyTest2 COMMAND my_test2) set_tests_properties(MyTest2 PROPERTIES DEPENDS MyTest) 

使用Google Test框架:

# 查找Google Test find_package(GTest REQUIRED) # 创建测试可执行文件 add_executable(unit_test test/unit_test.cpp) target_link_libraries(unit_test PRIVATE GTest::gtest GTest::gtest_main my_lib) # 添加测试 include(GoogleTest) gtest_discover_tests(unit_test) 

项目结构组织

多目录项目

对于较大的项目,通常会将代码组织到多个目录中。CMake支持这种多目录结构,并提供了相应的命令来管理子目录。

基本多目录结构:

my_project/ ├── CMakeLists.txt ├── src/ │ ├── CMakeLists.txt │ ├── main.c │ └── utils/ │ ├── CMakeLists.txt │ ├── utils.c │ └── utils.h └── lib/ ├── CMakeLists.txt └── mylib/ ├── CMakeLists.txt ├── mylib.c └── mylib.h 

根CMakeLists.txt:

cmake_minimum_required(VERSION 3.10) project(MyProject VERSION 1.0) # 添加子目录 add_subdirectory(src) add_subdirectory(lib) 

src/CMakeLists.txt:

# 添加utils子目录 add_subdirectory(utils) # 创建可执行文件 add_executable(my_app main.c) # 链接库 target_link_libraries(my_app PRIVATE utils mylib) 

lib/mylib/CMakeLists.txt:

# 创建库 add_library(mylib mylib.c) # 设置包含目录 target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 

子模块和子项目

CMake支持将外部项目作为子模块或子项目添加到主项目中。

使用add_subdirectory添加子项目:

# 添加子项目 add_subdirectory(third_party/mylib) # 使用子项目中的库 target_link_libraries(my_app PRIVATE mylib) 

使用ExternalProject添加外部项目:

include(ExternalProject) ExternalProject_Add( my_external_lib GIT_REPOSITORY https://github.com/example/mylib.git GIT_TAG main CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/install ) # 添加包含目录 include_directories(${CMAKE_BINARY_DIR}/install/include) # 添加库目录 link_directories(${CMAKE_BINARY_DIR}/install/lib) # 链接库 target_link_libraries(my_app PRIVATE my_external_lib) 

使用FetchContent(CMake 3.11+):

include(FetchContent) FetchContent_Declare( mylib GIT_REPOSITORY https://github.com/example/mylib.git GIT_TAG main ) FetchContent_MakeAvailable(mylib) # 使用库 target_link_libraries(my_app PRIVATE mylib) 

安装规则

CMake提供了安装规则,使得项目可以正确地安装到系统中。

基本安装规则:

# 安装可执行文件 install(TARGETS my_app RUNTIME DESTINATION bin ) # 安装库 install(TARGETS mylib LIBRARY DESTINATION lib ARCHIVE DESTINATION lib PUBLIC_HEADER DESTINATION include ) # 安装头文件 install(FILES include/utils.h DESTINATION include) # 安装配置文件 install(FILES my_app.conf DESTINATION etc) 

安装组件:

# 运行时组件 install(TARGETS my_app RUNTIME DESTINATION bin COMPONENT Runtime ) # 开发组件 install(TARGETS mylib LIBRARY DESTINATION lib ARCHIVE DESTINATION lib PUBLIC_HEADER DESTINATION include COMPONENT Development ) # 文档组件 install(FILES README.md DESTINATION share/doc/my_app COMPONENT Documentation ) 

打包支持:

# 设置包信息 set(CPACK_PACKAGE_NAME "MyApp") set(CPACK_PACKAGE_VERSION "1.0.0") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "My awesome application") # 包含CPack include(CPack) 

高级依赖管理

包管理器集成(Conan, vcpkg等)

现代C++/C项目经常使用包管理器来管理依赖。CMake可以与各种包管理器集成,如Conan、vcpkg等。

使用Conan:

# 查找Conan find_program(CONAN_CMD conan) if(NOT CONAN_CMD) message(FATAL_ERROR "Conan not found. Please install Conan.") endif() # 生成conanfile.txt file(WRITE "${CMAKE_BINARY_DIR}/conanfile.txt" " [requires] zlib/1.2.11 [generators] cmake ") # 安装依赖 execute_process( COMMAND ${CONAN_CMD} install . --build=missing WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) # 包含生成的CMake文件 include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) conan_basic_setup() # 使用库 target_link_libraries(my_app PRIVATE ${CONAN_LIBS}) 

使用vcpkg:

# 设置vcpkg工具链文件 set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake CACHE STRING "Vcpkg toolchain file" ) # 查找包 find_package(zlib CONFIG REQUIRED) # 使用库 target_link_libraries(my_app PRIVATE zlib::zlib) 

自定义Find模块

当CMake没有提供现成的Find模块时,可以创建自定义的Find模块来查找依赖库。

自定义Find模块示例(FindMyLib.cmake):

# 查找头文件 find_path(MYLIB_INCLUDE_DIR NAMES mylib.h PATHS /usr/include /usr/local/include ) # 查找库文件 find_library(MYLIB_LIBRARY NAMES mylib PATHS /usr/lib /usr/local/lib ) # 设置变量 include(FindPackageHandleStandardArgs) find_package_handle_standard_args(MyLib DEFAULT_MSG MYLIB_INCLUDE_DIR MYLIB_LIBRARY ) # 如果找到,创建导入目标 if(MYLIB_FOUND) set(MYLIB_INCLUDE_DIRS ${MYLIB_INCLUDE_DIR}) set(MYLIB_LIBRARIES ${MYLIB_LIBRARY}) # 创建导入目标 if(NOT TARGET MyLib::MyLib) add_library(MyLib::MyLib UNKNOWN IMPORTED) set_target_properties(MyLib::MyLib PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${MYLIB_INCLUDE_DIRS}" IMPORTED_LOCATION "${MYLIB_LIBRARY}" ) endif() endif() 

使用自定义Find模块:

# 将Find模块路径添加到CMake模块路径 list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") # 查找包 find_package(MyLib REQUIRED) # 使用导入目标 target_link_libraries(my_app PRIVATE MyLib::MyLib) 

导出目标和配置

CMake允许导出目标,以便其他项目可以使用这些目标,而无需知道它们的实现细节。

导出目标:

# 创建库 add_library(mylib mylib.c) target_include_directories(mylib PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> $<INSTALL_INTERFACE:include> ) # 安装目标并导出 install(TARGETS mylib EXPORT mylibTargets LIBRARY DESTINATION lib ARCHIVE DESTINATION lib PUBLIC_HEADER DESTINATION include ) # 安装导出文件 install(EXPORT mylibTargets FILE mylibTargets.cmake NAMESPACE MyLib:: DESTINATION lib/cmake/mylib ) # 创建配置文件 include(CMakePackageConfigHelpers) write_basic_package_version_file( "${CMAKE_CURRENT_BINARY_DIR}/mylibConfigVersion.cmake" VERSION ${PACKAGE_VERSION} COMPATIBILITY AnyNewerVersion ) # 安装配置文件 install(FILES "${CMAKE_CURRENT_BINARY_DIR}/mylibConfigVersion.cmake" DESTINATION lib/cmake/mylib ) 

在其他项目中使用导出的目标:

# 查找已安装的包 find_package(mylib REQUIRED) # 使用导入的目标 target_link_libraries(my_app PRIVATE MyLib::mylib) 

跨平台高级技巧

平台特定代码处理

在跨平台开发中,经常需要处理平台特定的代码。CMake提供了多种方式来处理这种情况。

使用预处理器指令:

#ifdef _WIN32 // Windows特定代码 #elif defined(__linux__) // Linux特定代码 #elif defined(__APPLE__) // macOS特定代码 #endif 

在CMake中定义平台特定的宏:

if(WIN32) add_definitions(-DWINDOWS_PLATFORM) elseif(UNIX AND NOT APPLE) add_definitions(-DLINUX_PLATFORM) elseif(APPLE) add_definitions(-DMACOS_PLATFORM) endif() 

条件编译源文件:

# 平台特定源文件 if(WIN32) list(APPEND SOURCES src/windows_utils.c) elseif(UNIX AND NOT APPLE) list(APPEND SOURCES src/linux_utils.c) elseif(APPLE) list(APPEND SOURCES src/macos_utils.c) endif() add_executable(my_app ${SOURCES}) 

交叉编译

CMake支持交叉编译,即在一个平台上为另一个平台编译代码。

使用工具链文件进行交叉编译:

# 设置目标系统 set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) # 指定编译器 set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) # 指定根目录 set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf) # 查找程序时只在主机上查找 set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) # 查找头文件和库时只在目标系统上查找 set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 

使用工具链文件:

cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain-arm.cmake .. 

Android交叉编译示例:

# 设置Android NDK路径 set(ANDROID_NDK /path/to/android/ndk) # 设置Android平台和ABI set(ANDROID_PLATFORM android-21) set(ANDROID_ABI armeabi-v7a) # 包含Android工具链文件 include(${ANDROID_NDK}/build/cmake/android.toolchain.cmake) 

工具链文件

工具链文件是CMake中用于指定编译器、工具和目标系统配置的文件,特别适用于交叉编译。

基本工具链文件结构:

# 目标系统 set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR arm) # 编译器 set(CMAKE_C_COMPILER arm-none-eabi-gcc) set(CMAKE_CXX_COMPILER arm-none-eabi-g++) # 查找根目录 set(CMAKE_FIND_ROOT_PATH /usr/arm-none-eabi) # 查找模式 set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) # 编译选项 set(CMAKE_C_FLAGS "-mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16" CACHE STRING "C compiler flags") set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}" CACHE STRING "C++ compiler flags") # 链接选项 set(CMAKE_EXE_LINKER_FLAGS "-specs=nosys.specs" CACHE STRING "Linker flags") 

使用工具链文件:

cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain-arm.cmake -DCMAKE_BUILD_TYPE=Release .. 

实际案例分析

简单项目示例

让我们从一个简单的C语言项目开始,该项目包含一个主程序和一个工具库。

项目结构:

simple_project/ ├── CMakeLists.txt ├── src/ │ ├── main.c │ └── utils.c └── include/ └── utils.h 

CMakeLists.txt:

# 设置最低CMake版本要求 cmake_minimum_required(VERSION 3.10) # 设置项目名称和版本 project(SimpleProject VERSION 1.0 LANGUAGES C) # 设置C标准 set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) # 添加可执行文件 add_executable(simple_app src/main.c src/utils.c) # 包含头文件目录 target_include_directories(simple_app PRIVATE include) # 设置编译选项 if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_definitions(simple_app PRIVATE DEBUG) endif() # 安装规则 install(TARGETS simple_app RUNTIME DESTINATION bin ) 

main.c:

#include <stdio.h> #include "utils.h" int main() { printf("Hello from SimpleProject!n"); int result = add_numbers(5, 3); printf("5 + 3 = %dn", result); return 0; } 

utils.h:

#ifndef UTILS_H #define UTILS_H int add_numbers(int a, int b); #endif // UTILS_H 

utils.c:

#include "utils.h" int add_numbers(int a, int b) { return a + b; } 

构建和运行:

mkdir build cd build cmake .. make ./simple_app 

中等复杂度项目

现在让我们看一个更复杂的项目,包含多个库和测试。

项目结构:

medium_project/ ├── CMakeLists.txt ├── src/ │ ├── CMakeLists.txt │ ├── main.c │ ├── math/ │ │ ├── CMakeLists.txt │ │ ├── math.c │ │ └── math.h │ └── string/ │ ├── CMakeLists.txt │ ├── string.c │ └── string.h ├── tests/ │ ├── CMakeLists.txt │ ├── test_math.c │ └── test_string.c └── external/ └── CMakeLists.txt 

根CMakeLists.txt:

# 设置最低CMake版本要求 cmake_minimum_required(VERSION 3.10) # 设置项目名称和版本 project(MediumProject VERSION 1.0 LANGUAGES C) # 设置C标准 set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) # 添加子目录 add_subdirectory(src) add_subdirectory(external) add_subdirectory(tests) # 启用测试 enable_testing() 

src/CMakeLists.txt:

# 添加子目录 add_subdirectory(math) add_subdirectory(string) # 创建可执行文件 add_executable(medium_app main.c) # 链接库 target_link_libraries(medium_app PRIVATE math string) # 包含头文件目录 target_include_directories(medium_app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/math ${CMAKE_CURRENT_SOURCE_DIR}/string ) # 安装规则 install(TARGETS medium_app RUNTIME DESTINATION bin ) 

src/math/CMakeLists.txt:

# 创建库 add_library(math math.c) # 设置包含目录 target_include_directories(math PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> $<INSTALL_INTERFACE:include> ) # 安装规则 install(TARGETS math LIBRARY DESTINATION lib ARCHIVE DESTINATION lib PUBLIC_HEADER DESTINATION include ) 

src/string/CMakeLists.txt:

# 创建库 add_library(string string.c) # 设置包含目录 target_include_directories(string PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> $<INSTALL_INTERFACE:include> ) # 安装规则 install(TARGETS string LIBRARY DESTINATION lib ARCHIVE DESTINATION lib PUBLIC_HEADER DESTINATION include ) 

tests/CMakeLists.txt:

# 创建测试可执行文件 add_executable(test_math test_math.c) add_executable(test_string test_string.c) # 链接库 target_link_libraries(test_math PRIVATE math) target_link_libraries(test_string PRIVATE string) # 包含头文件目录 target_include_directories(test_math PRIVATE ${CMAKE_SOURCE_DIR}/src/math) target_include_directories(test_string PRIVATE ${CMAKE_SOURCE_DIR}/src/string) # 添加测试 add_test(NAME MathTest COMMAND test_math) add_test(NAME StringTest COMMAND test_string) 

external/CMakeLists.txt:

# 使用FetchContent下载第三方库 include(FetchContent) FetchContent_Declare( unity GIT_REPOSITORY https://github.com/ThrowTheSwitch/Unity.git GIT_TAG master ) FetchContent_MakeAvailable(unity) 

main.c:

#include <stdio.h> #include "math/math.h" #include "string/string.h" int main() { printf("Hello from MediumProject!n"); int sum = add(5, 3); printf("5 + 3 = %dn", sum); char greeting[50]; create_greeting("World", greeting, sizeof(greeting)); printf("%sn", greeting); return 0; } 

构建和运行:

mkdir build cd build cmake .. make ./medium_app ctest 

大型项目结构

最后,让我们看一个大型项目的结构,它包含多个模块、插件和外部依赖。

项目结构:

large_project/ ├── CMakeLists.txt ├── cmake/ │ ├── FindSomeLib.cmake │ └── utils.cmake ├── src/ │ ├── CMakeLists.txt │ ├── core/ │ │ ├── CMakeLists.txt │ │ ├── core.c │ │ └── core.h │ ├── modules/ │ │ ├── CMakeLists.txt │ │ ├── module1/ │ │ │ ├── CMakeLists.txt │ │ │ ├── module1.c │ │ │ └── module1.h │ │ └── module2/ │ │ ├── CMakeLists.txt │ │ ├── module2.c │ │ └── module2.h │ └── app/ │ ├── CMakeLists.txt │ ├── main.c │ └── resources/ │ └── app.conf ├── plugins/ │ ├── CMakeLists.txt │ ├── plugin1/ │ │ ├── CMakeLists.txt │ │ ├── plugin1.c │ │ └── plugin1.h │ └── plugin2/ │ ├── CMakeLists.txt │ ├── plugin2.c │ └── plugin2.h ├── tests/ │ ├── CMakeLists.txt │ ├── unit/ │ │ ├── CMakeLists.txt │ │ ├── test_core.c │ │ ├── test_module1.c │ │ └── test_module2.c │ └── integration/ │ ├── CMakeLists.txt │ └── test_integration.c ├── docs/ │ └── CMakeLists.txt ├── packaging/ │ └── CMakeLists.txt └── external/ └── CMakeLists.txt 

根CMakeLists.txt:

# 设置最低CMake版本要求 cmake_minimum_required(VERSION 3.15) # 设置项目名称和版本 project(LargeProject VERSION 1.0.0 LANGUAGES C) # 设置C标准 set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) # 添加自定义模块路径 list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") # 包含实用函数 include(utils) # 设置构建类型 if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) endif() # 添加子目录 add_subdirectory(external) add_subdirectory(src) add_subdirectory(plugins) add_subdirectory(tests) add_subdirectory(docs) add_subdirectory(packaging) # 启用测试 enable_testing() # 配置头文件 configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/src/config.h.in" "${CMAKE_BINARY_DIR}/include/config.h" ) # 添加包信息 set(CPACK_PACKAGE_NAME "LargeProject") set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A large C project with multiple modules") set(CPACK_PACKAGE_CONTACT "developer@example.com") # 包含CPack include(CPack) 

src/CMakeLists.txt:

# 添加子目录 add_subdirectory(core) add_subdirectory(modules) add_subdirectory(app) # 导出目标 export(TARGETS core module1 module2 FILE "${CMAKE_BINARY_DIR}/LargeProjectTargets.cmake" ) 

src/core/CMakeLists.txt:

# 创建库 add_library(core core.c) # 设置包含目录 target_include_directories(core PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> $<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/include> $<INSTALL_INTERFACE:include> ) # 设置编译选项 target_compile_options(core PRIVATE -Wall -Wextra) # 设置属性 set_target_properties(core PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION 1 ) # 安装规则 install(TARGETS core EXPORT LargeProjectTargets LIBRARY DESTINATION lib ARCHIVE DESTINATION lib PUBLIC_HEADER DESTINATION include ) 

src/modules/CMakeLists.txt:

# 添加子目录 add_subdirectory(module1) add_subdirectory(module2) 

src/modules/module1/CMakeLists.txt:

# 创建库 add_library(module1 module1.c) # 设置包含目录 target_include_directories(module1 PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> $<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/include> $<INSTALL_INTERFACE:include> ) # 链接依赖 target_link_libraries(module1 PUBLIC core) # 设置属性 set_target_properties(module1 PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION 1 ) # 安装规则 install(TARGETS module1 EXPORT LargeProjectTargets LIBRARY DESTINATION lib ARCHIVE DESTINATION lib PUBLIC_HEADER DESTINATION include ) 

src/app/CMakeLists.txt:

# 查找外部依赖 find_package(SomeLib REQUIRED) # 创建可执行文件 add_executable(large_app main.c) # 链接库 target_link_libraries(large_app PRIVATE core module1 module2 SomeLib::SomeLib) # 包含头文件目录 target_include_directories(large_app PRIVATE ${CMAKE_BINARY_DIR}/include ) # 添加资源 target_sources(large_app PRIVATE resources/app.conf) # 设置属性 set_target_properties(large_app PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) # 安装规则 install(TARGETS large_app RUNTIME DESTINATION bin ) install(FILES resources/app.conf DESTINATION etc/large_project ) 

plugins/CMakeLists.txt:

# 添加子目录 add_subdirectory(plugin1) add_subdirectory(plugin2) 

plugins/plugin1/CMakeLists.txt:

# 创建共享库 add_library(plugin1 SHARED plugin1.c) # 设置包含目录 target_include_directories(plugin1 PRIVATE ${CMAKE_SOURCE_DIR}/src/core ) # 链接依赖 target_link_libraries(plugin1 PRIVATE core) # 设置属性 set_target_properties(plugin1 PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins" ) # 安装规则 install(TARGETS plugin1 LIBRARY DESTINATION lib/large_project/plugins ) 

tests/CMakeLists.txt:

# 添加子目录 add_subdirectory(unit) add_subdirectory(integration) 

tests/unit/CMakeLists.txt:

# 查找测试框架 find_package(Unity REQUIRED) # 创建测试可执行文件 add_executable(test_core test_core.c) add_executable(test_module1 test_module1.c) add_executable(test_module2 test_module2.c) # 链接库 target_link_libraries(test_core PRIVATE core Unity::Unity) target_link_libraries(test_module1 PRIVATE module1 Unity::Unity) target_link_libraries(test_module2 PRIVATE module2 Unity::Unity) # 包含头文件目录 target_include_directories(test_core PRIVATE ${CMAKE_SOURCE_DIR}/src/core ${CMAKE_BINARY_DIR}/include ) target_include_directories(test_module1 PRIVATE ${CMAKE_SOURCE_DIR}/src/modules/module1 ${CMAKE_BINARY_DIR}/include ) target_include_directories(test_module2 PRIVATE ${CMAKE_SOURCE_DIR}/src/modules/module2 ${CMAKE_BINARY_DIR}/include ) # 添加测试 add_test(NAME CoreTest COMMAND test_core) add_test(NAME Module1Test COMMAND test_module1) add_test(NAME Module2Test COMMAND test_module2) 

external/CMakeLists.txt:

# 使用FetchContent下载第三方库 include(FetchContent) # 下载Unity测试框架 FetchContent_Declare( unity GIT_REPOSITORY https://github.com/ThrowTheSwitch/Unity.git GIT_TAG master ) FetchContent_MakeAvailable(unity) # 下载SomeLib FetchContent_Declare( somelib GIT_REPOSITORY https://github.com/example/somelib.git GIT_TAG v1.0.0 ) FetchContent_MakeAvailable(somelib) 

构建和运行:

mkdir build cd build cmake -DCMAKE_BUILD_TYPE=Release .. make ctest --output-on-failure cpack -G TGZ 

最佳实践和常见问题

CMake最佳实践

  1. 使用现代CMake语法

    • 优先使用target_*命令而不是全局变量。
    • 使用target_include_directories()而不是include_directories()
    • 使用target_link_libraries()而不是link_libraries()
  2. 明确指定构建类型

    if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) endif() 
  3. 使用生成器表达式

    target_include_directories(mylib PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> $<INSTALL_INTERFACE:include> ) 
  4. 设置适当的C标准

    set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) 
  5. 使用PRIVATE/PUBLIC/INTERFACE限定符

    target_link_libraries(mylib PUBLIC dependency) target_link_libraries(myapp PRIVATE mylib) 
  6. 导出目标以便其他项目使用

    install(TARGETS mylib EXPORT mylibTargets LIBRARY DESTINATION lib ARCHIVE DESTINATION lib PUBLIC_HEADER DESTINATION include ) 
  7. 使用find_package查找依赖

    find_package(SomeLib REQUIRED) target_link_libraries(myapp PRIVATE SomeLib::SomeLib) 
  8. 组织项目结构

    • 将源代码、头文件、测试和文档分开。
    • 使用子目录组织模块和组件。
  9. 使用版本控制

    project(MyProject VERSION 1.2.3 LANGUAGES C) 
  10. 添加测试支持

    enable_testing() add_test(NAME MyTest COMMAND my_test_app) 

常见问题和解决方案

  1. 找不到头文件

    • 问题:编译器报告找不到头文件。
    • 解决方案:使用target_include_directories()添加包含目录。
    target_include_directories(myapp PRIVATE include) 
  2. 链接错误

    • 问题:链接器报告找不到符号。
    • 解决方案:确保所有必要的库都已链接,并且链接顺序正确。
    target_link_libraries(myapp PRIVATE lib1 lib2) 
  3. 跨平台问题

    • 问题:代码在一个平台上工作,但在另一个平台上失败。
    • 解决方案:使用CMake的平台检测功能,并添加适当的条件逻辑。
    if(WIN32) add_definitions(-DWINDOWS_PLATFORM) elseif(UNIX) add_definitions(-DUNIX_PLATFORM) endif() 
  4. 依赖管理问题

    • 问题:项目依赖的外部库找不到或版本不匹配。
    • 解决方案:使用find_package()并指定最低版本要求。
    find_package(SomeLib 1.2 REQUIRED) 
  5. 构建类型问题

    • 问题:Debug和Release构建行为不一致。
    • 解决方案:明确设置不同构建类型的编译选项。
    set(CMAKE_C_FLAGS_DEBUG "-g -O0") set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG") 
  6. 安装问题

    • 问题:安装后找不到文件或库。
    • 解决方案:确保所有必要的文件和目标都有适当的安装规则。
    install(TARGETS mylib LIBRARY DESTINATION lib PUBLIC_HEADER DESTINATION include ) 
  7. 子项目问题

    • 问题:子项目或外部项目无法正确集成。
    • 解决方案:使用add_subdirectory()FetchContent添加子项目。
    add_subdirectory(third_party/mylib) 
  8. 生成器问题

    • 问题:使用特定的生成器(如Visual Studio)时出现问题。
    • 解决方案:指定生成器并设置适当的选项。
    cmake -G "Visual Studio 16 2019" -A x64 .. 

性能优化

  1. 使用Unity构建(也称为FatLTO或Jumbo构建)

    set_target_properties(myapp PROPERTIES UNITY_BUILD ON UNITY_BUILD_BATCH_SIZE 8 ) 
  2. 启用链接时优化(LTO)

    include(CheckIPOSupported) check_ipo_supported(RESULT result) if(result) set_target_properties(myapp PROPERTIES INTERPROCEDURAL_OPTIMIZATION ON) endif() 
  3. 使用预编译头

    target_precompile_headers(myapp PRIVATE <vector> <string> "common.h" ) 
  4. 并行构建

    cmake --build . --parallel 8 
  5. 使用CCache加速编译

    find_program(CCACHE_PROGRAM ccache) if(CCACHE_PROGRAM) set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_PROGRAM}) endif() 
  6. 优化构建图

    set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 
  7. 减少不必要的依赖

    target_link_libraries(myapp PRIVATE lib1) # 而不是 target_link_libraries(myapp PUBLIC lib1) # 如果lib1不需要传递给依赖myapp的目标 
  8. 使用适当的警告级别

    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall") 

结论

CMake是一个强大而灵活的构建系统,特别适合C/C++项目的跨平台开发。通过本文的学习,我们了解了CMake的基础概念、项目配置、构建系统、依赖管理以及高级应用技巧。

从简单的单文件项目到复杂的多模块项目,CMake提供了丰富的功能和工具来管理项目的构建过程。通过合理使用CMake的特性,如目标属性、生成器表达式、导出和导入目标等,可以创建可维护、可扩展且跨平台的构建系统。

在实际开发中,遵循CMake的最佳实践,如使用现代CMake语法、明确指定构建类型、使用适当的限定符等,可以大大提高构建系统的质量和可维护性。同时,了解常见问题及其解决方案,可以帮助开发者快速解决构建过程中遇到的问题。

随着CMake的不断发展,新版本中引入了更多功能,如FetchContent、Unity构建等,这些功能进一步简化了项目构建和依赖管理。因此,保持对CMake新特性的关注,并在项目中适当使用这些新特性,可以提高开发效率和项目质量。

总之,掌握CMake是现代C/C++开发者的必备技能。通过深入学习和实践,开发者可以充分利用CMake的强大功能,构建高效、可靠且跨平台的软件项目。