Makefile文件管理

Makefile

使用 Makefile 比直接执行 gcc 命令编译程序的优点主要体现在以下几个方面:

  1. 自动判断依赖关系

Makefile 可以根据目标文件和依赖文件之间的依赖关系,自动判断哪些目标需要重新编译,只重新编译必要的文件,提高编译效率。

  1. 简化编译过程

编写 Makefile 后,编译过程可以简化为在命令行执行 make 命令,而不需要每次都输入复杂的 gcc/g++ 命令。同时 Makefile 也很容易配置、修改和维护。

  1. 指定编译规则

可以用 Makefile 中的规则精确控制文件编译的顺序、使用的编译器和编译参数等,避免重复工作。

  1. 支持增量编译

只需要重新编译修改过的文件,省去重复编译的时间。

  1. 支持并行编译

Makefile 支持同时利用多个 CPU 进行编译,缩短编译时间。

  1. 生成库文件等

可以通过 Makefile 自动生成项目所需的库文件等,简化整个构建流程。

  1. 规范化编译过程

使用 Makefile 可以将编译过程规范化,方便团队协作和程序维护。

总之,使用 Makefile 可以大大简化和规范化编译过程,提高编译效率,所以大型 C/C++ 项目基本上都会使用 Makefile 进行编译。

Makefile的执行是依赖于时间戳的新旧,当创建了一个大体量的文件后,我需要的是快速编译,但是每一次修改之后重新去实现编译花费的时间非常多,Makefile的目的就是在抛弃了小内存空间后用内存换取时间效率,通过在编译的时候,保存.o文件,这样在修改了某一个.c文件之后,只需要把那某一个.c文件编译为.o文件之后,同一链接。可以大幅度的降低消耗的时间

基本格式/规则

目标:依赖

命令

Makefile是一个用于构建项目的配置文件,主要用于C/C++项目,但也可用于其他语言。它由一系列的规则(rules)组成,每个规则定义了如何生成一个或多个目标文件(targets)。

#规则的格式
# 目标文件:[依赖对象]
# [<tab>命令1]
# [<tab>命令2]
# [<tab>...]

all:m

m:main.c
	gcc -o m main.c

这里的all是一个伪目标,并没有实际的意义,作为makefile的一个开头和进入

一个规则包含目标,依赖,命令可以没有依赖和命令,但是不能没有目标

目标可以理解为你最终需要实现的文件或者其他指令 依赖可以理解为这个目标的实现需要这个“依赖”才能完成

all:m

# 第二个规则,有目标(m),依赖(main.c),也有命令(gcc -o m main.c)
m:main.c
	gcc -o m main.c

# 第三个规则,有目标(clean),没有依赖,有命令(rm -rf m)
# 没有依赖的规则,会直接执行命令
clean:
	rm -rf m

# 目标和依赖都可以包含多项,项与项之间用空格分隔
aa:bb cc   # 依赖中包含两项

bb cc:     # 目标中包含两项,每个项会执行一次命令
	echo $@  # $@ 代表目标

规则的顺序

规则的顺序不太重要,make 会自动依据依赖关系查找规则执行

但入口规则(即第一个规则)是默认首先执行的规则,

除非在执行 make 时指定了目标,否则就会先执行入口规则

all:m

m:main.o
	gcc -o m main.o

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

个人觉得其实这个顺序的关系就是一层层的往下找。首先从all进入,发现依赖于m,想实现m,则需要main.o;main.o的实现是由main.c决定,则实现的是gcc -c -o main.o这样就是先实现main.c转换为main.o的工作;实现了main.o接下来就是找到上一层,会执行gcc -o m main.o的命令,生成的就是一个名为m的可执行文件

补充知识:

  • gcc <*.c>的文件的时候就是执行的是从.c文件直接产生了一个可执行文件a.out
  • gcc -c执行的是从.c文件到.o文件,只进行编译,但是不执行链接
  • gcc -o后面加上文件名执行的是指定输出的文件名
  • gcc -c -o mian.o mian.c 和gcc -c mian.c -o main.o是一样的,主要看那个-o后面跟的是什么

时间戳

Makefile 是根据每一次文件更新的时间所判断的,如果一个.c文件的时间比.o文件产生的时间更新,那就知道这个文件进行了更改,需要将这个文件重新编译

但是问题就来了,当发生在时间又差值的地区,例如东半球和西半球,收到同样的文件,但是时区不一样,Makefile就没法实现更新,无法执行编译的任务

问题的解决就是touch,touch的目的就是触摸一次时间戳,更新,不要小看touch的能力,不是只用来产生文件的,当文件已经产生了之后,就需要去一遍一遍的更新时间戳

伪目标

我们在前面的时候,举例的时候,用的是all作为文件的进入,这其中all就是一个伪目标,因为它不是最终要完成的目的/目标

伪目标是指并不是为了创建那个目标的目标,而是为了执行一些命令。

常见的伪目标有 all, install, uninstall, clean 等。

当我们在创建文件的时候,可能在某些时候,会恰好创建一些文件的名称为all,install,unintall,clean,那么这个时候是make,我就没法判断是否是Makefile中的目标,还是我书写的文件

解决办法就是在文件头加上这样一句话:

.PHONY:all clean install unstall

phony(虚伪的),意思就是在.PHONY:后面,就是伪目标,不是一个文件的名称

make的引用

make 命令默认按以下顺序查找 makefile 文件来执行

GNUmakefile, makefile, Makefile

也可以不使用以上三个名称,而另外指定 makefile 的文件名执行,

带 "-f makefile文件名" 选项执行即可,如

make -f GNUmakefile1

如果要执行指定目录下的 makefile 文件,使用 "-C 目录" 选项

-f 和 -C 选项可以同时使用,如:

make -C src -f Makefile

执行指定的目标

make all

make clean

当然,可以同时指定多个目标编译

make main.o test.o

all:test
	@echo "This is all"

test:
	@echo "This is test"
	
clean:
	@echo "This is clean"

install:
	@echo "This is install"

这里的@的作用是不会执行echo "This is all"只会执行This is all

自定义变量

makefile 中的变量有 :自定义变量,预定义变量,自动变量

分别类似于 shell 中的:自定义变量,环境变量,系统变量

自定义变量方式

  1. 递归展开

    VAR = value

    value 中的变量会递归展开,特别是如果 value 中又包含 VAR 时,会造成无穷循环展开,最终展开失败

  2. 简单拓展方式

    VAR := value

    value 中的值会展开一次,如果value 中又包含 VAR 时,则只会展开一次,不会无穷展开

  3. 变量为空时赋值

    VAR ?= value

    VAR 的值为空才赋值 value

  4. 追加赋值

    VAR += value

    在 VAR 原值的基础上追加 value 值,不是算术累加运算

AAA = $(BBB)
BBB = CCC     # 递归展开时不论定义的顺序
#CCC = $(CCC) DDD   # Makefile:39: *** Recursive variable 'CCC' references itself (eventually).  Stop.

EEE := 123
FFF := $(EEE)  # 非递归展开,引用的变量必须事先定义

GGG := $(HHH)  # 只展开一次,在后续定义的值不会用于当前变量
HHH := 456

JJJ ?= 789     # 此时 JJJ 为空,因此赋值成功
JJJ ?= abc     # 此时 JJJ 非空,因此不赋值

JJJ += abc     # 追加赋值

all:
	@echo AAA = $(AAA)
	@echo CCC = $(CCC)
	@echo FFF = $(FFF)
	@echo GGG = $(GGG)
	@echo JJJ = $(JJJ)

$的作用是用来引用变量的,定义的时候不用加$,在变量调用的时候需要加上$

预定义变量

常见的预定义变量,有的有默认值,有的没有:

AR 归档维护程序的名称,默认值为 ar。 ARFLAGS 归档维护程序的选项,默认值 rv。 CC C 编译器的名称,默认值为 cc。 CFLAGS C 编译器的选项,无默认值。 LDFLAGS C 链接器的选项,无默认值。 RM 文件删除程序名称,默认值为 rm –f。 CXX C++ 编译器的名称,默认值为 g++。 CXXFLAGS C++ 编译器的选项,无默认值。

可以重新定义预定义变量,即将其变为自定义变量。

使用 export 将 CFLAGS 导出为全局预定义变量,可以用于子 Makefile 中

export CFLAGS := -Wall

  • -Wall是一个GCC/G++常用的编译参数,代表“启用所有编译器警告”。
  • export表示将后续的变量导出,使其在Makefile规则中可见。

自动变量

在 Makefile 规则中,make 维护了一些自动变量,常用的有:

  • $< - 第一个依赖文件的名称
  • $@ - 目标的完整名称
  • $^ - 所有的依赖文件,以空格分开
  • $* - 不包含扩展名的目标名称
  • $? - 比目标新编译的依赖文件

使用 Markdown 语言描述如下:

Makefile自动变量

在 Makefile 每条规则中,make 提供以下自动变量:

  • $< - 第一个依赖文件的名称
  • $@ - 目标的完整名称
  • $^ - 所有的依赖文件,以空格分开
  • $* - 不包含扩展名的目标名称
  • $? - 比目标新编译的依赖文件

例如:

target: dep1.o dep2.o 
   gcc $< $@ -o $^

这里 $< 将被替换为 dep1.o,$@ 将被替换为 target,$^ 将被替换为 dep1.o dep2.o

最常用的自动变量有 $<$@$^

变量替换

OBJS := main.o fac.o array.o hello.o
SRCS1 := $(OBJS:o=c)
SRCS2 := $(OBJS:.o=.c)
SRCS3 := $(OBJS:%.o=%.c)

all:
	@echo OBJS  = $(OBJS)
	@echo SRCS1 = $(SRCS1)
	@echo SRCS2 = $(SRCS2)
	@echo SRCS3 = $(SRCS3)

这个 Makefile 中展示了三种变量替换的用法:

  1. $(OBJS:o=c)

    这种格式是把变量OBJS中的所有单词里匹配到的"o"替换成"c"。

    例如把 "main.o" 替换成 "main.c"。

  2. $(OBJS:.o=.c)

    这种是把变量OBJS中所有单词里匹配到的".o"替换成".c"。

    效果同上,把 "main.o" 替换成 "main.c"。

  3. $(OBJS:%.o=%.c)

    这种形式使用了模式匹配,%代表匹配任意字符。

    它匹配 OBJS 中所有符合 %.o 形式的单词,并替换为 %.c

    同样把 "main.o" 替换成 "main.c"。

注意,这并不是直接改变了文件的类型,而是改变的是保存的变量的名称罢了,实际的类型不变

以这个Makefile为例:

OBJS := main.o fac.o
SRCS := $(OBJS:.o=.c)

执行后,OBJS的值为main.o fac.o,SRCS的值为main.c fac.c。

但这仅仅是改变了Makefile变量SRCS的值,硬盘上实际的文件main.o和fac.o是不会被改名为main.c和fac.c的。

变量替换只在Makefile执行的时候临时生效,不会对实际文件系统产生影响。

这主要利用的是Makefile的变量替换功能,使得可以方便地在规则中使用修改后的变量值,例如:

SRCS := $(OBJS:.o=.c) 

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

这里可以先通过变量替换生成一个源代码文件的变量SRCS,然后在规则中使用%匹配来编译这些源文件。

所以,Makefile变量替换是一个隔离于实际文件系统的功能,它只会在Makefile内部生效,不会对外部产生副作用。我们可以利用这一点来方便地定义转换规则。

隐式规则

与 C 语言相关的 3 条隐式规则:

C语言编译隐式规则

  1. 编译C程序

    "%.o" 自动由 "%.c" 生成,执行命令为:

    $(CC) -c $(CPPFLAGS) $(CFLAGS)
    
  2. 汇编和需要预处理的汇编程序

    "%.s" 是不需要预处理的汇编源文件,"%.S" 是需要预处理的汇编源文件。汇编器为 "as"。

    "%.o" 可自动由 "%.s" 生成,执行命令是:

    $(AS) $(ASFLAGS)
    

    "%.s" 可由 "%.S" 生成,C预编译器 "cpp",执行命令是:

    $(CPP) $(CPPFLAGS)
    
  3. 链接单一的object文件

    "%" 自动由 "%.o" 生成,通过C编译器使用链接器(GNU ld),执行命令是:

    $(CC) $(LDFLAGS) %.o $(LOADLIBES) $(LDLIBS) -o %
    

    此规则仅适用于一个源文件直接生成executable的情况。如果有多个源文件,需在Makefile中增加隐式规则的依赖文件。

以Markdown格式写出来更清晰直观一点。让我知道如果还有什么问题。

# Makefile

SRC = main.c foo.c bar.c 
OBJ = $(SRC:.c=.o)

EXECUTABLE = prog

all: $(EXECUTABLE)

$(EXECUTABLE): $(OBJ) 
	$(CC) -o $@ $^

clean:
	rm -f $(EXECUTABLE) $(OBJ)

.PHONY: all clean

这里仔细看,首先将.c文件赋给了一个变量SRC,接着将.c文件名变成了.o文件名保存给了OBJ,只是保存了变量名,但是保存的是更改了.c为.o的文件名

接着,这个目标中没有使用%.o:%.c的操作,而是直接由%.o转换为了可执行文件,越过了.c到.o的操作

这就是一种隐式规则,是系统自带的方法,检测到了.c文件后,就会编译为.o文件

模式规则

模式规则(Pattern Rules)是Makefile中的一种特殊语法,它用来定义对一类文件进行批量处理的规则。

模式规则的基本语法如下:

targets : target-pattern : prereq-patterns
    commands

其中:

  • targets:规则的目标文件,可以指定多个目标。
  • target-pattern:目标文件模式,如%.o
  • prereq-patterns:依赖文件模式,如%.c
  • commands:生成目标的命令。

模式规则通过%通配符匹配某一类文件。例如:

# 将所有的.c文件编译成.o文件
%.o : %.c
    $(CC) -c $(CFLAGS) $< -o $@

这个模式规则表示,所有%.o后缀的文件依赖于%.c文件,运行命令时$<$@会被替换为相应的目标和依赖文件。

优点:

  • 避免重复输入命令
  • 简化语法
  • 自动推导依赖

问:上一个部分不是说了可以由隐式规则来完成.c文件转变为.o文件吗?为什么需要模式规则?

对于编译.c文件成.o文件的这个标准过程,Makefile中确实存在一个隐式规则可以自动完成:

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

但是,有时候我们可能还是要显式写出这个模式规则,主要有以下几个原因:

  1. 覆盖隐式规则中的变量值

隐式规则使用的变量可能和我们实际设置的变量值不同,通过显式覆盖可以使变量一致。

  1. 添加更复杂的命令

实际编译命令可能更复杂,不只是简单的-c参数。

  1. 提高可读性

尽管隐式规则可以自动推导,但直接写出模式规则可以增加Makefile的可读性。

  1. 与其他规则结合使用

配合静态模式规则可以实现更复杂的功能。

  1. 移植考虑

不同Makefile实现的隐式规则可能有细微不同,写明可以增加可移植性。

所以,即使有了隐式规则,我们有时候仍然会选择显示写出模式规则,这在实际的大型Makefile中是很常见的做法。

需要根据具体情况进行权衡,决定是否要利用隐式规则的自动推导,或者显示写出模式规则。

缺省规则

具体解释一下Makefile中的缺省规则(Default Rules)。

假设我们有以下的Makefile:

target: dep1 dep2 
    command to build target

dep1:
    command to build dep1

在执行make时,情况分为两种:

  1. dep2文件已存在,直接make

这时会按正常流程构建target。

  1. dep2文件不存在

由于Makefile中没有指定如何构建dep2,makefile会报错并停止。

这时就需要缺省规则:

%:
    touch $@

这表示对于任何没有指定构建规则的目标文件,执行touch命令生成一个空文件。

那么在make的时候,遇到不存在的dep2,就会执行缺省规则,生成一个空的dep2文件。这样make可以继续执行下去,最终生成target。

缺省规则的主要作用就是在makefile中某些依赖不存在时,提供一个备用机制,使得makefile不会直接报错停止。

它为那些没有构建规则的文件生成空文件,使得整个make过程可以继续进行。

在调试makefile时就很有用。

总结一下,缺省规则是makefile的一种补救机制,当缺少规则时执行默认命令,保证makefile可以继续运行。

引用其他的Makefile

在Makefile中可以使用include语句来引用其他 Makefile 文件,从而将一个大的 Makefile 拆分成多个小的文件进行组织。

include的语法为:

include filename

其中filename是要引用的Makefile文件名。

例如:

include config.mk

将引入config.mk文件。

include也支持通配符,例如:

include *.mk

将按字母顺序依次引入所有后缀为.mk的文件。

引用其他Makefile文件的主要优点:

  • 拆分复杂的Makefile,按功能分解为多个小文件,更易维护
  • 多个项目可以共用一份公共规则文件
  • 避免重复复制相同内容的Makefile文件
  • 不同人可以维护不同部分的Makefile

需要注意:

  • include是递归引入,可以多层嵌套
  • 被引入的文件与主Makefile目录相同
  • 如果同名,后引入的变量会覆盖之前的变量

#主文件中
include m1.mk m2.mk

all:m

m:main.o
	$(CC) $(CFLAGS) -o $@ $^
	
#m1.mk文件中
%.o:%.c
	$(CC) $(CFLAGS) -c -o $@ $<
	
#m2.mk文件中
export CC = gcc
export CFLAGS = -Wall

.PHONY: all clean install uninstall

三个文件,分别实现的是

  1. 在主文件中,引用了其他的两个makefile文件,并且用all作伪目标,将.o文件链接为可执行文件
  2. 在m1.mk文件中,实现的是将.c文件编译为.o文件
  3. 在m2.mk文件中,定义了两个变量,并用export导入到其他makefile文件中,.PHONY对其目标声明

条件控制

  • ifeq 相当于if()语句,其后可以使用(),也可以不使用,可以单/双引号

ifeq ($(AAA),ok) 使用 (),没有引号 ifeq "$(AAA)" "ok" 不使用 (),要去掉中间的逗号,使用双引号 ifeq ('$(AAA)','ok') 使用单引号

但是这个没有多个else if最后需要添加一个endif结束判断条件

条件控制的作用大多数时候用来定义变量,也可以包含规则

但是不能在命令规则中进行条件判断

target:dep1 dep2
	ifeq($(AAA),ok)
	command to build target
	endif

上述文本就是典型的错误,这种错误很明显,不可以粗略的认为和函数一样,不可以在规则内部进行判断

Makefile内置函数(origin)

总结一下Makefile中origin内置函数的用法:

origin函数用于显示一个变量的来源,调用格式为:

$(origin VARIABLE)

它会返回如下几种可能的值:

  • undefined - 变量未定义
  • default - 默认内置变量
  • environment - 环境变量
  • environment override - 使用-e选项的环境变量
  • file - 在Makefile中定义
  • command line - 在命令行中定义
  • override - 在Makefile中用override定义
  • automatic - 自动变量如$@

例如:

$(origin CC)

会显示CC变量的来源。

origin函数常用于调试Makefile,判断变量的值从何而来。

主要用法:

  • 查看变量是否被定义
  • 判断变量来源优先级
  • 调试多层次Makefile中的变量值

总之,origin函数可以让我们追踪变量的来源,从而更好地理解和调试Makefile的行为。

VPATH和vpath

Makefile中VPATH和vpath都用于定义搜索文件路径,区别和相同点如下:

相同点:

  • 都是用于指定make在当前路径找不到依赖文件时搜索的附加路径
  • 路径可以使用空格或冒号分隔多个路径
  • 都可以使makefile构建跨目录的项目

区别:

  • VPATH是make的一个预定义全局变量,适用于所有文件
  • vpath是make提供的一个命令,可以为不同文件指定不同的搜索路径,更灵活
  • VPATH只定义了一个搜索路径
  • vpath可以通过通配符为不同类型的文件定义不同的搜索路径
  • VPATH对所有文件生效
  • vpath需为特定文件指定匹配模式

例如:

# VPATH 
VPATH = src

# vpath
vpath %.c src
vpath %.h inc

可以用VPATH查找,但是就和环境变量一样,需要事先添加其路径,不然没有办法准确的查找到文件

相较而言,vapath就显得更高级,可以支持自由路径,并不限于当前makefile所在的目录。

vpath的搜索路径可以是相对路径,也可以是绝对路径,所以可以引用makefile目录外的任意位置。

综合实例

接下来是一个实例对于mian..c,fac.c,array.c,fac.h,array.h进行编写makefile脚本实战

由于繁琐,我将把每一部分写在一个代码块中

#在主文件中

export CC = gcc
export CFLAGS = -Wall
export RM += -r

BIN = main
SUBDIRS = fac array
INCLUDES = -I./inc

vpath %.o $(SUBDIRS)

# 查找所有子目录下的 .c 文件,并且去掉目录,只保留文件名
SUBSRCS = $(foreach dir,$(SUBDIRS),$(notdir $(wildcard $(dir)/*.c)))
# 根据 .c 文件得到 .o 文件
OBJS = $(SUBSRCS:.c=.o)
OBJS += main.o

.PHONY: all clean $(SUBDIRS)


ifndef OK

export OK = TRUE

all:$(SUBDIRS)
	make

$(SUBDIRS):
	make -C $@

else

all:$(BIN)

$(BIN):$(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

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

endif


clean:
	# 循环地从 $(SUBDIRS) 中取一个参数赋值给 dir,然后执行 do 和 done 之间的合作
	# 因为 dir 不是 Makefile 变量,而是 shell 变量,因此引用时需要两个 $$dir
	for dir in $(SUBDIRS); do $(RM) $$dir/*.o; done   # for 是 shell 的循环
	$(RM) $(BIN) *.o








#在m1.mk中

SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)

all:$(OBJS)
	echo $(OBJS)

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




#在fac文件夹中的makefile文件
include ../m1.mk



#在fac文件夹中的makefile文件
include ../m1.mk

可以将上述分为三类:

主文件夹,m1.mk补充文件夹,.c文件夹

主文件夹:

  1. 首先定义的是三个变量,CC,CFLAGS,RM这三个变量可以在所有的文件中使用,可以理解为全局变量
  2. 接下来是定义了BIN,SUBDIRS,INCLUDES三个变量,分别用来指代可执行文件,.c文件夹,其中INCLUDES表示设置包含头文件的搜索路径为当前makefile目录下的inc子目录
  3. vpath寻找.o文件,在SUBDIRS这个变量中保存的文件夹名称
  4. $(foreach dir,$(SUBDIRS),$(notdir $(wildcard $(dir)/*.c)))这个要分开看,首先看这里 的foreach表示一个循环,dir表示目录,目录的地址在SUBDIRS中,wiledcard表示找到目录下的所有.c文件,所以$ (wildcard $(dir)/*.c)就代表的是fac/fac.c和array/array.c但是我们不想要fac/和array/所以我们用notdir把.c前面的目录删除掉了,只保留了几个.c的文件名并赋值给SUBSRCS
  5. 接下来把SUBSRCS的.c文件名,改为.o文件名,字符串名赋值给OBJS
  6. OBJS再加上了main.o文件,其实现在OBJS中就保存的是三个文件名,fac.o,array.o,main.o
  7. 使用条件判断语句,因为这里有SUBDIRS和BIN都是编译的过程,但是SUBDIRS和BIN有先后顺序的,应该需要先执行SUBDIRS,跳转直至m1.mk文件中,将.c文件编译为.o文件
  8. 当执行了一次之后,就会再一次执行,make,m1.mk 中的编译规则可能只有 $(CC) $(CFLAGS),但 Makefile 可能需要增加$(INCLUDES)等参数。所以需要再次执行

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值