======================= **GNU  下 MAKEFILE 基本规则** =======================

前言: 

对于系统来讲,make 其实也是一个脚本,有着自身的一些规则和要求。而这个脚本主要做的任务就是帮助程序员减少源文件到可执行文件中间的一系列的(预处理,编译,汇编,链接)操作,提高效率。

环境(GNU Make 4.2.1 / gcc version 9.3.0 (Ubuntu 9.3.0-10ubuntu2)), 学习过程中涉及的文件github link;

学习主要参考链接:  跟我一起写Makefile /  MAKE 官方文档

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中使用 # 字符,可以用反斜杠进行转义,如: \# 。

======================= **基础知识** =======================

基本语法篇:

在Makefile中的命令,必须要以 Tab 键开始。

#####变量篇 : 变量类似 C的宏定义,在使用中直接展开

  • 变量的命名字可以包含字符、数字,下划线(可以是数字开头),但不应该含有 : 、 # 、 = 或是空字符(空格、回车等)。
  • 变量是大小写敏感的,“foo”、“Foo”和“FOO”是三个不同的变量名。
  • 变量在声明时需要给予初值;

  传统的Makefile的变量名是全大写的命名方式, 另外其中有写特殊字符变量$<, $^, 这些参考后面的cheat sheet;

 使用变量时候:一般最好使用\${ARGs}/\$(ARGS)  这种括号表示对应的ARGS 变量,前面的变量可以调用后面定义的变量);

objects = program.o foo.o utils.o
program : $(objects)
    cc -o program $(objects)

#可以相互调用,然后展开
foo = $(bar) #调用后面定义的变量;
bar = $(ugh) 
ugh = Huh
all:
    echo $(foo)  #最终的结果就是Hug

 因为变量就是展开,当出现递归时候,就会进入死循环(虽然make 会报错),但在实际使用中还是要尽量避免;

 此时可以使用下面三种方式来避免一些可能bug的出现;

   :=(如果调用未定义变量,为空) ;

  ?=(如果该变量未定义,则为后面定义的值,如果已经定义过,则不变);

  += 给变量追加值, 如果没有定义过该变量,就相当于=

y := $(x) bar #这里展开结果是 y = bar
x := foo  

#利用这个表达符,还可以有效定义空格:
nullstring :=
space := $(nullstring) #使用#表示变量定义终止;

FOO ?= bar  #如果FOO没有被定义过,那么变量FOO的值就是“bar”,如果FOO先前被定义过,那么这条语将什么也不做  

objects = main.o foo.o bar.o utils.o
objects += another.o  # $(objects) 值变成 main.o foo.o bar.o utils.o another.o

高级用法:

  • 替换字符
foo := a.o b.o c.o  
bar := $(foo:.o=.c)    #替换foo 中所有.o 后缀为.c
bar := $(foo:%.o=%.c)    #替换foo 中所有.o 后缀为.c,静态模式?
  • 变量值在当成变量
first_second = Hello
a = first
b = second
all = $($a_$b)  
 
#加强版:
a_objects := a.o b.o c.o
1_objects := 1.o 2.o 3.o
sources := $($(a1)_objects:.o=.c)  #这样a1 = a(l),就可以分别表示不同结果
  • 目标变量 : 为某个目标设置局部变量,它可以 和“全局变量”同名,因为它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效。
#语法示例:
<target ...> : <variable-assignment>;

#具体例子:
prog : CFLAGS = -g
prog : prog.o foo.o bar.o
    $(CC) $(CFLAGS) prog.o foo.o bar.o
prog.o : prog.c
    $(CC) $(CFLAGS) prog.c
foo.o : foo.c
    $(CC) $(CFLAGS) foo.c
bar.o : bar.c
    $(CC) $(CFLAGS) bar.c
    #不管全局的 $(CFLAGS) 的值是什么,这段依赖命令 $CFLAGS 都是 -g
  • 模式变量 : 给定一种“模式”,可以把变量定义在符合这种模式的所有目标上;类似上面目标变量,这里是对应一种模式的变量,目标变量则是指定了目标;
%.o : CC = XXXXG    #给所有以 .o 结尾的目标定义变量CC  为 XXXXG

 

#####通配符 很多与正则(linux 中特殊变量) 类似,也有一些是特有的;

最常见的通配符: * , 代表一个或多个字符;另外在模式规则中 % 代表非空字符串匹配;

tips: 模式规则:对应的是生成规则,

rm *.o #普通通配符
#模式规则中 通配符;
%.o : %.cpp
  ${CC} -c ${CFLAGS} %< -o $@

 

但是在变量定义中使用通配符有一个坑要注意:当定义变量为*.o 时候: 如果通配符表达式匹配不到任何合适的对象,通配符语句本身就会被赋值给变量;

所以下面看到的两次执行时候,过程与结果是不一样的;

EXAMPLE: makefile 中内容、执行结果 如下;

CC=g++
OBJ=*.o
.PHONY:test
test : ${OBJ}
        echo ${OBJ}
        ${CC} -c *.cpp 

.PHONY:clean
clean: 
        -rm *.o

echo "*.o" #第一次make 时候,echo 实际执行;
echo *.o     #第二次make 时候,echo 实际执行;

因为存在上面的问题,所以一般都是用通配符函数(wildcard)来解决上面的问题;

CC=g++
OBJ=${wildcard *.o}
.PHONY:test1
test1 : ${OBJ}
        ${CC}    -c *.cpp
        echo ${OBJ}

.PHONY:clean
clean: 
        -rm *.o

#第一次make test1, ${wildcard *.o}, 没有任何匹配中,所以为空;因为匹配在g++ -c *.cpp 之前,所以没有匹配;
#第二次make test2, ${wildcard *.o} 匹配中了,所以有对应值

上面举例通配符中要要注意的地方,正常来写可以用下面方法:

CC=g++
SRC=${wildcard *.cpp}
OBJ=${SRC:%.cpp=%.o}  ###模式规则中常用的语法,将SRC中所有.cpp 文件换成.o 文件

.PHONY:test
test : ${OBJ} 
        @echo "do the compile "

%.o : %.cpp  #定义模式规则
        ${CC} -c ${CFLAGS} $<

.PHONY:clean
clean: 
        -rm *.o   

 

#####条件判断篇 :关键字有 ifeq/ifneq/ifdef/ifndef  , else, endif;

ifeq (condition)  #condition可以是变量是否为空${VAR},可以直接比较值(${CC} gcc) ,也可以调用函数
    #operation1
else
    #operation2
endif

 对于if/else 来讲,可以对于不同的PHONY 给不同的定义/debug 选项,从而实现不同的编译条件 和 使用场景(debug/release/release with debug information...);

 

#####函数篇 : 常见函数link

#函数调用规则
$(<function> <argument0>,<argument1>) #函数名与参数之间使用空格分隔, 参数与参数之间用 , 分隔;

 

MAKEFILE执行逻辑:   

  1. GNU 中 make会在当前目录下依次寻找名字叫“GNUmakefile”、“makefile”和“Makefile" 的文件;

  2. 如果找到,它会找文件中的第一个目标文件(target),并把这个target文件作为最终的目标文件。

  3. 如果target文件不存在,或是target所依赖的后面的 .o 文件的文件修改时间要比 target这个文件新,那么,他就会执行后面所定义的命令来生成 target。

  4. 如果 target所依赖的 .o 文件也不存在,那么make会在当前文件中找目标为 .o 文件的依赖性,如果找到则再根据那一个规则生成 .o 文件。(类似栈操作);

  5. 当所有的前置文件都存在 且 是最新时候,于是make会生成最终执行文件;

tips: 出现错误时候,直接退出并报错;

   

make常见相关操作:

 

返回值:
0 :表示成功执行。
1: 如果make运行时出现任何错误,其返回1。
2: 如果你使用了make的“-q”选项,并且make使得一些目标不需要更新,那么返回2。#
指定目标:
make -f(/--file/--makefile) FILENAME.mk  #指定执行make 文件
make clean #指定特定的目标,例如:clean
#检查规则
-n/--just-print/--dry-run/--recon : 不执行参数,只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来;用来debug;
-t/--touch : 只更新目标文件时间戳,假装编译了,其实没有变化
-q/--question : 寻找目标,存在的话,就什么都不输出,否则会输出错误;
-W <file> : file 一般是源文件,make 会根据规则运行依赖这个文件的命令;
-w/--print-directory : 输出运行makefile 之前和之后的信息,对于嵌套调用makefile 很有用;
-k/--keep-going : 出错也不停止
-i : 执行时候忽略所有错误;
-I : 指定被包含在makefile 的搜索目标,可以通过多个-I 指定多个目录;

======================= **代码演示** =======================

文件的依赖规则: 也就是说生成target 需要 prerequisites 中的文件,所以如果prerequisites 中有文件更新,那么就要更新target 文件;

 target : 可以是一个obj/执行文件、标签, 可以是一个, 也可以是多个,中间用空格分开;

     如果是多目标的话,可以用$@ 来代表这个多目标的集合

 prerequisite : 生成target 所依赖的文件 

 commnad : 该target 要执行的文件, 如果不是于prerequisites 同一行的话,那么要用tap 开头;

注意MAKEFILE 中第一个目标会被作为默认的目标,

target ... : prerequisites ... 
    command
    ...
    ...

EXAMPLE: 

下面是最简单的例子:其中 \ 为换行符,对于太长行可以用来换行;

all : main.o module1.o \
        module2.o   # \换行符号
        g++ -o result.out main.o module1.o module2.o

main.o : main.cpp module1.h module2.h
        g++ -c main.cpp 

module1.o : module1.cpp module1.h
        g++ -c module1.cpp

module2.o : module2.cpp module2.h
        g++ -c module2.cpp

clean: 
        rm *out *.o

 其中有很多优化地方,优化后结果如下:

  1. 宏定义;变量定义

  2. 自动推导:

  3. 伪目标文件:只是一个标签,指明这个目标就是为目标文件,不管有没有clean 这个文件,make clean 就是进行下面操作;

   而且伪目标不会生成文件;

   伪目标一样可以有依赖,实际执行时候,就是先执行依赖,再执行自身;

我们可以根据这种性质来让我们的makefile根据指定的不同的目标来完成不同的事。在Unix世界中,软件发布时,特别是GNU这种开源软件的发布时,其makefile都包含了编译、安装、打包等功能。我们可以参照这种规则来书写我们的makefile中的目标。

all:这个伪目标是所有目标的目标,其功能一般是编译所有的目标。

clean:这个伪目标功能是删除所有被make创建的文件。

install:这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。

print:这个伪目标的功能是例出改变过的源文件。

tar:这个伪目标功能是把源程序打包备份。也就是一个tar文件。

dist:这个伪目标功能是创建一个压缩文件,一般是把tar文件压成Z文件。或是gz文件。

TAGS:这个伪目标功能是更新所有的目标,以备完整地重编译使用。

check和test:这两个伪目标一般用来测试makefile的流程。
伪目标规则的应用
########优化1:使用类似宏定义方式将一些参数集中,方便维护
OBJ=main.o module1.o module2.o 
RESULT = result.out

#all : main.o module1.o \
#       module2.o   # \换行符号
all : ${OBJ}
        g++ -o $(RESULT) $(OBJ)

#main.o : main.cpp module1.h module2.h
#       g++ -c main.cpp 
#module1.o : module1.cpp module1.h
#       g++ -c module1.cpp
#module2.o : module2.cpp module2.h
#       g++ -c module2.cpp

#####优化2: GNU make 会自动推导同名的.cpp 文件,并且推导出来要调用g++ -c 
main.o : module1.h module2.h
module1.o : module1.h
module2.o : module2.h


.PHONY : clean #####优化3:这里表明clean 是个伪目标文件,防止该名字 与 某个文件名字重合
clean: 
        rm ${RESULT} ${OBJ}

再优化: 

 静态模式规则:多目标规则,语法如下

<targets ...> : <target-pattern> : <prereq-patterns ...>
    <commands>
    ...

  targets: 定义了一系列的目标文件,可以有通配符。是目标的一个集合。如果其中有多种后缀,可以使用 $(filter %.o, ${OBJ)) 进行过滤;

  target-pattern: 是指明了targets的模式,也就是的目标集模式。 下面例子中就是说,%.o都是.o 结尾的

  prereq-patterns : 是目标的依赖模式,它对target-pattern形成的模式再进行一次依赖目标的定义;  下面例子中就是说,%.cpp都是对target-pattern 所形成的目标进行二次定义,就是将%,o集合中所有目标,后缀改成.cpp 后新的集合;

   命令中的 $< 和 $@ 则是自动化变量, $< 表示第一个依赖文件, $@ 表示目标集

OBJ1 = module1.o module2.o
OBJ2 = main.o  x.txt
OBJ= ${OBJ1} ${OBJ2}
RESULT = result.out
cc = g++

all : ${OBJ}
        @echo "complie the final output result.out file" #这行命令执行时候,不会输出具体命令过程,但是会正常执行;
        ${cc} -o $(RESULT) $(OBJ)

#####优化1: 静态模式
${OBJ1} : %.o : %.cpp %.h
        ${cc} -c $< -o $@          ## $<:表示第一个依赖文件,$@:表示目标集
#${OBJ2} : %.o : %.cpp 
${filter %.o, ${OBJ2}} : %.o : %.cpp  ##使用filter 进行过滤
        ${cc} -c $< -o $@

.PHONY : clean 
clean: 
        rm -f ${RESULT} ${OBJ}
Tips: 8af1961a73da6c7(hashcode in github)
1. 使用filter 筛选文件;
2. @ 使用,避免输出执行的命令过程;要是打算全面禁止输出可以使用 -s/--silent/--quiet 选项;

 

================在大工程中会涉及到的操作=================

嵌套操作:

在一些大工程中,需要将不同模块放在不同目录中,这样可以在每个目录中都书写一个该目录的makefile, 然后在最外层可以写一个总控makefile, 通过这个总控makefile 来实现对每个目录中文件控制;

基本命令格式

cd <subdir> && make #与下行命令等效 
make -C <subdir>

 

这里还有变量传递到下级makefile 的操作,基本语法如下;

不过SHELL/MAKEFLAGS  这两个变量总是会传递到下层的,这里涉及到一些关键字 和 参数选项;参考后面的cheatsheet;

export                           #要传递所有的变量,只要export 即可
export <variable ...>      #传递变量到下级Makefile中
unexport <variable ...>   #不想让某些变量传递到下级Makefile中
    

文件寻找:

在大工程中,有大量源文件,通过会将文件放在不同目录中,所以在make 做文件依赖关系时候,可以通过在文件前加上路径,但是最好是将路径告诉make,让make 自己寻找

 在MAKEFILE 中定义特殊变量VPATH, 可以定义多个目录文件,有冒号分隔;

VPATH = src:.../headers #这里定义了2个目录, src , ../headers, make会按照这个顺序去搜索f

 

将上面的嵌套操作文件寻找 结合在一起,可以用来编译不同目录下文件;

OBJ1 = module1.o module2.o
OBJ2 = main.o  x.txt
SUB_OBJ = sub_module.o

OBJ= ${OBJ1} ${OBJ2} ${SUB_OBJ}
RESULT = result.out
cc = g++

VPATH = ./ : subdir ## 增加文件链接范围,多个范围之间使用:分隔,按照定义的顺序寻找,直到找到为止;

all : ${OBJ}
        @echo "complie the final output result.out file" #这行命令执行时候,不会输出具体命令过程,但是会正常执行;可以使用-s 实现全面禁止执行命令输出
        ${cc} -o $(RESULT) $(OBJ)

#####优化1: 静态模式
${OBJ1} : %.o : %.cpp %.h
        ${cc} -c $< -o $@          ## $<:表示第一个依赖文件,$@:表示目标集
#${OBJ2} : %.o : %.cpp 
${filter %.o, ${OBJ2}} : %.o : %.cpp  ##使用filter 进行过滤
        touch x.txt
        ${cc} -c $< -o $@

${SUB_OBJ} : subsys


.PHONY : execmd
execmd:
        # 展示; 作用
        cd subdir 
        pwd
        cd subdir; pwd
        #展示忽略错误操作
        @echo "测试 - 作用"
        -no_cmd   # 也可以通过 -i(--ignore-errors) 参数忽略所有的错误
        @echo "contiue next cmd"

.PHONY : subsys
subsys :
        #cd subdir &&  make #与下行命令等效 
        make -C subdir


.PHONY : clean 
clean: 
        -rm -f ${RESULT} ${OBJ}
        make clean -C subdir

 tip: 953708b3079a109 (hash code in github) 

1.使用嵌套make 操作;
2. 指定文件寻找范围;

 

定义命令包:(类似自定义函数)

基本格式: define 开始,endef 结尾;

define <FUNC_NAME>
 <operation>
endefj

简单示例:

define create_file
touch x.txt
endef

${filter %.o, ${OBJ2}} : %.o : %.cpp  ##使用filter 进行过滤
        ${create_file}
        ${cc} -c $< -o $@

tip: aa8ef50dd0b (hash code in github)

1. makefile 中定义命令包;

 

make 命令使用是传递参数:

在实际使用make 命令时候,有时候需要传递一些参数进去,可以通过下面的方式来实现;

CFLAGS=${CFLAG}
CFLAGS+=-g -Wall 
all: 
        gcc ${CFLAGS} a.o b.o -o a.out 

 使用: make CFLAG=-DDEBUG, 就可以将-DDEBUG 参数传递进去;

 

隐含规则:

在makefile 中,有一些使用频率很高的规则,这些就被作为隐含规则来使用;

对于个人来讲,我们也可以通过上面的 “模式规则” 来写下自己的隐含规则;

下面是对于C/C++ 的隐含规则,但是在makefile 中还支持很多其他的语言

######C
x.o 的目标依赖自动推导为 x.c, 其生成命令是 ${CC} -c ${CPPFLAGS} ${CFLAGS}

######C++
x.o 的目标依赖自动推导为x.cc 或者x.c , 生成命令是 ${CXX} -c ${CPPFLAGS} ${CXXFLAGS}

同时在隐含规则中,makefile有很多预先设置的变量,可以通过在makefile 中改变这些变量

#####命令变量
CC : C语言编译程序。默认命令是 cc
CXX : C++语言编译程序。默认命令是 g++

####命令参数的变量
CFLAGS : C语言编译器参数。
CXXFLAGS : C++语言编译器参数
LDFLAGS : 链接器参数。(如: ld )

 

所以上面示例的makefile, 可以简化成下面的形式;

OBJ = module1.o module2.o main.o sub_module.o
RESULT = result.out

VPATH = ./ : subdir 

${RESULT} : ${OBJ}
        ${CXX} -o ${RESULT} ${OBJ}

.PHONY : clean 
clean: 
        -rm -f ${RESULT} ${OBJ}

  

后记:随着cmake一些更有效的工具出现,越来越多的项目都是用了cmake 来管理编译过程;该文章只是对自己学习过的知识的一次记录;

同时对于make 工具,个人理解最重要的是对于文件的依赖关系,以及为了makefile 文件的书写、阅读、维护的便利,利用变量定义/隐式规则/模式规则 /递归条用来简化文件依赖关系的编写;另外在linux 系统中,也可以利用makefile 来实现打包、备份、扩展等一些固定操作;

 

 CheatSheet:

make中定义的自动化变量:
% 的意思是表示一个或多个任意字符
$@ : 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么, $@ 就是匹配于 目标中模式定义的集合。
$< : 依赖目标中的第一个目标名字。如果依赖目标是以模式(即 % )定义的,那么 $< 将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
$% : 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是 foo.a(bar.o) , 那么, $% 就是 bar.o , $@ 就是 foo.a 。如果目标不是函数库文件 (Unix下是 .a ,Windows下是 .lib ),那么,其值为空。
$? : 所有比目标新的依赖目标的集合。以空格分隔。
$^ : 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重 复的,那个这个变量会去除 重复的依赖目标,只保留一份。
$+ : 这个变量很像 $^ ,也是所有依赖目标的集合。只是它不去除重复的依赖目标
$* : 这个变量表示目标模式中 % 及其之前的部分。如果目标是 dir/a.foo.b ,并且 目标的模式是 a.%.b ,那么, $* 的值就是 dir/a.foo

函数部分:
wildcard : %在变量定义和函数引用时无效,比如$SRC=$(wildcard *.c)不能写作$SRC=%.c 
notdir : 去除路径
patsubst :替换通配符

 
make 命令参数:

-c 只编译并生成目标文件。 
-g 生成调试信息。GNU 调试器可利用该信息,
增加调试信息,利用gdb进行调试, 使用方法  gdb ./a.out
-Wall 打开大部分警告信息
-O0 不进行优化处理。
-O 或 -O1 优化生成代码。
-O2 进一步优化。
-O3 比 -O2 更进一步优化,包括 inline 函数。 
:-M 自动寻找依赖关系,(GNU gcc/g++ 需要使用-MM, 否者会把标准库文件加进来)
@ 字符在命令行前,这个命令不会被显示出来, 但是仍然会执行;
-n(--just-print) 只显示命令,不执行命令,可以用来查看命令执行的样子与顺序, 用来DEBUG
-s(--silent/--quiet) 全面禁止命令执行中的输出;
; 来分割同一行多个命令,后面命令都是基于前面命令
- 在命令前面,不管命令出不出错,都认为成功
-i(--ignore-errors),Makefile 中所有命令都忽略错误
-k(--keep-going),某个规则出错,忽略该规则,继续执行其他规则,不至于中断其他命令的执行;
CFLAGS 环境变量,定义以后就会使用该环境变量; 当make嵌套调用时,CFLAGS会传递下去;
-e make命令行带入时候,会覆盖上面CFLAGS定义的环境变量;
-I ./include    //将当前目录下include 文件夹增加进系统目录中

 

posted on 2022-08-04 17:13  学海一扁舟  阅读(1696)  评论(0编辑  收藏  举报