makefile findstring_推荐几个 Makefile 进阶用法 (二)

f1c0191f419071e3c7b08b0407e67e4e.gif

作者简介:吴章金,十年 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 的 dirnamebasename 命令,它们是: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 的 dirbasename 跟 Shell 中的 dirnamebasename 有非常微妙的差异。如果理解成等价,那就很麻烦了,因为拿到的结果并不如预期。

对于文件,有如下等价关系:

e7ffa5f499cca7e395b0c05421701981.png

并且需要注意,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。

97a68e86c25d2eab3e659633a57604b9.gif
  • 推荐几个 Makefile 进阶用法 (一)

  • 在 Linux 下使用分屏提升工作效能

  • 大型 Git 仓库下载速度提升技巧

  • LXR 在线服务和搭建工具

  • 6 条 Git 实用技巧

  • 如何匹配字符或字符串的多次出现

  • Vim & Bash 常用快捷键

  • 上手 9 套工具,玩转二进制文件

0a6bcae150d3ccf051ac183be3f4dc11.png

扫码并+ tinylab

进Linux Lab用户群

泰  晓  科  技

16c5bb0c75e8b14919a4d9987c694787.png

db1639bc5616f5a1de07907b446acc83.png关注“泰晓科技”!点“在看”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值