强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

C/C++ Linux 开发教程(GCC + CMake) / CMake 进阶(函数/宏/Generator Expression)

CMake 进阶(函数/宏/Generator Expression)

掌握 CMake 基础之后,深入了解函数、宏、Generator Expression 和高级特性,能够编写更模块化、可复用的 CMake 代码。

function 与 macro

function 定义与调用

# 定义一个函数
function(print_target_info target)
    message(STATUS "Target: ${target}")
    message(STATUS "  Source dir: ${CMAKE_CURRENT_SOURCE_DIR}")
    message(STATUS "  Binary dir: ${CMAKE_CURRENT_BINARY_DIR}")
    
    get_target_property(type ${target} TYPE)
    message(STATUS "  Type: ${type}")
    
    get_target_property(sources ${target} SOURCES)
    message(STATUS "  Sources: ${sources}")
endfunction()

# 调用函数
add_executable(app main.c)
print_target_info(app)

函数参数

function(my_function arg1 arg2 arg3)
    message(STATUS "arg1 = ${arg1}")
    message(STATUS "arg2 = ${arg2}")
    message(STATUS "arg3 = ${arg3}")
    message(STATUS "ARGC = ${ARGC}")           # 参数个数
    message(STATUS "ARGV = ${ARGV}")           # 所有参数列表
    message(STATUS "ARGN = ${ARGN}")           # 多余的参数
endfunction()

my_function(a b c d e)
# arg1 = a, arg2 = b, arg3 = c
# ARGN = d;e (多余参数)

关键字参数(推荐方式)

function(add_my_library)
    set(options STATIC SHARED)                     # 布尔选项
    set(oneValueArgs NAME OUTPUT_DIR)              # 单值参数
    set(multiValueArgs SOURCES HEADERS DEPS)       # 多值参数
    
    cmake_parse_arguments(
        MYLIB                       # 前缀
        "${options}"                # 布尔选项
        "${oneValueArgs}"           # 单值参数
        "${multiValueArgs}"         # 多值参数
        ${ARGN}                     # 传入的参数
    )
    
    # 使用解析后的变量: MYLIB_STATIC, MYLIB_SHARED,
    # MYLIB_NAME, MYLIB_SOURCES, MYLIB_HEADERS, MYLIB_DEPS
    
    if(MYLIB_SHARED)
        add_library(${MYLIB_NAME} SHARED ${MYLIB_SOURCES})
    else()
        add_library(${MYLIB_NAME} STATIC ${MYLIB_SOURCES})
    endif()
    
    target_link_libraries(${MYLIB_NAME} PRIVATE ${MYLIB_DEPS})
endfunction()

# 调用
add_my_library(
    NAME mylib
    SHARED
    SOURCES src/a.c src/b.c
    HEADERS include/a.h include/b.h
    DEPS pthread m
)

完整实用函数示例

# cmake/Utils.cmake

# 添加带默认配置的可执行目标
function(add_app_target)
    set(options INSTALL)
    set(oneValueArgs NAME OUTPUT_DIR)
    set(multiValueArgs SOURCES LIBS DEFINES)
    
    cmake_parse_arguments(APP "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
    
    if(NOT APP_NAME)
        message(FATAL_ERROR "add_app_target: NAME is required")
    endif()
    
    if(NOT APP_SOURCES)
        message(FATAL_ERROR "add_app_target: SOURCES is required")
    endif()
    
    add_executable(${APP_NAME} ${APP_SOURCES})
    
    # 默认编译选项
    target_compile_options(${APP_NAME} PRIVATE
        -Wall -Wextra -Wpedantic
        $<$<CXX_COMPILER_ID:GNU>:-Wshadow -Wconversion>
    )
    
    # 链接库
    if(APP_LIBS)
        target_link_libraries(${APP_NAME} PRIVATE ${APP_LIBS})
    endif()
    
    # 预处理宏
    if(APP_DEFINES)
        target_compile_definitions(${APP_NAME} PRIVATE ${APP_DEFINES})
    endif()
    
    # 输出目录
    if(APP_OUTPUT_DIR)
        set_target_properties(${APP_NAME} PROPERTIES
            RUNTIME_OUTPUT_DIRECTORY ${APP_OUTPUT_DIR}
        )
    endif()
    
    # 安装规则
    if(APP_INSTALL)
        install(TARGETS ${APP_NAME} RUNTIME DESTINATION bin)
    endif()
endfunction()

# 使用
add_app_target(
    NAME server
    INSTALL
    SOURCES src/main.c src/server.c
    LIBS pthread ssl crypto
    DEFINES SERVER_VERSION="1.0"
)

macro 与 function 的区别

# macro: 在调用处展开(类似 C 的 #define)
macro(my_macro arg)
    message(STATUS "In macro: arg = ${arg}")
    set(${arg} "modified")      # 直接修改调用者的变量
endmacro()

# function: 创建新的作用域(类似 C 的函数)
function(my_function arg)
    message(STATUS "In function: arg = ${arg}")
    set(${arg} "modified" PARENT_SCOPE)  # 需要 PARENT_SCOPE 传回
endfunction()

set(my_var "original")
my_macro(my_var)
message(STATUS "After macro: ${my_var}")       # "modified"

set(my_var "original")
my_function(my_var)
message(STATUS "After function: ${my_var}")    # "original"

set(my_var "original")
my_function(my_var)
message(STATUS "After function with PARENT_SCOPE: ${my_var}")  # "modified"
特性functionmacro
作用域独立作用域在调用处展开
变量修改需要 PARENT_SCOPE直接修改
return()退出函数退出调用者
推荐使用✅ 默认选择谨慎使用

CMake 作用域

作用域规则

# 顶层作用域
set(TOP_VAR "top level")

function(outer_function)
    # 函数作用域(独立于顶层)
    message(STATUS "In outer: TOP_VAR = ${TOP_VAR}")  # 可以读取
    
    set(OUTER_VAR "outer level")
    message(STATUS "In outer: OUTER_VAR = ${OUTER_VAR}")
    
    function(inner_function)
        # 嵌套函数作用域
        message(STATUS "In inner: TOP_VAR = ${TOP_VAR}")    # 可以读取
        message(STATUS "In inner: OUTER_VAR = ${OUTER_VAR}") # 可以读取
        
        set(INNER_VAR "inner level" PARENT_SCOPE)  # 传回 outer_function
    endfunction()
    
    inner_function()
    message(STATUS "After inner: INNER_VAR = ${INNER_VAR}")  # 可以访问
endfunction()

outer_function()
# message(STATUS "${OUTER_VAR}")  # ❌ 错误:outer_function 作用域外不可见

PARENT_SCOPE 使用

function(set_version)
    set(PROJECT_VERSION_MAJOR 2 PARENT_SCOPE)
    set(PROJECT_VERSION_MINOR 1 PARENT_SCOPE)
    set(PROJECT_VERSION_PATCH 0 PARENT_SCOPE)
endfunction()

set_version()
message(STATUS "Version: ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}")
# 输出: Version: 2.1.0

CACHE 变量跨作用域

function(init_cache_var)
    # CACHE 变量全局可见(跨越作用域)
    if(NOT DEFINED MY_CACHE_VAR)
        set(MY_CACHE_VAR "default_value" CACHE STRING "Description")
    endif()
endfunction()

init_cache_var()
message(STATUS "MY_CACHE_VAR = ${MY_CACHE_VAR}")  # 全局可见

Generator Expression

什么是 Generator Expression

Generator Expression(生成器表达式)是 CMake 在生成构建系统时才求值的表达式。与普通变量不同,它可以感知构建配置、编译器、平台等上下文。

# 语法: $<OPERATOR:VALUE>
# 常用: $<CONFIG:Debug>, $<BOOL:var>, $<IF:cond,true_val,false_val>

条件表达式

# 根据构建类型选择
target_compile_definitions(app PRIVATE
    $<$<CONFIG:Debug>:DEBUG_MODE>
    $<$<CONFIG:Release>:NDEBUG>
)

# 等价于:
# if (Debug) → -DDEBUG_MODE
# if (Release) → -DNDEBUG

# 多条件
target_compile_options(app PRIVATE
    $<$<AND:$<CXX_COMPILER_ID:GNU>,$<CONFIG:Debug>>:-O0 -g3>
    $<$<AND:$<CXX_COMPILER_ID:GNU>,$<CONFIG:Release>>:-O3>
)

# IF 表达式(CMake 3.8+)
target_compile_definitions(app PRIVATE
    $<$<BOOL:${USE_OPENSSL}>:USE_SSL>
)

# 三元表达式
set_target_properties(app PROPERTIES
    POSITION_INDEPENDENT_CODE $<IF:$<BOOL:${BUILD_SHARED_LIBS}>,ON,OFF>
)

配置相关表达式

# 配置名称
message(STATUS "Config: $<CONFIG>")

# 配置比较
$<$<CONFIG:Debug>:...>           # Debug 配置
$<$<CONFIG:Release>:...>         # Release 配置
$<$<NOT:$<CONFIG:Debug>>:...>    # 非 Debug 配置

# 多配置比较
$<$<CONFIG:Debug,RelWithDebInfo>:...>  # Debug 或 RelWithDebInfo

目标属性表达式

# 获取目标属性
$<TARGET_PROPERTY:tgt,PROPERTY>

# 获取目标文件路径
$<TARGET_FILE:app>              # 可执行文件路径
$<TARGET_FILE_NAME:app>         # 文件名
$<TARGET_FILE_DIR:app>          # 文件所在目录

# 获取目标 SONAME
$<TARGET_SONAME_FILE:mylib>     # libmylib.so.1
$<TARGET_SONAME_FILE_NAME:mylib> # libmylib.so.1

# 获取目标 PDB 文件(Windows)
$<TARGET_PDB_FILE:app>

字符串操作表达式

# 转换
$<LOWER_CASE:string>            # 小写
$<UPPER_CASE:string>            # 大写
$<MAKE_C_IDENTIFIER:string>     # 转为 C 标识符

# 生成器表达式中的字符串
$<GENEX_EVAL:expr>              # 递归求值
$<TARGET_GENEX_EVAL:tgt,expr>   # 在目标上下文中求值

# 条件替换
$<IF:condition,true_string,false_string>

路径和文件表达式

# 当前源码/构建目录
$<TARGET_PROPERTY:SOURCE_DIR>
$<TARGET_PROPERTY:BINARY_DIR>

# 安装前缀
$<INSTALL_PREFIX>

# 连接多个路径
$<PATH:CMAKE_CURRENT_SOURCE_DIR,include>

实用组合示例

# 为不同配置设置不同的输出目录
set_target_properties(app PROPERTIES
    RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/debug/bin
    RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/release/bin
)

# 使用 Generator Expression 实现相同效果
set_target_properties(app PROPERTIES
    RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/$<CONFIG>/bin
)

# 条件链接库
target_link_libraries(app PRIVATE
    $<$<BOOL:${USE_OPENSSL}>:OpenSSL::SSL>
    $<$<BOOL:${USE_OPENSSL}>:OpenSSL::Crypto>
    $<$<PLATFORM_ID:Linux>:rt>
    $<$<PLATFORM_ID:Linux>:dl>
)

# 仅对 C++ 文件添加选项
target_compile_options(app PRIVATE
    $<$<COMPILE_LANGUAGE:CXX>:-std=c++17>
    $<$<COMPILE_LANGUAGE:C>:-std=c11>
)

自定义命令与目标

add_custom_command

# 生成文件的自定义命令(在构建时执行)
add_custom_command(
    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated.h
    COMMAND python3 ${CMAKE_SOURCE_DIR}/scripts/gen_header.py
            --input ${CMAKE_CURRENT_SOURCE_DIR}/config.json
            --output ${CMAKE_CURRENT_BINARY_DIR}/generated.h
    DEPENDS ${CMAKE_SOURCE_DIR}/config.json
            ${CMAKE_SOURCE_DIR}/scripts/gen_header.py
    COMMENT "Generating generated.h from config.json"
    VERBATIM
)

# 构建后执行的自定义命令(附加到目标)
add_custom_command(TARGET app POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E echo "Build completed!"
    COMMAND ${CMAKE_COMMAND} -E copy
        $<TARGET_FILE:app>
        ${CMAKE_BINARY_DIR}/dist/$<TARGET_FILE_NAME:app>
    COMMENT "Copying app to dist directory"
)

add_custom_target

# 自定义目标(不产生输出文件)
add_custom_target(docs
    COMMAND doxygen ${CMAKE_SOURCE_DIR}/Doxyfile
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    COMMENT "Generating documentation with Doxygen"
)

# 自定义目标依赖其他目标
add_custom_target(coverage
    COMMAND lcov --capture --directory . --output-file coverage.info
    COMMAND genhtml coverage.info --output-directory coverage_report
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
    COMMENT "Generating code coverage report"
)
DEPENDS app test_app  # 确保先构建这些目标

自定义命令 + 自定义目标组合

# 生成 protobuf 代码
find_program(PROTOC protoc)

add_custom_command(
    OUTPUT
        ${CMAKE_CURRENT_BINARY_DIR}/message.pb.cc
        ${CMAKE_CURRENT_BINARY_DIR}/message.pb.h
    COMMAND ${PROTOC}
        --cpp_out=${CMAKE_CURRENT_BINARY_DIR}
        --proto_path=${CMAKE_CURRENT_SOURCE_DIR}
        ${CMAKE_CURRENT_SOURCE_DIR}/message.proto
    DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/message.proto
    COMMENT "Compiling message.proto"
    VERBATIM
)

# 将生成的文件加入目标
add_library(proto_gen OBJECT
    ${CMAKE_CURRENT_BINARY_DIR}/message.pb.cc
)
target_include_directories(proto_gen PUBLIC ${CMAKE_CURRENT_BINARY_DIR})

属性系统

目标属性

# 设置属性
set_target_properties(app PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    POSITION_INDEPENDENT_CODE ON
    INTERPROCEDURAL_OPTIMIZATION ON
)

# 获取属性
get_target_property(app_type app TYPE)
message(STATUS "Type: ${app_type}")

# 自定义属性
set_property(TARGET app PROPERTY MY_CUSTOM_PROP "value")
get_property(result TARGET app PROPERTY MY_CUSTOM_PROP)

源文件属性

# 设置源文件特定的编译选项
set_source_files_properties(
    src/generated.c
    PROPERTIES COMPILE_FLAGS "-w"  # 禁用警告
)

# 标记为生成的文件(跳过依赖扫描)
set_source_files_properties(
    ${CMAKE_CURRENT_BINARY_DIR}/generated.c
    PROPERTIES GENERATED TRUE
)

# 设置源文件的编译定义
set_source_files_properties(
    src/feature_x.c
    PROPERTIES COMPILE_DEFINITIONS "FEATURE_X_ENABLED"
)

目录属性

# 获取目录属性
get_property(inc_dirs DIRECTORY PROPERTY INCLUDE_DIRECTORIES)
message(STATUS "Include dirs: ${inc_dirs}")

# 获取所有定义
get_property(defs DIRECTORY PROPERTY COMPILE_DEFINITIONS)
message(STATUS "Definitions: ${defs}")

全局属性

# 获取所有目标
get_property(all_targets GLOBAL PROPERTY TARGETS)
message(STATUS "All targets: ${all_targets}")

# 获取所有源文件
get_property(all_sources GLOBAL PROPERTY SOURCES)

# 自定义全局属性
set_property(GLOBAL PROPERTY MY_GLOBAL_LIST "a;b;c")
get_property(result GLOBAL PROPERTY MY_GLOBAL_LIST)

install 规则进阶

基本 install 规则

# 安装目标
install(TARGETS app mylib
    RUNTIME DESTINATION bin           # 可执行文件
    LIBRARY DESTINATION lib           # .so 文件
    ARCHIVE DESTINATION lib           # .a 文件
    INCLUDES DESTINATION include      # 头文件目录
    PUBLIC_HEADER DESTINATION include # 公共头文件
)

# 安装文件
install(FILES
    include/mylib.h
    include/utils.h
    DESTINATION include
)

# 安装目录
install(DIRECTORY config/ DESTINATION etc/myapp
    PATTERN "*.in" EXCLUDE           # 排除 .in 文件
    PATTERN ".git" EXCLUDE           # 排除 .git
)

export 与 config 文件

# 导出目标配置
install(TARGETS mylib
    EXPORT mylibTargets
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    INCLUDES DESTINATION include
)

# 生成并安装 Config 文件
install(EXPORT mylibTargets
    FILE mylibTargets.cmake
    NAMESPACE mylib::
    DESTINATION lib/cmake/mylib
)

# 生成 Config.cmake
include(CMakePackageConfigHelpers)

configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/cmake/mylibConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/mylibConfig.cmake
    INSTALL_DESTINATION lib/cmake/mylib
)

write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/mylibConfigVersion.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY SameMajorVersion
)

install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/mylibConfig.cmake
    ${CMAKE_CURRENT_BINARY_DIR}/mylibConfigVersion.cmake
    DESTINATION lib/cmake/mylib
)

组件安装

install(TARGETS app
    RUNTIME DESTINATION bin COMPONENT runtime
)

install(TARGETS mylib
    LIBRARY DESTINATION lib COMPONENT libraries
    ARCHIVE DESTINATION lib COMPONENT libraries
)

install(FILES include/mylib.h
    DESTINATION include COMPONENT development
)

# 按组件安装
# cmake --install build --component runtime
# cmake --install build --component development

CTest 集成

基本测试配置

enable_testing()

# 简单测试
add_executable(test_basic test/test_basic.c)
target_link_libraries(test_basic PRIVATE mylib)
add_test(NAME test_basic COMMAND test_basic)

# 带参数的测试
add_test(NAME test_with_args
    COMMAND test_basic --verbose --timeout 10
)

# 设置测试属性
set_tests_properties(test_basic PROPERTIES
    TIMEOUT 30                     # 超时秒数
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
    ENVIRONMENT "TEST_DATA_DIR=${CMAKE_SOURCE_DIR}/test/data"
    LABELS "unit;fast"
)

# 测试应该失败(期望失败的测试)
add_test(NAME test_should_fail COMMAND app --invalid-arg)
set_tests_properties(test_should_fail PROPERTIES WILL_FAIL TRUE)

fixture 测试

# 设置测试 fixture(setup/teardown)
add_test(NAME setup_db COMMAND ${CMAKE_COMMAND} -E touch test.db)
set_tests_properties(setup_db PROPERTIES FIXTURES_SETUP db_fixture)

add_test(NAME test_with_db COMMAND test_db_app)
set_tests_properties(test_with_db PROPERTIES FIXTURES_REQUIRED db_fixture)

add_test(NAME cleanup_db COMMAND ${CMAKE_COMMAND} -E remove test.db)
set_tests_properties(cleanup_db PROPERTIES FIXTURES_CLEANUP db_fixture)

跨平台配置

平台检测

# 操作系统检测
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    message(STATUS "Building for Linux")
    target_compile_definitions(app PRIVATE PLATFORM_LINUX)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    message(STATUS "Building for macOS")
    target_compile_definitions(app PRIVATE PLATFORM_MACOS)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    message(STATUS "Building for Windows")
    target_compile_definitions(app PRIVATE PLATFORM_WINDOWS)
endif()

# 更简洁的方式
if(UNIX AND NOT APPLE)
    message(STATUS "Linux/Unix")
endif()

if(WIN32)
    message(STATUS "Windows")
endif()

if(APPLE)
    message(STATUS "Apple (macOS/iOS)")
endif()

编译器检测

# 编译器检测
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
    message(STATUS "GCC compiler")
    target_compile_options(app PRIVATE
        -Wall -Wextra -Wpedantic
    )
elseif(CMAKE_C_COMPILER_ID STREQUAL "Clang")
    message(STATUS "Clang compiler")
    target_compile_options(app PRIVATE
        -Wall -Wextra -Weverything
    )
elseif(CMAKE_C_COMPILER_ID STREQUAL "MSVC")
    message(STATUS "MSVC compiler")
    target_compile_options(app PRIVATE /W4)
endif()

# 使用生成器表达式(更推荐)
target_compile_options(app PRIVATE
    $<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
    $<$<CXX_COMPILER_ID:Clang>:-Wall -Wextra>
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
)

头文件和功能检测

include(CheckIncludeFile)
include(CheckFunctionExists)
include(CheckTypeSize)

# 检测头文件
check_include_file("sys/epoll.h" HAVE_EPOLL)
check_include_file("sys/event.h" HAVE_KQUEUE)
check_include_file("openssl/ssl.h" HAVE_OPENSSL)

if(HAVE_EPOLL)
    target_compile_definitions(app PRIVATE HAVE_EPOLL)
endif()

# 检测函数
check_function_exists(mmap HAVE_MMAP)
check_function_exists(pthread_create HAVE_PTHREAD)

# 检测类型大小
check_type_size("void*" SIZEOF_VOID_P)
message(STATUS "sizeof(void*) = ${SIZEOF_VOID_P}")

生成配置头文件

# config.h.in
#cmakedefine HAVE_EPOLL
#cmakedefine HAVE_KQUEUE
#cmakedefine HAVE_OPENSSL
#cmakedefine HAVE_MMAP
#cmakedefine SIZEOF_VOID_P @SIZEOF_VOID_P@
#cmakedefine PROJECT_VERSION "@PROJECT_VERSION@"

# CMakeLists.txt
configure_file(
    ${CMAKE_SOURCE_DIR}/config.h.in
    ${CMAKE_BINARY_DIR}/config.h
)
target_include_directories(app PRIVATE ${CMAKE_BINARY_DIR})

CMake 模块编写

模块基本结构

# cmake/FindMyLib.cmake — 查找模块
include(FindPackageHandleStandardArgs)

# 查找头文件
find_path(MYLIB_INCLUDE_DIR
    NAMES mylib.h
    PATHS
        /usr/local/include
        /usr/include
        ${MYLIB_ROOT}/include
)

# 查找库文件
find_library(MYLIB_LIBRARY
    NAMES mylib
    PATHS
        /usr/local/lib
        /usr/lib
        ${MYLIB_ROOT}/lib
)

# 处理查找结果
find_package_handle_standard_args(MyLib
    DEFAULT_MSG
    MYLIB_LIBRARY
    MYLIB_INCLUDE_DIR
)

# 创建导入目标
if(MyLib_FOUND AND NOT TARGET MyLib::MyLib)
    add_library(MyLib::MyLib UNKNOWN IMPORTED)
    set_target_properties(MyLib::MyLib PROPERTIES
        IMPORTED_LOCATION "${MYLIB_LIBRARY}"
        INTERFACE_INCLUDE_DIRECTORIES "${MYLIB_INCLUDE_DIR}"
    )
endif()

mark_as_advanced(MYLIB_LIBRARY MYLIB_INCLUDE_DIR)
# 使用自定义模块
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
find_package(MyLib REQUIRED)
target_link_libraries(app PRIVATE MyLib::MyLib)

⚠️ 注意点

  1. 优先使用 function:macro 没有作用域隔离,容易产生副作用
  2. Generator Expression 求值时机:只能用于特定命令参数,不能用于普通 if() 判断
  3. PARENT_SCOPE 不传播到孙作用域:只影响直接父作用域
  4. install export 路径:使用相对路径以便可重定位安装
  5. CMake 版本兼容:使用新特性前检查 cmake_minimum_required

💡 提示

  1. 调试 CMake 脚本:使用 message(STATUS ...)cmake --trace
  2. 函数返回值:通过 set(result "value" PARENT_SCOPE) 实现
  3. 缓存变量默认值:使用 if(NOT DEFINED VAR) 检查
  4. Generator Expression 列表:使用 $<SEMICOLON> 转义分号
  5. 模块化 CMake:将常用的函数/宏放在 cmake/ 目录,用 include() 引入

工程场景

场景 1:创建可复用的库 CMake 模块

# cmake/MyLibHelpers.cmake
function(mylib_add_library NAME)
    set(options HEADER_ONLY)
    set(oneValueArgs NAMESPACE)
    set(multiValueArgs SOURCES PUBLIC_HEADERS PRIVATE_HEADERS DEPS)
    
    cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
    
    if(ARG_HEADER_ONLY)
        add_library(${NAME} INTERFACE)
        target_include_directories(${NAME} INTERFACE
            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
            $<INSTALL_INTERFACE:include>
        )
    else()
        add_library(${NAME} ${ARG_SOURCES} ${ARG_PUBLIC_HEADERS} ${ARG_PRIVATE_HEADERS})
        target_include_directories(${NAME} PUBLIC
            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
            $<INSTALL_INTERFACE:include>
        )
        if(ARG_DEPS)
            target_link_libraries(${NAME} PUBLIC ${ARG_DEPS})
        endif()
    endif()
    
    if(ARG_NAMESPACE)
        add_library(${ARG_NAMESPACE}::${NAME} ALIAS ${NAME})
    endif()
endfunction()

场景 2:条件编译特性模块

# cmake/FeatureDetection.cmake
include(CheckIncludeFile)
include(CMakePushCheckState)

function(detect_features target)
    cmake_push_check_state()
    
    set(CMAKE_REQUIRED_FLAGS "${CMAKE_C_FLAGS}")
    
    check_include_file("sys/epoll.h" HAVE_EPOLL)
    check_include_file("sys/event.h" HAVE_KQUEUE)
    check_include_file("sys/sendfile.h" HAVE_SENDFILE)
    
    if(HAVE_EPOLL)
        target_compile_definitions(${target} PRIVATE HAVE_EPOLL)
        message(STATUS "  ✓ epoll support")
    endif()
    
    if(HAVE_KQUEUE)
        target_compile_definitions(${target} PRIVATE HAVE_KQUEUE)
        message(STATUS "  ✓ kqueue support")
    endif()
    
    cmake_pop_check_state()
endfunction()

扩展阅读