一键编译包含多个独立模块和应用的工程(linux cmake)

2023年,曾经写过一篇cmake的基本用法,现在给出一个更好的实现策略:

1.整个工程的代码组织

1.1 示例

(.venv) root@PC-20230207TFDZ:/home/git/shm_vibration# ls -a -l

total 76

drwxr-xr-x 12 root root 4096 Jun 6 10:19 .

drwxr-xr-x 6 root root 4096 Jun 6 10:19 ..
drwxr-xr-x 8 root root 4096 Jun 6 10:20 .git
-rw-r--r-- 1 root root 210 May 29 11:12 .gitignore
drwxr-xr-x 2 root root 4096 May 29 14:46 .vscode

lrwxrwxrwx 1 root root 13 May 30 13:36 3rd -> /home/git/3rd
-rw-r--r-- 1 root root 478 Jun 5 15:55 CMakeLists.txt

drwxr-xr-x 8 root root 4096 May 13 08:54 alarm_daemon

drwxr-xr-x 3 root root 4096 Aug 20 2024 app_daemon
-rwxr-xr-x 1 root root 703 Jun 6 10:18 build.sh

drwxr-xr-x 8 root root 4096 Jul 2 2024 cfg_daemon
drwxr-xr-x 5 root root 4096 Jun 5 13:40 common

drwxr-xr-x 9 root root 4096 Jun 5 15:32 data_collect
drwxr-xr-x 7 root root 4096 Jun 5 14:30 include
drwxr-xr-x 10 root root 4096 Jun 6 10:16 main
-rwxr-xr-x 1 root root 791 Aug 20 2024 packcode.sh
-rwxr-xr-x 1 root root 578 Aug 20 2024 packgit.sh
-rwxr-xr-x 1 root root 1606 Aug 20 2024 release.sh
-rw-r--r-- 1 root root 2025 Jun 6 09:04 requirements.txt

drwxr-xr-x 2 root root 4096 May 30 16:15 sh

1.2 注意事项

  1. 这是一个工程的目录,工程包含一组应用程序,一些动态库
  2. .git说明它纳入了git源码管理
  3. .gitignore是这个工程级别的不需要导入的文件,比如.cache .o之类的
  4. .vscode说明这个工程现在是在.vscode下编辑编译的。
  5. 3rd是一些可复用的三方库或者控件
  6. CMakeLists.txt是最重要的cmake配置文件。回头会重点讲述,它是一个层级结构,对于工程的几种编译,顶级目录可以层层调用子目录里的CMakeLists.txt,完成一次编译,所有的.app.so.a批量生成。
  7. alarm_daemon, app_daemon,cfg_daemon,data_collect, main,都是一些独立的应用程序。
  8. common是一些工程级别的通用模块,我们待会儿会讲到。
  9. include是工程级别的通用模块的接口声明,对c项目而言,就是一些外部.h,注意,不需要暴露给使用者的.h文件不必放在这里。它有层级结构。
  10. requirements.txt 是python的三方库pip install安装的依赖项列表。
  11. 最后是一组.sh文件有四个:
    1. build.sh用来编译。
    2. packcode.sh 用来打包源码
    3. packgit.sh 用来打包包含.git的源码
    4. releash.sh 用来发布整个工程的二进制文件(包含完整发布和patch发布)

2. CMakeLists.txt

2.1 工程级别的配置 - 样例 ./CMakeLists.txt

  1. project(shm_vibration),用来定义工程,以及顶级目录,一般不允许有多个工程出现。即使在下级目录。这个相对目录可以在子目录的任意目录结构中使用到。
    1. 可能有多多Project的配置方式,我没有尝试过。
  2. add_subdirectory (common),这个用来指定参与递归式编译的下级目录。如果不包含,这个目录里的CMakeLists.txt不会参与整体的批量编译。
TypeScript 复制代码
cmake_minimum_required (VERSION 3.5)
project(shm_vibration)

add_compile_options(-I/usr/include/python3.12 -I. -I./common -Wall -Wextra -g -lpthread -lrt -luuid -lmosquitto -lcjson -lpython3.12 -L/usr/lib64 -Werror=implicit-function-declaration)

add_subdirectory (common)
#add_subdirectory (data_collect)
add_subdirectory (main)

2.2 共享模块 - 动态库静态库的编译。

2.2.1共享模块的集中编译 ./common/CMakeLists.txt

  1. 设置了静态库的统一输出位置,${CMAKE_BINARY_DIR}是最终的目标输出顶级目录。这个工程的输出目录实际在另一个地方,不在源码目录内,这是为了方便代码打包。
  2. ${CMAKE_CURRENT_SOURCE_DIR}这个目录始终是当前CMakeLists.txt所在目录。
  3. ${PROJECT_SOURCE_DIR},是工程的源码目录,其实就是最近一次声明了project(shm_vibration)的那个目录。
  4. 头文件的引用,现在不用分散查找了,因为这些头文件都是需要共享的外部接口,所以,必须要放置在./include目录内,.是整个工程的目录,也就是顶级源代码目录。
    1. 注意,可以将其分出子目录结构,以表明该模块的源码位置。
  5. add_subdirectory是提出了参与本层级批量build的子目录,这里有三个模块:calc, common,sensor,他们其实可以仿照include_directories放在一个括号里。
  6. add_library是.a的生成,STATIC,但是这里的MODULE_CALC和SOURCES_CALC在哪里呢?答案是,在calc那个目录里。我们稍后继续下潜一层。
  7. 注意,所有的共享模块。这里是集中批量编译的。他不见得一定要这样做,也可以分散编译。
TypeScript 复制代码
cmake_minimum_required(VERSION 3.28)
message(STATUS "build reused modules in ${CMAKE_CURRENT_SOURCE_DIR}") #编译时的提示信息

set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 设置静态库统一输出目录
find_package(Python3 EXACT 3.12 REQUIRED)

# 合并头文件路径(避免重复)
include_directories(
    ${PROJECT_SOURCE_DIR}/include
    ${PROJECT_SOURCE_DIR}/include/3rd
    ${PROJECT_SOURCE_DIR}/include/3rd/cjson
    ${PROJECT_SOURCE_DIR}/include/3rd/sensor
    ${PROJECT_SOURCE_DIR}/include/3rd/mosquitto
    ${PROJECT_SOURCE_DIR}/include/common
    ${PROJECT_SOURCE_DIR}/include/common/gpcommon
    ${PROJECT_SOURCE_DIR}/include/common/gpcalc
    ${PROJECT_SOURCE_DIR}/include/common/gpsensor
    ${PYTHON_INCLUDE_DIRS}
)

add_subdirectory(calc)
add_subdirectory(common) #files have attached yet.
add_subdirectory(sensor)

add_library(${MODULE_CALC} STATIC ${SOURCES_CALC}) #编译为.a静态库 => gpCommon.a
add_library(${MODULE_COMMON} STATIC ${SOURCES_COMMON}) #编译为.a静态库 => gpCommon.a
add_library(${MODULE_SENSOR} STATIC ${SOURCES_SENSOR}) #编译为.a静态库 => gpSensor.a

message(STATUS "leave common dir ... ${MODULE_CALC},${MODULE_COMMON},${MODULE_SENSOR} has compiled yet.") #编译时的提示信息

2.2.2 共享模块的资源集中于上传 - 这是一个叶子节点 ./common/calc/CMakeLists.txt

  1. 这里把MODULE_CALC这个自定义符号,传递给了PARENT_SCOPE,用来进行集中编译。
  2. 这里还把本目录里的所有的源码,补充它的全路径名,传递给了上层。用来集中编译。
  3. 只有这些。
TypeScript 复制代码
cmake_minimum_required(VERSION 3.28)
message(STATUS "Enter calc dir...pjt=${PROJECT_SOURCE_DIR}") #编译时的提示信息
set(MODULE_CALC    gpCalc) #将这个模块命名为gpCommon 并且定义了一个变量:MODULE_COMMON
set(MODULE_CALC ${MODULE_CALC} PARENT_SCOPE) #这里吧这个模块的所有的资源共享给了上一级目录:PARENT_SCOPE
aux_source_directory(. LOCAL_SOURCES)   #包含所有源代码【相对路径】

#将相对路径转换成绝对路径=》传递给上层模块【PARENT_SCOPE】
set(SOURCES_CALC "")  # 初始化
foreach(file ${LOCAL_SOURCES})
    list(APPEND SOURCES_CALC "${CMAKE_CURRENT_SOURCE_DIR}/${file}")  # 转换为绝对路径
endforeach()
set(SOURCES_CALC ${SOURCES_CALC} PARENT_SCOPE)  # 传递绝对路径列表

2.3 应用程序的编译 - 对项目级别共享模块的引用及其他

  1. 这里面包含一个mqtt静态库的创建
  2. 还有一个被称为shm_vibration的创建。
  3. 静态库的创建和之前的共享库创建是同一个逻辑。所不同的是,我补充了一些编译开关,还有一些include的头文件依赖,注意,这些依赖是局部的,仅仅作用于${MODULE_MQTT}这个编译对象。
  4. 主程序的代码,是有由本级别的.c文件,以及./calc ./cfg ./data目录下的文件拼接成的。
  5. 在link阶段,它混用了三种不同的链接语法。
    1. 有target_link_options里的命令行 -lpthread这种c运行库的引用
    2. 有target_link_libraries(shm_vibration PRIVATE "/usr/lib/libmosquitto.so.1")这类直接硬地址引用。
    3. 有包含友好提示信息的对libusb5633e.so这类三方库的引用。
  6. 注意,库的引用,某个库如果被更早的模块.a引用,那么它在CMakeLists.txt的位置需要更靠后:
    1. 比如:target_link_libraries(shm_vibration PRIVATE m)
    2. 它价于 -lm
    3. 因为我的一些自定义.a会引用它,所以,它的位置必须放在文件的更后面。
  7. 去年没有尝试成功的编译选项开关,现在是激活的状态,昨天第一次看到它生效,挺激动的。这两个编译选项建议始终打开,它能帮你发现一些源码的错误,这些错误到运行阶段是很难防护的,它分别成立了指针的类型,以及可能的函数调用错误。
    1. target_compile_options(shm_vibration PRIVATE -Werror=implicit-function-declaration)
    2. target_compile_options(shm_vibration PRIVATE -Werror=incompatible-pointer-types)
TypeScript 复制代码
cmake_minimum_required (VERSION 3.5)
message("-- Enter ./main/shmvibration...${CMAKE_CURRENT_SOURCE_DIR}")

add_subdirectory(mqtt)
add_library(${MODULE_MQTT} STATIC ${SOURCES_MQTT}) #编译为.a静态库 => gpCommon.a
target_compile_options(${MODULE_MQTT} PRIVATE -Werror=implicit-function-declaration -Werror=incompatible-pointer-types)
target_include_directories( 
    ${MODULE_MQTT} PRIVATE ${PROJECT_SOURCE_DIR}/include 
    ${MODULE_MQTT} PRIVATE ${PROJECT_SOURCE_DIR}/include/3rd 
    ${MODULE_MQTT} PRIVATE ${PROJECT_SOURCE_DIR}/include/3rd/cjson 
    ${MODULE_MQTT} PRIVATE ${PROJECT_SOURCE_DIR}/include/3rd/sensor 
    ${MODULE_MQTT} PRIVATE ${PROJECT_SOURCE_DIR}/include/3rd/mosquitto 
    ${MODULE_MQTT} PRIVATE ${PROJECT_SOURCE_DIR}/include/common 
    ${MODULE_MQTT} PRIVATE ${PROJECT_SOURCE_DIR}/include/common/gpcommon 
    ${MODULE_MQTT} PRIVATE ${PROJECT_SOURCE_DIR}/include/common/gpcalc 
    ${MODULE_MQTT} PRIVATE ${PROJECT_SOURCE_DIR}/include/common/gpsensor  
    ${MODULE_MQTT} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} 
    ${PYTHON_INCLUDE_DIRS})


file(GLOB SOURCES "*.c")      # 匹配当前目录所有.c文件
add_executable (shm_vibration ${SOURCES} ../3rd/cJSON-master/cJSON.c)
include_directories(
    shm_vibration PRIVATE ./calc
    shm_vibration PRIVATE  ./cfg
    shm_vibration PRIVATE ./data
)

#仅适用于特定编译对象的include包含,具备全局效力的是include_directories
target_include_directories(
    shm_vibration PRIVATE ${PROJECT_SOURCE_DIR}/include
    shm_vibration PRIVATE ${PROJECT_SOURCE_DIR}/include/3rd
    shm_vibration PRIVATE ${PROJECT_SOURCE_DIR}/include/3rd/cjson
    shm_vibration PRIVATE ${PROJECT_SOURCE_DIR}/include/3rd/sensor
    shm_vibration PRIVATE ${PROJECT_SOURCE_DIR}/include/3rd/mosquitto
    shm_vibration PRIVATE ${PROJECT_SOURCE_DIR}/include/common
    shm_vibration PRIVATE ${PROJECT_SOURCE_DIR}/include/common/gpcommon
    shm_vibration PRIVATE ${PROJECT_SOURCE_DIR}/include/common/gpcalc
    shm_vibration PRIVATE ${PROJECT_SOURCE_DIR}/include/common/gpsensor
    shm_vibration PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
    shm_vibration PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/mqtt
    ${PYTHON_INCLUDE_DIRS}
)
find_package(Python3 EXACT 3.12 REQUIRED)

target_link_options(shm_vibration PRIVATE -Wl,-rpath=./lib -L/usr/lib64 -L/usr/lib/x86_64-linux-gnu/ -L./lib -Wall -Wextra -g -lpython3.12 -lpthread -lrt)
target_compile_options(shm_vibration PRIVATE -Werror=implicit-function-declaration)
target_compile_options(shm_vibration PRIVATE -Werror=incompatible-pointer-types)
target_link_libraries(shm_vibration PRIVATE "/usr/lib/libmosquitto.so.1")
target_link_libraries(shm_vibration PRIVATE "/usr/lib/x86_64-linux-gnu/libpython3.12.so.1")
target_link_libraries(shm_vibration PRIVATE "/usr/lib/x86_64-linux-gnu/libuuid.so")  # 直接写绝对路径
target_link_libraries(shm_vibration PRIVATE "/home/build/lib/libgpCommon.a")
target_link_libraries(shm_vibration PRIVATE "/home/build/lib/libgpCalc.a")
target_link_libraries(shm_vibration PRIVATE "/home/build/lib/libgpSensor.a")
target_link_libraries(shm_vibration PRIVATE "/home/build/main/libgpMqtt.a")
target_link_libraries(shm_vibration PRIVATE ${PYTHON_LIBRARIES})
#set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
# 声明库搜索路径
set(LIB_SEARCH_PATHS "/home/git/shm_vibration/main/lib")
# 动态查找库
find_library(LIB_USB_SAMPLE_DEV libusb5633e.so PATHS ${LIB_SEARCH_PATHS})
if (NOT LIB_USB_SAMPLE_DEV)
    message(FATAL_ERROR "Required libusb5633e.so not found")
endif()
# 链接库并添加头文件
target_link_libraries(shm_vibration PRIVATE ${LIB_USB_SAMPLE_DEV})
target_link_libraries(shm_vibration PRIVATE m) #被依赖项必修放在后面等价于 -lm
target_link_libraries(shm_vibration PRIVATE uuid) #被依赖项必修放在后面等价于 -uuid

3.几个编译和打包的脚本

3.1 build.sh

bash 复制代码
#!/bin/bash

#跨目录的相对路径访问
echo "build shm_vibration to ../../lib"
cd "$(dirname "$0")"

#创建整个build目录结构,包含一些配置文件目录
mkdir -p ../../build
mkdir -p ../../release
mkdir -p ../../build/main/py
mkdir -p ../../build/main/sh
mkdir -p ../../build/main/cfg
mkdir -p ../../build/main/sensor
mkdir -p ../../build/main/lib
mkdir -p ../../build/common
mkdir -p ../../build/lib

#cmake 的指定编译目录编译模式
#build -S <src path> to -B <target path>
cmake -S . -B ../../build
cmake --build ../../build

#把可执行文件转回源码目录,便于本地手工调试
cp ../../build/main/shm_vibration ./main/app_shm_vibration


#这是一些额外的三方配置文件传递到编译目录,方便vscode集成debug.
yes|cp -rf ./main/py/* ../../build/main/py
yes|cp -rf ./main/cfg/* ../../build/main/cfg
yes|cp -rf ./main/lib/* ../../build/main/lib
yes|cp -rf ./main/sh/* ../../build/main/sh

3.2 release.sh

从略

这个发布过程设计python程序的混淆,以及配置文件的组合,还有patch包的生成,逻辑比较复杂。

3.3 packcode.sh

做了自动添加版本信息的处理

bash 复制代码
#!/bin/bash

# 获取脚本所在目录
SCRIPT_DIR=$(dirname "$(realpath "$0")")
# 切换到脚本所在目录
cd "$SCRIPT_DIR" || exit
# 打印不带路径的脚本名称
SCRIPT_NAME=$(basename "$0")
# 你的脚本其他部分
echo ">>>>exec $(pwd)/$SCRIPT_NAME in dir '$(pwd)'"


Ver="0.1."
Postfix="_debug"
timestamp=$(date +%Y%m%d_%H%M%S)   # 获取当前时间的时间戳
filename="source_code_vibration_calc_$ver${timestamp}_with_git_$Postfix.tar.gz"  # 生成带时间戳的文件名

mkdir -p ../../source_code_backup
tar czvf ../../source_code_backup/$filename app_daemon cfg_daemon main alarm_daemon *.sh CMake* --exclude "./main/data/" --exclude "main/py/build/*" --exclude "main/py_release/_internal/*" --exclude "main/*.tar.gz" --exclude "cfg_daemon/temp/*" --exclude "data/*"

4.杂项

4.1 .vscode/settings.json

处理了.venv,处理了字符集

TypeScript 复制代码
{
    "python.defaultInterpreterPath": "/home/git/xjtu_sy/.venv/bin/python3",
    "python.terminal.activateEnvironment": true,
    "files.encoding": "utf8",
    "python.analysis.addHoverSummaries": true,
    "python.analysis.autoFormatStrings": true,
    "python.analysis.autoImportCompletions": true,
    "python.analysis.completeFunctionParens": true
}

4.2 .gitignore

bash 复制代码
#使用时要用:
git add .
#不要用
git add *
TypeScript 复制代码
*.log
*.log.*
*.o
*.o.?
*.pyc
*.dumb
**/CMakeFiles/
CMakeFiles/
*.swp
__pycache__/
**/_pycache__/
**/*.log
**/*.log.*
**/*.o
**/*.o.?
**/.pyc
**/dumb
**/*.smp
*.tar.gz
**/*.tar.gz
*.zip
**/*.zip
*.rar
**/*.rar

4.3 include目录一瞥

4.3.1 源码中的头文件引用 -示例

cpp 复制代码
//位于inlcude目录的外部接口,如果在子目录,直接添加子目录名
#include "gpcalc/gpcalc.h"
//更深的目录
#include "app/main/mqtt_config.h"
//直接在inlcude目录的,可以直接引用
#include "pjt_global.h"

//所有的引用无需添加"../../include"之类的相对路径,更不需要用绝对路径。
//因为相关的头文件引用,cmakefiles.txt里已经处理过了。

附录A 编译过程

代码中打印信息有些错误,纠正后:

红框是模块的编译,

绿框是主应用程序的编译,其中还包含一个内部的子模块mqtt

附录B 源代码编译过程中的一些编译开关的暂时调整

我们有时候的编译开关会误伤一些并没有什么问题的一些写作风格很野蛮的代码,如果想暂时屏蔽掉这些已经上升为error的编译开关,可以使用下面的代码,针对某一段函数组,暂时屏蔽。这中年方法其实我们在编写跨语言的那种结构体定义时用到过类似的语法:

cpp 复制代码
#pragma GCC diagnostic push  // 保存当前诊断状态
#pragma GCC diagnostic ignored "-Wincompatible-pointer-types"

//code 

#pragma GCC diagnostic push  // 恢复源码诊断状态

指针转换错误,以及函数隐式定义,是我知道的危害极大的一类错误,比如:

类似这样的警告,提升为错误是更好的选择。

相关推荐
Tender_光39 分钟前
iptables实验
运维·服务器
szxinmai主板定制专家1 小时前
【飞腾AI加固服务器】全国产化飞腾+昇腾310+PCIe Switch的AI大模型服务器解决方案
运维·服务器·arm开发·人工智能·fpga开发
点击查询1 小时前
怎么把自己电脑设置成服务器?
运维·服务器
阿里云大数据AI技术2 小时前
ES Serverless 8.17王牌发布:向量检索「火力全开」,智能扩缩「秒级响应」!
大数据·运维·serverless
yzx9910132 小时前
Linux 系统中的算法技巧与性能优化
linux·算法·性能优化
fengyehongWorld2 小时前
Linux Docker的简介
linux·docker
wanhengidc2 小时前
服务器中日志分析的作用都有哪些
运维·服务器
Mikhail_G2 小时前
Python应用变量与数据类型
大数据·运维·开发语言·python·数据分析
曹瑞曹瑞2 小时前
VMware导入vmdk文件
linux·运维·服务器
Johny_Zhao2 小时前
2025年6月Docker镜像加速失效终极解决方案
linux·网络·网络安全·docker·信息安全·kubernetes·云计算·containerd·yum源·系统运维