【编程语言学习】GNU make (1)基础语法
本文最后更新于 47 天前,其中的信息可能已经有所发展或是发生改变。

参考文档

1.《GNU make中文手册》 整理翻译:徐海兵

2.《跟我一起写Makefile》 作者:陈皓
笔者注:此文仅涉及GNU make

1. 概述

  • make是一个在软件开发中所使用的工具程序,经由读取makefile的文件以自动化建构软件

1.1 相关知识

  • 链接:将多.o文件,或者.o文件和库文件链接成为可被操作系统执行的可执行程序(Linux环境下,可执行文件的格式为ELF格式);链接器不检查函数所在的源文件,只检查所有.o文件中的定义的符号。将.o文件中使用的函数和其它.o或者库文件中的相关符号进行合并,对所有文件中的符号进行重新安排(重定位),并链接系统相关文件(程序启动文件等)最终生成可执行程序
  • 静态库:又称为文档文件。它是多个.o文件的集合。Linux中静态库文件的后缀为.a。静态库中的各个成员(.o文件)没有特殊的存在格式,仅仅是一个.o文件的集合
  • 动态库:也是多个.o文件的集合,但是这些.o文件时有编译器按照一种特殊的方式生成。模块中各个成员的地址(变量引用和函数调用)都是相对地址。使用此共享库的程序在运行时,共享库被动态加载到内存并和主程序在内存中进行连接。多个可执行程序可共享库文件的代码段(多个程序可以共享的使用库中的某一个模块,共享代码,不共享数据)

2. Makefile规则

  • 当使用make工具进行编译时,工程中以下几种文件在执行make时将会被编译或重新编译:
  • 所有的源文件没有被编译过,则对各个C源文件进行编译并进行链接,生成最后的可执行程序
  • 每一个在上次执行make之后修改过的C源代码文件在本次执行make时将会被重新编译
  • .h头文件在上一次执行make之后被修改,则所有包含此.h头文件的C源文件在本次执行make时将会被重新编译

2.1 执行

  • Makefile文件的执行需要安装先make工具;
  • 执行Makefile文件时在终端输入对应命令
  #自动执行名称为makefile.mk或者Makefile.mk的文件
  make

  #执行指定的mk文件
  make -f XXX.mk

2.2 概念介绍

  • makefile文件执行时会从从上向下的第一个规则开始
  # 一个简单的makefile描述规则的组成结构
  target:prerequisites
      command
      ...
      ...
  • target:规则的目标
  • 通常是最后需要生成的文件名或者为了实现这个目的而必需的中间过程文件名。可以是.o文件、也可以是最后的可执行程序的文件名等。另外,目标也可以是一个make执行的动作的名称,如目标clean
  • prerequisites:规则的依赖
  • 生成规则目标所需要的文件名列表。通常一个目标依赖于一个或者多个文件
  • command:规则的命令行
  • 是规则所要执行的动作(任意的shell命令或者是可在shell下执行的程序)。它限定了make执行这条规则时所需要的动作
  • 一个规则可以有多个命令行,每一条命令占一行
  • 注意:命令行必须以[Tab]字符开始而不是四个空格[Tab]字符告诉make此行是一个命令行。make按照命令完成相应的动作。这也是书写Makefile中容易产生,而且比较隐蔽的错误

2.3 调试

  • makefile文件中有几种添加打印或者断点的方式
  • error函数,产生一个致命的错误并中断执行 $(error error test) #error test
  • warining函数,产生一个告警的信息不影响执行 $(warning warning test) #warning test
  • info函数,产生一个调试的信息不影响执行 $(info info test) #info test
  • echo函数,调用shell中的echo函数打印调式信息 @echo "echo test" #echo test

3. 特殊操作符

3.1 ?=符号

  • 只有在此变量之前没有赋值的情况下才会对这个变量进行赋值
   ?= bar 
  # 其等价于:
  ifeq ($(origin FOO), undefined) 
  FOO = bar 
  endif

3.2 -符号

  • Makefile中,“-” 符号有两个主要的用途:
  • 在命令行中的开头,用于忽略命令执行的返回码。这样,即使命令执行失败,Makefile也会继续执行后续的命令 target: -rm foo.txt # 删除操作,即使出现错误也忽略 cp bar.txt foo.txt
  • 在变量的使用中,用于展开一个变量,即使变量未定义也不会触发错误。这样可以提供某些默认值或允许选择性地使用变量 CC = gcc CFLAGS = -Wall -Werror ifdef DEBUG CFLAGS += -g endif target: $(CC) $(CFLAGS) -o target main.c # 如果未定义DEBUG变量,则不会将-g选项包含到CFLAGS变量中。这样,可以根据需要选择是否启用调试选项

4. 变量

  • makefile中变量是一个名字,代表一个文本字符串,在Makefile的目标、依赖、命令中引用变量的地方,变量会被它的值所取代
  • 变量的定义有两种方式,这两种风格的区别在于定义方式和展开时机

4.1 变量的常用属性

4.1.1 变量的引用

  • 变量的作用域
  • Makefile中定义一个变量,那么这个变量对此Makefile的所有规则都是有效的;它就像是一个“全局的”变量(仅限于定义它的那个Makefile中的所有规则)如果需要对其它的Makefile中的规则有效,就需要使用export对它进行声明
  • 变量的引用方式
  • 通过$(VARIABLE_NAME)或者${VARIABLE_NAME}来引用一个变量的定义
  • 变量展开方式
  • 变量引用的展开过程是严格的文本替换的过程

4.1.2 变量的使用

  • 变量名之中可以包含函数或者其它变量的引用,make在读入此行时根据已定义情况进行替换展开而产生实际的变量名
  • 变量的定义值在长度上没有限制。变量定义较长时,可以将比较长的行分多个行来书写,除最后一行外行与行之间使用反斜杠 \连接,表示一个完整的行
  • 当引用一个没有定义的变量时,make 默认它的值为空

4.1.3 追加变量值

  • 一个通用变量在定义之后的其他一个地方,可以对其值进行追加
  • Makefile 中使用+=(追加方式)来实现对一个变量值的追加操作
  # “another.o”添加到变量“objects”原有值的末尾,使用空格和原有值分开
  objects = main.o foo.o bar.o utils.o 
  objects += another.o

  # 大部分情况下等价于
  objects = main.o foo.o bar.o utils.o 
  objects := $(objects) another.o
  • 如果被追加值的变量之前没有定义,那么+=会自动变成=,此变量就被定义为一个递归展开式的变量
  • 直接展开式变量的追加过程
    • 变量使用:=定义,之后+=操作将会首先替换展开之前此变量的值,然后在末尾添加需要追加的值,并使用:=重新给此变量赋值;递归展开式使用=赋值同理

4.2 递归展开式变量

  • 定义方式:递归展开式变量是通过=或使用指示符define定义的
  • 使用define定义的变量和使用=定义的变量一样,属于“递归展开”式的变量,两者只是在语法上不同
  • 展开时机:使用=赋值的变量在在被引用的地方展开,包括此变量定义中对其他变量的引用;如果是在其他变量定义时被引用则不会展开
  • 在引用的地方变量是严格的文本替换的过程,此变量值的字符串原模原样的出现在引用它的地方
  ugh = Huh
  bar = $(ugh)
  foo = $(bar) 

  all:
      @echo $(foo)                #Huh

  # 1.$(foo)被替换为$(bar)
  # 2.$(bar)被替换为$(ugh)
  # 3.$(ugh)被替换为Hug
  # 整个替换的过程是在执行echo $(foo)时完成的

4.2.1 优点

  • 可以在变量定义时,引用其他的之前没有定义的变量(可能在后续部分定义,或者是通过make的命令行选项传递的变量)
  CFLAGS = $(include_dirs) -O 
  include_dirs = -Ifoo -Ibar

  # “CFLAGS”会在命令中被展开为“-Ifoo -Ibar -O”
  # 而在“CFLAGS”的定义中使用了其后才定义的变量“include_dirs”

4.2.2 缺点

  • 第一个缺点:此风格的变量定义,可能会由于出现变量递归定义而导致make陷入到无限的变量展开过程中,最终使make执行失败
  CFLAGS = $(CFLAGS) –O

  # 它将会导致 make 对变量“CFLAGS”的无限展过程中去(这种定义就是变量的递归定义)
  # 因为一旦后续同样存在对“CLFAGS”定义的追加,展开过程将是套嵌的、不能终止的(在发生这种情况时,make 会提# 示错误信息并结束)
  • 第二个缺点:这种风格的变量定义中如果使用了函数,那么包含在变量值中的函数总会在变量被引用的地方执行(变量被展开时)

4.3 直接展开式变量

  • 直接展开式变量可以避免递归式展开变量的问题,变量值中对其他变量或者函数的引用在定义变量时展开,所以变量被定义后就是一个实际需要的文本串,其中不再包含任何变量的引用
  x := foo 
  y := $(x) bar 
  x := later 

  # 就等价于:
  y := foo bar 
  x := later
  • 与递归式展开变量不同的是,在变量在定义时就完成了对所引用的变量和函数的展开,所以不能实现对其后定义变量的引用

4.4 变量的替换引用

  • 对于一个已经定义的变量,可以使用“替换引用”将其值中的后缀字符(串)使用指定的字符(字符串)替换
  • 格式为$(VAR:A=B)或者${VAR:A=B}意思是替换变量VAR中所有A字符结尾的字为B“结尾”的字,“结尾”的含义是空格之前(变量值多个字之间使用空格分开)
  foo := a.o b.o c.o 
  bar := $(foo:.o=.c)

  # foo = a.o b.o c.o
  # bar = a.c b.c c.c

4.5 变量的嵌套引用

  • 不建议任何情况下使用
  • 一个变量名(文本串)之中可以包含对其它变量的引用。这种情况我们称之为“变量的套嵌引用”或者“计算的变量名”
  x = y 
  y = z 
  a := $($(x))    # a = z

  # 1.变量引用“$(x)”被替换为变量名“y”(就是“$($(x))”被替换为了“$(y)”)
  # 2.“$(y)”被替换为“z”(就是 a := z)
  • 这个例子中(a:=$($(x)))所引用的变量名不是明确声明的,而是由$(x)扩展得到。这里$(x)相对于外层的引用就是套嵌的变量引用
  • 递归变量的套嵌引用过程,也可以包含变量的修改引用和函数调用
  x = variable1
  variable2 := Hello
  y = $(subst 1, 2, $(x))
  z = y
  a := $($($(z)))                   # a := Hello

  # 1.“$($($(z)))”首先被替换为“$($(y))”
  # 2.之后再次被替换为 $($(subst 1,2,$(x))) ”
  # 3.“$(x)”的值是“ variable1 ”,所以有“ $($(subst 1,2,$(variable1)))”)
  # 4.函数处理之后为“$(variable2)”。之后对它在进行替换展开。最终,变量“a”的值就是“Hello”

5. Makefile规则

Makefile中“规则”就是描述在什么情况下、如何重建规则的目标文件,通常规则中包括了目标的依赖关系和重建目标的命令

5.1 规则的语法

  • 一个简单的 Makefile 描述规则组成 # 风格一 TARGET... : PREREQUISITES... COMMAND ... # 风格二 TARGETS : PREREQUISITES ; COMMAND COMMAND ...
    • TARGETS:规则的目标;通常是最后需要生成的文件名或者为了实现这个目的而必需的中间过程文件名。可以是空格分开的多个文件名,也可以是一个标签(例如:执行清空的clean)。TARGETS的文件名可以使用通配符
    • PREREQUISITES:规则的依赖;生成规则目标所需要的文件名列表,通常一个目标依赖于一个或者多个文件。规则的依赖并不是必须的,例如可以创建一个clean规则专门用于清理编译产物,就不需要依赖而只需要执行相应的命令
    • COMMAND:规则的命令行;是规则所要执行的动作可以是任意的shell命令或者是可在shell下执行的程序。它限定了make执行这条规则时所需要的动作。
  • 一个规则可以有多个命令行,每一条命令占一行。注意:每一个命令行必须以[Tab]字符开始,[Tab]字符告诉 make 此行是一个命令行
  • 规则的中心思想是:目标文件的内容是由依赖文件文件决定,依赖文件的任何一处改动,将导致目前已经存在的目标文件的内容过期。规则的命令为重建目标提供了方法。这些命令运行在系统 shell 之上。

5.2 依赖的类型

​ 一个规则的依赖表明了两件事:首先,它决定了重建此规则目标所要执行规则的顺序;表明在更新这个规则的目标之前需要按照什么样的顺序、执行那些规则来重建这些依赖文件。其次,它确定了一个依存关系;例如,这样的规则:A:B C,那么在重建目标 A 之前,首先需要完成对它的依赖文件 BC 的重建。重建 BC 的过程就是执行Makefile中以文件 BC 为目标的规则。

​ 规则中如果依赖文件的任何一个比目标文件新,则认为规则的目标已经过期而需要重建目标文件。通常,如果规则中依赖文件中的任何一个被更新,则规则的目标相应地也应该被更新。有时,需要定义一个这样的规则,在更新目标(目标文件已经存在)时只需要根据依赖文件中的部分来决定目标是否需要被重建,而不是在依赖文件的任何一个被修改后都重建目标。为了实现这一目的,相应的就需要对规则的依赖进行分类

5.2.1 常规依赖类型

  • 依赖文件被更新后,需要更新规则的目标,这类依赖被称为常规依赖
  • 常规依赖类型即前文中所使用和提到的类型,是Makefile中最常见和最常用的依赖类型

5.2.2 order-only依赖类型

  • 依赖文件被更新后,可不需要更新规则的目标。我们把第这种依赖为:order-only依赖
  • 书写规则时,order-only依赖使用管道符号|开始,作为目标的一个依赖文件。规则依赖列表中管道符号|左边的是常规依赖,管道符号右边的就是order-only依赖。这样的规则书写格式如下: # |左侧为常规依赖,|右侧为order-only依赖 TARGETS : NORMAL-PREREQUISITES | ORDER-ONLY-PREREQUISITES # order-only依赖的使用举例: # make在执行这个规则时,如果目标文件“foo”已经存在。当“foo.c”被修改以后,目标“foo”将会被重建。 # 但是当“libtest.a”被修改以后。将不执行规则的命令来重建目标“foo”。 # 就是说,规则中依赖文件$(LIBS)只有在目标文件不存在的情况下,才会参与规则的执行 # 当目标文件存在时此依赖不会参与规则的执行过程。 LIBS = libtest.a foo : foo.c | $(LIBS) $(CC) $(CFLAGS) $< -o $@ $(LIBS)
    • 注意:规则依赖文件列表中如果一个文件同时出现在常规列表和order-only列表中,那么此文件被作为常规依赖处理

6. Makefile目标

当没有使用make命令行指定具体目标时,make默认的更新的哪一个目标被称为终极目标除了makefile的“终极目标”所在的规则以外,其它规则的顺序在makefile文件中没有意义。终极目标是执行 make 的唯一目的,其所在的规则作为第一个被执行的规则。而其它的规则是在完成重建“终极目标”的过程中被连带出来的。所以这些目标所在规则在Makefile中的顺序无关紧要。

6.1 伪目标

当使用.PHONY修饰一个目标时,我们称这个目标为伪目标;例如.PHONY clean,我们称这样的目标是伪目标。被声明修饰为伪目标的目标无论在当前目录下是否存在clean这个文件。我们输入make clean之后。对应的命令都会被执行。而且,当一个目标被声明为伪目标后,make在执行此规则时不会去试图去查找隐含规则来创建它。伪目标的目的并不是创建文件,只是执行其所对应的执行动作

6.1.1 为什么要使用伪目标

  • 伪目标并不是文件名,不会出现文件名和目标冲突的情况
  • make对于伪目标不会自动生成依赖关系和推导规则,可以提高允许效率

6.1.2 伪目标的声明

  • 将一个目标声明为伪目标的方法是将它作为特殊目标.PHONY的依赖
  .PHONY: clean 
  clean: 
  rm *.o temp
上一篇
下一篇