仅仅个人学习记录,非专业人士,并未系统学习CMake,以下均为从实践中总结,错误之处,万望指出。另,3日连续调试,疲惫不堪,AI润色,万望谅解。
来自C++的下马威

请检查这段代码,你发现什么问题了吗?
情况是这样的:当我编译完这个用C++移植的LVGL工程后,出现了一个奇怪的现象。在Debug模式下程序运行正常,但在Release模式下,程序总是会在LVGL初始化前卡住。更诡异的是,这个问题还具有一定的随机性,有时会卡住,有时又不会。
这个奇怪的bug困扰了我好几天。今天终于发现,问题出在我定义的一个全局时钟变量上。这个变量在main函数之前就被初始化了。由于LVGL的定时器必须在LVGL初始化完成后才能正常工作,在初始化前操作这个定时器就会导致未定义行为,从而引发这个bug。
Cmake使用心得
项目架构
管理大型项目时,手动输入编译指令显然不现实,因此我选择使用CMake来优化整个编译流程。首先,合理的项目结构至关重要:
对于工具
- 项目根目录下创建src文件夹用于存放项目源代码
- src目录下按功能模块划分,每个模块单独存放源代码
- 如项目引用第三方库,建议在src下创建third_party文件夹存放第三方代码
在项目根目录的CMakeLists.txt文件中,可以通过add_subdirectory指令将第三方库一并编译,避免手动处理各种依赖关系。
项目根目录还应当包含include文件夹:
- 在include下创建与项目同名的子文件夹
- 在该子文件夹中存放模块头文件(如module.hpp)
- 配置CMake的include_directories路径时,采用这种结构可以防止不同库的头文件引用冲突
此外,建议在项目根目录下创建test文件夹:
- 为每个模块编写对应的单元测试
- 可以使用Google Test框架(gtest/gmock)进行测试开发
这种结构化的项目布局能有效提升代码的可维护性和编译效率。
对于一个工具库,可以是这样的::
├─example
│ ├─algorithm
│ │ ├─a_start_find
│ │ ├─kmp_string_find
│ │ └─simulated_annealing
│ ├─chaotic_swing
│ ├─mesh_pbd
│ └─three_body
├─include
│ └─lv_toolkit
├─src
│ ├─core
│ ├─thirdparty
│ │ ├─box2d
│ │ ├─googletest
│ │ ├─lvgl
└─test
├─math_test
└─wgpu_test
CMakeList.txt(for testing)
CMakeList.txt(main toolkit)
对于main,可以是这样的
项目架构说明如下。首先介绍项目引用的库文件结构:
主文件目录下包含一个Src文件夹用于存放源码。Src目录内包含:
- lib文件夹:
- 存放工具库LV toolkit
- eez studio自动生成的文件(位于UI子文件夹内)
- main文件
- CMakeLists文件
- ui文件夹:
- 存放配置好UI界面的MVC架构相关文
- event directory
- 用于初始化UI系统后设置控件的信号和回调函数
项目根目录除了lib文件夹外,还包含log、document等其他辅助目录,这些可根据实际需求进行添加。
注:考虑到文件组织规范,目前没有将所有main文件放在根目录下,而是选择将其保留在src文件夹内。
eg:
build
log
res
src
lib
event
ui
CMakeLists.txt
Cmake脚本
1.最小可运行
那么说完了这两种架构的思路,现在我们开始详细讲解CMake的编译流程。首先,我们需要用project()命令来声明当前项目的名称和使用的编程语言。例如:
project(MyProject LANGUAGES C CXX)
这个命令不仅指定了项目名称为"MyProject",还明确了该项目会使用C和C++两种编程语言。项目名称在后续的target_link_libraries等命令中会被引用。
接下来,我们可以选择性地声明一些编译兼容性设置。对于C++项目来说,最常见的设置是:
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
这些设置确保了我们的项目需要使用C++17标准,并且如果编译器不支持该标准,CMake会报错而不是降级处理。此外,我们还可以设置编译器警告级别等选项:
if(MSVC)
add_compile_options(/W4 /WX)
else()
add_compile_options(-Wall -Wextra -Werror)
endif()
对于一个最小可运行的CMake项目示例,我们首先需要收集整个项目的源文件。源文件的收集方式有多种:
- 手动列出所有源文件:
set(SRCS
src/main.cpp
src/util.cpp
src/parser.cpp
)
- 使用
file(GLOB...)自动收集:
file(GLOB SRCS "src/*.cpp")
需要注意的是,File指令它会覆盖这个变量里面的东西,如果说你使用多条file指令,里面所有的源文件并不会累积,而是会被覆盖,请在这个file后面把多个这个路径写到一块,或者说使用list指令追加啊
这个错误困扰了我一天,最后使用message调试才发现
收集完源文件后,我们可以使用add_executable()命令来创建可执行文件:
add_executable(myapp ${SRCS})
这个命令会生成一个名为"myapp"的可执行文件。在Linux系统下,这个文件默认会生成在build目录中;在Windows系统下,则会生成带有.exe后缀的可执行文件。
如果我们需要生成静态库而不是可执行文件,可以使用add_library()命令并指定STATIC参数:
add_library(mylib STATIC ${SRCS})
静态库在项目中有多种重要用途,比如:
- 代码复用:将常用功能封装成库供多个项目使用
- 加快编译速度:只需编译一次,后续链接即可
- 保护源代码:可以分发库文件而不暴露源代码
在后续的讨论中,我们会详细展开静态库的使用场景、优缺点,以及如何将其集成到更大的项目结构中。特别是会讲解如何通过target_link_libraries()命令将静态库链接到可执行文件中,以及如何处理静态库与动态库的选择问题。
#CMake file for different scenarios: Native build on Linux or Windows (automatic), and cross-build from Unix to Windows (by defining build-host as Unix and build-target as Windows)
cmake_minimum_required( VERSION 3.10 )
project( phybox C CXX)
# 显式指定 C 和 C++ 编译器
set(CMAKE_C_COMPILER "D:/mingw64/bin/gcc.exe")
set(CMAKE_CXX_COMPILER "D:/mingw64/bin/g++.exe")
# 可选:禁用编译器 ABI 检测(进一步减少检测步骤)
set(CMAKE_C_COMPILER_WORKS TRUE)
set(CMAKE_CXX_COMPILER_WORKS TRUE)
file( GLOB_RECURSE PHYBOX_SRCS
${PROJECT_SOURCE_DIR}/event/*.c
${PROJECT_SOURCE_DIR}/event/*.cpp
${PROJECT_SOURCE_DIR}/ui/**/*.c
${PROJECT_SOURCE_DIR}/ui/**/*.cpp
${PROJECT_SOURCE_DIR}/ui/*.c
${PROJECT_SOURCE_DIR}/ui/*.cpp
)
add_executable( ${PROJECT_NAME} main.cpp ${PHYBOX_SRCS})
target_link_libraries(phybox toolkit )
main项目配置
刚才我们讨论的是构建一个最小可执行项目的基本配置,然而在实际开发环境中,我们还需要处理更复杂的构建需求。首先,我们需要解决头文件包含路径的问题,否则在代码中使用#include指令时可能会找不到对应的头文件。为此,我们需要通过include_directories()命令来添加头文件的搜索路径。
具体来说,我们需要执行以下步骤:
- 使用include_directories()命令添加主项目的头文件目录
- 由于我们的lib文件夹下还包含多个子库,我们需要使用foreach循环遍历这些子库目录
- 将每个子库的头文件目录都添加到include路径中
例如:
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB LIB_DIRS "${PROJECT_SOURCE_DIR}/lib/*")
foreach(LIB_DIR ${LIB_DIRS})
include_directories(${LIB_DIR}/include)
endforeach()
此外,我们还需要特别注意库的链接问题。在我们的项目中,toolkit库为了便于调试被编译为静态库。因此,在构建主可执行文件时,需要正确链接这个静态库。这里需要特别注意CMake命令的执行顺序:
- 必须先使用add_executable()定义可执行文件目标
- 然后才能使用target_link_libraries()进行链接
错误的顺序示例(会导致编译失败):
target_link_libraries(my_app PRIVATE toolkit) # 错误:此时my_app尚未定义
add_executable(my_app main.cpp)
正确的顺序应该如下:
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE toolkit)
这是因为CMake需要先知道你要构建的是可执行文件还是库文件,才能正确处理后续的链接指令。这种顺序要求是CMake工作流程中的重要特性。
以上讨论主要针对主项目的构建配置,在实际项目中,随着项目规模的扩大,构建配置可能会变得更加复杂。例如:
- 需要处理多个子项目的构建
- 需要管理不同构建类型(Debug/Release)的配置
- 需要处理跨平台的构建差异
- 需要集成第三方库的查找和链接
这些复杂情况都需要我们在CMake配置文件中进行更细致的处理,这也是为什么大型项目的CMakeLists.txt文件往往比较长的原因。
lib当中的toolkit库配置
在管理工具项目时,除了要处理好自身代码外,还需要妥善管理第三方库的依赖关系。这是一个相对复杂的问题,需要系统性地解决。以下是具体的处理步骤和注意事项:
- 源码收集与静态库声明
- 使用
file()命令收集子项目源码 - 通过
add_library()命令声明静态库 - 将收集的源码全部纳入静态库编译范围
- 处理第三方依赖
- 以LVGL工具包为例,它包含多个第三方依赖
- 这些依赖关系可能相当复杂(如
lvgl_conf.h中引入SDL依赖) - 需要确保LVGL能正确包含SDL的头文件
- 包含目录设置
- 使用
target_include_directories()命令 - 参数说明:
- 目标名称:当前构建目标
- 作用域:
- PUBLIC:用于共享依赖
- PRIVATE:仅内部使用
- INTERFACE:仅给依赖者使用
- 路径可以重复添加,系统会自动累加处理
- 文件收集注意事项!!!
- 必须一次性收集所有文件,不能分多次添加
- 两种实现方式:
- 将所有文件路径写在单个
file()命令后 - 使用
list(APPEND)进行追加
- 将所有文件路径写在单个
- 依赖关系处理
- 处理好子项依赖后
- 通过
include_directories()暴露全局头文件 - 确保更大的构建目标能够访问
- 链接设置!!!
- 使用
target_link_libraries()确保正确链接 - 处理静态库与第三方库的链接关系
- 保证所有引用都能正确解析
- 可选模块编译
- 通过
option()命令选择编译模块 - 典型可选内容:
- 测试模块(test)
- 示例程序(examples)
- 在这些模块的目录中单独编写CMakeLists.txt
这种分层管理方式具有以下优势:
- 代码简洁高效
- 各层级职责明确
- 依赖关系清晰
- 有效减少潜在bug
- 便于维护和扩展
实际应用示例:
# 收集源码
file(GLOB_RECURSE SOURCES "src/*.c" "src/*.h")
# 声明静态库
add_library(my_tool STATIC ${SOURCES})
# 处理第三方依赖
target_include_directories(my_tool
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/lvgl
${SDL2_INCLUDE_DIRS}
)
# 链接第三方库
target_link_libraries(my_tool
PRIVATE
lvgl
SDL2
)
# 可选模块
option(BUILD_TESTS "Build tests" OFF)
if(BUILD_TESTS)
add_subdirectory(tests)
endif()
调试心得
在实际使用CMake进行项目编译时,经常会遇到各种编译错误和配置问题。下面详细介绍几种常见的错误类型及其解决方案:
1. 引用无法找到的错误(cannot find reference)
这是非常常见的编译错误类型,通常表现为:
error: cannot find reference to 'xxx'
解决方案流程:
- 首先检查CMakeLists.txt文件中的
target_link_libraries是否正确设置了依赖关系 - 使用
message()命令打印调试信息,查看源文件路径是否正确:message(STATUS "Source directory: ${CMAKE_CURRENT_SOURCE_DIR}") message(STATUS "Looking for file in: ${PROJECT_SOURCE_DIR}/src")advanced:
message("*********************************************************************")
foreach(src_file ${PHYBOX_SRCS})
get_filename_component(src_bs ${src_file} NAME)
message(${src_bs})
if (${src_bs} STREQUAL "load.cpp")
message("``````````````````````````````````````````got it")
endif()
endforeach()
- 确认被引用的文件是否确实存在于打印的路径中
- 如果引用的是第三方库,检查是否正确设置了
find_package()或find_library()
2. 头文件包含错误(include错误)
这类错误通常表现为:
fatal error: xxx.h: No such file or directory
针对不同情况的解决方案:
对于子项目间的依赖:
当项目结构复杂,有多个子项目相互依赖时,推荐使用target_include_directories:
这边的跟那个用法差不多,只不过需要注意的是需要把这个放在add executable或者说add library命令之后
然后第一个参数是填写当前目标,第二个参数填写public private这些访问修饰符,然后第三个就跟正常的include director i是一致的
对于简单的项目依赖:
如果只是外部项目依赖toolkit中的头文件,可以使用更简单的include_directories:
include_directories(
${PROJECT_SOURCE_DIR}/toolkit/include
${PROJECT_SOURCE_DIR}/thirdparty/include
)
第三方库依赖的特殊情况:
当项目依赖多个第三方库且它们之间存在依赖关系时:
- 确保每个库都正确使用
find_package() - 使用
target_link_libraries指定依赖关系链 - 考虑使用CMake的
INTERFACE库来处理复杂的依赖关系
示例:
# 声明一个接口库来处理复杂依赖
add_library(complex_dependencies INTERFACE)
target_link_libraries(complex_dependencies
INTERFACE
dependency1
dependency2
)
# 主项目链接这个接口库
target_link_libraries(main_project
complex_dependencies
)

4121

被折叠的 条评论
为什么被折叠?



