【C语言】GNU make 和 Makefile :构建工具与构建描述文件的力量

本文将详细介绍make和Makefile,它们是软件开发中常用的构建工具和构建描述文件。本文将探讨make的作用、原理和用法,以及Makefile的结构、语法和常见用法。通过了解这些工具,开发者可以更高效地管理和构建复杂的软件项目。


引言

在软件开发过程中,构建(Build)是一个重要的环节,它涉及编译源代码、链接库文件、生成可执行文件等操作。为了自动化和简化构建过程,构建工具的出现变得至关重要。make是一种常用的构建工具,它使用Makefile作为构建描述文件。本文将深入介绍make和Makefile,让读者对它们有更全面的了解。

Makefile可以包含多个目标和对应的规则,以及一些变量、函数和条件判断等


一、make

1.1 make 基本介绍

make是一个构建工具,它可以根据一组规则和依赖关系,自动判断哪些文件需要重新构建,从而减少重复编译的工作量。它通过比较源文件和目标文件的时间戳来确定构建的必要性,并执行相应的构建命令。

make的原理是基于“依赖关系。通过分析Makefile中的规则和依赖关系,make可以构建出一个任务执行图,确保按正确的顺序执行构建任务。这使得make成为管理复杂项目的有力工具。


GUN make 官网:https://www.gnu.org/software/make/manual/make.html

官方PDF手册(last updated February 26, 2023):https://www.gnu.org/software/make/manual/make.pdf

Make有多个版本,其中最常用的是GNU Make,它是GNU项目的一部分。GNU Make的最新版本是4.4.1。除了GNU Make之外,还有其他一些Make的变体,如BSD Make,Microsoft NMAKE,CMake等,它们有不同的语法和功能。

在这里插入图片描述


1.2 make 的执行过程

make 的执行过程包括解析 Makefile、确定目标和依赖关系、检查文件的修改时间、执行构建命令和检查构建结果等步骤,以自动化软件构建过程。

make 的基本执行过程:

  1. make 命令由用户在终端中输入,格式通常为 make [target],其中 [target] 是 Makefile 中定义的目标。
  2. make 解析 Makefile 文件并确定要构建的目标。如果未指定目标,则默认构建第一个目标。
  3. 对于每个目标,make 检查其依赖关系。依赖关系是指生成目标所需的其他文件或目标。
  4. make 检查目标文件是否存在,并比较目标文件的修改时间与其所有依赖项的修改时间。如果目标文件不存在或某个依赖项已更新,则需要重新构建目标。
  5. 如果目标需要重新构建,make 将按照 Makefile 中定义的规则执行相应的命令。这些命令可以是编译源代码、链接对象文件、复制文件等任何构建步骤。
  6. 构建命令执行完毕后,make 检查目标文件是否已成功生成。如果成功生成,则继续构建其他目标或完成构建过程;否则,会报告构建失败并停止构建过程。

make 的关键是根据目标和依赖关系来确定构建的顺序和需要执行的命令。它通过比较文件的修改时间来判断文件是否需要重新构建,从而实现增量构建的效果,避免重复构建不必要的文件。

二、Makefile

2.1 Makefile 文件命名

Makefile 文件的命名没有严格的规定,但是有一些常见的命名约定可以遵循,以提高代码的可读性和维护性。

一些建议的 Makefile 文件命名方式:

  1. 默认规则:如果你的项目中只有一个 Makefile 文件,并且它是用于整个项目的默认构建规则,可以将其命名为 Makefile(注意首字母大写),这是 make 工具默认会查找的文件名。

  2. 根据项目名称:可以根据项目的名称来命名 Makefile 文件,这样可以更清晰地表达该文件与特定项目的关联性。

    • 例如,如果项目名为 “myproject”,可以将 Makefile 命名为 myproject.mkMakefile.myproject
  3. 根据构建目标:如果你的 Makefile 针对特定的构建目标,例如编译器选项不同或构建不同的二进制文件,可以根据目标的名称来命名 Makefile 文件。

    • 例如,如果 Makefile 用于构建 “debug” 和 “release” 两个目标,可以将其命名为 Makefile.debugMakefile.release
  4. 添加后缀:为了清晰地指示该文件是一个 Makefile,可以为其添加 .mk.make 等后缀。

    • 例如,可以命名为 Makefile.mkproject.make

无论选择何种命名方式,确保文件名具有描述性,能够清晰地表达其用途和关联性,以便团队成员或其他开发人员能够快速理解和找到所需的 Makefile 文件。

2.2 基本语法

2.21 注释

使用 # 开头的行表示注释,用于说明规则或指令的作用。

2.22 变量定义

变量用于存储和传递值,可以通过变量名引用其值,从而实现对构建过程中的参数化和配置。

Makefile 支持两种常见的变量定义方式:简单展开变量和递归展开变量:

  1. 简单展开变量:

    • 使用 = 运算符进行变量赋值,例如 VAR = value
    • 变量在使用时进行简单展开,即在变量被引用时,会将其值直接替换到引用的位置。
    • 简单展开变量示例:
      NAME = John
      GREETING = Hello, $(NAME)!
      
      target:
      	@echo $(GREETING)   # 将展开为 "Hello, John!"
      
  2. 递归展开变量:

    • 使用 := 运算符进行变量赋值,例如 VAR := value
    • 变量在赋值时进行递归展开,即在赋值时就会将变量引用展开为其值,后续的引用将保持展开后的值。
    • 递归展开变量示例:
      A := 1
      B := $(A)
      A := 2
      
      target:
      	@echo $(B)   # 将展开为 "1"
      
    • 在上面的示例中,B 的赋值发生在 A 的值发生变化之前,因此 B 的值是根据 A 的初始值展开的。

变量定义还可以包含其他变量的引用、函数调用、命令替换等。Makefile 提供了一些内置函数,如 $(wildcard)$(patsubst)$(shell) 等,可以用于处理变量的值。这些函数可以根据具体需求灵活使用。

除了使用 =:= 运算符进行变量赋值,还可以使用其他运算符如 +=?=!= 等来对变量进行修改、条件赋值和赋值防止覆盖等操作。

2.23 目标规则

目标规则定义了一个或多个目标文件以及构建该目标所需的依赖关系和构建命令。

目标规则是 Makefile 的核心部分,用于描述项目的构建过程。

目标规则由以下几个部分组成:

  1. 目标(Target):

    • 目标是构建过程中的输出文件,可以是可执行文件、库文件、中间文件等
    • 目标通常是一个文件名,但也可以是一个伪目标,用于触发特定的命令。
    • 目标规则可以定义一个或多个目标。
  2. 依赖关系(Dependencies):

    • 依赖关系定义了构建目标所需要的其他文件或目标
    • 依赖关系可以是源文件、头文件、其他目标等
    • 当依赖关系的文件被修改时,该目标需要重新构建。
  3. 构建命令(Commands):

    • 构建命令是执行构建目标所需的命令行。
    • 构建命令以 tab 字符缩进,并位于目标规则的下一行。
    • 可以使用 shell 命令、编译器命令、链接器命令等来执行构建操作。

下面是一个简单的目标规则示例:

# 目标规则
target: dependency1 dependency2
	commands
	commands
	...

在上面的示例中,target 是目标文件,dependency1dependency2 是构建 target 所需的依赖文件。commands 是构建 target 的命令,它们将在依赖文件被修改或目标文件不存在时执行。

Makefile 使用依赖关系来确定构建目标的顺序和需要重新构建的条件。如果某个依赖文件的修改时间比目标文件新,或者目标文件不存在,则会执行目标规则中的构建命令。

Makefile 还支持多个目标对应一个规则的情况。在这种情况下,构建命令只需要编写一次,并应用于多个目标。

目标规则可以包含多个依赖关系和构建命令,以满足具体构建需求。通过定义合适的目标规则,可以构建出整个项目或项目的特定部分。

2.24 伪目标

伪目标(Phony Targets)是一种特殊类型的目标,它们不对应实际的文件,而是用于触发特定的命令或行为。伪目标在 Makefile 中的主要作用是定义一些常用的操作或命令,以便通过 make 命令进行执行。

伪目标的特点如下:

  1. 不对应实际文件:伪目标没有与之关联的实际文件,其名称只是用于标识目标。
  2. 始终被执行:由于伪目标不对应实际文件,因此每次执行 make 命令时都会触发伪目标所定义的命令。
  3. 通常不满足构建条件:伪目标的构建条件通常为假,因为它们没有关联的实际文件。

伪目标的定义方式如下:

.PHONY: target1 target2 ...

target1:
	commands

target2:
	commands

在上面的示例中,.PHONY 是一个特殊的目标,用于声明伪目标。target1target2 是伪目标的名称,后面跟着对应的构建命令。可以根据需要定义多个伪目标,并在其后定义相应的构建命令。

伪目标常见的用途包括:

  • clean:执行清理操作,删除构建生成的文件。
  • all:构建所有目标文件。
  • install:安装构建生成的文件到指定位置。
  • test:执行测试操作。
  • dist:生成软件分发包。

通过定义伪目标,可以方便地执行这些常用操作,而无需手动输入复杂的命令。

在执行 make 命令时,可以指定要构建的伪目标。例如,执行 make clean 将触发 clean 目标的构建命令。

需要注意的是,伪目标的名称不能与实际文件的名称相冲突,否则会导致构建错误或不可预期的行为。因此,建议在伪目标名称前添加一个前缀,如 .PHONY 前缀或其他项目相关的前缀,以避免冲突。

2.25 条件语句

条件语句用于根据某些条件来执行不同的操作。条件语句允许根据变量的值或其他条件来选择性地执行特定的命令或操作。

Makefile 提供了以下条件语句:

  1. ifeqifneq

    • ifeq 用于检查变量的值是否等于指定的值。
    • ifneq 用于检查变量的值是否不等于指定的值。
    • 语法为:
      ifeq ($(variable), value)
      	# 条件为真时的操作
      else
      	# 条件为假时的操作
      endif
      
    • 可以在条件语句中使用多个 ifeqifneq 来进行复杂的条件判断。
  2. ifdefifndef

    • ifdef 用于检查变量是否已定义。
    • ifndef 用于检查变量是否未定义。
    • 语法为:
      ifdef variable
      	# 变量已定义时的操作
      else
      	# 变量未定义时的操作
      endif
      
  3. ifeqifdef 的嵌套:

    • 条件语句可以相互嵌套使用,以实现更复杂的条件判断。
    • 例如:
      ifeq ($(variable1), value1)
      	ifdef variable2
      		# 条件为真时的操作
      	else
      		# 条件为假时的操作
      	endif
      endif
      
  4. 条件语句中的命令:

    • 条件语句中可以包含命令,用于在满足条件时执行特定的命令。
    • 命令必须以 tab 字符缩进,并在条件语句内部的每个分支中都提供。
    • 例如:
      ifeq ($(variable), value)
      	@echo "Variable is equal to value."
      else
      	@echo "Variable is not equal to value."
      endif
      

条件语句在 Makefile 中非常有用,可以根据不同的条件执行不同的命令或操作,从而实现更灵活的构建过程。可以根据变量的值、变量是否已定义以及条件语句的嵌套等来进行复杂的条件判断。条件语句结合其他元素如目标规则、变量定义等,可以构建出更具有可配置性和可扩展性的 Makefile。

2.26 函数

函数用于对变量进行处理和操作。它们提供了一组预定义的操作,可以通过参数传递值并返回处理后的结果。函数可以用于生成文件列表、字符串操作、路径处理等。

Makefile 提供了许多内置函数,下面是其中一些常用的函数及其功能:

  1. $(subst from,to,text)

    • 替换字符串函数,将 text 中的所有 from 字符串替换为 to 字符串。
    • 示例:$(subst .c,.o,$(SRC_FILES)) 将将所有源文件的扩展名 .c 替换为 .o
  2. $(patsubst pattern,replacement,text)

    • 模式替换函数,将 text 中的符合 pattern 模式的部分替换为 replacement
    • 模式可以包含通配符 %,代表任意字符。
    • 示例:$(patsubst %.c,%.o,$(SRC_FILES)) 将所有以 .c 结尾的文件替换为以 .o 结尾的文件。
  3. $(wildcard pattern)

    • 文件匹配函数返回匹配 pattern 的文件列表
    • 可以使用通配符 *? 进行模式匹配。
    • 示例:$(wildcard src/*.c) 返回所有位于 src/ 目录下的 .c 文件列表。
  4. $(shell command)

    • 命令执行函数,执行指定的命令并返回其输出结果。
    • 可以执行任意的 shell 命令。
    • 示例:$(shell date +%Y-%m-%d) 返回当前日期。
  5. $(foreach var,list,text)

    • 循环函数,将 list 中的每个元素赋值给变量 var,并在每次迭代中展开 text
    • 示例:$(foreach file,$(SRC_FILES),$(shell wc -l $(file))) 对每个源文件执行 wc -l 命令获取行数。
  6. $(if condition,then-part,else-part)

    • 条件函数,根据 condition 的值选择性地返回 then-partelse-part
    • 示例:$(if $(DEBUG),$(CC_FLAGS_DEBUG),$(CC_FLAGS_RELEASE)) 根据 DEBUG 变量的值选择编译器标志。
  7. $(error message)

    • 错误函数,输出错误消息并停止 make 的执行。
    • 可以用于在特定条件下终止构建过程。
    • 示例:$(if $(VERSION),,$(error VERSION is not set)) 检查 VERSION 变量是否已定义,如果未定义则输出错误消息。

以上仅是一些常用的 Makefile 函数,还有许多其他函数可用于字符串操作、文件操作、路径操作等。可以参考 Makefile 的文档或手册以了解更多内置函数的详细用法。

函数在 Makefile 中提供了强大的处理

2.27 include 指令

include 指令用于在当前 Makefile 中引入其他的 Makefile 文件。它允许将其他文件中定义的变量、目标规则和其他内容合并到当前的 Makefile 中,以实现代码的复用和模块化。

include 指令有以下的语法:

include file1 file2 ...

其中,file1, file2, … 是要包含的 Makefile 文件的路径。

include 指令的工作方式如下:

  1. 当解析到 include 指令时,Make 解释器会在指定的文件中查找并读取内容。
  2. 解释器会将读取到的内容合并到当前的 Makefile 中。
  3. 继续解析后续的内容,包括变量定义、目标规则等。

include 指令通常用于以下情况:

  1. 模块化组织:将具有相似功能的目标规则或变量定义放在单独的 Makefile 文件中,然后使用 include 指令将它们包含到主 Makefile 中。这样可以使主 Makefile 更简洁,易于维护和阅读。

  2. 共享变量和规则:通过将共享的变量和规则定义在一个文件中,并在多个 Makefile 中使用 include 指令引入,可以确保它们的一致性和复用性。

  3. 外部配置文件:将构建系统的配置信息存储在一个独立的 Makefile 文件中,然后使用 include 指令将其引入到主 Makefile 中。这样可以方便地修改配置信息,而不需要修改主 Makefile。

在使用 include 指令时,需要注意以下事项:

  • 可以使用相对或绝对路径指定要包含的文件。相对路径是相对于当前 Makefile 的路径。
  • 可以使用变量来指定 include 指令中的文件路径,以实现动态包含。
  • 如果指定的文件不存在或无法读取,Make 将会发出警告,但不会中断构建过程。

2.3 头文件和库

分别使用-I-L来指定。

  1. 使用变量:可以定义变量来保存头文件和库的路径,并在整个 Makefile 中使用这些变量。例如:

    # 定义头文件和库路径的变量
    INCLUDE_PATH = -I/path/to/includes
    LIBRARY_PATH = -L/path/to/libraries
    
    # 使用变量来编译目标
    target: source.c
        gcc $(INCLUDE_PATH) $(LIBRARY_PATH) -o target source.c
    

    在上面的示例中,INCLUDE_PATH 变量指定头文件路径,LIBRARY_PATH 变量指定库路径。然后,您可以在编译目标时使用这些变量来指定路径。

  2. 使用 Makefile 内置变量:Makefile 提供了一些内置变量,如 CFLAGSLDFLAGS,可用于指定编译和链接选项。您可以将路径信息添加到这些变量中。例如:

    # 添加头文件和库路径到 CFLAGS 变量
    CFLAGS += -I/path/to/includes
    LDFLAGS += -L/path/to/libraries
    
    # 使用 CFLAGS 和 LDFLAGS 变量来编译目标
    target: source.c
        gcc $(CFLAGS) $(LDFLAGS) -o target source.c
    

    在上面的示例中,CFLAGS 变量用于指定编译选项,LDFLAGS 变量用于指定链接选项。您可以将头文件路径添加到 CFLAGS 变量中,将库路径添加到 LDFLAGS 变量中。

  3. 使用命令行参数:您还可以通过命令行参数的方式来指定头文件和库的路径。这样可以在运行 make 命令时灵活地传递路径信息。例如:

    # 使用命令行参数来编译目标
    target: source.c
        gcc -I$(INCLUDE_PATH) -L$(LIBRARY_PATH) -o target source.c
    

    在上面的示例中,您可以在命令行中通过 -D 参数指定头文件路径(INCLUDE_PATH)和库路径(LIBRARY_PATH),然后在 Makefile 中使用这些变量。

这些方法可以根据您的需要灵活地指定头文件和库的路径,以确保正确的编译和链接。请根据您的项目结构和需求选择适合的方法。

2.4 内置变量

Makefile 提供了许多内置变量,可用于控制编译和链接的过程。以下是一些常用的内置变量:

  1. CC:指定 C 编译器的名称,默认为 cc
  2. CFLAGS:指定 C 编译选项,如优化级别、警告级别等,没有初始值。
  3. CPPFLAGS:指定 C/C++ 预处理器选项,如宏定义、包含路径等,没有初始值。
  4. LDFLAGS:指定链接选项,如库路径、库名称等,没有初始值。
  5. LDLIBS:指定链接的库文件,如 -lm 表示链接数学库。
  6. AR:指定静态库打包工具的名称,默认为 ar
  7. ARFLAGS:指定静态库打包工具的选项,如创建库文件时的参数。
  8. RM:指定删除文件的命令,默认为 rm -f
  9. MAKE:指定调用子 Makefile 的命令,默认为 make
  10. MAKEFLAGS:指定 Make 的全局选项,如 -s 表示静默模式。
  11. SHELL:指定解释 Makefile 的 Shell 程序,默认为系统默认的 Shell。

除了上述变量,还有其他一些变量可用于更细粒度的控制编译和链接过程,例如:

  • CXX:指定 C++ 编译器的名称,默认为 g++
  • CXXFLAGS:指定 C++ 编译选项。
  • AS:指定汇编器的名称。
  • ASFLAGS:指定汇编选项。
  • CPP:指定 C/C++ 预处理器的名称,默认为 $(CC) -E
  • CPPFLAGS:指定 C/C++ 预处理器选项。
  • LDCONFIG:指定配置动态链接器的命令。

这些变量的名称可以在 Makefile 中重新定义,以满足特定的需求。在编写 Makefile 时,可以使用这些变量来控制编译器、选项和链接器的行为,以及执行其他与构建过程相关的任务。

2.5 自动变量

Makefile 中,存在一些特殊的自动变量,它们在规则的执行过程中会被自动替换为相应的值。以下是常用的自动变量:

  • $@:表示目标文件(Target),即规则中的目标。
  • $<:表示规则中的第一个依赖文件(Dependency)。
  • $^:表示规则中的所有依赖文件的列表,以空格分隔
  • $?:表示比目标文件新的所有依赖文件的列表,以空格分隔。
  • $*:表示目标文件去除后缀的部分,例如,foo.o$* 就是 foo
  • $(@D):表示目标文件所在的目录,不包括文件名。
  • $(@F):表示目标文件的文件名,不包括目录部分。

这些自动变量可以在规则的命令中使用,以方便地引用相应的文件名、目录等信息。通过使用自动变量,可以使 Makefile 的规则更加灵活和可扩展。

需要注意的是,自动变量在规则的命令中使用时,需要使用双引号将其括起来,以避免由于文件名中包含空格等特殊字符而导致的问题。例如:

$(TARGET): $(OBJS)
    $(CC) $(CFLAGS) -o "$@" "$^"

可以,使用双引号将 $@$^ 括起来,以确保即使文件名中包含空格等特殊字符,也能正确地传递给命令。

除了上述常用的自动变量,Makefile 还提供了其他一些自动变量,如 $(@D)$(<D) 等,用于获取文件名中的目录部分。

2.6 特殊字符

Makefile 中,有一些特殊字符具有特定的含义和作用。这些特殊字符在编写 Makefile 时需要特别注意和处理。以下是一些常见的特殊字符及其含义:

  1. #注释符号。在 Makefile 中以 # 开头的行被视为注释,其后的内容会被忽略。可以用于添加注释和解释代码的作用。

  2. $:变量引用符号。$ 后跟着变量名,表示引用该变量的值。例如,$(CC) 表示引用变量 CC 的值。

  3. :=:赋值操作符。用于进行变量的赋值操作。例如,CFLAGS := -Wall 表示将 -Wall 赋值给变量 CFLAGS

  4. =:简单赋值操作符。用于进行变量的赋值操作。与 := 不同,使用 = 进行赋值时,变量的值会在引用时进行延迟展开。例如,CFLAGS = -Wall

  5. ?=条件赋值操作符。用于进行条件赋值操作,只有当变量未定义时才进行赋值。例如,CFLAGS ?= -Wall 表示只有当变量 CFLAGS 未定义时,才将 -Wall 赋值给它。

  6. +=追加赋值操作符。用于将值追加到变量的末尾。例如,CFLAGS += -O2 表示将 -O2 追加到变量 CFLAGS 的末尾。

  7. @:静默模式符号。用于控制命令的输出是否显示在终端上。在命令前加上 @ 符号可以使该命令在执行时不显示其本身的命令行。例如,@echo "Hello, world!"

  8. *?%:通配符。用于进行模式匹配操作。在规则的依赖项和目标中,* 匹配任意字符序列,? 匹配单个字符,% 匹配任意字符序列。可以用于匹配文件名和路径等。

  9. ...:特殊目录符号。在路径中,. 表示当前目录,.. 表示父级目录。

  10. \:换行符号。用于将长行分隔成多行,使 Makefile 的行更易于阅读。

这些特殊字符在 Makefile 中具有特定的含义和作用,编写 Makefile 时需要遵守相应的语法规则,并根据需要正确处理和转义这些特殊字符。

2.7 其他特殊目标

  1. all: 目标:all 是一个常见的目标名称,用于定义构建项目的规则。通常,all: 目标会依赖于生成项目所需的所有目标文件,以及执行构建操作的规则。当运行 make 命令时,如果没有指定目标,make 会默认执行 all: 目标。因此,all: 目标的规则通常包含将源代码编译为可执行文件的步骤。

    以下是一个简单的示例:

    all: project
    
    project: main.o foo.o bar.o
        gcc -o project main.o foo.o bar.o
    
    main.o: main.c
        gcc -c main.c
    
    foo.o: foo.c
        gcc -c foo.c
    
    bar.o: bar.c
        gcc -c bar.c
    

    在上述示例中,all: 目标依赖于目标文件 main.ofoo.obar.o,并定义了将这些目标文件链接为 project 可执行文件的规则。

  2. clean: 目标:clean 是另一个常见的目标名称,用于定义清理项目的规则。clean: 目标的规则通常包含删除生成的目标文件、可执行文件和其他构建过程中生成的临时文件的操作。

    以下是一个简单的示例:

    clean:
        rm -f project main.o foo.o bar.o
    
  3. install: 目标:install 目标用于将构建的可执行文件、库文件或其他生成物安装到指定的位置。通常,install: 目标会将生成的文件复制到目标路径中,并执行必要的配置和权限设置。

    以下是一个简单的示例:

    install: project
        cp project /usr/local/bin
    

    在上述示例中,install: 目标依赖于 project 可执行文件,并定义了将该文件复制到 /usr/local/bin 目录的规则。

  4. uninstall: 目标:uninstall 目标用于从安装位置中卸载已安装的文件。它通常包含将已安装文件从目标路径中删除的操作。

    以下是一个简单的示例:

    uninstall:
        rm -f /usr/local/bin/project
    

    在上述示例中,uninstall: 目标定义了从 /usr/local/bin 目录中删除 project 可执行文件的规则。

2.8 Make 执行完毕的判定

Makefile 在执行过程中,会按照指定的目标规则和依赖关系进行构建操作。Make 工具会检查目标文件和其依赖项的时间戳,以确定是否需要重新构建目标文件。

Makefile 的执行完成有以下几种情况:

  1. 目标文件已经是最新的:如果所有目标文件都已经是最新的,即它们的时间戳不早于它们的所有依赖项,则 Make 工具会认为构建任务已完成,不需要执行任何操作。

  2. 执行完指定的目标规则:如果通过在命令行上指定目标规则来执行 Makefile,且该目标规则的构建命令执行成功,并且所有依赖项都已构建成功,则 Make 工具会认为 Makefile 执行完毕。

  3. 出现错误或中断:如果在执行 Makefile 的过程中出现错误,例如构建命令失败或依赖项无法找到等,Make 工具会立即停止执行,并输出错误信息。此时,Makefile 的执行被视为中断。

  4. 执行默认目标规则:如果在命令行上没有指定目标规则,并且 Makefile 中定义了一个名为 all 的目标规则作为默认目标规则,且该目标规则的构建命令执行成功,并且所有依赖项都已构建成功,则 Make 工具会认为 Makefile 执行完毕。

三、示例

学习make,首先要会使用gcc:【Linux C】GCC编译 && GDB调试 从入门到放弃 (gcc调试选项详解、gdb调试、条件断点、远程调试、脚本化调试)

3.1 项目结构

在这里插入图片描述

3.2 使用gcc

一步完成编译和链接:

gcc   -Wall -Wextra  -o gcc_out src/main.c src/foo.c src/bar.c -Iinclude

运行:
在这里插入图片描述

3.3 使用make

Makefile:

# 编译器和编译选项
CC = gcc
CFLAGS = -Wall -Wextra

# 源文件和目标文件
SRC_DIR = src
INC_DIR = include
OBJ_DIR = obj

# 获取源文件列表和对应的目标文件列表
SRCS = $(wildcard $(SRC_DIR)/*.c)
OBJS = $(patsubst $(SRC_DIR)/%.c,$(OBJ_DIR)/%.o,$(SRCS))

# 可执行文件
TARGET = make_learn

# 默认目标
all: $(TARGET)

# 生成目标文件
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	$(CC) $(CFLAGS) -I$(INC_DIR) -c $^ -o $@

# 生成可执行文件
$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

# 清理生成的文件
clean:
	rm -f $(OBJS) $(TARGET)
 

make和执行:
在这里插入图片描述

说明:$(CC) $(CFLAGS) -o $@ $^

$(CC) $(CFLAGS) -o $@ $^ 是一个链接选项,用于指示 gcc 编译器的行为。

  • $(CC):这是一个变量,表示所使用的编译器。在 Makefile 中,您可以通过定义 CC 变量来指定要使用的编译器。默认情况下,它是 gcc
  • $(CFLAGS):这也是一个变量,表示编译器的选项和标志。在 Makefile 中,您可以通过定义 CFLAGS 变量来指定要传递给编译器的选项。通常,CFLAGS 变量用于设置警告标志、优化级别等编译选项。
  • -o:这个选项用于指定输出文件的名称。
  • $@:这是一个自动变量,表示规则中的目标文件。在这个上下文中,它表示要生成的可执行文件的名称。
  • $^:这也是一个自动变量,表示规则中的所有依赖文件的列表,以空格分隔。在这个上下文中,它表示所有要链接的目标文件。

因此,$(CC) $(CFLAGS) -o $@ $^ 的含义是将所有的目标文件链接为一个可执行文件,并将可执行文件命名为规则中指定的名称。

例如,在以下规则中:

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

假设要链接目标文件 obj/foo.oobj/main.oobj/bar.o 生成可执行文件 project,那么链接命令将会是:

gcc -Wall -Wextra -o project obj/foo.o obj/main.o obj/bar.o

这个命令将目标文件链接为名为 project 的可执行文件。

注意,$@$^ 是 Makefile 中的自动变量,在每次执行规则时会根据具体的目标和依赖进行替换。这样可以使链接器命令根据当前的上下文自动生成。


四、常用make命令

  1. make:执行默认目标(通常是第一个目标)进行构建。
  2. make target:执行指定的目标进行构建。
  3. make -f filename:使用指定的Makefile文件进行构建,而不是默认的Makefile。
  4. make -nmake --just-print:仅显示执行的命令,而不实际运行它们(模拟运行)。
  5. make -C dir:在指定的目录中执行构建。
  6. make -j n:使用多个线程并行构建,其中n是并行任务的数量。
  7. make clean:清理生成的目标文件和中间文件,以便重新构建项目。
  8. make install:安装已构建的目标文件到系统中。
  9. make uninstall:从系统中卸载已安装的文件。
  10. make help:显示Makefile文件中定义的帮助信息。
  11. make -v:显示版本信息。

五、其他构建工具

常见的构建工具:

  1. Make:Make 是一个流行的构建工具,它可以自动化执行代码编译、构建和部署等过程。 Make 基于 Makefile 文件进行配置,通过 Makefile 文件中的规则和依赖关系,可以定义源代码文件、头文件和可执行文件等各个部分之间的关系 。
  2. CMake:CMake 是一个跨平台的构建工具,可以自动生成 Makefile 文件,从而简化构建过程。 CMake 支持多种平台和编译器,可以通过简单的命令行参数和配置文件进行配置 .
  3. Autotools:Autotools 是一个流行的自动化构建工具集,包括 Autoconf、Automake 和 Libtool 等组件 .
  4. SCons:SCons 是一个 Python 编写的构建工具,它可以自动化执行代码编译、构建和部署等过程 .
  5. Ninja:Ninja 是一个快速的构建工具,可以生成高效的构建图谱,从而提高代码编译、构建和部署的速度 .

说一下我用过的Ninja把:

OpenHarmony使用的就是Ninja+gn。

Ninja和gn是两个独立的工具,用于构建和管理项目,尽管它们通常一起使用,但它们并没有直接的关系。

  1. Ninja:Ninja是一个高效的构建系统,旨在快速构建软件项目。它以简洁的构建规则和快速的构建速度而闻名。Ninja使用基于文件的构建描述文件(通常是以.ninja扩展名结尾的文件),其中包含了构建规则、目标和依赖关系等信息。Ninja的设计目标是最小化构建系统本身的开销,并尽可能减少不必要的重复构建。

  2. gn:gn是Google开发的一个元构建系统,用于生成构建文件(如Ninja构建文件)。gn提供了一种声明式的语言和配置文件格式,用于描述项目的构建配置、目标和依赖关系等。gn支持高度可定制的构建过程,允许开发人员根据项目的需求配置构建规则和选项。gn的主要目标是提供一种统一的构建配置语言,可以用于多种构建系统(如Ninja)和多个平台。

虽然Ninja和gn可以单独使用,但它们通常一起使用。在这种情况下,gn被用来生成Ninja构建文件,描述项目的构建配置和规则,而Ninja则用于实际执行构建过程。通过将gn和Ninja结合使用,可以实现高效和可定制的项目构建流程。

在这里插入图片描述



~

猜你喜欢

转载自blog.csdn.net/weixin_43764974/article/details/131466035