【Linux】Makefile

参考文档:

https://www.gnu.org/software/make/manual/make.html

一、Makefile的规则

目标(target)…: 依赖(prerequiries)…
命令(command)

二、Makefile 总述

  1. 显式规则。显式规则说明了,如何生成一个或多的的目标文件。这是由Makefile的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
  2. 隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile,这是由make所支持的。
  3. 变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
  4. 文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。
  5.  注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用“#”字符,这个就像C/C++中的“//”一样。如果你要在你的Makefile中使用“#”字符,可以用反斜框进行转义,如:“\#”。

2.1 Makefile中的引用

 在Makefile使用include关键字可以把别的Makefile包含进来,这很像C语言的#include,被包含的文件会原模原样的放在当前文件的包含位置。include的语法是:

include filenames…

列如, 有这样几个Makefile:a.mk、b.mk、c.mk,还有一个文件叫foo.make,以及一个变量$(bar),其包含了bish、bash

include foo *.mk $(bar)

相当于:

include foo a.mk b.mk c.mk bish bash

2.2 环境变量 MAKEFILES

        如果你的当前环境中定义了环境变量 MAKEFILES ,那么,make会把这个变量中的值做一个类似于include 的动作。这个变量中的值是其它的Makefile,用空格分隔。只是,它和 include 不同的是,从这个环境变量中引入的Makefile的“目标”不会起作用,如果环境变量中定义的文件发现错误,make也会不理。

书中提到:但是在这里我还是建议不要使用这个环境变量,因为只要这个变量一被定义,那么当你使用 make 时,所有的 Makefile 都会受到它的影响,这绝不是你想看到的

2.3 在文件名中使用通配符

"*"
通配符代替了你一系列的文件,如“*.c”表示所以后缀为c的文件。

例如,以下是删除所有对象文件的规则:

clean:
        rm -f *.o

 2.4 伪目标

  • 有时生成的文件与伪目标重名,为了防止这种情况,使用特殊标记".PHONY"来显示的指明一个目标是伪目标,这样不管有没有这个文件,该目标都是伪目标;
  • 伪目标:它是结合make使用完成特殊作用的目标;
#       如果当前目录下没有名为“clean”的文件,则rm指令会被执行。如果有的话,由于clean没有依赖文件,所以目标被认为是最新的而不去执行rm指令。 
clean:
        rm *.o temp
# 在Makefile中加入伪目标声明可防止这种情况发生
.PHONY: clean
clean:
        rm *.o temp

 2.5 静态模式规则      

以下是静态模式规则的语法:

targets …: target-pattern: prereq-patterns …
        recipe
        …
  1. targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。
  2. target-parrtern是指明了targets的模式,也就是的目标集模式。
  3. prereq-parrterns是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。

以下是一个相应的实例:

objects = foo.o bar.o

all: $(objects)

$(objects): %.o: %.c
        $(CC) -c $(CFLAGS) $< -o $@

         上面的例子中,指明了我们的目标从 $(object) 中获取,“%.o”表明要所有以“.o”结尾的目标,也就是“foo.o bar.o”,也就是变量 $(object) 集合的模式,而依赖模式 “%.c” 则取模式 “%.o” 的 “%”,也就是 “foo bar”,并为其加下“.c”的后缀,于是,我们的依赖目标就是“foo.c bar.c”。而命令中的“$<”和“$@”则是自动化变量,“$<”表示所有的依赖目标集(也就是“foo.c bar.c”),“$@”表示目标集(也就是 "foo.o bar.o” )。于是,上面的规则展开后等价于下面的规则:

all:foo.o bar.o

foo.o : foo.c
    $(CC) -c $(CFLAGS) foo.c -o foo.o

bar.o : bar.c
    $(CC) -c $(CFLAGS) bar.c -o bar.o

2.6 自动生成依赖性

在Makefile中,我们的依赖关系可能会需要包含一系列的头文件,比如,如果我们的main.c中有一句“#include "defs.h"”,那么我们的依赖关系应该是:

main.o : main.c defs.h

        但是,如果是一个比较大型的工程,你必需清楚哪些C文件包含了哪些头文件,并且,你在加入或删除头文件时,也需要小心地修改Makefile,这是一个很没有维护性的工作。为了避免这种繁重而又容易出错的事情,我们可以使用C/C++编译的一个功能。大多数的C/C++编译器都支持一个 “-M” 的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系,例如,如果我们执行下面的命令:

cc -M main.c

其输出是:

main.o: main.c /usr/include/stdc-predef.h /usr/include/stdio.h \
 /usr/include/x86_64-linux-gnu/bits/libc-header-start.h \
 /usr/include/features.h /usr/include/features-time64.h \
 /usr/include/x86_64-linux-gnu/bits/wordsize.h \
 /usr/include/x86_64-linux-gnu/bits/timesize.h \
 /usr/include/x86_64-linux-gnu/sys/cdefs.h \
 /usr/include/x86_64-linux-gnu/bits/long-double.h \
 /usr/include/x86_64-linux-gnu/gnu/stubs.h \
 /usr/include/x86_64-linux-gnu/gnu/stubs-64.h \
 /usr/lib/gcc/x86_64-linux-gnu/12/include/stddef.h \
 /usr/lib/gcc/x86_64-linux-gnu/12/include/stdarg.h \
 /usr/include/x86_64-linux-gnu/bits/types.h \
 /usr/include/x86_64-linux-gnu/bits/typesizes.h \
 /usr/include/x86_64-linux-gnu/bits/time64.h \
 /usr/include/x86_64-linux-gnu/bits/types/__fpos_t.h \
 /usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h \
 /usr/include/x86_64-linux-gnu/bits/types/__fpos64_t.h \
 /usr/include/x86_64-linux-gnu/bits/types/__FILE.h \
 /usr/include/x86_64-linux-gnu/bits/types/FILE.h \
 /usr/include/x86_64-linux-gnu/bits/types/struct_FILE.h \
 /usr/include/x86_64-linux-gnu/bits/stdio_lim.h \
 /usr/include/x86_64-linux-gnu/bits/floatn.h \
 /usr/include/x86_64-linux-gnu/bits/floatn-common.h sub.h

 如果你不想得到标准的库文件,你的使用命令"-MM":

cc -MM main.c

其输出是:

main.o: main.c sub.h

        那么,编译器的这个功能如何与我们的Makefile联系在一起呢。因为这样一来,我们的Makefile也要根据这些源文件重新生成,让Makefile自已依赖于源文件?这个功能并不现实,不过我们可以有其它手段来迂回地实现这一功能。GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个“name.c”的文件都生成一个“name.d”的Makefile文件,[.d]文件中就存放对应[.c]文件的依赖关系。

        于是,我们可以写出[.c]文件和[.d]文件的依赖关系,并让make自动更新或自成[.d]文件,并把其包含在我们的主Makefile中,这样,我们就可以自动化地生成每个文件的依赖关系了。

这里,我们给出了一个模式规则来产生[.d]文件:

%.d: %.c
        @set -e; rm -f $@; \
         $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
         sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
         rm -f $@.$$$$
  1.  这个规则的意思是,所有的[.d]文件依赖于[.c]文件,“rm-f $@”的意思是删除所有的目标,也就是[.d]文件
  2. 第二行的意思是,为每个依赖文件“$<”,也就是[.c]文件生成依赖文件,“$@”表示模式“%.d”文件,如果有一个C文件是name.c,那么“%”就是“name”,“.$$$$”意为一个随机编号,第二行生成的文件有可能是“name.d.12345”

  3. 第三行使用sed命令做了一个替换

  4. 第四行就是删除临时文件

总而言之,这个模式要做的事就是在编译器生成的依赖关系中加入[.d]文件的依赖,即把依赖关系:

main.o : main.c defs.h

转成:

main.o main.d : main.c defs.h

        于是,我们的[.d]文件也会自动更新了,并会自动生成了,当然,你还可以在这个[.d]文件中加入的不只是依赖关系,包括生成的命令也可一并加入,让每个[.d]文件都包含一个完赖的规则。一旦我们完成这个工作,接下来,我们就要把这些自动生成的规则放进我们的主Makefile中。我们可以使用Makefile的“include”命令,来引入别的Makefile文件(前面讲过),例如:

sources = foo.c bar.c

include $(sources:.c=.d)

2.7 显示命令

        通常,make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用“@”字符在命令行前,那么,这个命令将不被make显示出来,最具代表性的例子是,我们用这个功能来像屏幕显示一些信息。如:

 @echo About to make distribution files

当make执行时,会输出 About to make distribution files,而不会输出命令,如果没有@,则会打印:

echo About to make distribution files
About to make distribution files

2.8  嵌套执行make

 例如,我们有一个子目录叫subdir,这个目录下有个Makefile文件,来指明了这个目录下文件的编译规则。那么我们总控的Makefile可以这样书写:

subsystem:
        cd subdir && $(MAKE)

 这个等效于:

subsystem:
        $(MAKE) -C subdir

其外,“-w”或是“--print-directory”会在make的过程中输出一些信息,让你看到目前的工作目录:

make: Entering directory `/u/gnu/make'.
make: Leaving directory `/u/gnu/make'.

2.9 导出变量给子Makefile

将特定变量导出到 sub

export variable …

阻止导出变量

unexport variable …

2.10 定义命令包

如果Makefile中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以“define”开始,以“endef”结束,如:

define run-yacc =
yacc $(firstword $^)
mv y.tab.c $@
endef

实例:

foo.c : foo.y
        $(run-yacc)

在这个命令包的使用中,命令包“run-yacc”中的“$^”就是“foo.y”,“$@”就是“foo.c”

2.11 变量符号

A = xxx // 延时变量
B ?= xxx // 延时变量,只有第一次定义时赋值才成功;如果曾定义过,此赋值无效
C := xxx // 立即变量
D += yyy // 如果 D 在前面是延时变量,那么现在它还是延时变量;如果 D 在前面是立即变量,那么现在它还是立即变量

2.12 变量替换引用

        我们可以替换变量中的共有的部分,其格式是“$(var:a=b)”或是“${var:a=b}”,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。这里的“结尾”意思是“空格”或是“结束符”

foo := a.o b.o l.a c.o
bar := $(foo:.o=.c)

这个示例中,我们先定义了一个“$(foo)”变量,而第二行的意思是把“$(foo)”中所有以“.o”字串“结尾”全部替换成“.c”,所以我们的“$(bar)”的值就是“a.c b.c l.a c.c”

2.13 计算变量名称

x = y
y = z
a := $($(x))

 在这个例子中,$(x)的值是“y”,所以$($(x))就是$(y),于是$(a)的值就是“z”。(注意,是“x=y”,而不是“x=$(y)”)

2.14 指令override 

如果有变量是通常make的命令行参数设置的,那么Makefile中对这个变量的赋值会被忽略。如果你想在Makefile中设置这类参数的值,那么,你可以使用“override”指示符。其语法是:

override variable = value 或者
override variable := value

2.15 多行变量

还有一种设置变量值的方法是使用define关键字。使用define关键字设置变量的值可以有换行,这有利于定义一系列的命令。define 指示符后面跟的是变量的名字,而重起一行定义变量的值,定义是以endef关键字结束。其工作方式和“=”操作符一样。变量的值可以包含函数、命令、文字,或是其它变量。因为命令需要以[Tab]键开头,所以如果你用define定义的命令变量中没有以[Tab]键开头,那么make就不会把其认为是命令

define two-lines
echo foo
echo $(bar)
endef

等效于:

two-lines = echo foo; echo $(bar)

 2.16 条件判断

条件表达式的语法为:

conditional-directive
text-if-true
endif


conditional-directive
text-if-true
else
text-if-false
endif

conditional-directive-one
text-if-one-is-true
else conditional-directive-two
text-if-two-is-true
else
text-if-one-and-two-are-false
endif

其中表示条件关键字,如下四个:

ifeq (arg1, arg2)
ifeq 'arg1' 'arg2'
ifeq "arg1" "arg2"
ifeq "arg1" 'arg2'
ifeq 'arg1' "arg2"
ifneq (arg1, arg2)
ifneq 'arg1' 'arg2'
ifneq "arg1" "arg2"
ifneq "arg1" 'arg2'
ifneq 'arg1' "arg2"
ifdef variable-name
ifndef variable-name

2.17 函数

2.17.1 $(subst from,to,text)

名称:字符串替换函数——subst。
功能:把字串中的字符串替换成
返回:函数返回被替换过后的字符串。

示例:

$(subst ee,EE,feet on the street)

生成值:‘fEEt on the strEEt’.

2.17.2 $(patsubst pattern,replacement,text)

名称:模式字符串替换函数——patsubst。
功能:查找中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式,如果匹配的话,则以替换。这里,可以包括通配符“%”,表示任意长度的字串。如果中也包含“%”,那么,中的这个“%”将是中的那个“%”所代表的字串。(可以用“\”来转义,以“\%”来表示真实含义的“%”字符)返回:函数返回被替换过后的字符串。 

示例:

$(patsubst %.c,%.o,x.c.c bar.c)

生成值:把字串“x.c.c bar.c”符合模式[%.c]的单词替换成[%.o],返回结果是“x.c.o bar.o”

2.17.3 $(strip string)

名称:去空格函数——strip。
功能:去掉字串中开头和结尾的空字符。
返回:返回被去掉空格的字符串值。

2.17.4 $(findstring find,in)

名称:查找字符串函数——findstring。
功能:在字串中查找字串。
返回:如果找到,那么返回,否则返回空字符串。
示例:

$(findstring a,a b c)
$(findstring a,b c)

第一个函数返回 “a” 字符串,第二个返回 “” 字符串(空字符串) 

2.17.5 $(filter pattern…,text)

名称:过滤函数——filter。
功能:以模式过滤字符串中的单词,保留符合模式的单词。可以有多个模式。
返回:返回符合模式的字串。
示例:

sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
cc $(filter %.c %.s,$(sources)) -o foo

$(filter %.c %.s,$(sources))返回的值是“foo.c bar.c baz.s”

2.17.6 $(filter-out , )

名称:反过滤函数——filter-out。
功能:以模式过滤字符串中的单词,去除符合模式的单词。可以有多个模式。
返回:返回不符合模式的字串。

objects=main1.o foo.o main2.o bar.o
mains=main1.o main2.o

$(filter-out $(mains),$(objects)) 返回值是“foo.o bar.o”。

2.17.7 $(sort list) 

名称:排序函数——sort。
功能:给字符串中的单词排序(升序)。
返回:返回排序后的字符串。
示例:$(sort foo bar lose)返回“bar foo lose” 。
备注:sort函数会去掉中相同的单词。

2.17.8 $(word n,text)

名称:取单词函数——word。
功能:取字符串中第个单词。(从一开始)
返回:返回字符串中第个单词。如果中的单词数要大,那么返回空字符串。
示例:$(word 2, foo bar baz)返回值是“bar”。

2.17.9 $(wordlist s,e,text)

名称:取单词串函数——wordlist。
功能:从字符串中取从开始到的单词串。是一个数字。
返回:返回字符串中从的单词字串。如果中的单词数要大,那么返回空字符串。如果大于的单词数,那么返回从开始,到结束的单词串。
示例: $(wordlist 2, 3, foo bar baz)返回值是“bar baz”。

2.17.10 $(words )

名称:单词个数统计函数——words。
功能:统计中字符串中的单词个数。
返回:返回中的单词数。
示例:$(words, foo bar baz)返回值是“3”。
备注:如果我们要取中最后的一个单词,我们可以这样:$(word $(words  ), )。

2.17.11 $(firstword )

名称:首单词函数——firstword。
功能:取字符串中的第一个单词。
返回:返回字符串的第一个单词。
示例:$(firstword foo bar)返回值是“foo”。
备注:这个函数可以用word函数来实现:$(word 1, )。

2.17.12 $(dir names…)

名称:取目录函数——dir。
功能:从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部分。如果没有反斜杠,那么返回“./”。
返回:返回文件名序列的目录部分。
示例: $(dir src/foo.c hacks)返回值是“src/ ./”。

2.17.13 $(notdir )

名称:取文件函数——notdir。
功能:从文件名序列中取出非目录部分。非目录部分是指最后一个反斜杠(“/”)之后的部分。
返回:返回文件名序列的非目录部分。
示例: $(notdir src/foo.c hacks)返回值是“foo.c hacks”。

2.17.14 $(suffix )

名称:取后缀函数——suffix。
功能:从文件名序列中取出各个文件名的后缀。
返回:返回文件名序列的后缀序列,如果文件没有后缀,则返回空字串。
示例:$(suffix src/foo.c src-1.0/bar.c hacks)返回值是“.c .c”。

2.17.15 $(basename )

名称:取前缀函数——basename。
功能:从文件名序列中取出各个文件名的前缀部分。
返回:返回文件名序列的前缀序列,如果文件没有前缀,则返回空字串。
示例:$(basename src/foo.c src-1.0/bar.c hacks)返回值是“src/foo src-1.0/bar hacks”。

2.17.16 $(addsuffix , )

名称:加后缀函数——addsuffix。
功能:把后缀加到中的每个单词后面。
返回:返回加过后缀的文件名序列。
示例:$(addsuffix .c,foo bar)返回值是“foo.c bar.c”。

2.17.17 $(addprefix , )

名称:加前缀函数——addprefix。
功能:把前缀加到中的每个单词后面。
返回:返回加过前缀的文件名序列。
示例:$(addprefix src/,foo bar)返回值是“src/foo src/bar”。

2.17.18 $(join , )

名称:连接函数——join。
功能:把中的单词对应地加到的单词后面。如果的单词个数要比的多,那么,中的多出来的单词将保持原样。如果的单词个数要比
多,那么,多出来的单词将被复制到中。
返回:返回连接过后的字符串。
示例:$(join aaa bbb , 111 222 333)返回值是“aaa111 bbb222 333”。

2.18 foreach 函数

$(foreach var,list,text)

这个函数的意思是,把参数中的单词逐一取出放到参数所指定的变量中,然后再执行所包含的表达式。每一次会返回一个字符串,循环过程中,的所返回的每个字符串会以空格分隔,最后当整个循环结束时,所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值,以下示例:

names := a b c d

files := $(foreach n,$(names),$(n).o)

         上面的例子中,$(name)中的单词会被挨个取出,并存到变量“n”中,“$(n).o”每次根据“$(n)”计算出一个值,这些值以空格分隔,最后作为foreach函数的返回,所以,     $(files)的值是“a.o b.o c.o d.o”

2.19 call函数

$(call ,,,...)

        当 make执行这个函数时,参数中的变量,如$(1),$(2),$(3)等,会被参数依次取代。而的返回值就是 call函数的返回值。例如: 

reverse = $(2) $(1)

foo = $(call reverse,a,b)

那么,foo的值就是“b a”

2.20 shell函数

        shell 函数也不像其它的函数。顾名思义,它的参数应该就是操作系统Shell的命令。它和反引号“`”是相同的功能。这就是说,shell函数把执行操作系统命令后的输出作为函数返回。于是,我们可以用操作系统命令以及字符串处理命令awk,sed等等命令来生成一个变量,如:

contents := $(shell cat foo)
files := $(shell echo *.c)

2.21 指定Makefile

我们可以使用“ -f”选项指定文件,不再使用名为“ Makefile”的文件,比如:

make -f Makefile.build

我们可以使用“ -C”选项指定目录,切换到其他目录里去,比如:

make -C a/ -f Makefile.build

我们可以指定目标,不再默认生成第一个目标:

make -C a/ -f Makefile.build other_target

三、通用Makefile

【Linux】Makefile_第1张图片

你可能感兴趣的:(Linux,linux,运维,服务器)