编辑
2021-08-19
工具
00
请注意,本文编写于 1248 天前,最后修改于 579 天前,其中某些信息可能已经过时。

目录

什么是 Makefile
如何编写 Makefile
自定义目标
自动化变量
隐式规则
显式规则
静态链接
特殊规则
VPATH
总结

什么是 Makefile

Makefile 是一个用于自动化构建和编译源代码的工具,可以帮助自动处理软件项目中的复杂依赖关系,并生成最终的可执行文件、库或其他类型的目标文件。Makefile 最初设计为在 Unix 系统上使用,但现在也可以在 Windows 和其他操作系统上使用。

Makefile 中包含一组规则,这些规则描述了如何从源代码创建目标文件。每个规则都由以下三部分组成:

  1. 目标(target):要生成的文件或其他输出。
  2. 依赖关系(dependencies):生成目标所需的文件、命令或其他目标。
  3. 命令(commands):生成目标所需的命令。

当运行 Makefile 时,Make 工具将按照规则中指定的顺序递归地处理所有依赖关系,并执行与每个目标相关联的命令。

如何编写 Makefile

下面是一个简单的 Makefile 示例,其中包含一个规则来生成名为 hello 的可执行文件:

makefile
CC = gcc CFLAGS = -Wall -Werror SRC = hello.c OBJ = $(SRC:.c=.o) all: hello hello: $(OBJ) $(CC) -o $@ $^ %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f *.o hello

这个 Makefile 包含三个规则:

  • all 规则:默认规则,会在没有指定目标时被执行。在这个例子中,它只是指向了另一个规则 hello
  • hello 规则:生成名为 hello 的可执行文件,它依赖于 hello.o 目标。
  • %.o 规则:对每个 .c 文件创建一个 .o 文件,这些文件是创建可执行文件所需的依赖项。

当运行 make hello 命令时,Make 工具将从 hello.c 创建 hello.o,然后使用 hello.o 生成名为 hello 的可执行文件。如果 hello.c 没有被修改,则不需要重新编译和链接。make会根据文件修改的时间戳来检查是否被修改

自定义目标

当然也可以通过在 Makefile 中定义自己的目标来扩展 Makefile 的功能。如果想要将最新的代码推送到 Git 存储库中,可以按照以下步骤进行操作:

  1. 在 Makefile 中定义一个名为 push 的规则,如下所示:

    makefile
    push: git add . git commit -m "Update" git push
  2. 运行 make push 命令,即可将最新的代码推送到存储库中。

自动化变量

在 Makefile 中,自动化变量是一组特殊的变量,用于表示当前正在处理的规则中的依赖项和目标文件。其中,$< 表示当前规则的第一个依赖项,$^表示当前规则的所有依赖项,而 $@ 则表示当前规则的目标文件。

例如,在 .o 规则中,可以使用以下命令将源代码编译为目标文件:

makefile
hello: $(OBJ) $(CC) -o $@ $^ %.o: %.c $(CC) $(CFLAGS) -c $< -o $@

在上面的命令中,$< 表示当前正在编译的 .c 文件,$^表示当前的依赖项$(OBJ)$@ 则表示当前正在生成的 .o 文件。言外之意%.o: %.c中的 $<也可换成$^,效果是一样的

隐式规则

隐式规则是一组预定义规则,用于根据文件名模式自动生成目标文件。例如,如果需要将 .c 文件编译为 .o 文件,则可以使用以下隐式规则:

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

在此示例中,%.o%.c 是文件名模式,Makefile 会自动将这些规则应用于所有符合模式的文件。因此,如果存在 foo.c 文件,则 Makefile 将使用上述规则编译并生成 foo.o 目标文件。

显式规则

相比之下,显式规则是您在 Makefile 中显式指定的规则,其中包含目标、依赖项和命令。例如,以下是一个显式规则的示例:

makefile
main: main.o helper.o $(CC) -o $@ $^

在此示例中,main 是目标文件,main.ohelper.o 是依赖文件,而 $(CC) -o $@ $^ 是要执行的命令。

显式规则通常用于更复杂的 Makefile,其中需要手动编写大量规则来管理源代码和目标文件之间的依赖关系。

静态链接

静态链接将所需的库文件嵌入到最终可执行文件中。这意味着可执行文件不依赖于系统上已安装的库文件,并且可以作为一个独立的单元进行移植和分发。

另外,静态链接还有以下优点:

  • 可以进行优化,以减少可执行文件的大小和内存占用。
  • 没有运行时链接的开销,因为所有依赖项都被编译到可执行文件中。

但是,静态链接的缺点是:

  • 如果有多个可执行文件使用相同的库文件,则每个文件都必须嵌入该库文件的副本,导致浪费空间和资源。

  • 每次更新库文件时,必须重新编译和链接所有依赖库文件的可执行文件。

    以下是更新后的 Makefile 示例:

静态链接

  1. 在 Makefile 中定义 LIBSINCLUDES 变量,这些变量分别存储库文件名和头文件路径。

    makefile
    LIBS = -lm INCLUDES = -I ./include
  2. 更新 $(CC) 命令,以便使用 -static 标志进行静态链接,并且将头文件路径传递给编译器。

    makefile
    $(CC) -static -o $@ $^ $(INCLUDES) $(LIBS)

动态链接

动态链接将所需的库文件作为单独的文件提供,并且在运行时通过共享库文件在内存中加载它们。这意味着可执行文件不包含库文件,而是在运行时从系统上已安装的共享库中获取所需的函数和代码。

动态链接的优点包括:

  • 可以减少可执行文件的大小,因为它们不包含任何库文件。
  • 可以允许多个可执行文件共享相同的库文件,从而节省磁盘空间。
  • 如果更新库文件,则只需要更新共享库文件,而不必重新编译所有依赖项。

但是,动态链接的缺点是:

  • 需要确保所有依赖库文件都已安装在系统上,并且在运行程序时可以访问这些库文件。如果缺少某个库文件,则可能会导致程序无法正常运行。
  • 运行时链接需要一些开销,因为需要加载和解析共享库文件。

动态链接

  1. 在 Makefile 中定义 LDFLAGSINCLUDES 变量,这些变量分别存储库文件名和头文件路径。

    makefile
    LDFLAGS = -lm INCLUDES = -I ./include
  2. 更新 $(CC) 命令,以便使用 -shared 标志进行动态链接,并且将头文件路径传递给编译器。

    makefile
    $(CC) -shared -o $@ $^ $(INCLUDES) $(LDFLAGS)

编译后运行

在编译完毕后自动运行可执行文件。这可以通过 Makefile 中的 .PHONYrun 规则来实现。具体步骤如下:

  1. 定义 run 规则,该规则依赖于可执行文件,并在命令中使用 ./ 运行可执行文件。

    makefile
    run: hello ./hello
  2. .PHONY 规则中将 run 添加为一个伪目标,并确保它不会被误认为是一个文件或目录名称。

    makefile
    .PHONY: all clean run

    在上述示例中,.PHONY 规则指定了三个伪目标:allcleanrun。其中 run 是我们新添加的伪目标。

  3. 最后,在 Makefile 的结尾处添加 run 规则作为默认目标,以便在编译并生成可执行文件后自动运行它。

    makefile
    all: hello hello: $(OBJ) $(CC) -o $@ $^ %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f *.o hello run: hello ./hello

现在,当运行 make 命令时,Makefile 将依次执行 all 规则和 run 规则。all 规则将编译并生成可执行文件,而 run 规则将自动运行该文件。此外,也可以在 Makefile 中的其他规则中添加其他操作,例如清理过程或其他构建步骤。

特殊规则

.PHONY 是一个特殊的规则,用于指定伪目标。它告诉 Make 工具,该目标不是实际的文件或目录名称,而只是一个标记,用于表示要执行的操作。.PHONY 规则通常与伪目标一起使用,例如 allcleanrun 等。

在 Makefile 中,如果某个目标的名称与已存在的文件或目录名称相同,则 Make 工具可能会将其误认为是文件或目录,并不会执行该目标所指定的命令。因此,通过在 .PHONY 规则中指定这些目标名称,可以确保它们被正确地处理为伪目标,从而使得 Make 工具能够正确地执行它们所指定的命令。

下面是一个示例:

makefile
.PHONY: all clean all: $(CC) -o hello main.c clean: -rm -f hello

在上述示例中,.PHONY 规则指定了两个伪目标:allclean。当您运行 make allmake clean 命令时,Make 工具将会执行对应的命令。由于 allclean 不是实际的文件或目录名称,因此需要通过 .PHONY 规则来指定它们为伪目标。

注意,.PHONY 规则不会生成任何实际的文件或目录,它只是告诉 Make 工具哪些目标名称应该被视为伪目标。在大多数情况下,并不需要使用 .PHONY 规则,除非项目的 Makefile 中存在与文件或目录名称相同的目标名称。

前面说过, .PHONY 表示 clean 是一个“伪目标”。而在 rm 命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然, clean 的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。

VPATH

VPATH,即文件搜寻。在一些大的工程中,有大量的源文件,我们通常的做法是把这许多的源文件分类,并存放在不同的目录中。所以,当make需要去找寻文件的依赖关系时,你可以在文件前加上路径,但最好的方法是把一个路径告诉make,让make在自动去找。

Makefile文件中的特殊变量 VPATH 就是完成这个功能的,如果没有指明这个变量,make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make就会在当前目录找不到的情况下,到所指定的目录中去找寻文件了。

VPATH = src:../headers

上面的定义指定两个目录,“src”和“../headers”,make会按照这个顺序进行搜索。目录由“冒号”分隔。(当然,当前目录永远是最高优先搜索的地方)

另一个设置文件搜索路径的方法是使用make的“vpath”关键字(注意,它是全小写的),这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种:

  • vpath <pattern> <directories>

    为符合模式的文件指定搜索目录。

  • vpath <pattern>

    清除符合模式的文件的搜索目录。

  • vpath

    清除所有已被设置好了的文件搜索目录。

vpath使用方法中的需要包含 % 字符。 % 的意思是匹配零或若干字符,(需引用 % ,使用 \ )例如, %.h 表示所有以 .h 结尾的文件。指定了要搜索的文件集,而则指定了< pattern>的文件集的搜索的目录。例如:

vpath %.h ../headers

该语句表示,要求make在“../headers”目录下搜索所有以 .h 结尾的文件。(如果某文件在当前目录没有找到的话)

可以连续地使用vpath语句,以指定不同搜索策略。如果连续的vpath语句中出现了相同的 ,或是被重复了的,那么,make会按照vpath语句的先后顺序来执行搜索。如:

vpath %.c foo vpath % blish vpath %.c bar

其表示 .c 结尾的文件,先在“foo”目录,然后是“blish”,最后是“bar”目录。

vpath %.c foo:bar vpath % blish

而上面的语句则表示 .c 结尾的文件,先在“foo”目录,然后是“bar”目录,最后才是“blish”目录。

总结

Makefile里有什么?

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中的命令,必须要以 Tab 键开始。

Makefile的文件名

默认的情况下,make命令会在当前目录下按顺序寻找文件名为 GNUmakefilemakefileMakefile 的文件。在这三个文件名中,最好使用 Makefile 这个文件名,因为这个文件名在排序上靠近其它比较重要的文件,比如 README。最好不要用 GNUmakefile,因为这个文件名只能由GNU make ,其它版本的 make 无法识别,但是基本上来说,大多数的 make 都支持 makefileMakefile 这两种默认文件名。

当然,可以使用别的文件名来书写Makefile,比如:“Make.Solaris”,“Make.Linux”等,如果要指定特定的Makefile,可以使用make的 -f--file 参数,如: make -f Make.Solarismake --file Make.Linux 。如果使用多条 -f--file 参数,你可以指定多个makefile。

包含其它Makefile

在Makefile使用 include 指令可以把别的Makefile包含进来,这很像C语言的 #include ,被包含的文件会原模原样的放在当前文件的包含位置。 include 的语法是:

include <filenames>...

<filenames> 可以是当前操作系统Shell的文件模式(可以包含路径和通配符)。

include 前面可以有一些空字符,但是绝不能是 Tab 键开始。 include<filenames> 可以用一个或多个空格隔开。举个例子,有这样几个Makefile: a.mkb.mkc.mk ,还有一个文件叫 foo.make ,以及一个变量 $(bar) ,其包含了 bishbash ,那么,下面的语句:

include foo.make *.mk $(bar)

等价于:

include foo.make a.mk b.mk c.mk bish bash

make命令开始时,会找寻 include 所指出的其它Makefile,并把其内容安置在当前的位置。就好像C/C++的 #include 指令一样。如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找:

  1. 如果make执行时,有 -I--include-dir 参数,那么make就会在这个参数所指定的目录下去寻找。
  2. 接下来按顺序寻找目录 <prefix>/include (一般是 /usr/local/bin )、 /usr/gnu/include/usr/local/include/usr/include

环境变量 .INCLUDE_DIRS 包含当前 make 会寻找的目录列表。你应当避免使用命令行参数 -I 来寻找以上这些默认目录,否则会使得 make “忘掉”所有已经设定的包含目录,包括默认目录。

如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”。如:

-include <filenames>...

其表示,无论include过程中出现什么错误,都不要报错继续执行。如果要和其它版本 make 兼容,可以使用 sinclude 代替 -include

环境变量MAKEFILES

如果当前环境中定义了环境变量 MAKEFILES ,那么make会把这个变量中的值做一个类似于 include 的动作。这个变量中的值是其它的Makefile,用空格分隔。只是,它和 include 不同的是,从这个环境变量中引入的Makefile的“目标”不会起作用,如果环境变量中定义的文件发现错误,make也会不理。

建议不要使用这个环境变量,因为只要这个变量一被定义,那么当使用make时,所有的Makefile都会受到它的影响。

make的工作方式

GNU的make工作时的执行步骤如下:

  1. 读入所有的Makefile。
  2. 读入被include的其它Makefile。
  3. 初始化文件中的变量。
  4. 推导隐式规则,并分析所有规则。
  5. 为所有的目标文件创建依赖关系链。
  6. 根据依赖关系,决定哪些目标要重新生成。
  7. 执行生成命令。

1-5步为第一个阶段,6-7为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。

本文作者:phae

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!