这是什么?
Makefile 是一个自动化编译工具的“说明书”。
在开发大型软件项目时,通常会有成百上千个源代码文件。如果你每次改动一点代码都要手动输入命令去编译每一个文件,不仅低效,还容易出错。Makefile 的出现就是为了解决这个问题。
1. Makefile 的核心作用
- 自动化编译:只需输入一个简单的命令(通常是
make),它就会根据 Makefile 里的规则,自动调用编译器(如 gcc, g++)完成整个项目的构建。 - 增量编译(按需编译):这是它最强大的地方。Makefile 会检查文件的时间戳。它只编译那些被修改过的文件以及受其影响的文件,而不会浪费时间去重新编译没变动的部分。
- 管理依赖关系:它能清晰地定义文件之间的依赖关系。比如 A 文件引用了 B 文件的函数,当 B 改变时,Makefile 知道必须重新编译 A。
2. Makefile 的基本结构
Makefile 的核心逻辑由一系列“规则”组成,格式如下:
1 | 目标(target): 依赖文件(prerequisites) |
举个栗子:
假设你有一个项目,包含 main.c 和 tool.c 两个文件。
1 | # 最终目标是生成可执行文件 my_program |
3. 为什么现在还要学它?
虽然现在有很多现代化的构建工具(如 CMake, Bazel),但 Makefile 依然是基石:
- 工程必备:绝大多数 Linux 内核和开源项目(如 Redis, Nginx)都直接使用 Makefile。
- 跨语言通用:虽然它常用于 C/C++,但其实你可以用它管理任何文件的转换任务(比如自动压缩图片、编译 LaTeX 文档等)。
- CMake 的底层:在 Linux 环境下,CMake 通常也是先生成 Makefile,然后再调用
make来执行。
4. 总结:它的价值
| 维度 | 手动编译 | 使用 Makefile |
|---|---|---|
| 速度 | 每次都要全量编译,慢 | 仅编译变动部分,快 |
| 准确性 | 容易漏掉某个文件 | 严格按依赖逻辑,准 |
| 复杂度 | 命令又长又难记 | 一个 make 搞定 |
为啥python项目里也有?
按理说,Python 是解释型语言,不需要像 C/C++ 那样经历复杂的“编译”过程,但 Makefile 在 Python 项目中依然非常流行,原因在于:它把 Makefile 当作了一个“快捷指令集(Task Runner)”来使用。
在 Python 开发中,Makefile 主要解决的是“命令太长记不住”和“环境初始化太繁琐”的问题。
1. Python 项目中 Makefile 的常见用途
Python 项目虽然不编译,但有很多重复性的“脏活累活”:
- 环境管理:创建虚拟环境、安装依赖包。
- 清理垃圾:删除生成的
__pycache__、.pyc文件或构建产物。 - 质量检查:运行代码格式化(
black)、静态检查(flake8)、类型检查(mypy)。 - 运行测试:启动
pytest并生成覆盖率报告。 - 打包发布:构建 Docker 镜像或上传包到 PyPI。
2. 一个典型的 Python Makefile 示例
你可以看看你项目里的 Makefile,是不是很像下面这种结构:
1 | # 定义变量 |
这样一来,你只需要输入 make test 就能跑测试,输入 make clean 就能清理环境,而不需要记住后面那一串复杂的参数。
3. 为什么不用 Shell 脚本(.sh)?
既然只是快捷指令,为啥不用 .sh 脚本呢?
统一入口:Makefile 提供了一个标准的入口。不管新同事是用什么系统,只要看到 Makefile,就知道输入
make install就能开始干活。依赖逻辑:Makefile 依然可以处理依赖。例如,你可以规定:运行测试前必须先安装依赖。
1
2test: install
pytest当你执行
make test时,如果install没跑过,它会自动先跑install。习惯使然:在 Linux/Unix 世界里,
make是装机自带的,不需要额外安装工具。
4. 现代替代品
虽然 Makefile 很香,但现在 Python 社区也有一些更“原生”的替代方案:
- Invoke: 用纯 Python 写的任务执行器。
- Poetry/Pdm: 自带了部分脚本管理功能。
- Tox/Nox: 专门用于管理多版本 Python 测试环境。
项目实战
整体层级结构
1 | graph TD |
要点解析:
- 参数透传:Makefile 里的不同命令(如
enc_build)本质上是在调用build.sh时塞入了不同的“旗标”(Flag),比如-u。 - 环境准备:在
build.sh内部,它会先通过source加载环境,确保 Go 版本正确,并自动下载缺少的工具(如garble或grpcurl)。 - 版本溯源:无论走哪条路径,最终都会经过
VER_FLAGS注入。这意味着你拿到的任何一个二进制文件,都能通过内部变量追溯到它是哪次 Git 提交。 - 并行与依赖:Makefile 保证了“先清理、后编译”的逻辑顺序(例如
pythonlib依赖clean),而build.sh则负责具体的“体力活”(循环遍历目录并一个个编译)。
有了这张图,当你下次需要定位编译报错时,只需看报错阶段处于哪个层级,就能快速锁定是 Makefile 规则写错了,还是 Shell 脚本逻辑有问题。
生成其他项目的依赖包
注意,两个build.sh并不一样,idt的那个只是用来生成wheel文件的!生成完之后可以上传到内部的镜像仓库。
重点是这个语句:python3 setup.py bdist_wheel,其中setup.py自己写,一般是去指定版本号等,避免重复。
上传命令:[凭证环境变量] rsync [参数] [源文件] [协议]://[用户名]@[地址]/[目标模块]/
例如,RSYNC_PASSWORD=<密码> rsync a.txt rsync://[用户名]@[地址]/[目标模块]/
整个完整周期为:
1 | graph TD |
而使用侧只需要在其脚本中指定版本号,就能下载到最新的wheel包。一般就是通过前面的版本号,>=某个版本号,就会自动拉取到最新的时间戳对应的wheel包。
1 | graph TD |
整个项目的build.sh的流程
1 | flowchart TD |
这个 build.sh 脚本是整个 Go 项目的核心编译大脑。它的任务不仅仅是运行 go build,还包含了版本注入、代码混淆、覆盖率统计和工具链管理等工业级生产任务。
以下是该脚本的详细功能拆解:
1. 核心编译模式(三种模式)
脚本通过 getopts 接收参数(如 -c, -u),决定了程序以哪种方式“出生”:
| 模式 | 触发参数 | 使用的工具 | 核心目的 |
|---|---|---|---|
| 标准模式 | 默认 | go build |
正常的开发与生产环境运行。 |
| 代码混淆模式 | -u (encrypt) |
Garble | 防止逆向工程。通过混淆代码中的字面量和函数名,让别人拿到二进制文件也难以反编译。 |
| 覆盖率模式 | -c (coverage) |
Goc | 自动化测试。配合测试平台统计代码运行到了哪几行,通常用于 QA 或集成测试阶段。 |
2. 关键功能点解析
A. 动态版本注入 (VER_FLAGS)
脚本中有一段很长的 VER_FLAGS,利用了 Go 的 -ldflags 特性:
1 | -X sangfor.com/xdr/go-idt/internal/version.version=${PROJECT_VERSION} ... |
- 作用:在编译时,强行把当前代码的版本号、Git 提交记录(Commit ID)、编译日期等信息塞进 Go 的变量里。
- 效果:当你运行
./my_app -v时,它能准确报出自己是哪天在哪个分支编译的。
B. 自动化扫描与构建
脚本并没有写死要编译哪个文件,而是遍历了 cmd/ 和 tools/ 目录:
1 | apps=$(ls -d -- ${inDir}/*/) |
- 逻辑:它会自动寻找这两个文件夹下的每一个子目录。只要目录下有
main.go,它就会为其创建一个文件夹并完成编译。 - 好处:你每增加一个微服务或工具,只需在
cmd/下建个文件夹,脚本会自动识别并打包。
C. 工具链自给自足 (install_tools)
脚本会检查并安装 grpcurl(一个类似 curl 但用于调试 gRPC 的工具):
- 逻辑:如果
dist目录下没有这个工具,它会自动go install并在本地备份。这保证了任何开发环境只要运行一次编译,调试工具就全齐了。
3. 脚本执行流程图
- 环境检查:加载
devenvrc,指定使用 Go 1.21 版本。 - 清理与预备:运行
go mod tidy整理依赖。 - 判断路径:
- 如果带了
-u:安装并执行garble混淆编译。 - 如果带了
-c:从内部仓库(Nexus)下载goc并进行带统计信息的编译。 - 默认:执行
go build -ldflags "-s -w"(其中-s -w是为了减小生成的二进制体积,删掉调试符号)。
- 如果带了
- 产物输出:所有编译好的二进制文件会被统一放在
dist/目录下。
4. 总结:为什么要写这么复杂的脚本?
如果你只写一个 go build,你得到的是一个“裸”的程序。而通过这个脚本,你得到的是:
- 可追踪的(知道是谁在什么时候编译的)。
- 安全的(如果是发布给外部,代码已被混淆)。
- 可监控的(支持覆盖率测试)。
- 标准化的(所有开发者产出的二进制格式完全一致)。
你想知道在 Makefile 里是如何给这个脚本传递 -u 或 -c 参数的吗?(提示:这对应你之前看到的 enc_build 和 cov_build 目标)