从零开始理解一个复杂的 C++/CUDA 项目 Makefile
你是否曾经面对一个庞大的 C++ 项目,看到 Makefile 就头大?
你是否好奇:“这么多 .cpp、.cu 文件,是怎么一步步变成可执行程序的?”
今天,我们就来逐行解析一个真实项目中的 Makefile,带你揭开构建系统的神秘面纱!
🎯 项目简介
这是一个融合了 C++、CUDA(GPU 编程)、C 语言的复杂项目,可能用于视频处理、AI 推理(如 RetinaFace、SCRFD、TensorRT) 等高性能场景。
项目结构:
makefile文件内容为:
# =============================================
# Makefile 详细注释版
# 用途:构建一个包含 C++、CUDA 和 C 源码的项目
# 作者:初学者友好版
# =============================================# === 1. 源文件查找与目标对象文件命名 ===# 使用 shell 命令 find 在 src 目录下查找所有以 .cpp 结尾的源文件
# 结果赋值给变量 cpp_srcs
cpp_srcs := $(shell find src -name "*.cpp")# 将所有 .cpp 文件名转换为对应的 .o(目标文件)名称
# $(patsubst 模式,替换,列表)
# %.cpp 表示任意 .cpp 文件
# 举例:src/main.cpp → src/main.o
cpp_objs := $(patsubst %.cpp,%.o,$(cpp_srcs))# src/*.cpp → src/*.o# 将所有目标文件路径从 src/ 替换为 objs/,实现源码与编译输出分离
# 例如:src/main.o -> objs/main.o
cpp_objs := $(subst src/,objs/,$(cpp_objs))# src/*.o → objs/*.o
# 打印 cpp_objs 的值
$(info cpp_objs = $(cpp_objs))
#结果 cpp_objs = objs/app_scrfd/scrfd.o objs/app_alphapose/alpha_pose.o objs/core/CvxFont.o objs/core/rgb_video_capture.o objs/core/dpb_ffmpeg_demuxer.o objs/core/util.o objs/core/http_server_modify.o objs/core/dpb_cuda_tools.o objs/core/http_request.o objs/core/engine_manager.o objs/core/process_work.o objs/core/binary_io.o objs/core/push_manager.o objs/core/yolo.o objs/core/dpb_cuvid_decoder.o objs/core/dpb_cuvid_capture.o objs/core/FfmpegEncode.o objs/core/main.o objs/core/PushRtmp.o objs/core/PushVideoStream.o objs/core/video_http_server.o objs/core/push_rtmp.o objs/core/CvxText.o objs/core/config_manager.o objs/core/http_server.o objs/core/http_client.o objs/core/plate_reco.o objs/core/cuda_common.o objs/core/h264_codec.o objs/core/hard_decoder.o objs/tools/zmq_remote_show.o objs/tools/auto_download.o objs/tools/zmq_u.o objs/tools/deepsort.o objs/app_fall_gcn/fall_gcn.o objs/tensorRT/import_lib.o objs/tensorRT/onnx/onnx-ml.pb.o objs/tensorRT/onnx/onnx-operators-ml.pb.o objs/tensorRT/onnxplugin/onnxplugin.o objs/tensorRT/onnxplugin/plugin_binary_io.o objs/tensorRT/builder/trt_builder.o objs/tensorRT/infer/trt_infer.o objs/tensorRT/common/trt_tensor.o objs/tensorRT/common/cuda_tools.o objs/tensorRT/common/json.o objs/tensorRT/common/ilogger.o objs/tensorRT/common/cc_util.o objs/tensorRT/onnx_parser/NvOnnxParser.o objs/tensorRT/onnx_parser/OnnxAttrs.o objs/tensorRT/onnx_parser/ModelImporter.o objs/tensorRT/onnx_parser/builtin_op_importers.o objs/tensorRT/onnx_parser/LoopHelpers.o objs/tensorRT/onnx_parser/onnxErrorRecorder.o objs/tensorRT/onnx_parser/ShapeTensor.o objs/tensorRT/onnx_parser/onnx2trt_utils.o objs/tensorRT/onnx_parser/RNNHelpers.o objs/tensorRT/onnx_parser/ShapedWeights.o objs/app_retinaface/retinaface.o objs/app_arcface/arcface.o objs/ffhdd/cuvid_decoder.o objs/ffhdd/ffmpeg_demuxer.o objs/ffhdd/simple-logger.o# 同理处理 C 源文件(.c)
c_srcs := $(shell find src -name "*.c")
c_objs := $(patsubst %.c,%.o,$(c_srcs))
c_objs := $(subst src/,objs/,$(c_objs))# 处理 CUDA 源文件(.cu)
# CUDA 编译后通常用 .cuo 或 .o,这里用 .cuo 区分
cu_srcs := $(shell find src -name "*.cu")
cu_objs := $(patsubst %.cu,%.cuo,$(cu_srcs))
cu_objs := $(subst src/,objs/,$(cu_objs))# === 2. 项目路径定义(头文件、库文件等) ===# 定义一个基础路径变量,指向您的依赖库安装目录
lean := /home/dh/data/lean118# include_paths:列出所有需要 include 的头文件搜索路径
# \ 是 Makefile 中的续行符,表示下一行是当前行的延续
include_paths := src \src/core \src/tensorRT \src/tensorRT/common \$(lean)/protobuf3.11.4/include \$(lean)/opencv4.2.0/include/opencv4 \$(lean)/opencv4.2.0/include/opencv4/opencv2 \/usr/local/cuda-11.8/include \/usr/local/cuda \$(lean)/Video_Codec_SDK_11.1.5/Interface \/nvcodec/include \$(lean)/ffmpeg4.2/include \$(lean)/x264/include \/usr/local/cuda-11.8/samples/common/inc \$(lean)/TensorRT-8.6.1.6/include/ \/usr/include/freetype2/ \/ffhdd/# library_paths:列出所有需要链接的库文件(.a 或 .so)所在目录
library_paths := $(lean)/protobuf3.11.4/lib \$(lean)/opencv4.2.0/lib \/usr/local/cuda-11.8/lib64 \$(lean)/ffmpeg4.2/lib \$(lean)/Video_Codec_SDK_11.1.4/Lib/linux/stubs/x86_64 \/usr/local/cuda-11.8/targets/x86_64-linux/lib/ \/usr/lib/x86_64-linux-gnu \$(lean)/TensorRT-8.6.1.6/targets/x86_64-linux-gnu/lib \$(lean)/TensorRT-8.6.1.6/lib/ \$(lean)/cudnn/lib \/usr/local/lib \$(lean)/x264/lib# link_librarys:列出需要链接的库名(不带 lib 前缀和 .so/.a 后缀)
# 例如:-lopencv_core 表示链接 libopencv_core.so
link_librarys := opencv_core opencv_imgproc opencv_videoio opencv_imgcodecs \opencv_video opencv_highgui \nvinfer nvinfer_plugin nvparsers \cuda curand cublas cudart cudnn nvcuvid nvidia-encode \stdc++ protobuf \avcodec avformat avresample swscale avutil dl curl freetype# === 3. 转换路径为编译/链接选项 ===#因为 编译器(如 g++)需要 -I 前缀 才能识别头文件搜索路径。
# 将 include_paths 中的每个路径前加上 -I,变成编译器可识别的 include 选项
# 例如:-Isrc -Isrc/core ...
include_paths := $(foreach item,$(include_paths),-I$(item))# 库目录 前面要加 -L(大写 L)# 库文件名 前面要加 -l(小写 L)# 将 library_paths 中的每个路径前加上 -L,表示库搜索路径
# 例如:-L$(lean)/opencv4.2.0/lib ...
library_paths := $(foreach item,$(library_paths),-L$(item))# 将 link_librarys 中的每个库名前加上 -l
# 例如:-lopencv_core -lnvinfer ...
link_librarys := $(foreach item,$(link_librarys),-l$(item))# === 4. 设置编译和链接参数 ===# rpath:运行时库搜索路径,确保程序运行时能找到动态库
# 这里为每个库路径添加 -Wl,-rpath=路径
# -Wl, 表示将后面的参数传递给链接器(ld)
# $(run_paths) 会被链接时使用
run_paths := $(foreach item,$(library_paths),-Wl,-rpath=$(item))# C++ 编译标志
cpp_compile_flags := -std=c++11 -fPIC -g -fopenmp -w -O -DHAS_CUDA_HALF
# -std=c++11: 使用 C++11 标准
# -fPIC: 生成位置无关代码(用于共享库)
# -g: 生成调试信息
# -fopenmp: 启用 OpenMP 多线程
# -w: 禁用所有警告(不推荐生产环境)
# -O: 优化(注意:-O 不是标准选项,应为 -O2 或 -O3,可能是笔误)
# -DHAS_CUDA_HALF: 定义宏,表示支持 CUDA half 精度# 添加 include 路径到 C++ 编译标志
cpp_compile_flags += $(include_paths)# CUDA 编译标志(使用 nvcc)
cu_compile_flags := -std=c++11 -m64 -Xcompiler -fPIC -g -w \-gencode=arch=compute_89,code=sm_89 \-DHAS_CUDA_HALF
# -m64: 生成 64 位代码
# -Xcompiler -fPIC: 将 -fPIC 传递给主机编译器(g++)
# -gencode: 指定 GPU 架构(compute_89 表示计算能力 8.9,如 A100)
# -DHAS_CUDA_HALF: 同上# 添加 include 路径到 CUDA 编译标志
cu_compile_flags += $(include_paths)# 链接标志
link_flags := -pthread -fopenmp
# -pthread: 启用 POSIX 线程支持
# -fopenmp: 支持 OpenMP 并行# 将库路径、库名、rpath 添加到链接标志
link_flags += $(library_paths) $(link_librarys) $(run_paths)# === 5. 定义构建目标(Targets) ===# 默认目标(Makefile 第一个目标)
# 当执行 make 时,会构建 daihai_ai
daihai_ai : workspace/daihai_ai@echo finished# 注意:这里 workspace/daihai_ai 是实际要生成的可执行文件
# daihai_ai 是一个别名,执行后会输出 "finished"# === 6. 最终可执行文件的链接规则 ===
# 目标:workspace/daihai_ai
# 依赖:所有 .o 和 .cuo 文件
workspace/daihai_ai : $(cpp_objs) $(cu_objs) $(c_objs)
#输出 Link workspace/daihai_ai@echo Link $@ # $@ 表示目标名@mkdir -p $(dir $@) # 创建输出目录(workspace/)@g++ $^ -o $@ $(link_flags)
# $^ 表示所有依赖项(所有 .o 和 .cuo 文件)
# -o $@ 表示输出到目标文件
# $(link_flags) 包含所有链接选项# === 7. 编译规则(模式规则) ===# 编译 .cpp 文件为 .o
# objs/%.o : src/%.cpp
# 这是一个“模式规则”,% 是通配符
# 表示:任何在 objs/ 下的 .o 文件,都依赖于 src/ 下同名的 .cpp 文件
objs/%.o : src/%.cpp #只要有人需要一个在 objs/ 目录下的 .o 文件,就去 src/ 目录下找同名的 .cpp 文件来编译。 如Make 会把 % 匹配为:app_scrfd/scrfd
#$< 是第一个依赖,即 src/app_scrfd/scrfd.cpp
#输出:Compile CXX src/app_scrfd/scrfd.cpp@echo Compile CXX $< # $@ 是目标文件:objs/app_scrfd/scrfd.o# $(dir $@) 是它的目录部分:objs/app_scrfd/# mkdir -p 会递归创建目录(如果不存在) 即使是多层目录如 objs/a/b/c/ 也能自动创建@mkdir -p $(dir $@) # 创建 objs/xxx/ 目录(如有子目录)# -c:只编译,不链接# $<:源文件 → src/app_scrfd/scrfd.cpp# -o $@:输出到目标文件 → objs/app_scrfd/scrfd.o# $(cpp_compile_flags):你的编译选项,比如 -Iinclude -std=c++11 等@g++ -c $< -o $@ $(cpp_compile_flags)
# -c 表示只编译不链接 没有可执行文件
# $(cpp_compile_flags) 包含所有编译选项# 编译 .c 文件为 .o
# 虽然用 g++,但可以编译 C 文件
objs/%.o : src/%.c@echo Compile CXX $< # 这里写 CXX 不准确,应为 CC,但无大碍@mkdir -p $(dir $@)@g++ -c $< -o $@ $(cpp_compile_flags) # 仍使用 C++ 编译器# 编译 .cu 文件为 .cuo
# 使用 nvcc(NVIDIA CUDA 编译器)
objs/%.cuo : src/%.cu@echo Compile CUDA $<@mkdir -p $(dir $@)@nvcc -c $< -o $@ $(cu_compile_flags)
# -c 表示只编译不链接
# $(cu_compile_flags) 包含 CUDA 编译选项# 运行主程序
run : workspace/daihai_aicd workspace && ./run.sh
# 可以取消注释下面这行来直接运行带参数的程序
# @cd workspace && ./pro 1 911 "rtsp://admin:a123456789@192.168.0.123:554/cam/realmonitor?channel=9&subtype=0" "rtmp://127.0.0.1:1935/camera/911?id=911"# 调试:打印 include 路径(用于检查变量)
debug :@echo $(include_paths)# 清理:删除编译生成的文件
clean :@rm -rf objs workspace/daihai_ai# 删除编译对象目录 objs 和可执行文件
🌟 总结:Makefile 的构建流程
收集源文件 → cpp_srcs, cu_srcs, c_srcs
规划输出路径 → cpp_objs, cu_objs, c_objs
设置编译/链接参数 → 包含路径、库路径、编译选项
编译:.cpp → .o,.cu → .cuo(增量编译,高效!)
链接:所有 .o + .cuo → workspace/daihai_ai
运行或清理