MAKEFILE 基本规则

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

环境(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中的命令,必须要以<span class="pre" style="box-sizing: border-box">Tab</span>键开始。

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

  • 变量的命名字可以包含字符、数字,下划线(可以是数字开头),但不应该含有 <span class="pre">:</span><span class="pre">#</span><span class="pre">=</span> 或是空字符(空格、回车等)。
  • 变量是大小写敏感的,“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

MAKEFILE 基本规则

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

MAKEFILE 基本规则

#第一次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 基本规则MAKEFILE 基本规则

我们可以根据这种性质来让我们的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 后新的集合;

命令中的<span class="pre" style="box-sizing: border-box">$<</span><span class="pre" style="box-sizing: border-box">$@</span>则是自动化变量,<span class="pre" style="box-sizing: border-box">$<</span>表示第一个依赖文件,<span class="pre" style="box-sizing: border-box">$@</span>表示目标集

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 文件夹增加进系统目录中

原文链接: https://www.cnblogs.com/ChunboBlog/p/15861210.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/190628

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月12日 下午4:19
下一篇 2023年2月12日 下午4:20

相关推荐