导言

C 编程中,一个程序往往被分割到多个源文件和头文件,以提高组织,再使用和编译速度。 但是, 这个模块化带来了一个挑战: 当一个源文件更改时, 包含它的所有源文件都必须重新编译。 手工操作是容易出错的。 Makefile, 由 [[FLT: 0] 驱动 Make [[FLT: 1] 构建自动化工具, 通过编码依赖性以及自动化构建进程来解决该问题。 Mastering Makefile 是任何严肃的 C 开发者的通过仪式, 然而许多新开发者却把它们当作黑盒处理。 本文解释了在 Makefile 中依赖性是如何工作的, 如何自动生成它们, 以及如何为可靠性和速度构建 Makefile 。

什么是Makefiles? 是什么? 是什么? 是什么? 是什么? 是什么? 是什么? 是什么? 是什么? 是什么?

Makefile是一个定义构建工程规则的Plain Qext文件。 制作工具读取这些规则,检查文件时间戳,只执行更新工程所需的命令。核心思想很简单:每个规则都有目标(要生成的文件],] 预先要求 (构建目标所需的文件]),[recipes(要运行的壳命令]。记录哪些文件取决于哪些文件, make 仅能逐步重建已改变的部分。

Makefile自1970年代起就成为Unix的一部分,GNU Make是Linux和macOS上事实上的标准. 语法简洁但可以微妙;获得依赖性正确是C开发者需要的主要技能.

了解依赖关系

在 C 项目中, 依赖性不限于 [[FLT: 0]] 文件。 每个源文件包括一个或多个头文件( 如 [[FLT: 1]]] 。 如果修改了头文件, 包含它的所有 [[FLT: 2]] 文件必须重新编译。 同样, 对象文件取决于它们相应的 文件, 最终可执行文件取决于所有对象文件 。

明确对隐含的依赖

在早期 Makefile 中, 程序员手动列出所有先决条件。 这种方法很脆弱 : 忘记一个标题意味着 stale building, 同时列出太多不必要的重编触发器。 更糟的是, 随着工程的扩展, 手动列表变得无法维护 。 现代的解决方案是让编译器自动生成依赖, 将它们变成默认的, 机器检查的前提 。

精心设计的 Makefile 将依赖性视为头等问题。 目标永远不是重建不需要重建的东西,而是总是重建一切做得到的东西。 这就是增量建筑中 正确性[效率的本质。

Make文件的基本结构

典型的 Makefile 包含可变定义、规则和假目标。这里是一个最小但功能性小的示例,用于一个有 和 的项目 :

CC = gcc
CFLAGS = -Wall -Wextra -O2

main: main.o utils.o
 $(CC) $(CFLAGS) -o main main.o utils.o

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

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

clean:
 rm -f main main.o utils.o

此 Makefile 有四个目标: [[FLT: 7] (可执行文件), 两个对象文件, 和一个假目标 [[FLT: 8]]。 结号后依赖行 [[FLT: 0]] make [[FLT: 1] 哪些文件在决定重建目标之前要检查 。

假象目标

目标或]不代表文件。为了防止使 与文件名混淆,应宣布为phonny[]:

.PHONY: clean all

如果没有这个,如果一个名为的文件存在, make 会考虑更新并跳过配方.

有效管理依赖性

上面的手册 Makefile 存在严重缺陷: 对 ] 的依赖性是正确的, 但是如果 更改, (包括它) 必须重建, 但规则说它只取决于 。 固定是让编译器生成真正的依赖性列表 。

与海湾合作委员会自动产生依赖关系

GCC(和Clang)可以使用旗系生成依赖性信息。对于大多数项目来说,最实际的组合是:

  • ] –在编译过程中写一个依赖文件([]),只列出用户定义头(而不是系统头).
  • ] –指定依赖文件的名称.
  • ] — — 为每个头添加假目标,防止一个头被删除时出错.

如何将自动依赖跟踪纳入 Make 文件:

CC = gcc
CFLAGS = -Wall -Wextra -O2 -MMD -MP
SRCDIR = src
OBJDIR = obj
SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SRCS))
DEPS = $(OBJS:.o=.d)

all: myprogram

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

$(OBJDIR)/%.o: $(SRCDIR)/%.c
 @mkdir -p $(OBJDIR)
 $(CC) $(CFLAGS) -c $< -o $@

-include $(DEPS)

.PHONY: all clean
clean:
 rm -rf $(OBJDIR) myprogram

解释:

  • ]在源目录中收集所有]文件.
  • 将其转换为对象文件路径.
  • [列出依赖文件(例如]]])。
  • 模式规则 汇编每个]文件,并且由于,生成一个]文件作为副作用.
  • 线条 读取生成的 文件,将它们变成真实的Makefile先决条件。当 文件不存在时(例如,在第一个构建时),破折号(])会压制错误。

现在,如果修改,则下一次引用将自动重新编译],因为的文件包括作为先决条件。

高级技术

安全处理生成的依赖

当一个头被删除时, [FLT: 44] 文件仍然可能引用它, 导致 [[FLT: 0] [FLT: 1] 使 [FLT: 1] 目标错误缺失而失败。 [[FLT: 45] 旗通过为每个依赖头添加空假规则来解决这个问题。 如果头已消失, [[FLT: 2] 使 仅仅运行假规则(它没有作用) 并继续下去 。

源码变化后包含依赖文件

一个微妙之处: 如果一个源文件添加或删除 ], 则必须重生成相应的 [FLT: 47] 文件。 因为 文件本身就是对象文件的前提, make 将会注意到时间戳和重编, 从而将 文件重生。 一旦完成第一个构建, 此重编将自动生效 。

仅使用命令

有时,在建构前需要有一个目录,但你不希望它的时间戳触发重建。这就是命令的前提条件 (由分离)的作用。在以上模式规则中,我们在食谱中使用了;一个替代方案是:

$(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR)
 $(CC) $(CFLAGS) -c $< -o $@

$(OBJDIR):
 mkdir -p $@

这保证了目录在任何编译之前被创建,但是对目录本身的更改(例如内部的新文件)不会触发重新编译.

管理依赖性的最佳做法

  1. 从第一天起就使用自动依赖生成。 即使对一个单一文件项目来说,它也是一种很好的习惯。它不费钱,也不会防止未来出现错误。
  2. 保持依赖文件与源文件分开。 把它们放入 或目录。这样可以更容易地清理并避免源树被擦伤。
  3. 包含创建它们的规则之后生成的文件。指令是好的,但将其放在模式规则之后,确保先知道如何在试图读取文件之前构建对象文件。
  4. 使用假目标进行户口管理。 ]、、]和]是常见的,总是用 申报。
  5. 用于编译器旗,源目录,和文件列表的版本变量. 这使得Makefile在跨项目中可以重新使用,并且更容易自定义.
  6. 以故意头修改测试您的 Makefile 。 修改头,运行 ], 并确认只重编受影响的对象文件。 如果完全重建发生, 依赖跟踪会出问题 。
  7. 保持 Makefile 简单但并非简单的必要. Over 工程具有高级功能,如[]]],可以使调试痛苦。从本条中显示的模式开始;它能很好地缩放到几十个文件。

外部资源

为了加深你的了解,请参考这些权威性参考文献:

结论

用 Makefile 管理 C 程序依赖性并不是奢侈,而是任何超过单一文件的项目的必然。 通过将模式规则、自动依赖生成和 合并起来,并仔细地纳入生成的 文件,您可以创建既快速又正确的构建系统。 这里显示的技术取消了手动跟踪, 减少了构建时间, 并防止了被僵化对象文件造成的微妙错误。 一旦将这些模式内部化, 您将永远不再写一个手动依赖性列表。 学习适当的依赖性管理的投资每次运行[[FLT: 70] , 并且只观看已修改的文件编译 。