命令规则
makefile的基本格式:
targets : prerequisties
[tab键]command
target: 目标文件,可以是ObjectFile,也可以是执行文件,还可以是标签Label。prerequisite: 要生成的哪个target所需要的文件或是目标command:是make需要执行的命令
伪目标
在makefile中,伪目标是一种特殊类型的目标,它并不表示生成任何文件,而是表示一个动作或者操作。
伪目标的意义
伪目标有以下几个主要作用:
命名约定:通过定义伪目标,可以对常用的操作或任务进行命名约定,使得其他开发者或使用者能够清晰地理解该目标的用途和作用避免与同名文件冲突:有时候,可能会有与目标同名的文件存在。通过定义伪目标,可以避免与同名文件产生冲突组织和管理构建任务:伪目标可以用来组织和管理构建流程中的各个任务或操作。通过定义伪目标,可以将相关的任务组织在一起,并通过依赖关系和命令定义来控制它们的执行顺序提高可读性和可维护性:通过使用伪目标,可以使Makefile更具可读性和可维护性。通过给任务命名,并将任务的逻辑和命令定义在一处,可以提高代码的清晰度和可维护性执行常用操作:伪目标通常用于执行一些常用操作,例如清理临时文件、测试项目、生成文档、打包发布等。通过定义伪目标,可以方便地执行这些操作,而无需手动输入复杂的命令
伪目标并不对应实际的文件,而是代表一种操作或任务。例如,clean这个伪目标就是用来清除生成的目标文件的。当你执行make clean时,Make就会执行clean伪目标下的命令,清除所有的目标文件。这样,我们就可以通过定义伪目标来组织和管理构建过程中的各种任务和操作。这也是Makefile的一个重要特性,它提供了一种灵活的方式来控制和管理构建过程。
伪目标是自己定义的,例如可以定义my_clean ,甚至是aaa,要注意点一点是,make工具并不提供内置伪目标,所有的伪目标都是自定义的,包括debug、clean等常用的伪目标。
.PHONY
在makefile中,.PHONY是一个特殊的目标,用于声明某些目标为伪目标。
.PHONY主要有两种作用:
- 避免同名文件冲突:如果Makefile的同级目录下存在一个与伪目标同名的文件,那么Make会优先考虑这个文件,而不是伪目标。但是,如果我们在Makefile中使用了.PHONY,那么Make就会忽略这个同名文件,而直接执行伪目标的命令。
- 确保伪目标总是被执行:在Make的逻辑中,如果目标文件(或者说是输出文件)已经存在,并且其依赖文件没有发生变化,那么Make就不会再执行该目标的命令。但是对于伪目标来说,我们通常希望无论何时,只要我们执行了make <伪目标>,就应该执行伪目标的命令。.PHONY可以帮助我们实现这一点。
举例:
.PHONY: clean
clean:
rm *.o
这个代码的作用是执行伪目标clean,将清除所有.o结尾的文件。
在输出结果中,能看到命令rm *.o ,如果不希望显示出来,可以在命令前面加上@符号,即@rm *.o
Make变量
基本的赋值符号和续行符
赋值符
= 最基本的赋值符号,其作用是系统自动推导将最终赋值作为变量的值。也叫做延迟赋值 ,即只有在等号调用的时候才开始运算,有点类似c语言中形参的概念,只有当调用函数的时候才分配空间。
举例:
B = $(A)b #$(A) 代表取A中的值
A = c
debug :
echo $(B)#调用B
如果调用B,那么=才开始运算,将$(A)换成c 最后输出cb ,正是因为在调用变量时才开始运算,所以才不会在B = $(A)b这段代码中出现未定义错误。
:= 是覆盖式赋值 也可以叫做立即赋值,如果一个变量在之前已经赋过值了,那么这次的赋值将会覆盖掉旧值,但是这个符号只能推导该符号之前的值。使用这种符号,会让等号右边的值立即赋值给左边的变量。
例如:
A := a
B := $(A)b #$(A) 代表取A中的值
A := c
debug : # 调用B
echo $(B)
输出结果:
echo ab
ab
我们可以观察到此时已经将a赋值给了A,但是如果是延迟赋值的结果则恰恰相反:
举例:
A = a
B = $(A)b #$(A) 代表取A中的值
A = c
debug : # 调用B
echo $(B)
输出结果:
echo cb
cb
我们可以观察到这个时候的A是取的最终值。
+= 追加赋值符号,会将新值直接粘贴在旧值的末尾,在变量首次赋值的时候,+=会被看作=符号,而且追加的时候,会在旧值和新值之间增加一个空格。
举例:
A += a #当变量首次赋值的时候,+=相当于=
A += c
debug : # 调用A
echo $(A)
输出结果:
echo a c
a c
?= 条件赋值,这个符号会首先确定这个变量是否被赋值过,如果未赋值,则将右值赋值,如果已经赋值了,则保持旧值
举例:
A ?= a
A ?= c
debug : # 调用A
echo $(A)
输出结果:
echo a
a
从结果可以看到,A只会在首次被赋值,第二次A的值已经为a,于是c没有赋值成功。
续行符
在开发中,有可能会出现参数太长,为了让板书更加美观和便于理解,于是可以使用续行符号进行换行,不过会在换行处自动添加一个空格。
举例:
A := aaaaaaaaaaaaaaaaaa\
bbbbbbbbbbbbbbbbbb\
cccccccccccccccccc\
debug :
@echo $(A)
输出结果:
aaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb cccccccccccccccccc
变量的调用
对于一个变量,如果想调用其代表的值,需要使用$()或者${},这两种表达方式是等价的。
不过makefile支持一些特殊的自动化变量,或者说是预定义变量。
常用的自动化变量:
- $@:表示规则的目标文件名。在多目标模式规则中,它代表的是触发规则被执行的文件名。
- $%:当目标文件是一个静态库文件时,代表静态库的一个成员名。
- $<:规则的第一个依赖的文件名。如果是一个目标文件使用隐含的规则来重建,则它代表由隐含规则加入的第一个依赖文件。
- $?:所有比目标文件更新的依赖文件列表,空格分隔。如果目标文件时静态库文件,代表的是库文件(.o 文件)。
- $^:代表的是所有依赖文件列表,使用空格分隔。如果目标是静态库文件,它所代表的只能是所有的库成员(.o 文件)名。一个文件可重复的出现在目标的依赖中,变量“$^”只记录它的第一次引用的情况。
- $+:类似“$^”,但是它保留了依赖文件中重复出现的文件。主要用在程序链接时库的交叉引用场合。
- $*:在模式规则和静态模式规则中,代表“茎”。 “茎”是目标模式中“%”所代表的部分(当文件名中存在目录时, “茎”也包含目录部分)
Make函数
函数的使用格式
函数的格式有两种——$(function arguments)或者${function arguments},当存在多个参数的时候,参数之间用 , 号连接,但是要注意,函数名和参数之间使用空格隔开
shell函数
shell函数的主要作用是可以调用shell命令,参数就是命令类型,其返回值是命令的结果。
标准格式:$(shell <command> <arguments>)
示例1:
A := $(shell ls)#打印当前目录中所有的文件
debug :
@echo $(A)
运行结果
2.c 2.h 3.c 3.h 4.c 4.h main.c Makefile
示例2:
A := $(shell ls | grep "\.c\$$")#打印当前目录下以.c结尾的文件
debug :
@echo $(A)
运行结果:
2.c 3.c 4.c main.c
通过这两个例子,可以清晰的看到,这个函数中是可以使用管道符号 | 连接然后执行组合命令的
subst函数
标准格式——$(subst FROM,TO,TEXT)
subst函数是一个文本替换函数,其主要作用是将函数中的 FROM 替换为 TO 的值,不过这个替换的文本是 TEXT ,就类似于 vscode 中的 ctrl+F 的替换功能,会将文本中所有的 FROM 替换为 TO 。
示例:
result := $(shell ls | grep "\.c\$$") #将当前目录中的所有.c文件存放到result中
result := $(subst .c,.cpp,$(result)) #将result中的.c替换为.cpp
debug : #打印
@echo $(result)
运行结果
2.cpp 3.cpp 4.cpp main.cpp
通过运行结果可以看到,所有的输出结果都是.cpp后缀
patsubst函数
标准格式 —— $(patsubst <pattern>,<replacement>,<text>)
这个函数的作用同样是替换,不过和subst有所不同,patsubst是以单词为单位进行的单词替换,至于这个单词的定义,系统会将空格、回车、Tab等作为单词的边界。同样是在text文本中,将单词pattern 替换为 replacement 。
示例1:
result := $(shell ls | grep "\.c\$$") #将当前目录中的所有.c文件存放到result中
result := $(patsubst .c,.cpp,$(result)) #将result中的.c替换为.cpp
debug : #打印
@echo $(result)
运行结果
2.c 3.c 4.c main.c
这里可以很清晰地看到单词没有被替换,因为这四个文件名没有叫做.c的文件,因为.c只是这个名字的一部分,就比如你是商家,一件商品卖99,顾客只愿意出其中的一个9,而且是个位,你是不可能答应他无理的要求的。那么问题来了,该如何进行匹配修改呢?
示例2:
result := $(shell ls | grep "\.c\$$") #将当前目录中的所有.c文件存放到result中
#可以使用通配符%来进行匹配
result := $(patsubst %.c,%.cpp,$(result)) #将result中的.c替换为.cpp
debug : #打印
@echo $(result)
运行结果
2.cpp 3.cpp 4.cpp main.cpp
结果显而易见。
foreach函数
标准格式——$(foreach var,list,text)
这个函数是一个循环函数 ,它的执行过程是将list中逐一提取出单词,然后存放到var中,接着让var的值在text中定义的表达式进行计算,然后将text计算结果返回,随后开始下一轮循环,让下一轮循环的结果和之前的结果拼接(单词与单词时间会添加空格隔开),当循环结束后将结果返回,这个过程有点类似python中的for x in。
示例:
#我在当前目录中定义了a b c d e5个文件
list := $(shell ls | grep "^[a-z]\$$") #将当前目录中的这五个文件存放到list中
result := $(foreach var,$(list),$(var).c) #将list中的数据逐个拿出来,给每个文件添加一个.c后缀
debug : #打印
@echo $(result)
运行结果
a.c b.c c.c d.c e.c
由示例代码可以看到,最终的输出是每个结果都加上了后缀,也就是完成了一轮循环
dir函数
标准格式——$(dir <names> )
names是一个包含了路径的文件名,dir的功能就是分离出文件的路径 ,如果文件没有写路径,仅仅只有一个名字,那么函数会默认补充一个./,names可以为多个文件,但是参数与参数之间不需要 , 符号
示例1:
#dir 参数为1时的情况
src_t := $(shell find ./ -name 2.c)
result := $(dir $(src_t))
debug :
@echo $(result)
运行结果
./
示例2:
#dir 参数大于一个的情况
src := $(shell pwd)
src_t := $(shell find ./ -name 2.c)
result := $(dir $(src) $(src_t))
debug :
@echo $(result)
运行结果
/home/chxzking/MR.XU/service/ ./
notdir函数
基本格式——$(notdir <names>)
这个函数和dir恰好相反,它的作用是提取路径中非目录的部分,也就是提取文件名 ,其用法和dir完全一样。
示例1:
#notdir 参数为1时的情况
src_t := $(shell find ./ -name 2.c)
result := $(notdir $(src_t))
debug :
@echo $(result)
运行结果
2.c
示例2:
#notdir 参数大于一个的情况
src := $(shell pwd)
src_t := $(shell find ./ -name 2.c)
result := $(notdir $(src) $(src_t))
debug :
@echo $(result)
运行结果
Core 2.c
filter函数
标准格式——$(filter PATTERN…,TEXT)
TEXT是一个字符串序列,PATTERN代表了不限数量的模式,filter函数能从TEXT中过滤出来符合要求的字符串,不符合PATTERN的字符串将从中删除。
示例1:
#单个模式
str := a.c 2.c b.cpp b.h d.s p.so a.a
result := $(filter %.c,$(str))
debug :
@echo $(result)
运行结果
a.c 2.c
示例2:
#多个模式
str := a.c 2.c b.cpp b.h d.s p.so a.a
result := $(filter %.c %.h,$(str)) #注意:模式与模式之间不需要,号
debug :
@echo $(result)
运行结果
a.c 2.c b.h
basename函数
标准格式——$(basename <names>)
这个函数的作用是从一个文件中提取基本名部分,基本名就是除去.及后缀的部分,例如1.c的基本名是1,/service/libfun.so的基本名是/service/libfun,这个函数的参数也可以是多个,且中间使用空格作为分隔符
示例1:
#单个情况
str := ./Core/moudle/1.c
result := $(basename $(str))
debug :
@echo $(result)
运行结果
./Core/moudle/1
示例2:
#多个情况
str := ./Core/moudle/1.c 2.c
result := $(basename $(str))
debug :
@echo $(result)
运行结果
./Core/moudle/1 2
编译
编译流程
将一个.c文件编译成可执行文件的过程通常包括四个步骤:预处理、编译、汇编和链接。
- 预处理(Preprocessing):预处理器(缩写:cpp)接受源代码
.c文件,处理其中的预处理指令。预处理指令是以#开头的,如#include、#define等。预处理器会处理这些指令,比如包含头文件、宏替换等,并生成一个预处理后的.i文件。 - 编译(Compilation):编译器(缩写:gcc)接受预处理后的
.i文件,进行词法分析、语法分析、语义分析和优化等步骤,生成汇编代码.s文件。 - 汇编(Assembly):汇编器(缩写:as)接受汇编代码
.s文件,将汇编代码转换为机器语言代码,生成目标文件.o。 - 链接(Linking):链接器(缩写:ld)接受一个或多个目标文件
.o,将它们与所需的库一起链接,生成可执行文件。
分别对应的命令是:
gcc -E main.c -o main.i # 预处理
gcc -S main.i -o main.s # 编译
gcc -c main.s -o main.o # 汇编
gcc main.o -o main.out # 链接
不过也可以使用gcc main.o -o main.out一条命令完成整个过程。
综合之前的知识,可以写出一个完整的多文件编译的makefile文件,假设在makefile同目录有三个文件:1.h 1.c main.c
示例:
//1.h文件
#ifndef _1_H
#define _1_H
#include<stdio.h>
void func2();
#endif //_1_H
//1.c文件
#include "1.h"
void func2(){
printf("this is 1.c\n");
}
//main.c
#include<stdio.h>
#include "1.h"
int main(){
printf("hello world\n");
func2();
return 0;
}
makefile代码:
c_file := $(shell ls | grep "\.c\$$") #获取当前文件夹中所有的.c文件名
c_o_temp := $(patsubst %.c,temp_o/%.o,$(c_file)) #将.c文件字符串替换为同名的temp_o/文件名.o文件
.PHONY : deubug clean #声明伪目标
temp_o/%.o: %.c #将每个.c文件编译成目标文件,并将文件放入temp_o目录中
@mkdir -p $(dir $@)
@gcc $^ -o $@ -c
workspace/main:$(c_o_temp) #将目标文件编译成可执行文件,并放入worksapce目录中
@mkdir -p $(dir $@)
@gcc $^ -o $@
debug: workspace/main #伪目标debug:自定义编译命令,并执行最终编译结果
@./$^
clean: #伪目标clean,清除所有的.o文件,删除workspace、temp_o目录
@rm -rf *.o workspace temp_o
运行结果(make debug):
hello world
this is 1.c
为了应对各种情况,以下还有常用的gcc编译选项
gcc编译的选项
-m64:生成64位代码。-std=:指定编译器应遵循的C++标准,例如-std=c++11表示使用C++11标准。-g:生成调试信息,以便使用gdb等调试器进行调试。-w:抑制所有警告信息的输出。-O:-O选项用于控制优化级别。-O1:启用一些优化,同时不会显著增加编译时间。-O2:启用进一步的优化,包括所有-O1级别的优化和其他大多数优化。-O3:启用所有优化,包括所有-O2级别的优化和其他一些额外的优化。-I:添加包含文件的搜索路径,例如-I/home/user/include会在/home/user/include目录下搜索头文件。-fPIC:生成位置无关代码(Position Independent Code),通常用于动态链接库的编译-l:链接库,后面通常跟库名,例如-lm表示链接数学库(libm)。-L:添加库文件的搜索路径,例如-L/home/user/lib会在/home/user/lib目录下搜索库文件。-Wl,<选项>:将<选项>传递给链接器。例如,-Wl,-rpath,/home/user/lib会将/home/user/lib添加到运行时库搜索路径。-rpath=:设置运行时库搜索路径,这是链接器选项,通常形式为-Wl,-rpath,路径。
以-I举例
依旧以1.h 1.c main.c三个文件举例,但是假设这三个文件在不同的目录中。那么makefile代码可以这样写:
c_file := $(shell find ./ -name "*.c") #获取make同目录及子目录中所有的.c文件名
c_o_temp := $(patsubst ./%.c,temp_o/%.o,$(c_file)) #将.c文件字符串替换为同名的temp_o/文件名.o文件
c_h :=$(shell find ./ -name "*.h") #获取make同目录及子目录中所有的.h文件名
c_h_path := $(dir $(patsubst %,-I%,$(c_h))) #整合-I命令
.PHONY : debug clean #声明伪目标
temp_o/%.o: %.c #将每个.c文件编译成目标文件,并将文件放入temp_o目录中
@gcc $< -o $@ -c $(c_h_path)
workspace/main:$(c_o_temp) #将目标文件编译成可执行文件,并放入worksapce目录中
@mkdir -p $(dir $@)
@gcc $^ -o $@
debug: workspace/main #伪目标debug:自定义编译命令,并执行最终编译结果
@./$^
clean: #伪目标clean,清除所有的.o文件,删除workspace、temp_o目录
@rm -rf *.o workspace temp_o
运行结果(make debug):
hello world
this is 1.c
静态库
静态库编译
动态库编译主要分为源文件编译和动态库创建两个过程
源文件编译:首先,你需要将每个源文件(.c文件)编译成目标文件(.o文件)。这可以使用gcc的-c选项来完成。
假设有一个temp.c文件
gcc temp.c -o temp.o -c
创建静态库:你可以使用gcc的-shared选项来将一个或多个目标文件打包成一个静态库(.a文件)。通常静态库的命名是lib开头.a结尾
ar rcs libtemp.a temp.o
除此之外,静态库是可以实现一次性编译的:
gcc temp.c -o temp.o -c && ar rcs libtemp.a temp.o
静态库的链接
假设我在当前目录有一个静态库libtemp.a,这个命令的-L需要指定静态库的位置,-l是静态库的名字,但是不需要加上开头的lib和结尾的.a
gcc main.c -o main -L./ -ltemp
示例:
假设当前目录存在1.c 1.h 2.c 2.h main.c 文件
c_file := $(shell ls | grep "[0-9]\.c\$$") #寻找.c文件
c_o := $(patsubst %.c,temp_o/%.o,$(c_file))
temp_o/%.o:%.c #汇编成目标文件
@mkdir -p $(dir $@)
@gcc $< -o $@ -c
workspace/libtemp.a:$(c_o) #生成静态库
@mkdir -p $(dir $@)
@ar rcs $@ $^
debug : workspace/libtemp.a #链接静态库
@gcc main.c -o main -L./workspace -ltemp
@./main
clean :
@rm -rf *.o main temp_o workspace
.PHONY : debug clean
动态库
动态库编译
动态库编译主要分为源文件编译和动态库创建两个过程
源文件编译:首先,你需要将每个源文件(.c文件)编译成目标文件(.o文件)。这可以使用gcc的-c选项来完成。不过,由于我们要创建的是动态库,所以还需要添加-fPIC选项来生成位置无关代码。
假设有一个temp.c文件
gcc temp.c -o temp.o -c -fPIC
创建动态库:你可以使用gcc的-shared选项来将一个或多个目标文件打包成一个动态库(.so文件)。通常动态库的命名是lib开头.so结尾
gcc temp.o -o libtemp.so -shared
此外,它也存在一次编译的代码的方式
gcc temp.c -o libtemp.so -fPIC -shared
动态库的链接
假设我在当前目录有一个动态库libtemp.so,这个命令的-L需要指定动态库的位置,-l是动态库的名字,但是不需要加上开头的lib和结尾的.so
gcc main.c -o main -L./ -ltemp
除此之外还需要其他操作以便于操作系统调用库,详情请见Linux下的动态库与静态库
示例:
假设当前目录存在1.c 1.h 2.c 2.h main.c 文件
c_file := $(shell ls | grep "[0-9]\.c\$$") #寻找.c文件
c_o := $(patsubst %.c,temp_o/%.o,$(c_file))
temp_o/%.o:%.c #汇编成目标文件
@mkdir -p $(dir $@)
@gcc $< -o $@ -c -fPIC
workspace/libtemp.so:$(c_o) #生成动态库
@mkdir -p $(dir $@)
@gcc $^ -o $@ -shared
debug : workspace/libtemp.so #链接动态库
@gcc main.c -o main -L./workspace -ltemp -Wl,-rpath,./workspace
@./main
clean :
@rm -rf *.o main temp_o workspace
.PHONY : debug clean
条件判断
Makefile 语法中存在条件判断。这些条件判断主要通过 ifeq、ifneq、ifdef 和 ifndef 等关键字来实现。
ifeq 和 ifneq 用于比较变量的值是否相等或不相等。
示例:
ifeq ($(first), $(second))
echo "first == second"
else
echo "first != second"
endif
在这个例子中,如果变量 first 和 second 的值相等,那么将执行 echo “first == second”,否则将执行 echo “first != second”
示例2:
ifdef 和 ifndef 用于检查变量是否已定义。
ifdef VAR
echo "VAR is defined"
else
echo "VAR is not defined"
endif
在这个例子中,如果变量 VAR 已定义,那么将执行 echo “VAR is defined”,否则将执行 echo “VAR is not defined”
模式规则
在 Makefile 中,模式规则是一种特殊的规则,它允许我们根据文件名的模式来定义规则。模式规则的语法如下:
目标模式: 依赖模式
命令
在模式规则中,目标名和依赖名中需要包含有模式字符 %。% 可以匹配任何非空字符串。例如,对于模式规则 %.o : %.c,它表示的含义是:所有的 .o 文件依赖于对应的 .c 文件。
在执行模式规则时,命令行中的自动化变量将根据实际的目标和依赖文件取对应值,如:
%.o : %.c
$(CC) -c $(CFLAGS) $< -o $@
