先说背景。手上有个 imx6ull 的板子,之前一直用 Buildroot 一键出镜像,日子过得挺舒服。最近接了个小活,要在一个裸的交叉编译环境里单独编几个驱动模块和一个测试程序,人家给了个 SDK,里面 Makefile 一大堆,我打开一看,满屏的 $(CC)、$@、$<、:=、?=,头皮发麻。
说白了,我以前的水平就是"抄一个能跑的 Makefile,改改文件名,能编过就不管了"。这回跑不动了,只能硬着头皮从头捋。下面就是我这几天的折腾记录,写给和我一样"只会抄不会写"的朋友。
一、先搞清楚 Makefile 到底在干嘛
一句话:Makefile 就是告诉 make 这个工具"什么文件依赖什么文件,依赖变了就重新生成"。
类比一下,你做饭。菜谱上写"番茄炒蛋需要番茄、鸡蛋,番茄切好之后才能下锅"。哪天番茄换了新的,那就得重炒一遍;番茄没变,鸡蛋也没变,那这盘菜就不用动。make 干的就是这个"看谁变了就重做哪一步"的活。
涉及的知识点,先摆几个最基本的:
1、规则的基本样子
makefile
目标: 依赖
命令
注意命令前面必须是 Tab,不能是空格。这一条坑了无数人,包括我。
2、三个自动变量
$@:目标文件名$<:第一个依赖文件名$^:所有依赖文件
3、四种赋值
makefile
A = 1 # 递归展开,用到时才展开
B := 1 # 立即展开,定义时就定死
C ?= 1 # 没定义过才赋值
D += 1 # 追加
4、伪目标 .PHONY,防止目录里正好有个同名文件导致规则不执行。
这些书上都有,我原来也"看过",但没真用过。下面上真实过程。
二、解决过程
1、先写个最小的,跑通再说
我在 ~/work/mk_demo 下建了三个文件:main.c、foo.c、foo.h。
先写一个最土的 Makefile:
makefile
hello: main.o foo.o
gcc -o hello main.o foo.o
main.o: main.c foo.h
gcc -c main.c -o main.o
foo.o: foo.c foo.h
gcc -c foo.c -o foo.o
敲 make,输出:
css
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o
gcc -o hello main.o foo.o
跑起来了。然后我 touch foo.h 再 make,两个 .o 都重新编了,说明依赖写对了。这一步让我第一次真切感觉到"依赖关系"不是摆设。
2、换成变量和自动变量,把它写"像样"一点
makefile
CC = gcc
CFLAGS = -Wall -O2
TARGET = hello
OBJS = main.o foo.o
$(TARGET): $(OBJS)
$(CC) -o $@ $^
%.o: %.c foo.h
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f $(OBJS) $(TARGET)
这里 %.o: %.c 是模式规则,$@ 是目标,$< 是第一个依赖,$^ 是全部依赖。make clean 也能用了。
到这一步我还挺得意,觉得 Makefile 不过如此。
3、换成交叉编译,翻车开始
SDK 里要用 arm-linux-gnueabihf-gcc,我就把 CC = gcc 改成 CC = arm-linux-gnueabihf-gcc,然后 make。
报错:
go
make: arm-linux-gnueabihf-gcc: 命令未找到
哦,交叉工具链没加到 PATH。export PATH=$PATH:/opt/gcc-arm-linux-gnueabihf/bin,再 make,编过了。file hello 一看是 ARM 的,没问题。
然后我想试试 make CC=gcc 覆盖一下,编个本机版本调试用。结果发现没生效,还是交叉编译器。
这里就是我第一个真正的弯路。原因是 CC = gcc 这种写法,在命令行上传 CC=gcc 其实是能覆盖的......我试了半天还是不行呀,折腾了好久才反应过来:我那个 Makefile 里写的是 CC := arm-linux-gnueabihf-gcc,而命令行变量本来就能覆盖任何赋值方式------问题不在这。
真正的问题是:我后来又在文件末尾加了一行 CC = gcc,把前面的覆盖了。make 里同名变量后面定义的会盖掉前面的。删掉那行,make CC=gcc 立刻生效。
这个错误特别蠢,但我确实卡了大半天。教训是:Makefile 里同一个变量别到处赋值,尤其别在末尾随手补一行。
4、头文件依赖,又栽一次
代码里 foo.c 也 include 了 foo.h。我改 foo.h 里的一个宏,make 之后发现 foo.o 没重编,只有 main.o 重编了。
原因很简单:我的模式规则写的是 %.o: %.c foo.h,这条规则只对 main.o 和 foo.o 都成立------等等,其实它对两个都成立啊。问题在哪?
问题在 foo.c 里 include 的头文件不止 foo.h,还有一个 config.h,而我的规则里根本没写 config.h。make 不知道 foo.o 依赖 config.h,自然不重编。
手动把所有头文件列进依赖,文件一多就崩溃。正确做法是用 -MMD -MP 让编译器自动生成依赖文件:
makefile
CFLAGS += -MMD -MP
DEPS = $(OBJS:.o=.d)
-include $(DEPS)
-MMD 生成 .d 文件记录头文件依赖,-include 把 .d 文件包含进来。这样改任何头文件都能正确触发重编。这一招是 SDK 里学来的,之前完全不知道。
5、清理和目录
最后我把源码挪进 src/,输出放 build/,规则改成:
makefile
SRCS = $(wildcard src/*.c)
OBJS = $(patsubst src/%.c,build/%.o,$(SRCS))
wildcard 取文件列表,patsubst 做路径替换。这里要注意 build/ 目录得先存在,否则 gcc -o build/main.o 会报"没有那个目录"。加一条:
makefile
$(OBJS): | build
build:
mkdir -p build
那个 | 是 order-only 依赖,表示"build 目录得先有,但它变了不用重编目标"。这个语法我原来见都没见过。
三、结果和遗留
现在这个 Makefile 能用了:改源码、改头文件都能正确重编,make clean 干净利落,交叉和本机编译靠命令行传 CC 切换。
遗留问题也有:还没做多目录递归,SDK 里那种 make -C subdir 的写法我还没完全吃透;另外 $(shell ...) 和条件判断 ifeq 我只看了没敢用,怕再翻车。
参考资料主要是 GNU make 官方手册,还有 SDK 里那份别人写的 Makefile,我一行一行对着猜的。感谢写那份 Makefile 的前辈,虽然不认识,但真是无私奉献。
我这水平也就刚入门,上面要是有说错的地方,欢迎留言指正,别客气。