作者简介:吴章金,十年 Linux 研发经验,Linux Committer,前魅族内核团队技术总监。热门开源书《C 语言编程透视》作者。
版权声明:本文最先发表于 “泰晓科技” 微信公众号,欢迎转载,转载时请在文章的开头保留本声明。
Makefile 调试与跟踪方法一览
Debugging
$ make --debug xxx
展开整个 make 解析和执行 xxx 的过程。
Tracing
$ make --trace xxx
展开 xxx
目标代码的执行过程,有点像 Shell 里头的 set -x
。该功能在 make 4.1 及之后才支持。
Logging
$(info ...)
$(warning ...)
$(error ...)
error
打印日志后立即退出,非常适合已经复现的错误。
Environment dumping
$ make -p xxx > xxx.data.dump
打开 xxx.data.dump
找到 xxx 的位置可以查看相关变量是否符合预期。
Makefile 与 Shell 中的文件名处理差异
Makefile 中有类似 Shell 的 dirname
和 basename
命令,它们是:dir
, basename
, notdir
,但是用法有差异,千万别弄混,下面来一个对比。
$ cat Makefile
makefile:
@echo $(dir $a)
@echo $(basename $a)
@echo $(notdir $a)
shell:
@echo $(shell dirname $a)
@echo $(shell basename $a)
$ make makefile a=/path/to/abc.efg.tgz
/path/to/
/path/to/abc.efg
abc.efg.tgz
$ make shell a=/path/to/abc.efg.tgz
/path/to
abc.efg.tgz
$ make makefile a=/path/to/
/path/to/
/path/to/
$ make shell a=/path/to/
/path
to
$ make makefile a=/path/to
/path/
/path/to
to
通过对比,可以看到,Makefile 的 dir
和 basename
跟 Shell 中的 dirname
和 basename
有非常微妙的差异。如果理解成等价,那就很麻烦了,因为拿到的结果并不如预期。
对于文件,有如下等价关系:
并且需要注意,Makefile 的 dir
取到的目录带有 /
后缀,而 Shell 的 dirname
结果不带 /
。对于目录,两者的认知千差万别,Makefile 的 dir
和 basename
拿到的都是目录,而 Shell 能够拆分出父目录和字目录的文件名。如果要对齐到 Makefile,用 dir
和 notdir
起到类似 Shell dirname
和 basename
的效果,得先 strip 掉后面的 '/'。
下面改造一下:
$ cat Makefile
makefile:
@echo $(patsubst %/,%,$(dir $(patsubst %/,%,$a)))
@echo $(notdir $(patsubst %/,%,$a))
shell:
@echo $(shell dirname $a)
@echo $(shell basename $a)
$ make makefile a=/path/to/abc.efg.tgz
/path/to
abc.efg.tgz
$ make shell a=/path/to/abc.efg.tgz
/path/to
abc.efg.tgz
$ make shell a=/path/to/
/path
to
$ make makefile a=/path/to/
/path
to
可以看到,改造完以后,结果跟 Shell 结果对齐了。
在 Makefile 表达式中使用逗号和空格变量
逗号和空格是 Makefile 表达式中的特殊符号,如果要用它们的愿意,需要特别处理。
empty :=
space := $(empty) $(empty)
comma := ,
在 Makefile 中对软件版本号做差异化处理
Makefile 通常需要根据软件版本传递不同的参数,所以经常需要对软件版本号做比较。
例如,在 Linux 4.19 之后删除了 oldnoconfig,并替换为了 olddefconfig,所以之前用到的 oldnoconfig 在新版本用不了,直接改掉老版本又用不了,得做差异化处理。
大家觉得应该怎么处理呢?先思考一下再看答案吧。
下面贴出关键片段:
LINUX_MAJOR_VER := $(subst v,,$(firstword $(subst .,$(space),$(LINUX))))
LINUX_MINOR_VER := $(subst v,,$(word 2,$(subst .,$(space),$(LINUX))))
ifeq ($(shell [ $(LINUX_MAJOR_VER) -lt 4 -o $(LINUX_MAJOR_VER) -eq 4 -a $(LINUX_MINOR_VER) -le 19 ]; echo $$?),0)
KERNEL_OLDDEFCONFIG := oldnoconfig
else
KERNEL_OLDDEFCONFIG := olddefconfig
endif
类似地,如果要同时兼容不同版本的 GCC,得根据 GCC 版本传递不同的编译选项,也可以像上面这样去做识别,Linux 源码下就有很多这样的需求。
不过它用了 try-run
的方式实现了一个 cc-option-yn
(见 linux-stable/scripts/Kbuild.include
),它是试错的方式,避免了堆积大量的判断代码,不过这里用的版本判断不多,而且调用这类 target 开销较大,没必要,直接加判断即可。
需要注意的是,考虑到版本号命名的潜在不一致性,比如说,后面加个 -rc1
,再加点别的什么,判断的复杂度会增加不少,所以,这类逻辑可以替换为其他方式,比如说,这里可以直接去 linux-stable/scripts/Makefile
下用 grep 查询 olddefconfig
是否存在:
KCONFIG_MAKEFILE := $(KERNEL_SRC)/scripts/kconfig/Makefile
KERNEL_OLDDEFCONFIG := olddefconfig
ifeq ($(KCONFIG_MAKEFILE), $(wildcard $(KCONFIG_MAKEFILE)))
ifneq ($(shell grep olddefconfig -q $(KCONFIG_MAKEFILE); echo $$?),0)
ifneq ($(shell grep oldnoconfig -q $(KCONFIG_MAKEFILE); echo $$?),0)
KERNEL_OLDDEFCONFIG := oldconfig
else
KERNEL_OLDDEFCONFIG := oldnoconfig
endif
endif
endif
修改默认执行目标的简单方法
如果不指定目标直接敲击 make 的话,Makefile 中的第一个目标会被执行到。这个是比较自然的逻辑,但是有些情况下,比如说,在代码演化以后,如果需要调整执行目标的话,得把特定目标以及相应代码从 Makefile 中搬到文件开头,这个改动会比较大,这个时候,就可以用 Makefile 提供的机制来修改默认执行目标。
来看看上面那个例子:
$ make -p | grep makefile | grep -v ^#
.DEFAULT_GOAL := makefile
makefile:
可以看到,makefile
被赋值给了 .DEFAULT_GOAL
变量,通过 override
这个变量,就可以设置任意的目标了,把默认目标改为 shell
看看。
$ make -p .DEFAULT_GOAL=shell a=/path/to/abc.efg.tgz | grep ^.DEFAULT_GOAL
.DEFAULT_GOAL = shell
确实可以改写,这个要永久生效的话,直接加到 Makefile 中即可:
override .DEFAULT_GOAL := shell
检查文件是否存在的两种方法
在 Makefile 中,通常需要检查一些环境或者工具是否 Ready,检查文件是否存在的话,可以用 wildcard
展开再匹配,也可以用 Shell 来做判断。
ifeq ($(TEST_FILE), $(wildcard $(TEST_FILE)))
$(info file exists)
endif
ifeq ($(shell [ -f $(TEST_FILE) ]; echo $$?), 0)
$(info file exists)
endif
第二种方法比较自由,可以扩展用来检查文件是否可执行,也可以调用 grep 做更多复杂的文本内容检查。在复杂场景下,通过第二种方法调用 Shell 是比较好的选择。
如何类似普通程序一样把目标当变量使用
如果执行 make test-run arg1 arg2
想达到把 arg1 arg2
作为 test-run
目标的参数这样的效果该怎么做呢?可以用 eval
指令,它能够动态构建编译目标。
通过 eval 指令把 arg1 arg2
这两个目标变成空操作,即使有 arg1 arg2
这样的目标也不再执行, 然后执行 test-run
运行。
大概实现为:
$ cat Makefile
# Must put this at the end of Makefile, to make sure override the targets before here
# If the first argument is "xxx-run"...
first_target := $(firstword $(MAKECMDGOALS))
reserve_target := $(first_target:-run=)
ifeq ($(findstring -run,$(first_target)),-run)
# use the rest as arguments for "run"
RUN_ARGS := $(filter-out $(reserve_target),$(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)))
# ...and turn them into do-nothing targets
$(eval $(RUN_ARGS):;@:)
endif
test-run:
@echo $(RUN_ARGS)
$ make test-run test1 test2
这个的实际应用场景有,比如说想在外面的目标中调用内核的编译目标,通常得进入内核源码,再执行 make target
,可能得写很多条这样的目标:
kernel-target1:
@make target1 -C /path/to/linux-src
kernel-target2:
@make target2 -C /path/to/linux-src
有了上面的支持,就可以实现成这样:
kernel-run:
@make $(arg1) -C /path/to/linux-src
使用时也不复杂,内核的各种目标都可以作为参数传递进去:
$ make kernel-run target1
$ make kernel-run target2
虽然说,上述 arg1,也可以这样写:
$ make kernel-run arg1=target1
$ make kernel-run arg1=target2
但是在使用效率上明显不如前者来得直接。
Makefile 实例模板
本文的内容大部分都汇整到了 Linux Lab: https://gitee.com/tinylab/linux-lab/tree/master/examples/makefile/template。
推荐几个 Makefile 进阶用法 (一)
在 Linux 下使用分屏提升工作效能
大型 Git 仓库下载速度提升技巧
LXR 在线服务和搭建工具
6 条 Git 实用技巧
如何匹配字符或字符串的多次出现
Vim & Bash 常用快捷键
上手 9 套工具,玩转二进制文件
扫码并+ tinylab
进Linux Lab用户群
泰 晓 科 技
关注“泰晓科技”!点“在看”