【实战指南】 CMake搭建编译环境总结

简介: 本文总结了使用CMake搭建编译环境的技巧,涵盖单个及多个源文件的编译、CMakeLists嵌套管理、变量设置、交叉编译配置、常用编译选项及警告处理等内容。通过实例说明了如何高效组织工程结构,并利用CMake灵活控制编译流程,适用于嵌入式开发场景。

CMake搭建编译环境总结


目录



个人简介提供更多文章介绍入口


前言

  交叉编译算是每个嵌入式开发者都会经历的一道坎吧,通俗的描述就是搭建Arm板代码编译环境,让代码能够在Arm板子上跑起来。常用到的编译工具为Makefile和CMake,本篇记录下CMake的常用技巧。

入门案例:单个源文件

代码路径:

https://gitee.com/LinuxTaoist/DesignMode/tree/master/FactoryMode

工程结构

. 
├── CMakeLists.txt
├── abstract_factory.cc
├── factory_method.cc   
└── simple_factory.cc

CMakeList

# 指定最低版本
cmake_minimum_required(VERSION 2.8)
## 指定为C++11 版本
set(CMAKE_CXX_STANDARD 11)
## 指定项目名称
project(FactoryMode)
## 为当前路径以及子目录的源文件加入由-D预编译定义
## add_definitions(-DFOO -DDEBUG ...)
## 编译工具
set(CMAKE_C_COMPILER "gcc")
set(CMAKE_CXX_COMPILER "g++")
## 设置C++编译参数(CMAKE_CXX_FLAGS是全局变量)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -g3")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11 -g3 -fpermissive")
## 生成bin文件 AbFactory
add_executable(AbFactory  abstract_factory.cc)
## 生成bin文件 FacMethod
add_executable(FacMethod  factory_method.cc)
## 生成bin文件 SmpFactory
add_executable(SmpFactory simple_factory.cc)

工程编译

  CMakeList编写完以后,先执行cmake [CMakeList路径],然后make即可。

$ cmake .
-- The C compiler identification is GNU 7.5.0
-- The CXX compiler identification is GNU 7.5.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /work/src/DesignMode/FactoryMode
$ make
Scanning dependencies of target SmpFactory
[ 16%] Building CXX object CMakeFiles/SmpFactory.dir/simple_factory.cc.o
[ 33%] Linking CXX executable SmpFactory
[ 33%] Built target SmpFactory
Scanning dependencies of target AbFactory
[ 50%] Building CXX object CMakeFiles/AbFactory.dir/abstract_factory.cc.o
[ 66%] Linking CXX executable AbFactory
[ 66%] Built target AbFactory
Scanning dependencies of target FacMethod
[ 83%] Building CXX object CMakeFiles/FacMethod.dir/factory_method.cc.o
[100%] Linking CXX executable FacMethod
[100%] Built target FacMethod

多个源文件

代码路径:

https://gitee.com/LinuxTaoist/DesignMode/tree/master/Proxy

工程结构

  对于工程中存在大量的文件夹和文件时,一个CMakeLst虽然可以将其全部编译,但是维护起来非常麻烦。

  对于庞大的代码架构场景,通常会按模块划分,将一个模块的代码放到一个CMakeList中配置编译,若模块代码还是很多,将此模块再细分成多个小模块用多个CMakeList管理编译。然后将这些CMakeList按照路径层层嵌套。

  如此工程中各个CMakeList树状层层嵌套,最终都会被嵌套至最顶层CMakeList。类似如下结构:

Proxy/
├── CMakeLists.txt
├── Client
│   ├── CMakeLists.txt
│   └── main_client.cc
├── Ipc
│   ├── CMakeLists.txt
│   ├── msg_manager.cc
│   └── msg_manager.h
├── Server
│   ├── Api
│   │   ├── common_type.h
│   │   ├── led_manager_proxy.cc
│   │   └── led_manager_proxy.h
│   ├── CMakeLists.txt
│   ├── Led
│   │   ├── led_manager.cc
│   │   └── led_manager.h
│   └── main_server.cc
└── build
    └── build.sh

  其中Proxy下的CMakeList会包含Client、Ipc、Server中的CMakeList。假设Server子路径还有子文件夹,Server的CMakeList就继续向下包含。

这么做的优点如下:

  • 每个CMakeList职责清晰。只需专注于当前模块或者当前路径的源码编译。
  • 方便模块化编译管理。当不需要编译哪个模块时,只需在顶层CMakeList屏蔽包含指定路径CMakeList即可。
  • 便于维护。每个CMakeList的代码量都比较少,且功能明确,维护者一眼就能看懂。

CMakeList

  • 顶层CMakeList
# 指定最低版本
cmake_minimum_required(VERSION 2.8)
## 指定为C++11 版本
set(CMAKE_CXX_STANDARD 11)
## 指定项目名称
project(ProxyMode)
## 为当前路径以及子目录的源文件加入由-D预编译定义
## add_definitions(-DFOO -DDEBUG ...)
## 编译工具
set(CMAKE_C_COMPILER "gcc")
set(CMAKE_CXX_COMPILER "g++")
## 设置C++编译参数(CMAKE_CXX_FLAGS是全局变量)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -g3")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11 -g3 -fpermissive")
## 包含子路径
add_subdirectory(Server)
add_subdirectory(Client)
add_subdirectory(Ipc)

顶层CMakeList一般需要做如下事项:

① 配置工程相关的属性:使用CMake版本、工程名

② 配置交叉工具:设置编译器、增加编译参数

③ 包含需要嵌套的子路径CMakeList

  • Server路径 CMakeList
## 指定最低版本
## 指定最低版本
cmake_minimum_required(VERSION 2.8)
## 宏
set(PROJECT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/..)
cmake_policy(SET CMP0046 NEW)
## 输出路径
set(OUTPUT_PATH            ${PROJECT_PATH}/Out)
set(LIBRARY_OUTPUT_PATH    ${OUTPUT_PATH}/lib)
set(EXECUTABLE_OUTPUT_PATH ${OUTPUT_PATH}/bin)
## includes
include_directories(${PROJECT_PATH}/Ipc)
include_directories(${PROJECT_PATH}/Server/Api)
## 库路径
link_directories(${OUTPUT_PATH}/lib)
## main_server.exe
set(SRC_BIN_CLIENT   main_client.cc)
add_executable       (client ${SRC_BIN_CLIENT})
set_target_properties(client PROPERTIES OUTPUT_NAME "mainclient")
target_link_libraries(client c pthread ledapi)
add_dependencies     (client libledapi)

子路径下的CMakeList需要关心编译文件:

① 包含头文件路径

② 设置目标生成路径

③ 设置编译目标,bin或so

然后就是根据预期编译的结果,使用相关的变量即可。例子中,为了方便执行,增加了build.sh编译脚本。这个脚本代替执行编译命令,同时将编译生成的缓存文件放到指定路径管理。

## buid.sh
rm -rf ../Out/Cache
rm -rf ../Out/bin/*
rm -rf ../Out/lib/*
mkdir -p ../Out/cache/
cd ../Out/cache/
cmake ../../
make

其他用法

设置局部变量

## 设置局部变量PROJECT_PATH
set(PROJECT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/..)
## 使用局部变量PROJECT_PATH
include_directories(${PROJECT_PATH}/Ipc)

设置自定义全局变量

## Proxy/CMakelists.txt
## 设置自定义全局变量 PROJECT_DESC
set(PROJECT_DESC "This is project")
set_property(GLOBAL PROPERTY source_list_property "${PROJECT_DESC}")

获取自定义全局变量

## Proxy/Ipc/CMakeLists.txt
## 获取自定义全局变量 PROJECT_DESC
get_property(PROJECT_DESC GLOBAL PROPERTY source_list_property)
message("PROJECT_DESC=${PROJECT_DESC}")

指定目标(bin/库)输出路径

## 设置库输出路径
set(LIBRARY_OUTPUT_PATH     xx/Out/lib)
## 设置bin文件输出路径
set(EXECUTABLE_OUTPUT_PATH  xx/Out/bin)

设置环境变量

set(ENV{<variable>} [<value>])   
ENV:环境变量标志性前缀
variable:变量名称
value:变量值

E.g 设置环境 CMAKE_FILE

## 设置环境变量
set(ENV{CMAKE_FILE} "./IPC")

获取环境变量

# 判断CMAKE_FILE环境变量是否定义
if(DEFINED ENV{CMAKE_FILE})
    message("CMAKE_FILE: $ENV{CMAKE_FILE}")
else()
    message("NOT DEFINED CMAKE_FILE VARIABLES")
endif()

设置编译器

## 指定C编译工具
set(CMAKE_C_COMPILER "gcc")
## 指定C++编译工具
set(CMAKE_CXX_COMPILER "g++")

当编译工具链路径被加到环境变量中,可以直接写编译工具的名称。在配交叉编译工具时,此处应写对应交叉编译工具链的绝对路径。

设置依赖库路径

## 括号为依赖库的绝对路径
link_directories(${OUTPUT_PATH}/lib)

包含头文件路径

## 括号为包含头文件的绝对路径
include_directories (${PROJECT_PATH}/Ipc)

添加编译器编译选项

## 针对所有编译器,开启编译警告 (包括C、C++编译器)
add_compile_options("-Wall -Werror")
## 针对C编译器,开启编译警告
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror")
## 针对C++编译,开启编译警告
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror")

添加打印

## 打印CMAKE_CXX_FLAGS的值
message("${CMAKE_CXX_FLAGS}")

CMakeLists路径嵌套

## 添加当前路径Client文件
add_subdirectory(Client)

控制编译流程

option 语法

## option编译流程控制
 option(<variable> "<help_text>" [value])
 variable 选项名
 help_text 描述、解释、备注
 value 选项初始化值(除ON而外全为OFF)

E.g TEST_OPTION宏控制编译流程[1]

option(TEST_OPTION "test opiton" ON)
if (DEFINED TEST_OPTION)
        message(STATUS "TEST_OPTION defined: " ${TEST_OPTION})
else ()     
        message(STATUS "TEST_OPTION un-defined: " ${TEST_OPTION})
endif()
 
if (TEST_OPTION)
        message(STATUS "TEST_OPTION ON.")
        add_definitions(-DTEST_OPTION)
else ()
        message(STATUS "TEST_OPTION OFF.")
endif()
 
if (NOT TEST_OPTION)
        message(STATUS "NOT-TEST_OPTION ON.")
else ()
        message(STATUS "NOT-TEST_OPTION OFF.")
endif()

Shell脚本传递宏至CMakeList

命令行执行cmake时,跟随-DXXX,即可从命令行传递宏XXX至CMakeList。将此命令行写入脚本,便能实现Shell脚本传递宏至CMakeList。

## 增加TEST宏
cmake . -DTEST
## 增加TEST_OPTION=ON
cmake . -DTEST_OPTION=ON

CMakeLists传递变量至代码工程

## 向代码工程添加TEST宏
add_definitions(-DTEST)

代码判断宏TEST是否有定义,实现宏控

// *.c / *.cpp
#ifdef TEST
...  // code
#endif
#if defined TEST
...  // code
#endif

编译警告

CMake编译警告和报错设置

gcc本身设置了一些编译告警/报错选项,归类如下[2]

  • -Werror:-Werror=xxx,表示将xxx的warning变为error,例如-Werror=select, -Werror=return-type
  • -Wall:激活所有的warnings
  • -Wextra:激活不在-Wall所在的warning的其它warnings
  • -Wpedantic: 对于所有不符合 ISO C/ISO C++ 语言标准的源代码发出警告,等价于-pedantic
    -pedantic-errors参数将这些警告视为错误,等同于-Werror=pedantic
  • -Wconversion: 在隐式转换可能导致值变化的时候发出警告。在隐式转换的时候,如果值发生变化,那么结果可能就不是预料中的,所以最好使用显式转换。
  • -Wshadow:激活遮蔽(如两个嵌套的for循环都用变量i做index)类型的warning,即:
    -Wshadow=global:激活任意类型的遮蔽;
    -Wshadow=local:激活local变量的遮蔽(如两个嵌套的for循环都用变量i做index);
    -Wshadow=compatible-local:激活local变量的遮蔽,考虑变量类型(如上例中的i在内外两层的for循环中是不同的类型);

E.g 打开所有编译告警,并视警告为错误,出现任何警告放弃编译

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror")

常用警告

GCC编译器支持对代码进行诊断,针对代码本身不是错误但是疑似错误或者可能存在风险的地方发出警告,而警告编译选项就是用于控制需要告警的警告类型的。常见告警如下[3]

  • -Wall
    这是一个非常常用的编译选项,用于启用一批比较常见且易于修改的警告,这些选项都是对代码进行基本的检查,比如下面这些:
选项 作用
-Waddress 检查是否存在可疑的内存地址使用
-Wformat 检查标准库函数的使用格式是否正确,比如printf的格式化字符串中的格式符和对应的参数是否匹配
-Wunused-function 对已声明但是未定义的静态函数和未被使用的非内联静态函数发出警告
-Wswitch 当用switch用于枚举类型时,判断分支是否包含所有枚举值,否则发出警告
-Wunused-variable 对声明但未被使用的变量发出警告
-Wunused-but-set-variable 对声明且被赋值但未被使用的变量发出警告
-Warray-bounds=1 数组越界检查,需启用选项-ftree-vrp

完整列表参考 Warning-Options[4]

:当需要排除某些类型的警告,使用-Wno-xxx。 比如使用-Wall -Wno-unused-variable可以从-Wall中排除-Wunused-variable

  • -Wextra
    单单只有-Wall可能还不够严格,GCC还有-Wextra作为补充,包括另外一些没有被-Wall包含的警告类型,譬如:
选项 作用
-Wcast-function-type 当函数被强转为不兼容的函数指针时发出警告
-Wempty-body 当存在空的if、else或者do while语句时发出警告
-Wunused-parameter 当函数有未被使用的参数时发出警告,需配合-Wall
-Wunused-but-set-parameter 当存在被设置但是未被使用的参数发出警告,需配合-Wall
-Wsign-compare 当比较有符号和无符号值时发出警告

配置交叉编译环境常需要的修改

设置默认库和头文件搜索路径

编译默认会从/usr/include目录中搜索头文件、从/usr/lib中搜索依赖库。当设置了CMAKE_SYSROOT后,则会从xxx/usr/include搜索头文件、从xxx/usr/lib中搜索依赖库。

## 系统库路径:${SDKTARGETSYSROOT}/usr/lib
## 系统头文件:${SDKTARGETSYSROOT}/usr/include
set(CMAKE_SYSROOT "${SDKTARGETSYSROOT}")

设置交叉编译工具链

Linux系统在嵌入式板子上运行,需要与嵌入式板配套的交叉编译工具链编译。

同样的,个人代码也需要与编译Linux配套的交叉工具编译,才能在Linux环境运行。一般在Ubuntu上编译运行,只需要设置为gcc/g++即可。

编译工具链都是由厂商提供,用户只需要在编译脚本配置即可。设置交叉编译工具链方式如下:

## 绝对路径
set(CMAKE_C_COMPILER "xxx/arm-linux-gcc")
set(CMAKE_CXX_COMPILER "xxx/arm-linux-g++")

设置浮点运算处理方式

在某些gcc编译器会检查软浮点和硬浮点设置,报错log如下:

armv7at2hf-neon-poky-linux-gnueabi/usr/include/gnu/stubs-32.h:7:11: fatal error: gnu/stubs-soft.h: No such file or directory
    7 | # include <gnu/stubs-soft.h>

初步看报错log,是因为编译器没有文件stubs-soft.h。猜测可能此编译器不支持软浮点运算?

解决方法是在编译脚本将其设置为硬浮点运算,如下方式:

## 第一种
add_definitions("-mfloat-abi=hard -mfpu=neon")
## 第二种
add_compile_options(-mfpu=neon -mfloat-abi=hard)
## 第三种
set(CMAKE_C_FLAGS   "${CMAKE_C_FLAGS} -mfloat-abi=hard -mfpu=neon")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfloat-abi=hard -mfpu=neon")

常见场景

编译动态库

## 生成libtest.so
### 添加源码路径
aux_source_directory (xxx/src            SRC_LIB_TEST)
### 生成so库
add_library          (libtest SHARED     ${SRC_LIB_TEST})
### 指定生成目标名libtest.so
set_target_properties(libtest PROPERTIES OUTPUT_NAME "test")
### 链接语言C++
set_target_properties(libtest PROPERTIES LINKER_LANGUAGE CXX)
### 链接依赖库
target_link_libraries(libtest stdc++)
### 添加依赖库,会先检查依赖库是否生成
add_dependencies     (libtest libstdc++)

编译静态库

## 生成libtest.a
aux_source_directory (xxx/src            SRC_LIB_TEST)
add_library          (libtest STATIC     ${SRC_LIB_TEST})
set_target_properties(libtest PROPERTIES OUTPUT_NAME "test")
set_target_properties(libtest PROPERTIES LINKER_LANGUAGE CXX)
target_link_libraries(libtest stdc++)
add_dependencies     (libtest libstdc++)

编译可执行文件

## 生成test bin
include_directories  (${PROJECT_SOURCE_DIR}/inc)
set(SRC_BIN_TEST  ${PROJECT_SOURCE_DIR}/src/test.cpp)
add_executable       (test ${SRC_BIN_TEST})
set_target_properties(test PROPERTIES OUTPUT_NAME "test")
set_target_properties(test PROPERTIES LINK_FLAGS "-Wl,-rpath-link=${LIBRARY_OUTPUT_PATH}")
target_link_libraries(test stdc++)
add_dependencies     (test libstdc++)

[1]

CMake之Option使用简介: https://blog.csdn.net/lhl_blog/article/details/123553686

[2]

gcc和cmake的编译警告和报错设置: https://blog.csdn.net/LCHUESTC/article/details/122602001

[3]

GCC常用编译选项: https://zhuanlan.zhihu.com/p/393419013

[4]

Warning-Options: https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html

相关文章
|
JavaScript 前端开发 安全
nodejs 跨域设置
nodejs 跨域设置
445 0
|
11月前
|
安全 Linux 测试技术
【实战指南】记一次定位fd泄漏问题
本文记录了一次文件描述符(fd)泄漏问题的排查过程。在项目压测中,进程因打开过多文件导致fd资源耗尽,最终无法创建新文件。通过分析错误码、查看/proc/pid/fd路径下的文件句柄信息,定位到临时文件未正确关闭的问题根源,并修复代码中遗漏的close调用。同时总结了Linux下进程资源限制的相关知识点,强调开发中应关注资源使用情况,避免类似问题发生。
527 79
|
11月前
|
设计模式 人工智能
单例模式中的隐藏陷阱:你真的了解单例吗?
本文通过一个实际案例揭示了单例模式中常见的隐藏陷阱——在析构函数中调用其他单例对象导致程序崩溃的问题。代码示例展示了因单例析构顺序不确定而引发的 `crash`,并结合 AI 的分析提出两种规避策略:一是避免析构、二是通过全局状态检测对象存活。文章指出,依赖不确定的语言特性并非良策,真正的解决之道在于合理设计,确保程序行为可预期、可控制。
335 83
|
11月前
|
缓存 NoSQL Unix
【实战指南】守护进程服务实现
本文介绍了在Linux系统中实现守护进程异常重启的几种方案。通过理解僵死进程和信号处理机制,提出了基于SIGCHLD信号监听、轮询proc文件系统及waitpid接口的三种方法,并给出了C++实现代码。最终选择轮询方式以提升稳定性,确保服务进程在崩溃后能自动重启,保障系统可靠性。
504 89
|
监控 Linux C++
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展(2)
本文是《4步实现C++插件化编程》的延伸,重点介绍了新增的插件“热拔插”功能。通过`inotify`接口监控指定路径下的文件变动,结合`epoll`实现非阻塞监听,动态加载或卸载插件。核心设计包括`SprDirWatch`工具类封装`inotify`,以及`PluginManager`管理插件生命周期。验证部分展示了插件加载与卸载的日志及模块状态,确保功能稳定可靠。优化过程中解决了动态链接库句柄泄露问题,强调了采纳用户建议的重要性。
570 101
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展(2)
|
传感器 数据采集 IDE
【ESP32双核运行Freertos及互斥量】
【ESP32双核运行Freertos及互斥量】
1518 0
|
测试技术
[googletest] --- 简易使用教程
[googletest] --- 简易使用教程
1285 0
|
编译器 Linux 开发者
【cmake 交叉编译配置设置】CMAKE_TOOLCHAIN_FILE:跨平台编译的秘密武器
【cmake 交叉编译配置设置】CMAKE_TOOLCHAIN_FILE:跨平台编译的秘密武器
2733 0
|
Linux 程序员 API
信号的机制——信号处理函数的注册
【9月更文挑战第17天】在 Linux 系统中,信号用于响应各种事件,可通过 `kill -l` 查看所有信号。每个信号有唯一 ID 及默认操作,如终止(Term)或生成核心转储(Core)。进程可执行默认操作、捕获信号或忽略信号,但无法忽略 SIGKILL 和 SIGSTOP。常用 `signal` 或 `sigaction` 函数注册信号处理函数,后者更灵活且推荐使用。信号处理涉及系统调用和内核设置,建议根据需求定制参数。
|
安全
qt.qpa.xcb: could not connect to display 问题解决
【5月更文挑战第16天】qt.qpa.xcb: could not connect to display qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even though it was found. This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem. 问题解决
8799 1