11 个 Makefile 实战技巧

本文首次发表在 11 个 Makefile 实战技巧 -- 泰晓科技linux

过去数月,笔者一直在重构开发了数年的开源项目:Linux Lab(Linux 内核实验室)。git

在开发过程当中,须要跟 Makefile 打交道,完善功能,优化速度,提高体验。shell

数月下来,积累了很多 Makefile 实战技巧。回过来看,以前对 Makefile 的熟知程度只能说是幼儿园托班水平 ;-)bash

本文篇幅较长,请先看大纲。建议从大纲中选择感兴趣的部分阅读(文末有彩蛋 ^_^):ide

1. 当即赋值(:=)和延迟赋值(=)
2. 变量赋值 和 目标执行 之间的时序关系
3. 如何获取 Make 传递的全部参数和编译目标
4. Makefile 调试与跟踪方法一览
5. Makefile 与 Shell 中的文件名处理差别
6. 在 Makefile 表达式中使用逗号和空格变量
7. 在 Makefile 中对软件版本号作差别化处理
8. 修改默认执行目标的简单方法
9. 检查文件是否存在的两种方法
10. 如何相似普通程序同样把目标当变量使用
11. Makefile 实例模板
复制代码

本文汇总了诸多 Makefile 进阶用法,便于提高 Makefile 阅读和编写效率。工具

当即赋值(:=)和延迟赋值(=)

  • :=: 强制按前后顺序执行,当即赋值。
  • =:赋值的结果会等到整个路径执行完再决定,后面的会覆盖前面的,延迟赋值。

按照常规逻辑,建议默认选用 ":="。优化

实例以下:ui

$ cat Makefile

a = foo
b1 := $(a) bar
b2 = $(a) bar
a = xyz

all:
	@echo b1=$(b1)
	@echo b2=$(b2)

$ make
b1=foo bar
b2=xyz bar
复制代码

变量赋值 和 目标执行 之间的时序关系

这里再看看变量赋值和编译目标之间的关系,以及不一样的变量传递和设置方式。this

先看看一般可能会传递参数的方式,你们以为哪一个会生效呢?spa

$ make a=b target
$ make target a=b
$ a=b make target
$ export a=b && make target
复制代码

另外,这种状况下,target1 和 target2 打印的变量同样吗?

a = aaa

test1:
	echo $a

a = bbb

test2:
	echo $a
复制代码

下面看一个案例(注意:target 下命令缩进必须是一个 TAB)。

$ cat Makefile

a ?= aaa
b := $(a)
c = $(a)

a_origin = $(origin a)
b_origin = $(origin b)
c_origin = $(origin c)

all:
	@echo all:$(a)
	@echo all:$(b)
	@echo all:$(c)
	@echo all:$(a_origin)
	@echo all:$(b_origin)
	@echo all:$(c_origin)

a = bbb
b := $(a)
c = $(a)

test1:
	@echo test1:$(a)
	@echo test1:$(b)
	@echo test1:$(c)
	@echo test1:$(a_origin)
	@echo test1:$(b_origin)
	@echo test1:$(c_origin)

a = ccc
b := $(a)
c = $(a)

test2:
	@echo test2:$(a)
	@echo test2:$(b)
	@echo test2:$(c)
	@echo test2:$(a_origin)
	@echo test2:$(b_origin)
	@echo test2:$(c_origin)

a = ddd
复制代码

看看执行状况。

关于 变量赋值 和 目标中的变量引用 的顺序

首先,执行默认 target,也就是第一个出现的 target,这里是 "all":

$ make
all:ddd
all:ccc
all:ddd
all:file
all:file
all:file
复制代码

比较奇怪的是?为何 "all" 目标恰好在这三条以后,却拿到了 ddd, ccc 和 ddd 呢?

a ?= aaa
b := $(a)
c = $(a)
复制代码

为何不是 aaa, aaa 和 aaa 呢?

接着,执行 test1, test2:

$ make test1
test1:ddd
test1:ccc
test1:ddd
test1:file
test1:file
test1:file

$ make test2
test2:ddd
test2:ccc
test2:ddd
test2:file
test2:file
test2:file
复制代码

发现,test1, test2 都同样?因此,结论是,Makefile 中全部变量赋值的语句在全部 target 以前完成,跟变量赋值与 target 的相对位置无关。

另外,咱们能够看到 b 没有跟上 c 的节奏,拿到 ccc 就再也不跟 c 同样去拿最后设置的 ddd 了,体现了 “:=” 的 “当即赋值”,而 c 一直等到了 Makefile 最后的 a。另外,三个变量最后的值都是文件内部赋值,因此 origin 是 file.

经过命令行赋值

$ make a=fff
all:fff
all:fff
all:fff
all:command line
all:file
all:file
复制代码

发现命令行覆盖了 Makefile 中全部的变量赋值,a 的优先级很高。

$ make b=fff
all:ddd
all:fff
all:ddd
all:file
all:command line
all:file
复制代码

因为 a 和 c 没用引用 b,因此这里只有 b 发生了变化。

$ make c=fff
all:ddd
all:ccc
all:fff
all:file
all:file
all:command line
复制代码

一样,a 和 b 没有引用 c,只有 c 发生了变化。

经过环境变量赋值

$ a=xxx make
all:ddd
all:ccc
all:ddd
all:file
all:file
all:file
复制代码

发现并无生效,仍是用的 make 的内部赋值语句。

$ a=xxx make -e
all:xxx
all:xxx
all:xxx
all:environment override
all:file
all:file
复制代码

确实都改了,因此要让环境变量生效,得给 make 传递 -e

$ b=xxx make -e
all:ddd
all:xxx
all:ddd
all:file
all:environment override
all:file
复制代码

这个的效果一样:

$ export b=fff
$ make -e
all:ddd
all:fff
all:ddd
all:file
all:environment override
all:file
复制代码

只是建议不要随便用 -e,万一有人在 .bashrc 或者 .profile 提早 export 了一个环境变量,本身没有主动设置的话,可能就会怀疑人生了,程序行为可能会出人意料而很难 debug。

环境变量和命令行哪一个优先

$ b=xxx make -e b=yyy
all:ddd
all:yyy
all:ddd
all:file
all:command line
all:file
复制代码

能够看到 命令行 优先。

小结一下:

  • 全部变量语句的执行在 target 下的语句以前(每一个 target 所属语句有一个 TAB 的缩进)。
  • 变量 override 优先级:command line > environment override > file

最后布置一个小做业?这个的结果是什么呢?

$ b=xxx make -e b=yyy all b=zzz test2 b=mmm
复制代码

如何获取 make 传递的全部参数和编译目标

先来看看这样一个问题:

$ make test1 test2 test3 a=123 b=456
复制代码

如何在 Makefile 中获取 make 命令后面的全部参数呢?

在 Shell 脚本里头这个是很经常使用的,参数列表:$1, $2, $3, $4 ... $@

一样地,在 Makefile 中有这样的需求,好比说想看看到底有没有传进来某个参数,根据参数不一样作不同的动做。

make 后面的参数有两种类型,一种是命令行变量,一种是编译目标。

这两个分别存放在 MAKEOVERRIDESMAKECMDGOALS 变量中。

判断有没有传递某个编译目标,能够这么作:

ifeq ($(filter test1, $(MAKECMDGOALS)), test1)
    do something here
endif
复制代码

上述代码实际是也至关于能够用来把一些变量赋值放到目标相关的代码块中。这个能够大幅提高大型 Makefile 的执行效率,在执行特定的目标时,不去执行无关的代码块。

判断有没有传递某个参数,能够这么作:

ifeq ($(origin a), command line)
    do something here
endif
复制代码

固然,也能够从 MAKEOVERRIDES 中作 findstring 检查,只是没有用 origin 来得简单。

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 有很是微妙的差别。若是理解成等价,那就很麻烦了,由于拿到的结果并不如预期。

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

参数 动做 Makefile Shell
/path/to/abc.efg.tgz 取目录 dir dirname
同上 取文件名 notdir basename

而且须要注意,Makefile 的 dir 取到的目录带有 / 后缀,而 Shell 的 dirname 结果不带 /。对于目录,二者的认知千差万别,Makefile 的 dirbasename 拿到的都是目录,而 Shell 可以拆分出父目录和字目录的文件名。若是要对齐到 Makefile,用 dirnotdir 起到相似 Shell dirnamebasename 的效果,得先 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: examples/makefile/template

送您一枚免费体验卡

更多 Linux 精彩欢迎透过下方免费体验卡访问『Linux 知识星球』:

『Linux 知识星球』免费体验卡
相关文章
相关标签/搜索