简介
Linux的make程序用来自动化编译大型源码,很多时候,我们在Linux下编译安装软件,只需要敲一个make
就可以全自动完成,非常方便。
make能自动化完成这些工作,是因为项目提供了一个Makefile文件,它负责告诉make,应该如何编译和链接程序。
Makefile相当于Java项目的pom.xml,Node工程的package.json,Rust项目的Cargo.toml,不同之处在于,make虽然最初是针对C语言开发,但它实际上并不限定C语言,而是可以应用到任意项目,甚至不是编程语言。此外,make主要用于Unix/Linux环境的自动化开发,掌握Makefile的写法,可以更好地在Linux环境下做开发,也可以为后续开发Linux内核做好准备。
在本教程中,我们将由浅入深,一步一步学习如何编写Makefile,完全针对零基础小白,只需要提前掌握如何使用Linux命令。
安装make
在Linux(Ubuntu)系统中,我们可以使用包管理工具直接安装make以及GCC工具链。例如在Ubuntu下,使用以下命令:
bash
$ sudo apt update
$ sudo apt install build-essential
安装完成后,可以输入以下命令验证安装是否成功:
bash
$ make -v
GNU Make 4.3
$ gcc --version
gcc (Ubuntu ...) 11.4.0
这样,我们就成功地安装了make,以及GCC工具链。
Makefile基础
在Linux环境下,当我们输入make
命令时,它就在当前目录查找一个名为Makefile的文件,然后,根据这个文件定义的规则,自动化地执行任意命令,包括编译命令。
Makefile由若干条规则(Rule)构成,每一条规则指出一个目标文件(Target),若干依赖文件(Prerequisites),以及生成目标文件的命令。规则的基本格式如下:
makefile
目标文件: 依赖文件1 依赖文件2 ...
命令
紧接着,以Tab开头的是命令,用来生成目标文件。命令可以是任何Shell命令,如gcc
、cp
、mv
等。以#
开头的是注释,会被make命令忽略。
编译C语言程序
下面是一个典型的Makefile,用于编译C语言程序:
makefile
BIN=code # 目标文件
CC=gcc # 编译器
SRC=$(wildcard *.c) # 所有的.c文件
OBJ=$(SRC:.c=.o) # 替换为.o文件
LFLAGS=-o
FLAGS=-c
RM=rm -f # 删除方式
$(BIN): $(OBJ)
@$(CC) $(LFLAGS) $@ $^
%.o: %.c
@$(CC) $(FLAGS) $<
.PHONY: clean
clean:
$(RM) $(OBJ) $(BIN)
在这个Makefile中:
BIN=code
:变量BIN
的值是code
,表示目标文件名。CC=gcc
:变量CC
的值是gcc
,表示所使用的编译器。SRC=$(wildcard *.c)
:SRC
表示当前目录下所有的.c
文件,使用wildcard
函数动态生成。OBJ=$(SRC:.c=.o)
:将SRC
中的.c
文件名替换为.o
,表示目标文件对应的中间文件(对象文件)。LFLAGS=-o
:链接选项,指定输出文件名。FLAGS=-c
:编译选项,用于生成中间文件。RM=rm -f
:删除命令,用于清理目标文件和中间文件。$(wildcard *.c)
用于匹配当前目录下的所有.c
文件。$(SRC:.c=.o)
将所有的.c
文件替换为.o
文件。$@
表示目标文件。$<
表示第一个依赖文件。$^
表示所有依赖文件。.PHONY
声明clean
为伪目标,避免与同名文件冲突。
执行make
即可编译,执行make clean
可清理生成的文件。
**注意:**如果在修改文件内容,但是其他文件没有改变内容(Modify time)没有更新,则未改变的文件不会进行重新编译,可能会造成程序出现问题。所以当项目出现问题的时候可以对项目进行重新创建,以此解决问题。
使用隐式规则
Makefile支持许多内置的隐式规则,例如编译C语言程序的规则:
makefile
%.o: %.c
$(CC) $(FLAGS) $<
含义:
- 将每个
.c
文件编译为对应的.o
文件(对象文件)。 $<
表示第一个依赖文件。
所以该规则会使*.c
从第一个开始,逐个编译为对应的.o
文件。
小结:隐式规则让Makefile更简洁,很多情况下不需要显式定义规则。
使用变量
赋值形式 =
这种赋值在解析时是延迟展开的。也就是说,右侧的值在被使用时才会被计算,左值变为变量来使用。这种赋值方式是 GNU Make 的标准形式。
例如:
plain
SRC=$(wildcard *.c)
变量可以提高Makefile的可读性和可维护性。例如:
makefile
CC=gcc
CFLAGS=-Wall -g
SRC=main.c utils.c
OBJ=$(SRC:.c=.o)
program: $(OBJ)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f $(OBJ) program
符号说明:
$(变量名)
:表示引用变量,例如$(CC)
引用了变量CC
,其值为gcc
。
示例:
$@
示例
makefile
objects = foo.o bar.o
$(objects): %.o: %.c
gcc -c $< -o $@
在这个例子中,$@
将被替换为每个 .o
文件的目标名称,如 foo.o
或 bar.o
。
$@
代表当前规则的目标文件名(即左侧的文件)。- 它用于在命令中引用规则左侧的目标。
$^
示例
makefile
target: dependency1 dependency2
gcc -o $@ $^
在这个例子中,$^
将被替换为 dependency1 dependency2
。
$^
代表当前规则中所有依赖文件的列表。- 它用于在命令中引用规则右侧的所有依赖文件。
$<
示例
makefile
%.o: %.c
gcc -c $< -o $@
在这个例子中,$<
将被替换为规则中的第一个依赖文件,如 main.c
。
$<
专门用来引用规则中列出的第一个依赖文件。
通过这些示例,可以清楚地理解这些符号的用途及功能。
使用模式规则
模式规则支持使用通配符定义一组目标文件的规则,例如:
makefile
%.o: %.c
$(CC) -c $< -o $@
符号说明:
%
:通配符,表示任意文件名。例如,%.o
匹配所有.o
文件,%.c
匹配所有.c
文件。$<
:表示当前规则中的第一个依赖文件。例如,main.c
。$@
:表示目标文件。例如,main.o
。
通过模式规则,可以避免重复定义规则,适用于大规模项目中的批量操作。
自动生成依赖
对于大型项目,手动维护依赖关系容易出错。我们可以使用gcc
的-M
选项自动生成依赖关系:
bash
gcc -MM main.c > deps.d
然后在Makefile中包含生成的依赖文件:
makefile
include deps.d
自动生成依赖的意义:
- 动态更新依赖关系 :当源码文件发生变动时,重新运行依赖生成命令即可更新
deps.d
。 - 减少人工维护的错误:避免手动书写依赖关系带来的遗漏问题。
注意:
- 包含依赖文件时,如果依赖文件不存在,Makefile可能会报错。为此,可以使用如下方式:
makefile
-include deps.d
这表示如果deps.d
不存在,Makefile将不会报错。
示例:
- 自动生成依赖文件
makefile
%.d: %.c
@set -e; rm -f $@; \
$(CC) -MM $(CFLAGS) $< > $@.$$$$; \
mv $@.$$$$ $@
在这个规则中,依赖文件 .d
将自动根据对应的 .c
文件生成。
- 包含所有依赖
makefile
include $(SRC:.c=.d)
通过这条规则,可以动态包含所有的依赖文件。
完善Makefile
通过上述方法,我们可以逐步完善一个Makefile。以下是一个完整的示例:
makefile
BIN=program
CC=gcc
CFLAGS=-Wall -g
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
$(BIN): $(OBJ)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f $(OBJ) $(BIN)
.PHONY: all
all: clean $(BIN)
-include deps.d
%.d: %.c
@set -e; rm -f $@; \
$(CC) -MM $(CFLAGS) $< > $@.$$$$; \
mv $@.$$$$ $@
通过以上内容的学习,已经足够编写寄出的Makefile~