Linux操作系统——Makefile

Makefile

1.概念

  在Window操作系统中,我们可以使用很多优秀的IDE(Integrated Development Environment)来进行开发,如Visual Studio等,在这些集成开发环境中,我们只需要编写好代码再点击编译就可以运行了。而在Linux操作系统中,通常我们编写好源码后需要使用GCC等编译器进行手动编译。当我们的项目文件有大量的源文件(.c)及头文件(.h)时,手动进行编译就会非常繁琐,这时候如果一个工具,我们只需要提供输入输出及依赖就可以帮助我们完成整个编译过程,那么将会大大减少我们的工作量,提高我们的开发效率。

  Makefile的作用就是帮助我们解决上面这个问题,我们只需要编辑好整个项目的Makefile文件后通过一个make命令就能够实现自动编译。准确地说,make是一个命令工具,是一个解释Makefile中指令的命令工具,它会根据我们编写的Makefile来确定哪些文件需要进行编译。

2.示例

2.1 编译单个文件

  以C语言最经典的入门程序Hello World为例,在Linux下编译好该源代码后,我们通常是通过gcc -c helloworld.c -o helloworld.o以及gcc helloworld.o -o helloworld命令进行编译来生成可执行文件的文件。我们将上述命令改造为Makefile文件,通过make命令来进行自动编译。

helloworld.c文件:

#include 
int main()
{
    printf("Hello World");
    return 0;
}

Makefile文件:

写法一:

helloworld : helloworld.o
	gcc helloworld.o -o helloworld
helloworld.o : helloworld.c
	gcc -c helloworld.c -o helloworld.o

写法二:

helloworld : helloworld.c
	gcc -c helloworld.c -o helloworld.o
	gcc helloworld.o -o helloworld

  在上述编写的写法一Makefile文件中,根据我们编译helloworld.c的过程,首先我们最后生成的可执行文件helloworld需要helloworld.o目标文件,而生成可执行文件helloworld则是通过命令gcc helloworld.o -o helloworld;接着我们生成helloworld.o目标文件需要helloworld.c源文件,通过命令gcc -c helloworld.c -o helloworld.o来生成。这里我们简单了解Makefile的基本规则,根据我们最终生成的文件反推回去所需要依赖的文件来编写,并按照 目标文件 : 依赖文件 的格式来编写,接着在下方编写生成目标文件的指令(注意指令前面要按TAB键)。当然我们也可以按照写法二来进行Makefile文件的编写,此时需要注意依赖文件的先后顺序,需要先执行gcc 命令生成目标文件helloworld.o再执行gcc命令生成可执行文件hellowolrd。

2.2 编译多个文件

  在上述例子中我们只是编译了单个源文件,还不能够体现出编写Makefile文件进行自动编译的便捷性。这里我们编写两个源文件(.c)来实现一个加法功能,通过手动编译以及编写Makefile文件进行自动编译来体会两者的效率。

add.c文件:

int add(int num_a, int num_b)
{
	return num_a + num_b;
}

main.c文件:

#include 

int add(int num_a,int num_b);

int main()
{
	int rst;
	rst = add(1,2);
	printf("1 + 2 = %d\n",rst);
	return 0;
}

  编写完成上述两个源文件后,我们通过命令gcc -c add.c -o add.o生成目标文件add.ogcc -c main.c -o main.o生成目标文件main.o,最后通过命令gcc main.o add.o -o main将两个目标文件(add.o和main.o)链接生成可执行文件main。这时候如果对上面两个源文件进行了改动,我们都需要重新编译生成目标文件再将两个目标文件链接生成可执行文件,这将会给我们的开发增加难度,大大降低我们的开发效率。我们编写Makefile文件来实现上述的编译过程,这时候只需要通过一个简单的指令make就可以替我们实现上面的整个过程,并且它还会智能检测我们哪一些文件是需要生成的,哪一些文件是不需要的。

main : main.o add.o
	gcc main.o add.o -o main
main.o : main.c
	gcc -c main.c -o main.o
add.o : add.c
	gcc -c add.c -o add.o

  此时,我们修改main.c源文件,将1 + 2修改为1 + 3,直接执行命令make进行重新编译后运行可执行文件main,得到1 + 3的结果。

main.c文件:

#include 

int add(int num_a,int num_b);

int main()
{
	int rst;
	rst = add(1,3);
	printf("1 + 3 = %d\n",rst);
	return 0;
}

执行结果:

$ ./main
1 + 3 = 4

3.基本语法

Linux操作系统——Makefile_第1张图片

3.1 变量

  在Makefile文件中定义的变量,与在C语言中定义的宏类似代表一个字符串文本,在执行Makefile的时候会用该变量的值进行替换,使用变量可以使得Makefile文件易于维护和扩展。

  变量的命名可以是字母、数字、下划线的任意组合形式,但不能够包含#= (空格)。变量名是大小写敏感的,当需要使用变量时需要在变量名前加上$符号,并且建议在使用中用圆括号()或花括号{}将变量名包括起来,如果需要使用真实字符$,则需要用$$来表示,以下为变量的三种使用方式(常用方式二):

variable = test
$variable
$(variable)
${varialbe}
3.1.1 自定义变量

  Makefile中的变量只能是字符串类型,它包括四种赋值方式:

  1. 简单赋值(:=) 如果赋的值存在变量,那么该变量的值为执行此赋值语句前的值,否则直接赋值。

    a := 1
    b := $(a)2
    a := 3
    

    此时a和b的值分别为 3 以及 12,相当于C语言中的变量赋值,将变量a的值拼接2后的结果赋予b。

  2. 递归赋值(=) 如果赋的值存在变量,那么会以该变量最终赋的值来进行替换,否则直接赋值。

    a = 1
    b = $(a)2
    a = 3
    

    此时a和b的值分别为 3 以及 32,与简单赋值不同,使用递归赋值如果存在变量会以该变量的最终结果来赋值,所以可以使用赋值语句后面定义的变量。

  3. 条件赋值(?=) 如果赋值的变量在此之前未进行定义,那么会对变量进行赋值;若已经定义,则该赋值语句无效。

    a := 1
    b := $(a)2
    a ?= new
    

    此时a和b的值分别为 1 以及 12,在执行a ?= new时,由于之前已经执行了a := 1进行赋值,所以这里不会进行赋值。

  4. 追加赋值(+=) 将在赋值的变量后面追加新的值,原变量值与新追加的值之间由空格隔开。

    a := 1
    b := $(a)2
    a += b
    

    此时a和b的值分别为 1 12 以及 12,执行a += b进行赋值时,原先a的值为1,b的值为12,将ab进行拼接并在中间增加一个空格

3.1.2 环境变量

  make运行时的系统环境变量可以在make开始运行时被载入到Makefile文件中,但如果Makefile文件中已经定义了这个变量,或是这个变量由make命令行指定,那么将会覆盖系统的环境变量的值。(如果make命令指定了-e参数,那么系统环境变量会覆盖Makefile中定义的变量。)

3.1.3 目标变量

  在3.1.1中我们自定义的变量的作用域为整个Makefile文件,即相当于C语言中的全局变量。同样地,我们也可以为某个目标设定局部变量,这种变量被称为"Target-specific Variable"目标变量,因为它的作用范围只在某条规则以及其连带规则中,所以它可以和"全局变量"同名,其语法结构为:

 :  [=、:=、?=、+=、] 

这个特性非常有用,当我们设置一个这样的局部变量后,可以在指定的目标中及其所包含的所有规则中生效。如在test中改变a的值并打印出来:

a := 1
b := $(a)2

test : a = 3

test :
	@echo "a => $(a)"
	@echo "b => $(b)"

在上述的Makefile文件中,当执行make test命令时,由于在test中对a执行了赋值a = 3而对b进行b := $(a)2赋值时a的值为1,所以输出的a和b分别为 312

3.1.4 模式变量

  在make中还支持模式变量(Pattern-specific Variable),它是目标变量的扩展,可以根据相应的规则作用到符合规则的目标中。在模式变量中必须包含一个"%“字符,用来匹配任意字符,例如想匹配所有以**.o**结尾的字符串可以使用”%.o"来匹配,其语法结构与目标变量一致:

 :  [=、:=、?=、+=、] 

在编写Makefile文件时使用模式变量能够大大简化我们编写的文件,提高Makefile文件的可读性以及扩展性。如在所有以 t结尾的目标中将变量a重新赋值为new value

a := 1
b := 2

%t : a = new value

test :
	@echo "a => $(a)"
	@echo "b => $(b)"

此时执行命令 make test后,由于模式变量%t匹配到了目标test所以会对a赋值a = new value,打印的a和b的值分别为 new value2

3.1.5 override修饰符

  当我们make执行Makefile文件时,如果想在使用make命令时修改Makefile中定义的变量的值,那么只需要在执行make命令时直接输入 [=、:=、?=、+=、] 即可进行赋值。当我们不想Makefile文件中的变量被随意改变时,只需要对该变量使用override进行修饰,就会忽略该变量在make命令行参数中的赋值,其语法结构为:

override  [=、:=、?=、+=、] 

例如通过在make命令行参数中动态指定欲生成的可执行文件名,若不指定则以默认文件名(main)来生成:

filename = main

$(filename) : main.o add.o
	gcc main.o add.o -o $(filename)
main.o : main.c
	gcc -c main.c -o main.o
add.o : add.c
	gcc -c add.c -o add.o

当执行命令make filename=run时就会生成可执行文件run,若想禁止通过命令行参数进行修改,只需要在filename前加上override即可。

3.1.6 多行变量

  在Makefile中还可以通过使用define关键字来修饰一个变量,被修饰的变量在赋值的时候可以进行换行,其变量的值可以是函数、命令、文字或是其他变量,命令需要以Tab键开头,其语法结构为:

define 

   ·
   ·
   ·
   ·
   ·
   ·

endef

例如通过定义一个多行变量并调用该变量来实现多个变量的打印输出:

a = 1
b = 2

define show
	@echo "a => $(a)"
	@echo "b => $(b)"
endef

test :
	$(show)

当执行命令make test后打印输出 a => 1b => 2

3.2 变量的高级用法

3.2.1 变量值的替换
  1. 可以将变量中共有的部分进行替换,如将某个变量的值中所有以a结尾的字符串中的最后一个字符a替换为b,这里的结尾是指字符串中的空格或结束符,其语法结构为:

     [=、:=、?=、+=、] $(:=)
     [=、:=、?=、+=、] ${:=}
    

    ​ 例如将所有以.o结尾的字符串替换成以.c结尾

    old = a.o b.o c.o
    new = $(old:.o=.c)
    
    test :
    	@echo $(new)
    

    ​ 当执行命令make test后打印输出变量new的值为 a.c b.c c.c

  2. 我们在修改某个变量值时很多时候并不一定只是它的一部分,这时候上面的方法就不能够满足我们的需求了,那么我们可以通过在表达式中使用通配符%来进行匹配,其语法结构与上述一致。例如对某个变量的值,其形式都是以a开头和以b结尾的,我们需要将开头和结尾替换成x和y,可以通过编写以下Makefile文件来实现这个效果:

    old = a123b a456b a789b
    new = ${old:a%b=x%y}
    
    test :
    	@echo $(new)
    

    ​ 当执行命令make test后打印输出变量new的值为 x123y x456y x789y在日常开发过程中,通常会使用这种方式来进行变量替换引用的操作

3.2.2 变量的嵌套使用

  我们对一个变量进行赋值时可以引用其他的变量,并且引用变量的数量和次数是不限的。如对一个变量赋值时进行两层的嵌套引用:

x = y
y = double reference
z = $($(x))

test :
	@echo $(z)

当执行命令make test后打印变量z的值为 double reference。首先对 z 进行赋值时进行了变量的嵌套引用,最里层是$(x)引用了变量x的值为 y,将$(x)替换为 y 再进行赋值,此时赋值语句变为z = $(y),最后将 y 的值 double reference 赋给变量 z 。

3.3 条件判断

  在Makefile文件中,我们可以使用条件判断让make根据运行时不同的情况来选择不同的执行分支,其语法结构如下:


	
endif
或

	
else
	
endif

其中表示条件关键字,一共有四种条件关键字。

  1. ifeq关键字

    比较参数arg1和arg2的值是否相同,可以为以下五种语法中的任意一种:

    ifeq (,)
    ifeq ''''
    ifeq """"
    ifeq ""''
    ifeq ''""
    
  2. ifneq关键字

    比较参数arg1和arg2的值是否不相同,与ifeq关键字的语法相同。

  3. ifdef关键字

    判断变量的值是否非空,非空则为真,否则为假,其语法结构为:

    ifdef 
    
  4. ifndef关键字

    判断变量的值是否为空,空则为真,否则为假,与ifdef关键字的语法相同。

    判断一个变量的值是否为空:

     x = 
     y = $(x)
    
     ifDef :
     ifdef y
     	@echo not empty
     else
     	@echo empty
     endif
    
     ifEq :
     ifeq ($(y),)
     	@echo empty
     else
     	@echo not empty
     endif
    
     test : ifDef ifEq
    

    当执行命令make test后打印值为 not emptyempty 。通过上面的例子可以发现,当我们要判断一个变量的值是否为空时应该使用ifeq关键字来判断,而对于ifdef关键字我的理解是判断一个变量是否有定义;在定义变量y时引用了变量x的值,而变量x赋值为空,所以变量y的值也为空。

3.4 自动化变量

  自动化变量可以理解为由Makefile自动产生的变量。在Makefile中描述规则时,依赖文件和目标文件是变动的,这时候我们不应当使用具体的文件名称,那么如何书写一个命令来完成从不同的依赖文件生成对应的目标?这时候我们就应该使用Makefile提供的自动化变量。使用Makefile提供的自动化变量,我们不需要关心这些不确定的目标和依赖,只需要编写其生成过程的命令,让Makefile自动地帮我们获取相对应的值并替换进去即可。

变量 含义
$@ 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,$@就是匹配于目标中模式定义的集合。
$% 仅当目标是函数库文件中,表示规则中的目标成员名。
$< 依赖目标中的第一个目标名字。如果依赖目标是以模式(即"%")定义的,那么$<将是符合模式的一系列的文件集。
$? 所有比目标文件更新的依赖文件列表,空格分隔。如果目标文件是静态库文件,代表的是库文件(.o 文件)。
$^ 所有的依赖目标的集合,以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。
$+ $^变量类似,也是所有依赖目标的集合,但当依赖目标存在重复的时候它不会去除重复的依赖目标。
$* 这个变量表示目标模式中"%"及其之前的部分,在日常使用中应尽量避免使用该变量,除非是在隐含规则或是静态模式中。

​使用上面的这些自动化变量,我们可以写一个Makefile文件来快速编译一个有多个源文件的项目:

NAME ?= main
OBJS := main.o add.o

CC := gcc

$(NAME) : $(OBJS)
	$(CC) $^ -o $@
	
%.o : %.c
	$(CC) -c $< -o $@

在这个Makefile文件中,我们将默认定义生成的可执行文件名为main,除非我们手动在make命令行参数中指定输出名。同样地,我们定义了一个OBJS变量来存放我们所依赖的文件,以及CC变量存放我们的编译命令,提高这个Makefile文件的扩展性。

3.5 常用函数

  在Makefile文件中除了可以定义变量,同样地我们也可以使用函数来帮助我们实现各种功能,从而使Makefile文件更加灵活和智能。函数的调用与变量的使用方式相似,也需要使用$来标识,其语法结构如下:

$( )
${ }
3.5.1 字符串函数
  1. subst函数

    • 字符串替换函数
    • 字符串中的字符串替换为字符串。
    • 返回被替换后的字符串。
    • 语法
    $(subst ,,)
    
  2. patsubst函数

    • 模式字符串替换函数
    • 查找字符串中的子字符串(以空格、Tab、回车、换行分隔)是否符合模式,若匹配则以进行替换。其中可以是包含通配符%的任意长度字符串,若中也包含通配符%,则中通配符%的值为通配符%所匹配的值,如果需要使用真实字符%,需要使用\%来表示。
    • 返回被替换后的字符串。
    • 语法
    $(patsubst ,,)
    
  3. strip函数

    • 去除空格函数
    • 去除字符串中开头和结尾的空字符。
    • 返回去除空格后的字符串。
    • 语法
    $(strip )
    
  4. findstring函数

    • 查找字符串函数
    • 字符串中查找字符串。
    • 如果找到,则返回字符串;否则返回空字符串。
    • 语法
    $(findstring ,)
    
  5. filter函数

    • 过滤函数
    • 模式过滤字符串,保留符合模式中的子字符串,支持多个模式过滤。
    • 返回符合模式的字符串。
    • 语法
    $(filter ,)
    
  6. filter-out函数

    • 反过滤函数
    • 模式过滤字符串,去除符合模式中的子字符串,支持多个模式过滤。
    • 返回去除符合模式后的字符串。
    • 语法
    $(filter-out ,)
    
  7. sort函数

    • 排序函数
    • 字符串中的子字符串进行排序(升序),同时去除字符串中重复的子字符串。
    • 返回排序后的字符串。
    • 语法
    $(sort )
    
  8. word函数

    • 取字符串函数
    • 字符串中第个子字符串。
    • 返回字符串中第个子字符串,若超出字符串长度则返回空字符串。
    • 语法
    $(word ,)
    
  9. wordlist函数

    • 取字符串列表函数
    • 字符串中从第开始到的子字符串的列表。
    • 返回字符串中第的子字符串列表,若超出字符串长度则返回空字符串,若超出字符串长度则返回从开始的所有子字符串列表。
    • 语法
    $(wordlist ,,)
    
  10. words函数

    • 子字符串统计函数
    • 统计字符串中的子字符串数量。
    • 返回字符串中子字符串的数量。
    • 语法
    $(words )
    
  11. firstword函数

    • 首子字符串函数
    • 字符串的第一个子字符串。
    • 返回字符串的第一个子字符串。
    • 语法
    $(firstword )
    
3.5.2 文件与目录函数
  1. dir函数

    • 取目录函数
    • 截取文件列表中文件路径的目录部分,若提供文件的路径中不存在/,则返回./
    • 返回文件列表的目录列表。
    • 返回字符串的第一个子字符串。
    • 语法
    $(dir )
    
  2. notdir函数

    • 取文件名函数
    • 截取文件列表中文件路径的文件名部分。
    • 返回文件列表的文件名列表。
    • 语法
    $(notdir )
    
  3. suffix函数

    • 取后缀函数
    • 截取文件列表中文件的后缀部分,若不存在后缀则返回空字符串。
    • 返回文件列表的后缀列表。
    • 语法
    $(suffix )
    
  4. basename函数

    • 取前缀函数
    • 截取文件列表中文件的前缀部分,若不存在前缀则返回空字符串。
    • 返回文件列表的前缀列表。
    • 语法
    $(basename )
    
  5. addsuffix函数

    • 加后缀函数
    • 文件列表中文件添加后缀部分。
    • 返回文件列表添加后缀后的列表。
    • 语法
    $(addsuffix ,)
    
  6. addprefix函数

    • 加前缀函数
    • 文件列表中文件添加前缀部分。
    • 返回文件列表添加前缀后的列表。
    • 语法
    $(addprefix ,)
    
  7. join函数

    • 链接函数
    • 中的子字符串按顺序链接到中对应的子字符串,若的子字符串个数与的子字符串个数不相等,则多出来的子字符串将保持原样不进行链接。
    • 返回链接后的列表。
    • 语法
    $(join ,)
    
  8. realpath函数

    • 取绝对路径函数
    • 文件列表的文件相对路径转换成绝对路径,当提供的文件为链接时会进行解析其所链接的文件路径。
    • 返回转换后的列表。
    • 语法
    $(realpath )
    
  9. abspath函数

    • 取绝对路径函数
    • 文件列表的文件相对路径转换成绝对路径,当提供的文件为链接时不会进行解析其所链接的文件路径。
    • 返回转换后的列表。
    • 语法
    $(abspath )
    
3.5.3 其他函数
  1. wildcard函数

    • 扩展通配符函数
    • 获取所有满足模式的文件,使用通配符*来匹配任意字符。
    • 返回匹配的文件列表。
    • 语法
    $(wildcard )
    
  2. foreach函数

    • 循环遍历函数
    • 循环获取字符串中的每一个子字符串(以空格分隔开来),将其赋值到变量并执行操作作为结果。
    • 返回循环遍历操作后的结果。
    • 语法
    $(foreach ,,)
    

    获取执行make test命令的路径下所有以.c结尾的文件,并在每个文件后面增加.o

    FILES = $(wildcard *.c)
    
    test :
    	@echo $(foreach n,$(FILES),$(n).o)
    
  3. if函数

    • 判断函数
    • 如果条件为非空字符串,则条件为真执行,否则执行
    • 如果条件为真,则返回的值,否则返回的值。若返回时未定义则返回空字符串。
    • 语法
    $(if ,[,])
    
  4. shell函数

    • 执行shell命令函数
    • 命令当做一条shell命令来执行,并返回shell命令的执行结果。
    • 返回shell命令执行结果。
    • 语法
    $(shell )
    

  利用上面的函数来编写一个Makefile文件,实现自动将执行make命令的路径下的所有源文件.c文件进行编译链接,若不设置输出的可执行文件名则默认设置为该路径的文件夹名:

TARGET ?= $(subst $(realpath ..)/,,$(realpath .))
ALLSRC = $(foreach SRC,$(shell find . -name "*.c"),$(notdir $(SRC)))
OBJS = $(ALLSRC:.c=.o)
CC = gcc

$(TARGET) : $(OBJS)
	$(CC) $^ -o $@
	
%.o : %.c
	$(CC) -c $< -o $@

clean :
	rm -rf *.o $(TARGET)

3.6 隐式规则

  在上面由我们编写的Makefile文件中的各个生成规则,称为显式规则。除了显式规则,Makefile文件还包括隐式规则,它由make自动推导出来不需要用户进行编写。隐式规则只能用于推导对象文件.o源文件.c之间的依赖关系,当Makefile的某一目标文件依赖于某个对象文件,但没有对应的显式规则时make就会自动添加一个隐式规则:

main : main.o add.o
	gcc main.o add.o -o main

  对于上面这样一个Makefile文件,我们并没有编写生成对象文件.o的规则,但是当我们执行make命令的时候仍然能够完成编译生成可执行文件,并且我们可以看到在终端中显示了cc -c -o main.o main.ccc -c -o add.o add.c这两条指令,这就是Makefile的隐式规则。隐式规则中使用的变量分为两种:一种是命令相关的,一种是参数相关的。

  • 与命令相关的变量
变量 含义
AR 函数库打开包程序。默认命令是"ar"
AS 汇编语言编译程序。默认命令是"as"
CC C语言编译程序。默认命令是"cc"
CXX C++语言编译程序。默认命令是"g++"
CO 从RCS文件中扩展文件程序。默认命令是"co"
CPP C程序的预处理器(输出是标准输出设备)。默认命令是"$(CC)-E"
FC Fortran和Ratfor的编译器和预处理程序。默认命令是"f77"
GET 从SCCS文件扩展文件的程序。默认命令是"get"
LEX Lex方法分析器程序(针对于C或Ratfor)。默认命令是"lex"
PC Pascal语言编译程序。默认命令是"pc"
YACC Yacc文法分析器(针对C程序)。默认命令是"yacc"
YACCR Yacc文法分析器(针对Ratfor程序)。默认命令是"yacc -r"
MAKEINFO 转换Texinfo源文件(.texi)到info文件程序。默认命令是"makeinfo"
TEX 从TeX源文件创建TeX DVI文件的程序。默认命令是"tex"
WEAVE 转化Web到TeX的程序。默认命令是"weave"
TEXI2DVI 从Texinfo源文件创建TeX DVI文件的程序。默认命令是"texi2dvi"
CWEAVE 转化C Web到TeX的程序。默认命令是"cweave"
TANGLE 转换Web到Pascal语言的程序,默认命令是"tangle"
CTANGLE 转换C Web到C。默认命令是"ctangle"
RM 删除文件命令。默认命令是"rm-f"
  • 与参数相关的变量
变量 含义
ARFLAGS 函数库打包程序AR命令的参数。默认值是"rv"
ASFLAGS 汇编语言编译参数(当明显地调用".s"或".S"文件时)
CFLAGS C语言编译器参数
CXXFLAGS C++语言编译器参数
COFLAGS RCS命令参数
CPPFLAGS C预处理器参数(C和Fortran编译器也会用到)
FFLAGS Fortran语言编译器参数
GFLAGS SCCS "get"程序参数
LDFLAGS 连接器参数(如"ld")
LFLAGS Lex文法分析器参数
PFLAGS Pascal语法编译器参数
RFLAGS Ratfor程序的Fortran编译器参数
YFLAGS Yacc文法分析器参数

你可能感兴趣的:(Linux操作系统,linux)