Makefile 使用指南

本文介绍了在Linux环境中使用`make`和Makefile进行自动化编译的基本概念和操作。通过简单的命令,开发者可以高效地编译和安装软件。Makefile文件指导`make`如何根据源文件生成目标文件,并通过定义规则、依赖关系和命令来实现增量编译。文章还讲解了如何安装`make`、编写基本规则、使用伪目标、执行多条命令及定义和引用环境变量等技巧,从而提高开发效率,为Linux内核开发打下基础。

  ·   2 min read

简介

在Linux中,make程序用于自动化编译大型源代码。通过运行简单的命令make,我们可以轻松完成编译和安装软件的工作,极大地方便了开发者。

make能够自动化完成这些任务,是因为项目中提供了一个名为Makefile的文件。Makefile负责指导make如何编译和链接程序。

Makefile的作用可以类比于Java项目的pom.xml、Node项目的package.json、Rust项目的Cargo.toml。不同之处在于,尽管make最初是为C语言开发的,但实际上它可以用于任何类型的项目,甚至不局限于编程语言。此外,make主要应用于Unix/Linux环境,掌握Makefile的编写可以帮助我们在Linux环境下更好地进行开发,并为后续的Linux内核开发做好准备。

在本教程中,我们将逐步学习如何编写Makefile,内容完全针对零基础的小白用户,只需提前掌握基本的Linux命令使用。

安装make

由于make只能在Unix/Linux环境下运行,因此在Windows系统中,我们需要先在Windows下运行Linux。

方法一: 安装VirtualBox,并下载Linux发行版的安装镜像,推荐使用Ubuntu 22.04,以便在虚拟机中运行Linux。

方法二: 对于Windows 10/11用户,可以安装WSL(Windows Subsystem for Linux):

  1. 安装WSL
  2. 在Windows应用商店中搜索并安装Ubuntu 22.04,安装完成后启动,Windows会弹出PowerShell窗口连接到Linux,此时可以输入Linux命令,类似于SSH连接。

以Ubuntu为例,在Linux命令行中使用apt命令安装make和GCC工具链:

$ sudo apt install build-essential

对于macOS系统,由于其内核是BSD(Unix的一种变体),同样可以直接运行make。推荐安装Homebrew,并通过Homebrew安装make和GCC工具链:

$ brew install make gcc

安装完成后,可以通过输入以下命令验证安装是否成功:

$ make -v
GNU Make 4.3
$ gcc --version
gcc (Ubuntu ...) 11.4.0

通过上述步骤,我们成功安装了make和GCC工具链。

Makefile基础

在Linux环境中,当我们输入make命令时,它会在当前目录查找一个名为Makefile的文件,并根据该文件定义的规则自动执行编译命令等。

Makefile可以理解为“生成文件”的说明书。

举个例子,假设在当前目录下有3个文本文件:a.txtb.txtc.txt。我们希望将a.txtb.txt合并生成中间文件m.txt,再用m.txtc.txt合并生成最终的目标文件x.txt,整个逻辑如下:

以下是将“文件和依赖”部分转化为有序列表的内容:

  1. a.txtb.txtc.txt 是三个独立的文件,作为输入或源文件。
  2. m.txt 是一个中间文件,依赖于 a.txtb.txt。这意味着 m.txt 的内容可能由这两个文件的数据生成。
  3. x.txt 是最终文件,依赖于 m.txt。这表明 x.txt 的内容依赖于中间文件 m.txt 的生成。

根据上述逻辑,我们编写Makefile。

规则

Makefile由若干条规则(Rule)构成,每条规则指定一个目标文件(Target)、若干依赖文件(prerequisites)以及生成目标文件的命令。

例如,要生成m.txt,其依赖文件为a.txtb.txt,规则如下:

# 目标文件: 依赖文件1 依赖文件2
m.txt: a.txt b.txt
	cat a.txt b.txt > m.txt

规则的格式为:

目标文件: 依赖文件1 依赖文件2 ...

接下来,命令必须以Tab开头,用于生成目标文件。上述规则使用cat命令合并了a.txtb.txt,并将结果写入m.txt。生成目标文件的方法不拘一格,命令可以是编译命令,也可以是其他任意命令。

#开头的行为注释,会被make忽略。

注意:Makefile的规则中,命令必须以Tab开头,不能使用空格。

接下来,生成x.txt的规则如下:

x.txt: m.txt c.txt
	cat m.txt c.txt > x.txt

由于make在执行时默认执行第一条规则,我们将x.txt的规则放在前面。完整的Makefile如下:

x.txt: m.txt c.txt
	cat m.txt c.txt > x.txt

m.txt: a.txt b.txt
	cat a.txt b.txt > m.txt

在当前目录创建a.txtb.txtc.txt并输入一些内容,然后执行make命令:

$ make
cat a.txt b.txt > m.txt
cat m.txt c.txt > x.txt

make默认执行第一条规则,创建x.txt,但由于m.txt不存在,必须先执行规则生成m.txt。执行完成后,当前目录下生成了m.txtx.txt

可以看出,Makefile定义了一系列规则,在满足依赖条件时执行命令,从而生成目标文件,这就是Makefile的基本概念。

默认执行的规则放在第一条,其他规则的顺序无关紧要,因为make会自动判断依赖关系。

此外,make会打印出执行的每条命令,便于观察执行顺序以便于调试。

如果再次运行make,输出如下:

$ make
make: `x.txt' is up to date.

make检测到x.txt已是最新版本,无需再次执行,因为x.txt的创建时间晚于其依赖的m.txtc.txt的最后修改时间。

make利用文件的创建和修改时间判断是否更新目标文件。当修改c.txt后,再次运行make,则会触发x.txt的更新:

$ make
cat m.txt c.txt > x.txt

但并不会触发m.txt的更新,因为m.txt的依赖a.txtb.txt未更新。因此,make仅会执行必要的规则,而不会无脑执行所有规则。

在编译大型程序时,全量编译可能需要几十分钟或更长时间。如果仅修改了几个文件,使用Makefile实现增量编译能够大大节省时间。

当然,是否能正确实现增量更新,取决于我们的规则写得是否正确,make本身并不会检查规则逻辑的正确性。

伪目标

由于m.txtx.txt都是自动生成的文件,因此可以安全地删除它们。

我们希望编写一个clean规则来删除这些文件,而不是手动删除:

clean:
	rm -f m.txt
	rm -f x.txt

clean规则与之前编写的规则不同,它没有依赖文件。因此,要执行clean,必须用命令make clean

$ make clean
rm -f m.txt
rm -f x.txt

但是,如果我们手动创建一个名为clean的文件,这个clean规则就不会执行了!

如果希望makeclean视为伪目标,可以添加一个标识:

.PHONY: clean
clean:
	rm -f m.txt
	rm -f x.txt

此时,clean就不再被视为一个文件,而是伪目标(Phony Target)。

大型项目通常会提供cleaninstall等约定俗成的伪目标名称,以方便用户快速执行特定任务。

一般情况下,除非有人故意创建一个名为clean的文件,否则并不需要用.PHONY标识约定俗成的伪目标名称。

执行多条命令

一个规则可以有多条命令,例如:

cd:
	pwd
	cd ..
	pwd

执行cd规则:

$ make cd
pwd
/home/ubuntu/makefile-tutorial/v1
cd ..
pwd
/home/ubuntu/makefile-tutorial/v1

观察输出,可以发现cd ..命令执行后,当前目录没有改变,两次pwd的输出是

相同的。这是因为每条命令都会在一个新的Shell中执行。

为了在同一Shell中执行多条命令,可以用&&连接每条命令,例如:

cd:
	pwd && cd .. && pwd

执行后,第二条pwd将输出上一级目录路径。

另外,若想在多条命令中使用同一个变量,直接写在第一条命令前面的花括号内,例如:

cd:
	{ 
	pwd
	cd .. 
	pwd 
	}

但大多数情况下推荐将命令放在同一个规则下,便于阅读和维护。

环境变量

Makefile也支持使用环境变量。我们可以在Shell中执行export命令设置一个变量,例如:

$ export VAR=value

然后在Makefile中引用该变量:

echo:
	echo $$VAR

注意,在Makefile中引用Shell变量时必须使用两个美元符号$$。执行后将输出value

同时,Makefile支持定义变量。例如,想要定义一个变量并在规则中使用,可以如下操作:

CC=gcc
CFLAGS=-Wall

hello: hello.c
	$(CC) $(CFLAGS) -o hello hello.c

这样定义的变量可以在Makefile中多次使用,提高了可维护性。

结语

本教程简单介绍了Makefile的基本概念、使用方法以及一些常见的规则和变量使用技巧。掌握Makefile的编写能够提高我们在Linux环境中的开发效率,为后续的Linux内核开发奠定基础。希望大家能在实际项目中不断探索和应用Makefile的强大功能!