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

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
注册配置 MSE Nacos/ZooKeeper,182元/月
简介: 本文总结了使用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

相关文章
|
3月前
|
设计模式 人工智能
单例模式中的隐藏陷阱:你真的了解单例吗?
本文通过一个实际案例揭示了单例模式中常见的隐藏陷阱——在析构函数中调用其他单例对象导致程序崩溃的问题。代码示例展示了因单例析构顺序不确定而引发的 `crash`,并结合 AI 的分析提出两种规避策略:一是避免析构、二是通过全局状态检测对象存活。文章指出,依赖不确定的语言特性并非良策,真正的解决之道在于合理设计,确保程序行为可预期、可控制。
159 54
|
Ubuntu Linux Python
Linux(15)Ubuntu安装ninja构建工具
Linux(15)Ubuntu安装ninja构建工具
2522 0
|
3月前
|
缓存 NoSQL Unix
【实战指南】守护进程服务实现
本文介绍了在Linux系统中实现守护进程异常重启的几种方案。通过理解僵死进程和信号处理机制,提出了基于SIGCHLD信号监听、轮询proc文件系统及waitpid接口的三种方法,并给出了C++实现代码。最终选择轮询方式以提升稳定性,确保服务进程在崩溃后能自动重启,保障系统可靠性。
195 52
|
3月前
|
安全 Linux 测试技术
【实战指南】记一次定位fd泄漏问题
本文记录了一次文件描述符(fd)泄漏问题的排查过程。在项目压测中,进程因打开过多文件导致fd资源耗尽,最终无法创建新文件。通过分析错误码、查看/proc/pid/fd路径下的文件句柄信息,定位到临时文件未正确关闭的问题根源,并修复代码中遗漏的close调用。同时总结了Linux下进程资源限制的相关知识点,强调开发中应关注资源使用情况,避免类似问题发生。
160 46
|
3月前
|
设计模式 C++
【实战指南】设计模式 - 工厂模式
工厂模式是一种面向对象设计模式,通过定义“工厂”来创建具体产品实例。它包含简单工厂、工厂方法和抽象工厂三种形式,分别适用于不同复杂度的场景。简单工厂便于理解但扩展性差;工厂方法符合开闭原则,适合单一类型产品创建;抽象工厂支持多类型产品创建,但不便于新增产品种类。三者各有优缺点,适用于不同设计需求。
125 42
|
3月前
|
C语言 C++
【实战指南】 C/C++ 枚举转字符串实现
本文介绍了在C/C++中实现枚举转字符串的实用技巧,通过宏定义与统一管理枚举名,提升代码调试效率并减少维护错误。
257 58
|
3月前
|
程序员 编译器 C++
【实战指南】C++ lambda表达式使用总结
Lambda表达式是C++11引入的特性,简洁灵活,可作为匿名函数使用,支持捕获变量,提升代码可读性与开发效率。本文详解其基本用法与捕获机制。
136 46
|
12月前
|
搜索推荐 安全 API
|
存储 算法 编译器
【C++ 引用 】C++深度解析:引用成员变量的初始化及其在模板编程中的应用(一)
【C++ 引用 】C++深度解析:引用成员变量的初始化及其在模板编程中的应用
1676 0
|
Ubuntu 网络协议 数据安全/隐私保护
【Ubuntu】sudo apt-get update 无法解析域名(亲测有效)
在Ubuntu 18.04系统中,用户在执行sudo apt-get update时遇到“无法解析域名‘ip’”的错误。经分析,问题源于之前设置的网络代理配置未完全清除。解决方案是找到并重命名/etc/apt/apt.conf.d下的proxy.conf文件,使其不再生效。操作后,sudo apt-get update命令恢复正常,问题得到完美解决。
3495 4
【Ubuntu】sudo apt-get update 无法解析域名(亲测有效)