目录

GNU Make 项目管理

make的作用是让“将源代码转换为可执行文件”之类的例行性工作自动化,把可执行文件到源代码的依赖关系通过makefile告知make,然后make会根据这些关系以及文件的时间戳判断,应该重新执行哪些步骤,用以编译出可执行文件。

编写makefile

target : requires ...
    commands
# 注释

target 是工作目标,requires 是生成工作目标需要的 必要文件 或 其他工作目标 ,commands 是完成工作目标所执行的命令(子shell执行),以[tab]开头。

Make 是分两阶段执行:

  • 读完所有的makefile文件,推倒出所有目标和依赖之间的依赖关系

  • 根据依赖关系,决定更新哪些目标,执行哪些命令

makefile 中变量和函数在 targetrequires 里是立即生效的,而在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 同时运行的命令的个数,也就是多线程执行Makefile

  • make -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用于指明,makeVPATH的某个目录中寻找什么样的文件,%意思是匹配一个或多个字符

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命令来指定。

条件与判断

所有的条件语句都是立即展开的,包括ifdefifeqifndefifneq

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 $@

隐含变量

隐含规则的命令中,使用的变量都是预定义变量,称为 隐含变量。

WX20190221-194042.png

常见命令隐含变量:

  • CC = gcc : C编译程序, 改变CC变量的设定值就可以更换C编译器

  • CPP = $(CC) -EC程序的预处理器

  • AS = as :汇编程序

  • AR = ar :函数库打包程序,可创建静态库文档.a

  • CXX = g++: C++编译程序

  • RM = rm -f

常见命令参数隐含变量:

  • ARFLAGS = rv : $(AR) 命令使用的参数

  • ASFLAGS : $(AS) 汇编程序使用的参数

  • CFLAGS : $(CC) 使用的参数

  • CXXFLAGS : 执行g++编译器时,使用的参数

  • CPPFLAGS :执行$(CC) -E时,使用的参数

  • LDFLAGS : 链接器参数 如 ld

  • LOADLIBES LDLIBS 包含了要链接的程序库列表

常见组合起来的隐含变量:

  • COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c

  • OUTPUT_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中的所有变量都传递给子makefile

MAKELEVEL环境变量是make递归调用的深度,最上一级是0。子makefile则是1,孙子makefile2,以此类推。

.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

参考