0%

Makefile再学习

make和Makefile的基本逻辑

make以及Makefile完成的最基本的功能是,根据定义的依赖关系,决定是否执行相应的命令。
相关的依赖描述可以写在默认名字的文件里,也可以用make -f决定使用哪个makefile文件。

makefile文件只有一个最终目标target,是make解析makefile文件遇到的第一个target,
但是,这并不是说只能编译出一个程序。可以像 all:target1 target2 target3 这样定义
多个target,可以make all一次编译出所有target,也可以make target1,只编译出target1。

make也支持只有target没有被依赖对象的定义,比如我们看到的clean目标,这是这样的目标
叫做伪目标,要执行伪目标对应的命令需要显示的使用make,比如make clean。伪目标一般
放到makefile文件的后面,并用.PHONY这样的伪目标定义下:

1
2
.PHONY: clean
rm *.o

伪目标可以支持和程序编译无关的一起操作,比如,程序的打包、安装、删除等等。

可以想象要支持复杂的功能,make会定义自己的变量语法,流程控制的语法和函数。这些
语法的定义都和shell的里的定义长的差不多。学习Makefile也就是对这些语法的学习。

make的官方指导文档在这里

变量

变量在makefile里可以理解为一个宏,make在运行时会做变量展开,这个变量展开的逻辑里
有立即展开(immediate)和延后展开(deferred)两个概念,我们后面再具体看变量展开的细节。
一般变量直接定义,使用$()来引用,比如:

1
2
PREFIX=/usr/local
SBINDIR=$(PREFIX)/sbin

变量的定义方式还可以用 := , ?= 和 +=。这些变量定义的方式定义变量的行为都有所
不同,=定义变量,变量的右值可以出现在定义的后面,:=中变量的右值只能在定义之前,
否则右值相当于是空的,?=会看下之前有没有定义过变量,如果有就覆盖掉之前的,覆盖的
逻辑和=一样(和:=不一样),+=是在变量上做追加。

上面讲的各种不同定义变量的方式看起来有点神叨叨的,不过我们继续看下变量展开的内在
逻辑,就可以理解这里的关键了。Makefile的运行逻辑大体分两个步骤,第一步是把Makefile
文件读进来进行依赖解析,第二步是根据依赖关系执行对应的命令,所以变量的展开也分如上
提到的立即展开和延后展开,立即展开是在第一步解析时就展开,延后展开是在随后的执行
过程中展开。

举个例子:

1
2
B = C
A := $(B)

如果如上第二个定义的右值是延后展开的话,第一步解析完A的值就是$(B)这个字符串本身,
注意不是C。

整个Makefile文件中,各个地方都可能定义变量,那每个地方的变量是立即展开还是延后
展开? make具体定义了各种情况的展开方式,具体可以查make的文档。可以看到变量的不同定义中
展开方式是不一样的: immediate = deferred, immediate := immediate, immediate += deferred or immediate。
可以看到=的方式中右值一定是deferred,而:=的右值一定是immediate,所以上面的例子里
A会在第一步解析后是C。

变量有其相关的作用域。一般定义的变量的作用域就是本makefile。使用export可以把所有
当前makefile中的变量export到子makefile中,也可以export A只把A变量export到子makefile中。
makefile里的变量,如果和环境变量一样,就会覆盖环境变量。还可以使用目标变量,把
一个变量的作为范围只绑定到对应目标的命令上,目标变量的定义在下面pciutils makefile
的分析中有提及。

自动变量,$@, $<, $^, 其中$@表示target的集合,$<表示依赖中的第一个名字,$^表示
依赖目标的集合。

make里的函数也可以看成是自定义的变量,和变量一样有各种定义形式,对应的是不同定义
的展开方式不一样,比如=的定义rule是immediate,内容是deferred。

1
2
3
4
define rule=
xxxx
xxxx
endef

因为可以给自定义的函数传参,这种方式可以结合循环可以批量的定义规则。这里我们放
一个makefile的小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
_all=a b c d
__all=$(patsubst %, %_lib, $(_all))

all: $(_all) $(__all)

define test_rule=
$(1): test.o
gcc test.o -o $(1)
$(1)_lib: test.o lib.o
gcc test.o lib.o -DLIB -o $(1)_lib
endef

$(foreach n,$(_all),$(eval $(call test_rule,$(n))))

.PHONY: clean
clean:
rm $(_all) $(__all) *.o

可以看见这里用一个自定义的函数,对a/b/c/d每一个字符都生成了x和x_lib的目标。

流程控制

makefile可以把另一个makefile include进来。比如下面pciutils makefile中的
-include lib/config.mk

make可以进入另外的目录执行。比如pciutils makefile中的$(MAKE) -C lib all
这个会进入lib目录,运行lib里的Makefile,进入退出lib目录的时候会有log提示。

make中有分支语句。一般是,判断变量的值,然后根据判断结果决定是否执行命令,或者
有相关的依赖条件。

make中有支持循环的函数。其语法是:$(foreach ,,),其中list是变量
list,text是执行的表达式,var是每次取出的变量。比如:

1
2
names := a b c d
files := $(foreach n,$(names),$(n).o)

$(files)的值是 a.o b.o c.o d.o

make还有条件语句的支持,大概可以分为:1. 判断变量有无定义的条件语句(#ifdef-#else-#endif),
2. 判断两个变量关系的条件语句(#ifeq ($(xxx), $(xxx))-#else-#endif)。

函数

我们把Makefile里常用的函数收集在这里。

patsubst

$(patsubst ,,),语意是把text里的符合pattern模式的
单词替换成replacement,其中pattern/replacement里可以用通配符%表示任意字符串。

比如,$(patsubst %.o, %.c, a.o b.o c.o)替换后的结果是a.c b.c c.c

call

$(call expression,para1,para2,para3),语意是把参数para1/2/3传给表达式里的$(1)/$(2)/$(3)。
注意这里看起来是函数,其实就是自定义的一个表达式而已。关于自定义表达式的语法,
可以参考上面的介绍。

eval

$(eval expression),展开变量或者函数。

foreach

$(foreach var,list,text),语意是从list里依次取出一个变量赋给var,然后进行text里
定义的运算,返回运算的结果。比如,$(foreach n,a b c,$(n).o)返回a.o b.o c.o。

info/error/warning

$(info xxx),这样使用可以在打印xxx的内容,error/warning的用法类似。似乎info是在
第一步解析Makefile时做打印的。

一些高级用法

make可以自动推导依赖关闭和要执行的命令,这叫make的隐含规则,其实就是对一些基本
写法的省略表示。比如,对于.o的target,make自动推导依赖里有相同名字的.c存在,自动
可以自动推导出基本的编译命令。

比如,如果编译链条中需要通过file.c编译生成file.o,这个规则是全部不用写的,make
会使用隐含规则自动都补上,但是make补齐的时候是按照一定的模版来的,比如这里make
使用$(CC) $(CFLAGS) -c file.c -o file.o生成命令,所以如果你期望的编译命令不是这样
的,就需要自己定义。

你也可以自定义模版,就是自己定义规则,自己定义的规则还可以覆盖make的隐含规则。
比如,可以把.c到.o的规则定义成如下,这样这个makefile里对应的隐含规则就被覆盖了。

1
2
%.o: %.c
gcc --static -g -c $< -o $@

(持续增加…)

pciutils Makefile分析

我们把pciutils Makefile copy到这里,然后逐行分析下。pciutils的github地址在这里
我们直接用注释的方式写分析。用这个makefile做例子分析,有个缺点,是这个里面没有
使用make的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# Makefile for The PCI Utilities
# (c) 1998--2020 Martin Mares <mj@ucw.cz>

OPT=-O2

# 定义编译参数。make的隐含规则会自动的把这个编译参数加到编译里。
# 这里真是有点行业黑话的意思了 :)
CFLAGS=$(OPT) -Wall -W -Wno-parentheses -Wstrict-prototypes -Wmissing-prototypes

VERSION=3.7.0
DATE=2020-05-31

# Host OS and release (override if you are cross-compiling)
HOST=
RELEASE=
CROSS_COMPILE=

# Support for compressed pci.ids (yes/no, default: detect)
ZLIB=

# Support for resolving ID's by DNS (yes/no, default: detect)
DNS=

# Build libpci as a shared library (yes/no; or local for testing; requires GCC)
SHARED=no

# Use libkmod to resolve kernel modules on Linux (yes/no, default: detect)
LIBKMOD=

# Use libudev to resolve device names using hwdb on Linux (yes/no, default: detect)
HWDB=

# ABI version suffix in the name of the shared library
# (as we use proper symbol versioning, this seldom needs changing)
ABI_VERSION=.3

# Installation directories
PREFIX=/usr/local
SBINDIR=$(PREFIX)/sbin
SHAREDIR=$(PREFIX)/share
IDSDIR=$(SHAREDIR)

# 通过shell调用shell命令,这里判断特定的安装目录是否存在。
MANDIR:=$(shell if [ -d $(PREFIX)/share/man ] ; then echo $(PREFIX)/share/man ; else echo $(PREFIX)/man ; fi)
INCDIR=$(PREFIX)/include
LIBDIR=$(PREFIX)/lib
PKGCFDIR=$(LIBDIR)/pkgconfig

# Commands
INSTALL=install
DIRINSTALL=install -d
STRIP=-s
CC=$(CROSS_COMPILE)gcc
AR=$(CROSS_COMPILE)ar
RANLIB=$(CROSS_COMPILE)ranlib

# Base name of the library (overridden on NetBSD, which has its own libpci)
LIBNAME=libpci

# 使用include关键字把lib下的配置文件包含进来。可以看到config.mk只是定义了一些变量。
# 这里include之前的-表示忽略include过程中的报错。
-include lib/config.mk

PCIINC=lib/config.h lib/header.h lib/pci.h lib/types.h lib/sysdep.h
PCIINC_INS=lib/config.h lib/header.h lib/pci.h lib/types.h

# export这个makefile中的所有的变量到子make流程里,比如,下面使用$(MAKE) -C lib all
# 进入lib目录,调用lib目录里的Makefile文件编译其中定义的target,lib Makefile里就
# 可以看见本Makefile里定义的所有变量。
export

# 定义这个Makefile的总目标all,可以all又有一堆依赖,可以make all编译出所有,也可以
# make加一个依赖,只编译出其中的一个target。
all: lib/$(PCILIB) lspci setpci example lspci.8 setpci.8 pcilib.7 pci.ids.5 update-pciids update-pciids.8 $(PCI_IDS)

# 这里的定义是,如果头文件有变动,那么重新编译lib。似乎也有道理,头文件不动的话,
# 依然可以正常link,如果头文件都变动了,link一定会错。至于,头文件不同,lib的实现
# 变动了的情况,上面也只是单独编译lib。
#
# force是不管依赖条件,强制都跑下面命令的意思。这里的force和下面的force是一起的
# 逻辑,make的手册里相关的解释:gnu.org/software/make/manual/html_node/Force-Targets.html
#
# 简单讲就是force作为一个没有依赖,没有命令的伪目标,make认识force每次都update,
# 所以依赖force的target也没有都要执行下。
#
lib/$(PCILIB): $(PCIINC) force
$(MAKE) -C lib all

force:

# 对于多目标的语法,一般拆开理解,比如下面的两个目标就可以拆开成:
# lib/config.h:
# cd lib && ./configure
# lib/config.mk:
# cd lib && ./configure
#
# 但是,拆开也比较费解?
#
lib/config.h lib/config.mk:
cd lib && ./configure

# 如下的编译命令都是由make的隐含规则推导出来的。
lspci: lspci.o ls-vpd.o ls-caps.o ls-caps-vendor.o ls-ecaps.o ls-kernel.o ls-tree.o ls-map.o common.o lib/$(PCILIB)
setpci: setpci.o common.o lib/$(PCILIB)

LSPCIINC=lspci.h pciutils.h $(PCIINC)
lspci.o: lspci.c $(LSPCIINC)
ls-vpd.o: ls-vpd.c $(LSPCIINC)
ls-caps.o: ls-caps.c $(LSPCIINC)
ls-ecaps.o: ls-ecaps.c $(LSPCIINC)
ls-kernel.o: ls-kernel.c $(LSPCIINC)
ls-tree.o: ls-tree.c $(LSPCIINC)
ls-map.o: ls-map.c $(LSPCIINC)

setpci.o: setpci.c pciutils.h $(PCIINC)
common.o: common.c pciutils.h $(PCIINC)

# 这里的一个点是目标变量,他的语法是这样的:
#
# <target>: <variable-assignment>
# 语义是限定变量的作用范围,比如,如下,lspci的所有相关依赖命令中LDLIBS都用这里
# 赋予的值。CFLAGS的含义是一样的。
#
# LDLIBS, CFLAGS成了lspci,ls-kernel.o的相关命令的局部变量。
lspci: LDLIBS+=$(LIBKMOD_LIBS)
ls-kernel.o: CFLAGS+=$(LIBKMOD_CFLAGS)

# 如果update-pciids.sh有变化,要重新生成下update-pciids, sed命令没有看懂?
update-pciids: update-pciids.sh
sed <$< >$@ "s@^DEST=.*@DEST=$(IDSDIR)/$(PCI_IDS)@;s@^PCI_COMPRESSED_IDS=.*@PCI_COMPRESSED_IDS=$(PCI_COMPRESSED_IDS)@"
chmod +x $@

# The example of use of libpci
example: example.o lib/$(PCILIB)
example.o: example.c $(PCIINC)

# 定义模式规则。所有,xxx: xxx.o的编译命令都使用如下的隐含规则。
#
%: %.o
$(CC) $(LDFLAGS) $(TARGET_ARCH) $^ $(LDLIBS) -o $@ --static -L../zlib

# 多target + 定义模式规则。可以展开成:
#
# %.8: %.man
# command
# %.7: %.man
# command
# %.5: %.man
# command
#
%.8 %.7 %.5: %.man
M=`echo $(DATE) | sed 's/-01-/-January-/;s/-02-/-February-/;s/-03-/-March-/;s/-04-/-April-/;s/-05-/-May-/;s/-06-/-June-/;s/-07-/-July-/;s/-08-/-August-/;s/-09-/-September-/;s/-10-/-October-/;s/-11-/-November-/;s/-12-/-December-/;s/\(.*\)-\(.*\)-\(.*\)/\3 \2 \1/'` ; sed <$< >$@ "s/@TODAY@/$$M/;s/@VERSION@/pciutils-$(VERSION)/;s#@IDSDIR@#$(IDSDIR)#"

ctags:
rm -f tags
find . -name '*.[hc]' -exec ctags --append {} +

TAGS:
rm -f TAGS
find . -name '*.[hc]' -exec etags --append {} +

clean:
rm -f `find . -name "*~" -o -name "*.[oa]" -o -name "\#*\#" -o -name TAGS -o -name core -o -name "*.orig"`
rm -f update-pciids lspci setpci example lib/config.* *.[578] pci.ids.gz lib/*.pc lib/*.so lib/*.so.* tags
rm -rf maint/dist

distclean: clean

# install伪目标依赖全部的目标。make install 会执行到下一个依赖之前。
install: all
# -c is ignored on Linux, but required on FreeBSD
$(DIRINSTALL) -m 755 $(DESTDIR)$(SBINDIR) $(DESTDIR)$(IDSDIR) $(DESTDIR)$(MANDIR)/man8 $(DESTDIR)$(MANDIR)/man7 $(DESTDIR)/$(MANDIR)/man5
$(INSTALL) -c -m 755 $(STRIP) lspci setpci $(DESTDIR)$(SBINDIR)
$(INSTALL) -c -m 755 update-pciids $(DESTDIR)$(SBINDIR)
$(INSTALL) -c -m 644 $(PCI_IDS) $(DESTDIR)$(IDSDIR)
$(INSTALL) -c -m 644 lspci.8 setpci.8 update-pciids.8 $(DESTDIR)$(MANDIR)/man8
$(INSTALL) -c -m 644 pcilib.7 $(DESTDIR)$(MANDIR)/man7
$(INSTALL) -c -m 644 pci.ids.5 $(DESTDIR)$(MANDIR)/man5
ifeq ($(SHARED),yes)
ifeq ($(LIBEXT),dylib)
ln -sf $(PCILIB) $(DESTDIR)$(LIBDIR)/$(LIBNAME)$(ABI_VERSION).$(LIBEXT)
else
ln -sf $(PCILIB) $(DESTDIR)$(LIBDIR)/$(LIBNAME).$(LIBEXT)$(ABI_VERSION)
endif
endif

ifeq ($(SHARED),yes)
install: install-pcilib
endif

install-pcilib: lib/$(PCILIB)
$(DIRINSTALL) -m 755 $(DESTDIR)$(LIBDIR)
$(INSTALL) -c -m 644 lib/$(PCILIB) $(DESTDIR)$(LIBDIR)

install-lib: $(PCIINC_INS) lib/$(PCILIBPC) install-pcilib
$(DIRINSTALL) -m 755 $(DESTDIR)$(INCDIR)/pci $(DESTDIR)$(PKGCFDIR)
$(INSTALL) -c -m 644 $(PCIINC_INS) $(DESTDIR)$(INCDIR)/pci
$(INSTALL) -c -m 644 lib/$(PCILIBPC) $(DESTDIR)$(PKGCFDIR)
ifeq ($(SHARED),yes)
ifeq ($(LIBEXT),dylib)
ln -sf $(LIBNAME)$(ABI_VERSION).$(LIBEXT) $(DESTDIR)$(LIBDIR)/$(LIBNAME).$(LIBEXT)
else
ln -sf $(LIBNAME).$(LIBEXT)$(ABI_VERSION) $(DESTDIR)$(LIBDIR)/$(LIBNAME).$(LIBEXT)
endif
endif

uninstall: all
rm -f $(DESTDIR)$(SBINDIR)/lspci $(DESTDIR)$(SBINDIR)/setpci $(DESTDIR)$(SBINDIR)/update-pciids
rm -f $(DESTDIR)$(IDSDIR)/$(PCI_IDS)
rm -f $(DESTDIR)$(MANDIR)/man8/lspci.8 $(DESTDIR)$(MANDIR)/man8/setpci.8 $(DESTDIR)$(MANDIR)/man8/update-pciids.8
rm -f $(DESTDIR)$(MANDIR)/man7/pcilib.7
ifeq ($(SHARED),yes)
rm -f $(DESTDIR)$(LIBDIR)/$(PCILIB) $(DESTDIR)$(LIBDIR)/$(LIBNAME).so$(ABI_VERSION)
endif

#
pci.ids.gz: pci.ids
gzip -9n <$< >$@

.PHONY: all clean distclean install install-lib uninstall force tags TAGS