blog:programming:makefile

Makefile

Basic rules

每条规则由 "目标,依赖,命令" 组成
TARGET : PREREQUISITES
    COMMAND
    ...
    ...

make 根据规则的依赖关系,决定是否执行规则所定义的命令

Makefile中的内容: 1. 显式规则

  描述了如果更新目标文件,需要明确的给出目标文件,依赖文件,命令.
 (有些规则没有命令,这样的规则只是描述了文件的依赖关系)
 

2. 隐含规则

  由make自动推导出来的规则, make根据目标文件名,自动得到目标的依赖文件,
  并使用默认的命令来更新目标文件.
  

3. 变量定义 4. 提示符

  include指示符:  include FILENAMES
      告诉make停止读取当前的Makefile, 
      先读取include指定的文件,可以使用通配符来指定FILENAMES
      
  如果include指定的文件不存在,make将会提示错误信息,
  在include指示符前面加上"-",可以忽略错误提示.
  -include FILENAMES
  

5. 注释

  “#”后面的内容被认为是注释, 如果第行首的第一个字符是"#", 那么此行是注释行.

终极目标:

  Makefile中的第一条规则定义的目标是Makefile的"终极目标", make命令默认执行"终极目标".
  如果第一条规则中有多个目标,那么多个目标中的第一个目标被认为是make的终极目标
  有两种情况例外:
      1. 目标名以点号"."开始,并且其后不存在斜线"/"
      2. 模式规则的目标 

例 Makefile for Project bar

foo.o : foo.c defs.h
    cc -c -g foo.c

其中foo.o是目标文件, foo.c defs.h是依赖文件, cc -c -g foo.c是规则的命令 如何确定目标文件是否过期(需要重建目标文件)

目标文件不存在或者目标文件的时间戳比依赖文件中的任何一个"老"

Makefile中的$有特殊含义(表示变量或者函数定义)

通配符只能用在 “目标,依赖, 命令” 中, 其他地方不能直接使用通配符, 可以通过函数来实现(例如wildcard)

Searching Directories for Prerequisites

在一个较大的工程中,一般会将源代码和二进制文件放在不同的目录。 可以使用make的目录搜索功能自动搜索依赖文件。

例: project的目录结构如下:

   .
   ├── foo
   ├── inc
   │   ├── comm.h
   │   └── foo.h
   ├── Makefile
   ├── obj
   │   ├── event.o
   │   ├── foo.o
   │   ├── message.o
   │   └── utils.o
   └── src
         ├── event.c
       ├── foo.c
       ├── message.c
       └── utils.c

Makefile:

TARGET=foo

SRC_DIR=./src
INC_DIR=./inc
OBJ_DIR=./obj

SRCS:=$(foreach dir, $(SRC_DIR), $(notdir $(wildcard $(dir)/*.c)))
OBJS=$(SRCS:%.c=$(OBJ_DIR)/%.o)

INC_FLAGS := -I$(INC_DIR)

.PHONY:all clean

VPATH=$(SRC_DIR)
#vpath %.c $(SRC_DIR)

$(OBJ_DIR)/%.o : %.c
	@if [ ! -d $(OBJ_DIR) ]; then mkdir -p $(OBJ_DIR); fi
	$(CC) -c -o $@ $^

all : $(OBJS)
	@echo "VPATH=$(VPATH)"
	@echo ".LIBPATTERNS=$(.LIBPATTERNS)"
	$(CC) -o $(TARGET) $(OBJS)

clean:
	@rm -f $(OBJS)
	@rm -rf $(OBJ_DIR)

VPATH是一个make变量,可以指定搜索目录。 make use VPATH as a search list for both prerequisities and targets of rules

定义VPATH变量时, 使用空格或者冒号将多个搜索目录分开.例如: VPATH = src:../headers 如果目标或者依赖中的文件在当前目录不存在,那么make将根据VPATH中定义的顺序进行搜索。

上面例子中中定义的VPATH=./src obj/foo.o : foo.c 被解释为 obj/foo.o : ./src/foo.c

vpath和VPATH类似,但更灵活。 和VPATH不同的是,vpath不是一个变量,而是make的一个关键字, 可以通过模式匹配,为不同类型的文件指定不同的搜索目录。

vpath有3种使用形式: 1. vpath pattern directories

  为所有符合模式pattern的文件指定搜索目录,多个目录使用空格或冒号隔开

2. vpath pattern

  清除为pattern指定的搜索目录 

3. vpath

  清除之前用vpath设置的所有搜索目录

vpath的pattern是一个包含%的字符串. 例如 %.h匹配所有以.h结尾的文件 例如上面例子中的 vpath %.c $(SRC_DIR) 表示在SRC_DIR指定的目录中搜索.c文件 和VPATH一样, vpath也是在当前目录下不能找到匹配文件时,才按照vpath指定的directories顺序搜索。

在Makefile中如果存在多个连续的vpath语句使用了相同的“pattern”, make就对这些vpath一个一个处理。 搜索目录的顺序由vpath语句在Makefile中出现的先后顺序决定。

例如:

vpath %.c foo:bar
vpath %   blish

搜索.c文件的顺序为 “foo”, “bar”, “blish”

静态模式规则: 规则存在多个目标,并且不同的目标可以根据目标的文件名来自动构造出依赖文件. 静态模式的语法:

targets ...: target-pattern : prereq-patterns ...
    recipe
    ...

target-pattern和prereq-patterns说明了如何为没一个目标文件生成依赖文件。 从目标名字中取出一部分字符串(称为stem), 使用stem代替依赖模式中相应的部分来产生对应目标的依赖文件。

上面例子中的Makefile还使用了静态模式: $(OBJ_DIR)/%.o : %.c 这条规则表示所有.o文件的依赖文件为对应的.c文件, 例如foo.o 对应的依赖文件为foo.c, stem为foo 在Commands中可以使用自动化变量$*来表示stem

Phony Targets

伪目标并不表示真正的文件名,在执行make时,可以指定这个目标,来执行其所在规则定义的命令。

使用伪目标的原因: 1. 避免目标名和工作目录下的实际文件名相同,出现冲突 2. 提高make时的效率

例如:

clean:
    rm *.o temp

如果当前目录下有一个文件的名字为clean, 由于这个规则没有任何依赖, 所以目标被认为是最新的,命令就得不到执行。 将clean声明为伪目标可以解决这个问题。

例如: .PHONY : clean

当一个目标被声明为伪目标后,make将不会查找隐含规则来创建它,这样也可以提高make的执行效率

伪目标的另外一种使用场合是用于make的并行和递归执行

例如:

SUBDIRS := foo bar baz

subdirs :
	for dir in $(SUBDIRS); do \
		$(MAKE) -C $$dir; \
	done

在一个规则中使用for循环对多个目录进行make

这种方式存在的问题: 1. 当某一个子目录make失败时,会继续对其他目录进行make. 在最后很难定位是在哪个地方发生的错误 2. 这种shell循环的方式没有用到make对目录的并行处理功能, 因为只有一条规则.

使用伪目标可以解决上面两个问题

SUBDIRS := foo bar baz

.PHONY:subdirs $(SUBDIRS)

subdirs : $(SUBDIRS)

$(SUBDIRS):
	$(MAKE) -C $@;	

foo:baz

上面有一个没有命令行的规则 foo:baz, 表示foo依赖于baz, 用于限制子目录的make顺序,必须先make baz目录,然后才能make foo目录

一般情况下,一个伪目标不能作为另外一个目标的依赖, 因为当一个目标文件的依赖包含伪目标时,每一次执行这个规则时,伪目标所定义的规则都会被执行。

Generate Prerequisites Automatically

gcc 的 -M选项可以通过查找源文件中的#include 自动生成依赖关系

如果main.c中只包含了头文件 “defs.h” 那么执行gcc -M main.c, 其输出如下: main.o : main.c defs.h

-M的输出结果包括标准库的头文件, 使用-MM选项可以去掉标准库的头文件

推荐为每一个源文件生成一个描述其依赖关系的的makefile文件, 对于一个源文件“NAME.c” 对应的这个makefile文件为 “NAME.d” 采用这种方式,只有源文件再修改后,才会重新生成对应的依赖文件。

对“Makefile for Project bar”进行改造一下,自动生产依赖文件

TARGET=bar

SRC_DIR=./src
INC_DIR=./inc
OBJ_DIR=./obj
DEPEND_DIR=.dpend

SRCS:=$(foreach dir, $(SRC_DIR), $(notdir $(wildcard $(dir)/*.c)))
OBJS=$(SRCS:%.c=$(OBJ_DIR)/%.o)
DEPEND_FILES=$(SRCS:%.c=$(DEPEND_DIR)/%.d)

INC_FLAGS := -I$(INC_DIR)

CFLAGS := -Wall -g
CFLAGS += $(INC_FLAGS)

.PHONY:all clean

#VPATH=$(SRC_DIR)
vpath %.c $(SRC_DIR)

$(OBJ_DIR)/%.o : %.c
	@echo -----------------
	@if [ ! -d $(OBJ_DIR) ]; then mkdir -p $(OBJ_DIR); fi
	$(CC) $(CFLAGS) -c -o $@ $<

all : $(OBJS)
	@echo "VPATH=$(VPATH)"
	@echo ".LIBPATTERNS=$(.LIBPATTERNS)"
	$(CC) -o $(TARGET) $(OBJS)

clean:
	@rm -f $(TARGET)
	@rm -f $(OBJS)
	@rm -rf $(OBJ_DIR)
	@rm -f *.o
	@rm -rf $(DEPEND_DIR)

sinclude $(DEPEND_FILES)

$(DEPEND_DIR)/%.d : %.c
	@if [ ! -d $(DEPEND_DIR) ]; then mkdir -p $(DEPEND_DIR); fi
	@$(CC) $(CFLAGS) -MM $< > $@.$$$$; \
	sed 's,\($*\)\.o[ :]*,$(OBJ_DIR)/\1.o $@ :,g' < $@.$$$$ > $@; \
	rm -f $@.$$$$

说明: 1. sinclude-include 的另一种写法, 如果include要包含的某些文件不存在,或者无法被创建,make会继续执行。

  只有真正由于不能完成终极目标的重建时才会提示错误,并退出。  

2. make在试图读取include包含的*.d文件时,发现这些文件不存在,会试图创建他们。 3. 定义依赖文件的生成规则又用到了静态模式规则

  说明如何生成依赖文件
  例如gcc -MM src/bar.c 生成的依赖为"bar.o: src/bar.c inc/foo.h"
  使用sed命令替换为"./obj/bar.o .dpend/bar.d : src/bar/c inc/foo.h"
  因为将bar.d也加入到bar.o相同的依赖里面,所以只要bar.o的任何依赖文件发生变化后,都会导致bar.d被重建

4. sed命令中的$$$$表示当前进程号,用于生成临时文件。

  注意: sed命令和下一行rm命令一定要用续行符连起来,如何分开为两行,那么rm命令和sed将会是两个不同的进程,
  `导致$@.$$$$指的不是同一个文件`

5. sed命令替换是还到了$*这个自动化变量(stem),在静态模式规则中表示被匹配到的字符串, 例如“bar”

多规则目标:

  一个文件可以作为多个规则的目标(但是重建此目标的命令只能出现在其中一个规则中)
  以这个目标为规则的所有依赖文件将会合并成此目标的依赖文件列表
  

上面的例子中就用到了多规则目标,因为gcc -MM的 src/bar/c 生成的依赖 bar.o : src/bar.c inc/foo.h 被替换为 ./obj/bar.o .dpend/bar.d : src/bar.c inc/foo.h 由于之前的静态模式规则定义了./obj/bar.o的依赖为 src/bar/c 所以依赖合并后, obj/bar.o的依赖为 src/bar.c src/bar/c inc/foo.h

Writing Recipes in Rules

规则的命令由一些shell命令行组成。

make在执行命令行之前会把要执行的命令行输出到stdout, 这种行为称为回显 如果规则的命令行以@开头, 则make在执行前不会回显这个命令

如果使用make -n 或者 “–just-print” 或者 “–dry-run”, 那么make执行时只显示要执行的命令,而不会执行这些命令。 其中包括@开头的命令也会被显示出来, 这个选项对调试Makefile非常有用.

命令的执行: 1. 如果是多行命令,那么每个命令将在一个独立的子shell进程中被执行.

2. 如果同一行里面写了多条命令,那么这几条命令在同一个shell进程中被执行.

命令执行的错误处理

规则的命令在运行结束后, make会检测命令执行的返回状态. 如果返回成功,就启动另外一个子shell来执行下一条命令 如果某一个命令出错,make会停止执行后续的命令

在命令前加一个减号-, 来告诉make忽略此命令的执行失败.

make的递归过程指的是: 在Makefile中使用make作为一个命令来执行本身或者其他Makefile文件的过程

递归调用在存在有多级子目录的项目中非常有用.

例如:

subsystem:
    cd subdir && $(MAKE)
    
等价于
subsystem:
    $(MAKE) -C subdir

变量CURDIR表示make的工作目录, 在使用-C 进入子目录后,CURDIR会被重新赋值

在make的递归调用时,最好使用$(MAKE)来代替make, 这样做的好处是: “当使用一个其他版本的make程序时, 可以保证最上层使用的make程序和子目录执行的make程序一致”

执行make时,MAKE变量为make make的命令行选项参数被通过一个变量“MAKEFLAGS”传递给子目录下的make程序。

有几个特殊的命令行参数除外, "-C" "-f" "-o" "-W" 不会被赋值给MAKEFLAGS

Variables in Makefile

变量的引用方式为 $(VARIABLE_NAME)

当引用一个没有定义的变量时,make默认它的值为空。 如果要引入一个shell中的变量,需要使用$${VAR}.

示例如下:

$(SUBDIRS):
	@echo "MAKELEVEL=$(MAKELEVEL)"
	@for var in 1 2 3 4; do\
		echo "var = $${var}"; \
	done
	$(MAKE) -C $@;

自动化变量

auto var descriptions
—- —-
$@ 规则的目标文件名, 在多目标模式规则中,表示触发规则执行的那个目标的文件名.
$< 规则的第一个依赖文件名
$? 所有比目标文件新的依赖文件列表,空格分隔.
$^ 规则的所有依赖文件列表, $^会去掉重复的依赖文件列表
$* 在模式规则和静态模式规则中,表示“茎”(stem), 即目标模式%所代表的部分
$(@D) 表示目标中的目录部分,如果$@dir/foo.o, 那么$(@D)表示dir
$(@F) 表示目标中的文件部分,如果$@dir/foo.o, 那么$(@F)表示foo.o

变量的定义方式

  1. 递归展开式变量 通过= 或者 define 定义的变量。在引用的时候是严格的文本替换过程 例如: foo=$(bar) bar=$(ugh) ugh=UGH all : ; @echo "foo=$(foo)" 执行的时候将会打印foo=UGH 替换过程是这样的: $(foo) 被替换为$(bar), $(bar)被替换为$(ugh), 最后$(ugh)被替换为UGH
    变量名两边的空格和=之后的空格在make处理的被忽略.
    这种定义方式的缺点: 可能由于变量的递归定义而导致make陷入到无限的变量展开过程,不过make会阻止这种情况发生,打印出错误信息并退出 如果在这种定义方式中使用了函数,那么其中的函数会在变量被引用是被执行, 降低make的执行效率
    2. 直接展开式变量 使用:=定义 变量值中对其他变量或者函数的引用在定义变量时被展开
    这种形式的定义,不能实现对其后定义的变量的引用 例如: CFLAGS := $(inc_dirs) -O inc_dirs := -Ifoo -Ibar 由于变量inc_dirs的定义在CFLAGS之后,所以CFLAGS的值是-O
    3. 条件赋值 使用?=给变量赋值 只有此变量之前没有被赋值的情况下才会对这个变量赋值 例如: foo ?= bar 如果foo之前已经被定义过了,那么foo的值就不会被改变为bar

变量的替换引用

将变量的后缀字符(串)使用指定的字符(串)替换 格式: $(VAR:A=B)

例如:

foo := a.o b.o c.o
bar := $(foo:.o=.c)

也可以使用模式字符 %, 例如:

foo := a.o b.o c.o
bar := $(foo:%.o=%.c)

追加变量值

例如:

objects := main.o foo.o bar.o
objects += another.o

如果被追加值的变量之前没有被定义,那么 += 会自动变成 =, 此变量就被定义为一个递归展开式变量.

如果之前存在该变量的定义,那么保持之前的变量风格不变。

Functions

make的函数提供了处理文件名,变量,文本和命令的方法。

函数的调用语法:

$(function arguments)

  1. function是需要调用的函数名,必须是make的内嵌函数。 如果是用户自己定义的函数,需要使用make的call函数来间接调用。
  2. 函数名和参数之间使用空格分隔; 如果有多个参数,参数之间使用逗号 , 分开。

$(subst FROM,TO,TEXT) 字符串替换函数, 把字符串TEXT中的FROM替换为TO

返回值: 被替换后的新字符串

$(patsubst PATTERN, REPLACEMENT, TEXT) 模式替换函数,搜索TEXT中以空格分开的单词, 将符合模式“PATTERN”的字符串替换为“REPLACEMENT”

  1. PATTERN中可以使用模式通配符%来代表一个单词中的若干个字符。
  2. 如果“REPLACEMENT”中也包含一个%, 那么“REPLACEMENT”中的%将是“PATTERN”中的那个%所代表的字符串。
  3. 在“PATTERN”和“REPLACEMENT”中只有地一个%被作为模式字符处理,之后出现%表示普通字符。

返回值: 被替换后的新字符串

示例:

$(patsubst %.c,%o,x.c.c bar.c)

把字符串“x.c.c bar.c”中以.c结尾的字符替换为.o结尾的字符, 结果为 “x.c.o bar.o”

变量的替换引用$(VAR:PATTERN=REPLACEMENT) 是一个简化版的patsubst在变量引用过程的实现。

$(strip STRING) 去空字符函数

去掉字符串开头和结尾的空字符,并将其中多个连续空字符合并为一个空字符。 空字符包括空格,Tab等不可显示的字符

示例:

str=        a   b c
str1=$(strip $(str))

str1为“a b c”

$(findstring FIND,IN) 在字符串IN中查找字串FIND 返回值: 如果IN中存在FIND, 则返回FIND, 否则返回空。

示例:

$(findstring a,a b c)   结果为"a"
$(findstring a,b c)     结果为空

$(filter PATTERN..., TEXT) 过滤掉字符串TEXT中所有不符合PATTERN的单词,保留所有符合此模式的单词。

可以使用多个模式,模式中需要包含模式字符%, 存在多个模式时,表达式之间使用空格分隔。

返回值: TEXT中所有符合PATTERN的的字串,不同模式的结果使用空格分隔。

示例:

sources:=foo.c bar.c baz.S ugh.h
foo:$(sources)
    $(CC) $(filter %.c %.S,$(sources)) -o foo

$(filter-out PATTERN...,TEXT)

和filter功能相反

$(sort LIST) 对字符串LIST中的单词排序(按首字母升序排列), 并去掉重复的单词

示例:

$(sort foo bar lose foo)
结果为 "bar foo lose"

$(dir NAMES...) 从文件名序列中取出文件名的目录部分(最后一个/之前的部分)

示例:

$(dir src/foo.c hacks)
结果为"src/ ./"

$(notdir NAMES...) 取文件名, 从文件名序列中取出非目录部分

示例:

$(dir src/foo.c hacks)
结果为"foo.c hacks"

$(suffix NAMES...) 取后缀, 从文件名序列中取出个文件名的后缀(最后一个点.开始的部分, 包含点号) 如果文件名中不包含点号,则为空

示例:

$(suffix src/foo.c src-1.0/bar.c hacks)
结果为".c .c"

$(basename NAMES...) 取前缀, 从文件名序列中取出文件名的前缀(最后一个点之前的部分) 如果文件名中没有., 那么文件名保持不变 如果文件名中存在多个点号., 那么取最后一个点号之前的部分

示例: $(basename src/foo.c src-1.0/bar.c /home/jack/.font.cache-1 hacks)

结果为“src/foo src-1.0/bar /home/jack/.font hacks”

$(wildcard PATTERN) 获取匹配模式的文件名 PATTERN使用shell可识别的通配符,包括?(单字符) *(多字符)

示例:

$(wildcard *.c)
返回当前目录下所有.c文件的列表

在Makefile中添加调试信息 可以在Makefile中的任意位置使用,而echo命令只能在target中使用

$(info TEXT)

打印TEXT信息,也可以打印变量

示例:

$(info A top-level warning)
$(info CURDIR=$(CURDIR))

$(warning TEXT)

打印TEXT信息,也可以打印变量 和info的区别是, warning可以打印在makefile中的行号

示例:

$(warning A top-level warning)
$(warning CURDIR=$(CURDIR))

$(error TEXT)

打印TEXT信息,也可以打印变量 打印信息后,会停止当前makefile的编译

$(foreach VAR, LIST, TEXT) 把LIST中使用空格分隔的单词依次取出赋值给变量VAR, 然后执行TEXT表达式 如果在TEXT中存在对VAR的引用,那么VAR在TEXT中,每次会得到不同的值,因为TEXT的变量是在执行时才被展开

示例:

dirs := a b c d
files := $(foreach dir, $(dirs), $(wildcard $(dir)/*.c))

TEXT的表达式为 $(wildcard $(dir)/*.c) 表达式第一次被执行时将展开为 $(wildcard a/*.c), 第二次被执行时将展开为 $(wildcard b/*.c)

当TEXT比较复杂时,也可以通过定义一个中间变量,在TEXT表达式中引用这个变量, 由于TEXT将在执行时才展开变量,所以该变量需要定义为递归展开式变量.

示例:

find_files=$(wildcard $(dir)/*.c)
dirs := a b c d
files := $(foreach dir, $(dirs), $(find_files)

$(origin variable) 获取变量variable是哪里来,variable为变量的名字,而不是引用 所以变量名前没有$字符.

  1. 如果从来没有定义过变量V, 则origin函数返回“undefined”
  2. 如果变量为环境变量,则origin函数返回“enviroment”
  3. 如果变量是默认定义的,则origin函数返回“default”
  4. 如果变量是为定义在Makefile文件中,那么origin函数返回“file”
  5. 如果变量由命令行传入,那么返回“command line”
  6. 如果变量被重新定义过,则origin函数返回“override”
  7. 如果变量是自动化变量,则origin函数返回“automatic”

例如: Makefile中的内容如下:

OBJS=dummy.o
override SHELL=/bin/sh

all:
	@echo "V:     $(origin V)"
	@echo "USER:  $(origin USER)"
	@echo "CC:    $(origin CC)"
	@echo "OBJS:  $(origin OBJS)"
	@echo "ARCH:  $(origin ARCH)"
	@echo "SHELL: $(origin SHELL)"
	@echo "@:     $(origin @)"
$ make ARCH=arm
V:     undefined
USER:  environment
CC:    default
OBJS:  file
ARCH:  command line
SHELL: override
@:     automatic
  • blog/programming/makefile.txt
  • 最后更改: 2022/01/09 22:37
  • 127.0.0.1