GNU Make 项目管理
make的作用是让将源代码转换为可执行文件之类的例行性工作自动化,把可执行文件到源代码的依赖关系通过makefile告知make,然后make会根据这些关系以及文件的时间戳判断,应该重新执行哪些步骤,用以编译出可执行文件。
编写makefile
# 注释
target : requires ...
commandstarget 是工作目标,requires 是生成工作目标需要的 必要文件 或 其他工作目标 ,commands 是完成工作目标所执行的命令(子shell执行),以[tab]开头。
Make 是分两阶段执行:
读完所有的
makefile文件,推倒出所有目标和依赖之间的依赖关系根据依赖关系,决定更新哪些目标,执行哪些命令
makefile 中变量和函数在 target 和 requires 里是立即生效的,而在commands中,是要等到执行该命令时,才能明确变量的值和函数的展开。
变量
# 变量定义
IMMEDIATE = DEFERRED # 在执行时扩展,允许递归扩展
IMMEDIATE ?= DEFERRED # 只有在该变量为空时才设置值
IMMEDIATE := IMMEDIATE # 在定义时扩展 简单变量,立即展开
# 将值追加到变量的尾端。
# 当此前这个变量是一个简单变量(:=定义)时,是立即展开的,其他情况是延后展开
IMMEDIATE += value(DEFERRED or IMMEDIATE)
define IMMEDIATE
DEFERRED
endef
# 使用变量 使用 $(变量名)
$(var)请注意:
:=定义的变量是立即展开的,=定义的变量延后展开的
OBJS2 = $(OBJS1) programC.o
OBJS1 = programA.o programB.o
all:
@echo $(OBJS2)
# bash中执行 make, 可以看出虽然 OBJS1 是在 OBJS2 之后定义的, 但在 OBJS2中可以提前使用
$ make
programA.o programB.o programC.o变量替换操作
SRCS := programA.c programB.c programC.c
OBJS := $(SRCS:%.c=%.o)
all:
@echo "SRCS: " $(SRCS)
@echo "OBJS: " $(OBJS)
$ make
SRCS: programA.c programB.c programC.c
OBJS: programA.o programB.o programC.o变量追加值
SRCS := programA.c programB.c programC.c
SRCS += programD.c
all:
@echo "SRCS: " $(SRCS)
$ make
SRCS: programA.c programB.c programC.c programD.c变量覆盖参数 override
SRCS := programA.c programB.c programC.c
all:
@echo "SRCS: " $(SRCS)
$ make SRCS=nothing
SRCS: nothing
#################################################
override SRCS := programA.c programB.c programC.c
all:
@echo "SRCS: " $(SRCS)
$ make SRCS=nothing
SRCS: programA.c programB.c programC.c自动变量
$@:工作目标$^:所有必要文件, 空格隔开, 已去重,比如,规则为t: p1 p2,那么$^就指代p1 p2$+:同$^,未去重$<:第一个必要文件$?:时间戳在工作目标之后的所有必要文件, 空格隔开,比如,规则为t: p1 p2,其中p2的时间戳比t新,$?就指代p2$%:.a中的.o文件。比如foo.a包含bar.o。$%是bar.o,$@是foo.a。当工作目标不是.a文件,$%为空$*:指代匹配符%匹配的部分,比如%匹配f1.txt中的f1,$*就表示f1。$(@D)和$(@F):$(@D)和$(@F)分别指向$@的目录名和文件名。比如$@是src/input.c,那么$(@D)的值为src,$(@F)的值为input.c$(<D)和$(<F):$(<D)和$(<F)分别指向$<的目录名和文件名
# 静态模式 + 多目标文件 + $* 的例子
# $* 分别是 big , litter
bigoutput litteroutput : %output : text.g
generate text.g -$* > $@dest/%.txt: src/%.txt
@[ -d dest ] || mkdir dest
cp $< $@上面代码将src目录下的txt文件,拷贝到dest目录下。首先判断dest目录是否存在,如果不存在就新建,然后$< 指代前置文件src/%.txt, $@指代目标文件dest/%.txt。
执行选项
make --debug=[a,b,v]输出调试信息make -j同时运行的命令的个数,也就是多线程执行Makefilemake -n打印将要执行的命令,但不实际执行它们make --print-data-base打印出所有规则和变量make -w在编译一个目录之前和完成此目录的编译之后,给出相应的提示信息make -f rules.txt指定make命令依据rules.txt文件中的规则,进行构建make -r禁止使用任何隐含规则make -R禁止使用任何作用于变量上的隐含规则make -B强制所有目标都更新
伪目标
# all工作目标会创建一个 bash 和 一个bashbug
.PHONY: all
all: bash bashbug常用的标准的伪目标:
all # 执行编译应用程序的所有工作
install # 从已编译的二进制文件中进行应用程序的安装
clean # 将产生自源代码的二进制文件删除
distclean # 删除编译过程中产生的任何文件
TAGS # 建立可供编辑器使用的标记表
info # 从 Texinfo 源代码来创建 GNU info 文件
check # 执行与应用程序相关的任何测试
print # 列出改变过的源文件
tar # 把源程序打包备份. 也就是一个tar文件
dist # 创建一个压缩文件, 一般是把tar文件压成Z文件. 或是gz文件规则中部分依赖
LIBS = libtest.a
foo : foo.c | $(LIBS)
$(CC) $(CFLAGS) $(LIBS) $< -o $@如果foo已经存在,当foo.c被修改以后,foo将被重建,但是当libtest.a被修改以后,不会重建foo。规则中|左边的内容被修改,需要根据规则重建,右边的被修改,不会重建。
规则中的通配符
clean :
rm -f *.o*: 表示任意一个或多个字符?: 表示任意一个字符[...]:[abcd]表示a,b,c,d中任意一个字符,[^abcd]表示除a,b,c,d以外的字符,[0-9]表示0~9中任意一个数字
可直接使用在以下两种场合:
规则的目标文件、依赖文件列表中,
make是立即展开通配符匹配到的值的规则的命令中,其展开是在
shell执行此命令时完成
变量与函数中是无法直接使用通配符的,需要通过wildcard函数来使用通配符。比如objects = *.o中$(objects)的值就是*.o,而不是使用空格分隔开的所有.o文件列表。正确写法是:objects = $(wildcard *.o),代表使用空格分隔开的所有.o文件列表。
objects = *.o
foo : $(objects)
cc -o foo $(CFLAGS) $(objects)上述makefile执行后报错 " 没有创建 *.o 文件的规则 ",原因就是规则展开后是foo : *.o,目录中不存在*.o这样一个文件,所以make将*.o当做依赖的目标文件,然而也没有*.o目标文件的创建规则,所以报错。
objects := $(patsubst %.c,%.o,$(wildcard *.c))
foo : $(objects)
cc -o foo $(objects)上述makefile将工作目录下的所有.c文件进行编译,并最后链接成为一个可执行文件foo。
文件搜索路径
make在不指明的情况下,只会在当前目录中寻找工作目标和必要文件VPATH dir1 dir2用于指明,寻找工作目标和必要文件的多个目录vpath PATTERN dirs用于指明,make去VPATH的某个目录中寻找什么样的文件,%意思是匹配一个或多个字符
VPATH = src include # 到src 和 include 中去搜寻文件
vpath %.l %.c src # 在src 中 只搜寻 .l 和 .c 文件
vpath %.h include # 在include中只搜寻 .h 文件vpath %.h include指定的路径仅限于在makefile文件内容中出现的.h文件,并不能指定源代码文件中包含的头文件所在的路径,在.c源文件中所包含的头文件的搜索路径需要使用gcc中的-I命令来指定。
条件与判断
所有的条件语句都是立即展开的,包括ifdef、ifeq、ifndef、ifneq。
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
LIST = one two three
all:
for i in $(LIST); do \
echo $$i; \
done
# 等同于
all:
for i in one two three; do \
echo $i; \
done规则中的命令行
在一个规则的命令中,[tab] 命令行;[换行符]交给/bin/sh去执行。
foo : bar/lose
cd bar;gobble lose > ../foo; # 整行是一个shell去执行,在执行 gobble 之前会进入 bar 目录foo : bar/lose
cd bar;
gobble lose > ../foo; # 是一个新shell去执行,所以当前目录不是 bar 目录命令前缀
无前缀,输出执行的命令以及命令执行的结果, 出错的话停止执行
-前缀,可以忽略命令的执行错误报告,忽略错误, 继续执行,比如[tab]-rm -rf *.o。@前缀,可以关闭命令的回显输出,出错的话停止执行
定义命令包
命令包有点像是个函数, 将连续的相同的命令合成一条, 减少 Makefile 中的代码量, 便于以后维护。
# 定义一个命令包
define run-yacc;
yacc $(firstword $^)
mv y.tab.c $@
endef
# 使用命令包
foo.c : foo.y
$(run-yacc)模式规则 pattern rule
使用 通配符 而不是明确的文件名称书写的规则。模式里的 % 等效于Unix shell中的 * 号,可以代表任意多字符,用法举例: %,v , s%.o , wrapper_%
prog : %.c
gcc $^ -o $@
% : %.cpp
$(LINK.cpp) $^ $(LOADLIBES) $(LDLIBS) -o $@
% : %.sh
cat $< > $@
chmod a+x $@静态模式规则
OBJECTS = foo.o bar.o
all : $(OBJECTS)
# 将 %.o : %.c 这个模式约束为: 只能应用在 $(OBJECTS) 所列举的文件上
$(OBJECTS) : %.o : %.c
$(CC) -c $(CFLAGS) $< -o $@隐含规则 implicit rule
所有 隐含规则 都是 模式规则 的实例。
一个工作目标,如果找不到可以更新它的具体规则,就会使用隐含规则,abcde.o在没有明确的命令行生成的情况话,make自动会执行gcc -c abcde.c -o abcde.o
GNU Make对-lfl这种语法提供了特别的支持,它会去库搜索路径中查找,确认libfl.a或者libfl.so的存在
常用的隐含规则一览
# 编译C程序 : N.o 自动由 N.c 生成
%.o : %.c
$(CC) -c $(CPPFLAGS) $(CFLAGS) $<
# 编译C++程序 :N.o 自动由 N.cc 生成
%.o : %.cc
$(CXX) -c $(CPPFLAGS) $(CFLAGS) $<
# 汇编和需要预处理的汇编程序
%.o : %.s
$(AS) $(ASFLAGS) $<
%.s : %.S
$(CPP) $(CPPFLAGS)
# 链接单一 object 文件 : N自动由多个N.o生成
% : %.o
$(CC) $(LDFLAGS) $< $(LOADLIBES) $(LDLIBS)
% : %.c
$(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@隐含变量
隐含规则的命令中,使用的变量都是预定义变量,称为 隐含变量。

常见命令隐含变量:
CC = gcc:C编译程序, 改变CC变量的设定值就可以更换C编译器CPP = $(CC) -E:C程序的预处理器AS = as:汇编程序AR = ar:函数库打包程序,可创建静态库文档.aCXX = g++:C++编译程序RM = rm -f
常见命令参数隐含变量:
ARFLAGS = rv:$(AR)命令使用的参数ASFLAGS:$(AS)汇编程序使用的参数CFLAGS:$(CC)使用的参数CXXFLAGS: 执行g++编译器时,使用的参数CPPFLAGS:执行$(CC) -E时,使用的参数LDFLAGS: 链接器参数 如ldLOADLIBESLDLIBS包含了要链接的程序库列表
常见组合起来的隐含变量:
COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -cOUTPUT_OPTION = -o $@LINK.o = $(CC) $(LDFLAGS) $(TARGET_ARCH)
直接修改这些内置变量要特别小心,比如make CPPFLAGS=-DDEBUG就会将在makefile里定义的CPPFLAGS = -I include覆盖掉
函数
函数格式
# 函数调用
$(function-name arg1[,argn])在规则中通配符会被自动展开,但在变量的定义和函数引用时,通配符将失效。这种情况下如果需要通配符有效,就需要使用函数$(wildcard PATTERN...)。它被展开为已经存在的、使用空格分开的、匹配此模式的所有文件列表。如果不存在任何符合此模式的文件,函数会忽略模式字符并返回空。
# 首先使用 wildcard 获取工作目录下的 .c 文件列表
# 之后将列表中所有文件名的后缀 .c 替换为 .o,就可以得到在当前目录可生成的 .o 文件列表
objects := $(patsubst %.c,%.o,$(wildcard *.c))
foo : $(objects)
cc -o foo $(objects)src = $(wildcard *.c ./sub/*.c) # 扩展通配符,把 ./ 和 ./sub/ 下的所有后缀是c的文件全部展开
dir = $(notdir $(src)) # 去除路径, 把展开的文件去除掉路径信息
obj = $(patsubst %.c,%.o,$(dir) ) # 替换通配符, patsubst把$(dir)中的变量符合后缀是.c的全部替换成.o
.PHONY = all
all:
@echo $(src) # 输出 a.c b.c ./sub/sa.c ./sub/sb.c
@echo $(dir) # 输出 a.c b.c sa.c sb.c
@echo $(obj) # 输出 a.o b.o sa.o sb.o
@echo "end"常用函数
# filter 把text当做一系列被空格隔开的单词,与pattern比较后,会返回相符者
$(filter pattern ...,text)
eg.
files = foo.elc bar.o lose.o
$(filter %.o,$(files)) : %.o:%.c
$(CC) -c $(CFLAGS) $< -o $@
$(filter %.o %.a,program.c program.o program.a)
# 与filter相反,返回不符合者
$(filter-out pattern ...,text)
# 从text里找string,找到了就返回string,未找到就返回空值
$(findstring string...,text)
# 不具通配符能力的搜索和替换函数
$(subst search-string,replace-string,text)
$(subst .c,.o,$(sources)) # 将sources中的.c替换为.o
# 具备通配符能力的搜索和替换函数,此次的通配符只包含一个%,与text的整个值进行匹配
$(patsubst search-pattern,replace-pattern,text)
eg. $(patsubst %.c,%.o,programA.c programB.c)
# 去掉 <string> 字符串中开头和结尾的空字符,返回被去掉空格的字符串值
$(strip <string>)
# 返回text中单词的数量
$(words text)
# 返回text中第n个单词
$(words n,text)
# 取字符串 <text> 中的第一个单词
$(firstword <text>)
eg. $(firstword aa bb cc dd) # aa
# 返回从start(含)到end(含)的单词
$(wordlist start,end,text)
eg. $(wordlist 1,3,aa bb cc dd) # aa bb cc
# 排序list参数,并且去重,返回按字典排序的不重复单词列表,以空格作为分割符
$(sort list)
# 使用subshell执行命令 它的作用就是执行一个shell命令, 并将shell命令的结果作为函数的返回
$(shell command)
# 支持通配符函数,匹配字符包括 ~、*、?、[...]和[^...]
$(wildcard pattern...)
sources = $(wildcard *.c *.h)
# 返回list中每个单词的目录部分
$(dir list...)
eg. $(dir /home/a.c ./bb.c ../c.c d.c) # /home/ ./ ../ ./
# 返回文件路径中的文件名部分
$(notdir name...)
eg. $(notdir /home/a.c ./bb.c ../c.c d.c) # a.c bb.c c.c d.c
# 返回参数中每个单词的后缀
$(suffix name...)
eg. $(suffix /home/a.c ./b.o ../c.a d) # .c .o .a
# 返回参数中每个单词不含后缀的部分
$(basename name...)
eg. $(basename /home/a.c ./b.o ../c.a /home/.d .e) # /home/a ./b ../c /home/
# 加后缀
$(addsuffix suffix,name...)
eg. $(addsuffix .c,/home/a b ./c.o ../d.c) # /home/a.c b.c ./c.o.c ../d.c.c
# 加前缀
$(addprefix prefix,name...)
eg. $(addprefix test_,/home/a.c b.c ./d.c) # test_/home/a.c test_b.c test_./d.c
# 链接 把prefix-list的第一个参数与suffix-list的第一个参数接在一起,以此类推
$(join prefix-list,suffix-list)
eg. $(join a b c d,1 2 3 4) # a1 b2 c3 d4
# foreach 函数 $(foreach <var>,<list>,<text>)
targets := a b c d
objects := $(foreach i,$(targets),$(i).o)
all:
@echo $(targets)
@echo $(objects)
# bash 中执行 make
$ make
a b c d
a.o b.o c.o d.o
# if 函数 $(if <condition>,<then-part>,<else-part>)
val := a
objects := $(if $(val),$(val).o,nothing)
no-objects := $(if $(no-val),$(val).o,nothing)
all:
@echo $(objects)
@echo $(no-objects)
# bash 中执行 make
$ make
a.o
nothing
# call 创建新的参数化函数 $(call <expression>,<parm1>,<parm2>,<parm3>...)
log = "====debug====" $(1) "====end===="
all:
@echo $(call log,"正在 Make")
# bash 中执行 make
$ make
====debug==== 正在 Make ====end====
# 产生一个致命错误,输出错误信息, 停止Makefile的运行 $(error <text ...>)
all:
$(error there is an error!)
@echo "这里不会执行!"
# bash 中执行 make
$ make
Makefile:2: *** there is an error!. Stop.
# 输出警告信息, Makefile继续运行 $(warning <text ...>)
all:
$(warning there is an warning!)
@echo "这里会执行!"
$ make
Makefile:2: there is an warning!
这里会执行!
参考实例
CC = g++
CPPFLAGS = -Wall -g -pedantic
BIN = main
OBJS = main.o error.o func.o
$(BIN):$(OBJS)
$(CC) $(CPPFLAGS) $^ -lpthread -o $@
%.o : %.c
$(CC) $(CPPFLAGS) -c $< -o $@
.PHONY : clean
clean:
rm -f *.o $(BIN)包含其他makefile文件
make处理指示符include时,将暂停对当前文件的读取,而转去读取include指定的文件列表。直到完成所有这些文件以后再回过头继续读取当前文件。如果指示符include指定的文件不是绝对路径而且当前目录下也不存在此文件。make将根据文件名试图在以下几个目录下查找:
首先,查找使 用命令行选项
-I或者--include-dir指定的目录,如果找到指定的文件,则使用这个文件否则依此搜索以下几个目录:
/usr/gnu/include、/usr/local/include和/usr/include
include xxx.mk yyy.mk格式A(M)表示档案文件.a的成员M。
$在makefile中有特殊含义,表示变量或者函数的引用,如果规则中需要$字符,则需要书写为$$
查看C文件的依赖关系
写 Makefile 的时候, 需要确定每个目标的依赖关系.GNU提供一个机制可以查看C代码文件依赖那些文件, 这样我们在写 Makefile 目标的时候就不用打开C源码来看其依赖那些文件了.
比如, 下面命令显示内核源码中 virt/kvm/kvm_main.c 中的依赖关系
$ cd virt/kvm/
$ gcc -MM kvm_main.c
kvm_main.o: kvm_main.c iodev.h coalesced_mmio.h async_pf.h
# 上句就可以加到 Makefile 中作为编译 kvm_main.o 的依赖关系make的递归执行
进入子目录(那个子目录下有makefile文件),然后执行make命令。
subsystem:
cd subdir && $(MAKE);
# 等价于
subsystem:
$(MAKE) -C subdir;父makefile中将变量传递给子makefile:
# 父makefile
export VARIABLE; # export 后,子makefile就能访问到该变量了
unexport VARIABLE; # 不希望将一个变量传递给子makefile
export; # 不带任何参数,则表示将父makefile中的所有变量都传递给子makefileMAKELEVEL环境变量是make递归调用的深度,最上一级是0。子makefile则是1,孙子makefile是2,以此类推。
.PHONY : test
test :
@echo "makelevel : $(MAKELEVEL)";嵌套 Makefile 之间传递参数
# Makefile 内容
export VALUE1 := export.c <-- 用了 export, 此变量能够传递到 ./other/Makefile 中
VALUE2 := no-export.c <-- 此变量不能传递到 ./other/Makefile 中
all:
@echo "主 Makefile begin"
@cd ./other && make
@echo "主 Makefile end"# ./other/Makefile 内容
other-all:
@echo "other makefile begin"
@echo "VALUE1: " $(VALUE1)
@echo "VALUE2: " $(VALUE2)
@echo "other makefile end"执行 make
$ make
主 Makefile begin
make[1]: Entering directory '/path/to/test/makefile/other'
other makefile begin
VALUE1: export.c <-- VALUE1 传递成功
VALUE2: <-- VALUE2 传递失败
other makefile end
make[1]: Leaving directory `/path/to/test/makefile/other'
主 Makefile end环境变量
MAKEFILES:make执行时首先将此变量作为需要读入的makefile文件MAKEFILE_LIST: 所有make加载的变量,都会被追加记录到MAKEFILE_LIST中.VARIABLES:makefile中所定义的所有全局变量列表、包括:空变量和make内嵌变量.LIBPATTERNS: 默认值为lib%.so lib%.a,因此-lNAME默认加载libNAME.so