
前阵子翻自己的项目目录发现散落着一堆 sh 脚本有 build.sh、test.sh、run.sh还有一个整天跑测试的 python 脚本。每个脚本里的命令各自为政参数写死到好几份想改一次路径得把所有文件翻个遍。后来我把整套流程统一成了 “Makefile python 脚本” 的组合Makefile 负责把命令整理成面板python 脚本负责真正干活的逻辑这一改不光命令行短了整个人也清爽了。这篇文章就是我的随手记把 Makefile 和 python 脚本怎么配合、哪些细节最容易踩坑、遇到问题怎么查一次性写清楚。特别适合那些写过脚本但还没系统整理过构建流程的人不管你是做嵌入式、后端还是测试这套组合都能让你少折腾很多。1. 为什么我最终选择了 Makefile Python 这个组合1.1 Makefile 管流程Python 管逻辑先说结论Makefile 不是只能拿来编译 C/C 程序它本质是一套“任务编排工具”我把它当项目的操作面板来用。什么叫操作面板就是你在终端敲一个make build、make test、make flash后面那一长串复杂的命令、参数、路径、环境变量全部被收纳进一个文件里不需要再背。python 脚本在这个组合里承担的是“业务逻辑”的角色。凡是涉及字符串处理、文件解析、网络请求、生成报告、异常重试这类稍复杂的活儿我不再写 shell 脚本而是用 python 来写。原因很简单shell 脚本处理文本行还行一旦遇到 JSON 解析、条件分支很多、或者要抛出带上下文的错误信息写起来非常憋屈维护起来更想骂人。很多人的疑问是Makefile 里也能直接写 python 一行命令啊为什么要分两个文件我的回答是职责边界。Makefile 里如果塞了大量 python 逻辑比如在 recipe 里写 for 循环、if 判断、awk 取字段这个文件很快就没法看了。把逻辑抽到.py文件里Makefile 只负责调用、传参、控制顺序两边都简单。1.2 三个让我痛下决心重组的真实场景第一个场景是设备老化测试。当时要跑一整夜的老化测试每十分钟采集一次设备状态如果中途进程挂掉还得自动恢复。用 shell 写了个 while 循环跑了一天发现日志时间戳格式不统一、失败重试逻辑有 bug又因为set -e和管道符搭配的问题中途悄悄断掉还找不到原因。后来整个重写成 python 脚本用argparse接参数用logging输出统一格式的日志配合一个 Makefile 目标来启动问题立刻少了一半。第二个场景是批量处理数据。我需要把几十个 CSV 文件里的数据做清洗、合并、画图。shell 脚本做这种事只能到处调用 awk、paste、gnuplot每次换个需求就得重写。用 python 的 pandas 和 matplotlib 处理起来顺手得多但每次敲python3 process.py --input xxx --output yyy也烦。用 Makefile 定义好make report脚本参数全部写在里面别人拿到项目跑一个命令就能出结果。第三个场景是交叉编译。嵌入式项目里要设置工具链路径、头文件路径、库路径还要根据不同的板子切换配置。这些用 Makefile 的变量来管理是最自然的因为 Makefile 本身就是为“依赖 命令 变量”设计的。python 脚本可以在编译之前做配置校验、检查依赖工具是否存在两者配合整个构建流程既清晰又灵活。2. 把 Makefile 当“操作面板”而不是构建工具2.1 先搞懂 Makefile 的三段式结构Makefile 的核心机制就三条目标、依赖、规则。用一句话解释就是如果“依赖”比“目标”新或者“目标”不存在就去执行“规则”里的命令。这句话听着简单但很多人没真正理解所以才会出现各种“make 没有指明目标并且找不到 makefile”的懵圈时刻。output.txt: input.txt python3 process.py input.txt output.txt上面这个例子里output.txt是目标input.txt是依赖下面是规则命令。如果input.txt比output.txt更新这条命令就会执行。这就是 Makefile 的增量判断逻辑。对编译任务来说它能做到“只重新编译改动的文件”对自动化任务来说它能做到“资源没变就不重复跑”。注意那个缩进规则命令前面必须是Tab 键不能用空格代替。这是所有新手必踩的坑后面我会专门讲。如果目标不是实际文件比如clean、test那要把它声明为“伪目标”告诉 make 不要去找同名文件。这一句一定要加否则哪天目录里真冒出一个叫clean的文件你这辈子都别想make clean成功了。.PHONY: clean test clean: rm -rf build/2.2 用变量、函数和模式规则把 Makefile 写“软”写 Makefile 最忌讳的就是到处写死路径。我见过有人把/home/xxx/project/src这种绝对路径直接写进规则里换台电脑就要全局替换。正确做法是把可变的东西全部抽成变量TOOLCHAIN_PATH : /opt/rv1106-toolchain CROSS_COMPILE : $(TOOLCHAIN_PATH)/bin/arm-rockchip-linux-gnueabihf- CC : $(CROSS_COMPILE)gcc CFLAGS : -I./include -Wall -O2这样想换工具链或者换编译器只需要改最上面几行。这里:表示立即展开表示递归展开平时用:更直观不容易出现变量互相引用的死循环。我省略之前还常用wildcard和patsubst这两个函数。wildcard用来收集满足条件的文件列表patsubst用来做文件名转换比如把src/*.c转成build/*.oSRC : $(wildcard src/*.c) OBJ : $(patsubst src/%.c, build/%.o, $(SRC))有了这两个函数加一个新源文件不需要改 Makefile重跑就自动纳入了。这种“懒人写法”才是真的效率提升。自动变量$、$、$^也建议记牢$是当前目标名$是第一个依赖$^是所有依赖的列表。配合模式规则能把编译规则收敛成一条build/%.o: src/%.c $(CC) $(CFLAGS) -c $ -o $这条规则的意思是任何build/xxx.o都靠编译src/xxx.c得到。你不需要为每个 C 文件写一条规则make 会自动匹配%。2.3 头文件路径和工程化参数怎么塞进去热词里有一组“makefile 头文件路径”这其实是编译场景经常遇到的问题。如果你写程序时#include foo.h但foo.h不在当前目录编译器就报找不到头文件。解决办法不是在源码里写一堆../../include而是在 CFLAGS 里加-I参数CFLAGS : -I./include -I./third_party/libfoo/include头文件路径越多CFLAGS 越长这时候 Makefile 的价值就越明显。同样如果是链接阶段报找不到库用-L指定库路径用-l指定库名LDFLAGS : -L./third_party/libfoo/lib -lfoo还有 pybind11、OpenCV 这类带pkg-config的库可以直接把输出引入 MakefileCFLAGS $(shell pkg-config --cflags opencv4) LDFLAGS $(shell pkg-config --libs opencv4)很多刚接触的人分不清 CMake 和 Makefile 的区别。简单说Makefile 是一种规则文件CMake 是一种生成规则文件的工具。CMake 读 CMakeLists.txt然后帮你在本地生成对应的 Makefile 或 Ninja 文件。如果你的项目只需要在 Linux 上手动编译手写 Makefile 完全够用如果项目要跨平台、要自动找依赖库、要给不同模式生成不同构建配置那用 CMake 管理再让它生成 makefile更省心。不要陷入“哪个更强”的争论这两个根本不是同一层的东西。3. Python 脚本在这套组合里承担什么角色3.1 什么时候别硬写 Shell我判断该不该用 python 标准很简单如果这个任务需要用grep、awk、sed组合出三条以上的命令才能完成或者需要判断多种异常情况那就别用 shell 硬顶。举几个典型场景解析 JSON 配置文件、扫描目录下所有文件并按照规则重命名、从日志里提取指标并生成报表、批量请求某个服务接口并处理返回结果。这些任务用 python 写起来是降维打击因为标准库里的json、argparse、logging、pathlib已经把底层的脏活处理好了。我之前遇到过一个实际问题要让设备老化测试全自动执行需求是每隔一段时间采集设备温度、CPU占用率和日志如果某一次采集失败要重试三次全部结束之后生成一份 CSV 报告并且把异常时段单独列出来。用 shell 写这套逻辑代码量少说两百行而且到处是if [ $? -ne 0 ]这种手写错误处理越写越心虚。换成 python五十行内解决还用上了try/except和with open这种语义清晰的写法。3.2 Makefile 与 Python 脚本之间的参数传递这两者结合最核心的技术点就是参数传递。我的经验是分三种方式按场景选用。第一种命令行参数。Python 脚本用argparse定义好参数Makefile 里调用时直接拼接test: python3 scripts/run_test.py --device $(DEVICE) --duration $(DURATION)调用时用make test DEVICE192.168.1.10 DURATION3600这样不用改文件就能改变运行参数。注意 Makefile 变量可以用命令行传入但前提是 Makefile 里没有用override强制赋值。第二种环境变量。在 Makefile 里export一个变量python 里用os.environ读取export LOG_DIR : ./logs run: python3 scripts/run.pyimport os log_dir os.environ.get(LOG_DIR, ./logs)这种方式适合传递运行环境相关的配置比如日志目录、临时文件位置。好处是即使不走 Makefile直接在终端里设置环境变量再执行 python脚本也能正常工作。第三种标准输入和文件。python 脚本从 stdin 读数据或者 Makefile 在调用前生成一个临时配置文件python 去读取。这也是最灵活的方式适合参数特别多的时候import sys for line in sys.stdin: handle(line.strip())3.3 一个真实案例设备老化测试全自动执行脚本我来展示一个我自己实际用过的简化版本让大家直观感受这套组合怎么落地。先看 Makefile 部分它负责定义参数、启动测试、查看日志DEVICE : 192.168.1.20 ROUNDS : 1000 INTERVAL : 60 LOG_DIR : logs .PHONY: start stop log start: python3 scripts/aging_test.py --device $(DEVICE) --rounds $(ROUNDS) --interval $(INTERVAL) --log-dir $(LOG_DIR) stop: touch $(LOG_DIR)/stop.flag log: tail -f $(LOG_DIR)/aging_*.log这里我加了一个stop目标用创建标记文件的方式让正在跑的 python 脚本优雅退出而不是直接 kill 进程可以保证日志完整写入。再看 python 脚本的核心逻辑import argparse import json import logging import os import sys import time from pathlib import Path def parse_args(): parser argparse.ArgumentParser(description设备老化测试) parser.add_argument(--device, requiredTrue) parser.add_argument(--rounds, typeint, default1000) parser.add_argument(--interval, typeint, default60) parser.add_argument(--log-dir, default./logs) return parser.parse_args() def collect(device: str) - dict: # 这里对应真实的采集动作 return {device: device, temp: 62, cpu: 30} def run(): args parse_args() Path(args.log_dir).mkdir(parentsTrue, exist_okTrue) logging.basicConfig( filenameos.path.join(args.log_dir, faging_{args.device.replace(., _)}.log), levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, ) stop_file Path(args.log_dir) / stop.flag if stop_file.exists(): stop_file.unlink() for i in range(1, args.rounds 1): if stop_file.exists(): logging.warning(检测到停止标记准备退出) break try: data collect(args.device) logging.info(json.dumps(data)) except Exception: logging.exception(第 %d 轮采集失败, i) time.sleep(args.interval) if __name__ __main__: run()这个脚本的好处是结构清晰参数统一从命令行来日志格式统一异常被捕获并记录完整堆栈。Makefile 在这里就是一个薄薄的壳它负责把参数默认值和命令行输入对接起来。真正干活的是 python但使用者不需要接触 python他只需要敲make start。4. 常见问题排查与避坑实录4.1 “make 没有指明目标并且找不到 makefile”到底怎么回事这个报错信息有两种常见前缀。第一种是make: *** No targets specified and no makefile found. Stop.第二种是make: *** No rule to make target xxx. Stop.。这两种我都踩过原因和处理方法完全不同。第一种英文提示是“没有指定目标也找不到 makefile”。这通常意味着你进入了错误的目录或者文件名不对。make 默认查找的文件名是GNUmakefile、makefile和Makefile推荐用Makefile这个大小写命名的版本因为它更符合大多数项目的约定。如果文件叫build.mk这种自定义名字就必须用make -f build.mk来指定。我还遇到过文件在config/子目录里而你在根目录执行 make自然找不到。第二种是 make 找到了主 Makefile但找不到你指定的目标。常见原因是你想执行make clean但 Makefile 里并没有clean这个目标。解决方法是先执行make help或者直接查看 Makefile 里有哪些目标。4.2 Tab 键、退出码、环境变量这三座大山第一座大山是 Tab 键。Makefile 的规则命令必须以 Tab 开头不能用空格否则报missing separator. Stop.。这个问题的经典之处在于用编辑器把 Tab 自动替换成空格后肉眼看着没有任何问题但 make 就是不认。我的处理办法是在编辑器里开启“显示空白字符”并且把这类文件固定用 4 个空格做缩进的标准关掉确保文件里保留真实的 Tab。第二座大山是退出码。make 执行 recipe 时每一条命令是独立的 shell 进程。如果某一条命令返回非零退出码make 默认就停下来报错这对构建流程是合理的但对某些场景就很烦。比如 python 脚本明明执行完了只是返回了1make 就觉得失败了。我的建议是python 脚本的入口函数里用sys.exit(0)明确告诉 make 成功如果某条命令是你知道可能失败但不想中断的可以在命令后面加|| true表示“失败也不当回事”但这属于有意的逃生舱不要滥用。第三座大山是环境变量。make 本身有一套变量机制recipe 里的每条命令又是独立 shell你在这个 recipe 里export的环境变量下一个 recipe 里是看不到的。如果你要让某个变量对所有 recipe 生效要在 Makefile 顶层export VAR : xxx。另外make 默认不会自动读取你 shell 里的.bashrc定义除非用$(shell ...)去取或者显式在 Makefile 里设置。这个细节掌握不好就会出现“终端里手动执行 python 正常一用 make 就报找不到模块”的诡异现象。4.3 我用这套组合整理出来的五个实用习惯习惯一做一个help目标。把自己项目的所有快捷命令列出来让任何新人拿到项目后执行make help就能了解全貌help: grep -E ^[a-zA-Z_-]: Makefile | sed s/:// | sort习惯二默认目标放最常用的命令。很多人喜欢在第一个目标写all但如果你的项目主要任务是运行测试直接把test放在最前面这样别人敲make就会执行最常用的操作而不是陷入“默认目标到底是啥”的困惑。习惯三在 recipe 里加前缀隐藏命令回显。默认 make 会打印每条命令本身加之后只打印命令输出hello: echo hello, makefile and python习惯四把公共参数提到文件顶部并加注释。尤其是路径、设备地址、版本号这些容易变的东西集中放在前面而不是散落在各个 recipe 里。这个习惯能救你于水火的时刻是半年后你回头改代码忘了当时设的 IP结果顶部几行注释直接帮你找回记忆。习惯五对于多步骤流水线在 Makefile 里定义中间目标和级联依赖。比如make all依赖prepare、build、test三个目标每个目标依赖各自的前置这样整个流程既严格有序又允许你单独执行其中某一步。all: prepare build test prepare: python3 scripts/prepare.py build: python3 scripts/build.py test: python3 scripts/test.py这种写法的好处是make test可以单独跑make build可以单独跑make all按顺序跑一遍。如果你在 prepare 阶段已经生成过数据再次执行make all时只要数据没变prepare 就会跳过这又是 Makefile 的增量判断在起作用。我个人在实际操作中最深的一个体会是Makefile 和 python 的配合与其说是一门技术不如说是一个“项目管理习惯”。Makefile 负责把复杂度藏在背后python 负责把真正的逻辑表达清楚两者合在一起项目的入口变得极其简单而内部又可以极其复杂。每当你发现一个流程要重复执行、并且参数老是记不住的时候就是该把这条流程收进 Makefile 的时候了。写下来、封装好、下次一键执行这种正向反馈会让你越来越愿意把工作流程做沉淀。最后再分享一个小技巧给 Makefile 里每个目标都写一行注释说明它是干嘛的这样回头看的时候你不需要重新读整份文件才能回想起来当时的设计思路。工具本身不复杂复杂的是如何让工具替你省掉重复劳动。