banner
Zein

Zein

x_id

make最佳实践手册

# 伪目标
PHONY := __build
__build:             #空操作
 
# 清空需要的变量
obj-y :=            #需要编译的目标文件列表
subdir-y :=          #子目录列表
EXTRA_CFLAGS :=      #额外编译选项
 
# 包含同级目录Makefile
include Makefile
 
# 获取当前 Makefile 需要编译的子目录的目录名
# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y))   : c/ d/
# __subdir-y  : c d
# subdir-y    : c d
__subdir-y  := $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y  += $(__subdir-y)
 
# 生成各子目录的目标文件列表:要编译的子目录下目标文件都打包成了dir/built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)
 
# 过滤 obj-y,得到当前目录下需要编进程序的文件名作为,并写为目标
# a.o b.o
cur_objs := $(filter-out %/, $(obj-y))
# 使修改头文件 .h 后,重新make后可以重新编译(重要)
dep_files := $(foreach f,$(cur_objs),.$(f).d)
# 包含所有依赖文件
dep_files := $(wildcard $(dep_files))
ifneq ($(dep_files),)        #不为空(即存在依赖文件),则执行下一步操
  include $(dep_files)  #Make 工具会读取这些依赖文件,从而确保 Makefile 知道源文件与头文件之间的正确依赖关系,当头文件修改后可以重新编译受影响的源文件
endif


PHONY += $(subdir-y)
# 第一个目标
__build : $(subdir-y) built-in.o
# 优先编译 子目录的内容;-C切换到子目录执行$(TOPDIR)/Makefile.build
$(subdir-y):
  make -C $@ -f $(TOPDIR)/Makefile.build 
 
# 把subdir的built-in.o和cur_objs链接成 总的built-in.o目标
built-in.o : $(cur_objs) $(subdir_objs)
  $(LD) -r -o $@ $^

dep_file = [email protected]
 
# 生成 cur_objs 目标
%.o : %.c
  $(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
  
.PHONY : $(PHONY)

子目录下 makefile,用于添加 obj-y 信息#

# 可以不添加下面两个变量
EXTRA_CFLAGS  := 
CFLAGS_view.o := 

obj-y += $(patsubst %.c,%.o,$(shell ls *.c))
# obj-y += subdir/

自动化构建工具需要实现:
1)工程从未被编译,则所有文件都要编译,链接
2)工程的某几个文件被修改,则只编译修改的文件,并链接至目标程序
3)工程的头文件被改变,则编译引用了这几个头文件的 c 文件,并链接目标程序
*)打包一些 shell 命令,方便工程管理,比如打包,备份,清理中间文件

make 运行#

make 启动#

默认脚本优先级:GNUmakefile > makefile > Makefile

make -f custom.mk       #指定入口makefile文件

command 打印#

make 执行 command 时会把 command 打印出来;用 @ 字符在命令前,则不会打印出来;
比如echo 正在编译XXX模块,终端显示:
echo 正在编译XXX模块 正在编译XXX模块
如果是@echo 正在编译XXX模块就不会输出

make -s           #静默执行command

command 运行的 shell 环境#

make 命令默认使用环境变量 SHELL 指定的 Shell 执行,用户没指定SHELL时, Unix Shell 默认是 $(SHELL)=/bin/sh;Windows 环境下,make 会自动寻找合适的命令解释器,例如 cmd.exe 或者你指定的 Unix 风格 Shell(如 bash

-------------------让上一条命令的结果应用在下一条命令时,用分号分隔这两条命令。分行写,则前一命令的效果不会保留
.RECIPEPREFIX = >
exec:
>  cd /home/hchen; pwd
# 打印 /home/hchen

.RECIPEPREFIX = >
exec:
>  cd /home/hchen
>  pwd
# 打印 /home

make 命令失败处理#

————————忽略错误,不终止 make 的执行
clean:
    -rm -f *.o           #command前加减号

.IGNORE: clean           #特殊目标.IGNORE指定clean规则忽略错误,不终止 make 的执行
clean:
    rm -f *.o

make -i                  #用 -i 或 --ignore-errors 参数选项
make --ignore-errors

————————只跳过当前失败的规则,继续执行其它规则,不会终止 make 执行
make -k
make --keep-going

make 查看规则#

make -p                # 输出所有的规则和变量。
make -n                # 打印将要执行的规则和命令,但不执行
make --debug=verbose   #
make -t                # 相当于UNIX touch,把目标的修改日期变成最新的,相当于假编译,只是把目标变成已编译状态
make -q                # 检查target是否存在,返回码指示结果(0=需更新,2=错误)。
make -W <file>         # 一般指定源文件(或依赖文件),Make根据规则推导运行依赖于这个文件的命令,一般和-n一同使用查看此文件所发生的规则命令

make 退出#

Make 退出码:
1)0:执行成功。
2)1:执行中出现错误。
3)2-q选项启用时,目标不需要更新

make 参数#

选项功能
-b, -m忽略与其他版本兼容性相关的警告。
-B, --always-make强制重编译所有目标。
-C <dir>切换到指定目录运行 Makefile。
--debug[=<options>]输出调试信息,<options>包括 allbasicverboseimplicitjobsmakefile
-d等同于 --debug=all
-e, --environment-overrides环境变量覆盖 Makefile 中定义的变量。
-f=<file>指定 Makefile 文件。
-h显示帮助信息
-i, --ignore-errors忽略所有错误。
-I <dir>指定一个可导入 makefile 的搜索路径。可以使用多个 "-I" 参数来指定多个目录
-j [<jobsnum>]指定最大并行执行任务数。
-k, --keep-going忽略失败的目标,继续处理其他目标。
-l <load>指定允许的最大负载值,超出则暂停新的任务。
-n打印将要执行的规则和命令,但不执行
-o <file>不重新生成的指定的<file>,即使这个目标的依赖文件新于它。
-p, --print-data-base输出所有的规则和变量。
-q, --question检查 target 是否存在,返回码指示结果(0 = 需更新,2 = 错误)。
-r禁用所有内置规则。
-R禁用所有内置变量。
-s, --silent禁止命令输出。
-S, --no-keep-going禁用 -k 参数的作用
-t相当于 UNIX touch,把目标的修改日期变成最新的,相当于假编译,只是把目标变成已编译状态
-v, --version显示版本信息。
-w, --print-directory显示当前目录及嵌套调用信息。
--no-print-directory禁止 "-w" 选项
-W <file>一般指定源文件 (或依赖文件),Make 根据规则推导运行依赖于这个文件的命令,一般和 - n 一同使用查看此文件所发生的规则命令
--warn-undefined-variables警告未定义的变量。

规则#

一条构建规则由依赖关系和命令组成:
target:要生成的目标文件、可执行文件或 label
prerequisites:目标文件依赖的源文件或中间文件
command:调用的 shell 命令;通常是构建工具命令;一定要以一个 制表符Tab 作为开头;或者 command 跟依赖关系在同一行,用;隔开

执行 make 命令时,Make 会:
1)查找当前目录下的 Makefile 或 makefile 文件。也可指定路径 make -f ~/makefile
2)查找 Makefile 中第一个 target;也可指定目标make target
3)检查 target 的 prerequisites(.c/.o)是否需更新。如 prerequisites 比 target 新,或 target 不存在,则执行 command
4)prerequisites (.o) 可能本身也作为 target 依赖其他文件(.c),Make 会递归追踪 .o 文件的依赖,直到最终的源文件 .c 被编译。
5)编译错误或文件缺失,则 Make 报错并终止执行;在命令前 +- 忽略文件未找到的警告并继续执行

如: file.c 文件被修改, file.o 会重新编译,且 edit 会重新链接

target: prerequisites; command; command
    command
    command

嵌套执行 make#

大型项目中,将代码划分为多个模块或子目录,每个子目录维护独立的 Makefile 是一种常见的工程管理方式。这种方法使每个模块的编译规则更加清晰,简化了 Makefile 的维护。

用到的 shell 变量:
MAKE:GNU make 中的一个特殊变量,用于递归调用 make 自身;假设在 shell 执行make -j4,总控 Makefile 有$(MAKE) -C subdir;则相当于在 subdir 执行make -j4 -C subdir
MAKEFLAGS :系统级变量,会自动传递到下级 Makefile,可通过设置为空来阻止传递(MAKEFLAGS=
MAKELEVEL : 系统变量,记录当前 Make 执行的嵌套层数。如果你有嵌套的 make 调用,MAKELEVEL 会告诉你当前是在第几层。

假设有一个子目录叫 subdir,这个目录下有个 Makefile 文件,来指明了这个目录下文件的编译规则。

总控 Makefile#

.RECIPEPREFIX = >
#如果当前 make 在最外层执行(MAKELEVEL= 0);定义一些与系统相关的变量,
ifeq (0,${MAKELEVEL})
    cur-dir   := $(shell pwd)
    whoami    := $(shell whoami)  #当前用户
    host-type := $(shell arch)    #架构
    MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}
endif

.PHONY: all subsystem

all: subsystem   #all依赖于subsystem 

subsystem:
>  $(MAKE) -C subdir MAKEFLAGS= VAR=value     # -C subdir 表示在指定目录中执行 make
#或>  cd subdir && $(MAKE)

subdir/Makefile#

.PHONY: all

all:
    echo "Received VAR = $(VAR)"

例子的执行结果:

-w--print-directory 是调试嵌套 Makefile 的好帮手,它会输出当前的工作目录信息。如果使用 -C 参数,-w 会自动启用。如果包含 -s--silent 参数,则 -w 会失效。

make: Entering directory `subdir'
Received VAR = value
make: Leaving directory `subdir'

命令包#

define <命令包名>
<command1>
<command2>
...
endef

-------eg
.RECIPEPREFIX = >

define run-yacc
yacc $(firstword $^)    # $(firstword $^) 会获取 $^ 中的第一个依赖文件。
mv y.tab.c $@           # 将生成的 y.tab.c 文件重命名为 foo.c
endef

foo.c : foo.y
>  $(run-yacc)

变量#

变量用于存储字符串,类似于 C/C++ 的宏;能提高脚本的可维护性,避免重复代码;eg:只需维护 objects 变量,不必修改规则;

命名规则: 可包含字母、数字、下划线 (_),且可以以数字开头。大小写敏感;系统变量通常全大写,如 CCCFLAGS。用户自定义变量建议驼峰命名,如 MyFlags
声明:VAR = value
引用:$(VAR)

#使命令前缀变为 >,而不是默认的制表符(tab)
.RECIPEPREFIX = >
objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

#第一个目标
edit: $(objects)
    cc -o edit $(objects)

# 各个目标文件的规则
main.o: main.c defs.h
    cc -c main.c
kbd.o: kbd.c defs.h command.h
    cc -c kbd.c
# 其他 .o 文件的规则...

自动变量#

含义eg
$@依次从 target 集中取出 target,并执于命令
$<表示依次从依赖集中取出依赖文件,并执于命令
$^所有依赖文件的集合(去重)
$+所有依赖文件的集合(不去重)
$?所有比target新的依赖文件target: dependency1 dependency2 command $? dependency1target 新,则 $?dependency1
$*依次从格式规则的 target 集中取出 target,去除后缀,剩路径部分target: %.c command $* 如果目标是 file.c,则 $*file
$%仅当目标是静态库时,表示依次取出库中的成员libfoo.a(bar.o): bar.o ar r $@ $% 如果目标是静态库 libfoo.a,则 $% 是库中的成员 bar.o
$(@D)依次从 target 集中取出 target 的目录部分,并执于命令target = dir/subdir/foo.o $(target): @echo $(@D) # dir/subdir
$(@F)依次从 target 集中取出 target 的文件名部分,并执于命令target = dir/subdir/foo.o $(target): @echo $(@F) # foo.o
$(<D)依次从依赖集中取出依赖文件的目录部分,并执于命令target: dir/file.c command $(<D) 如果第一个依赖文件是 dir/file.c,则 $(<D)dir
$(<F)依次从依赖集中取出依赖文件的文件名部分,并执于命令target: dir/file.c command $(<F) 如果第一个依赖文件是 dir/file.c,则 $(<F)file.c
$(^D)所有依赖文件的目录部分的集合(去重)target: dir/file1.c dir/file2.c command $(^D) 如果依赖文件是 dir/file1.cdir/file2.c,则 $(^D)dir
$(^F)所有依赖文件的文件名部分的集合(去重)target: dir/file1.c dir/file2.c command $(^F) 如依赖文件是 dir/file1.cdir/file2.c,则 $(^F)file1.c file2.c
$(+D)所有依赖文件的目录部分的集合(不去重)target: dir/file1.c dir/file2.c dir/file1.c command $(+D) 依赖文件是 dir/file1.c dir/file2.c dir/file1.c,则 $(+D)dir dir dir
$(+F)所有依赖文件的文件部分的集合(不去重)target: dir/file1.c dir/file2.c dir/file1.c command $(+F) 依赖文件是 dir/file1.c dir/file2.c dir/file1.c,则 $(+F)file1.c file2.c file1.c
$(?D)被更新过的依赖文件的目录部分target: file1.c file2.c command $(?D) 如果 file1.c 更新,则 $(?D)file1
$(?F)被更新过的依赖文件的文件部分target: file1.c file2.c command $(?F) 如果 file1.c 更新,则 $(?F)file1.c
$(*D)依次从格式规则的 target 集中取出 target,去除后缀和文件名部分%.o: %.c @echo $(*D)
$(*F)依次从格式规则的 target 集中取出 target,去除后缀和目录部分%.o: %.c @echo $(*F)

静态模式规则:
1)<targets ...> 目标集,可以有通配符
2)目标集文件格式,从 <targets ...> 中匹配出符合的新目标集
3)<prereq-patterns ...> 依赖文件集格式,匹配新目标集 的依赖集

.RECIPEPREFIX = >
bigoutput littleoutput: text.g
>  generate text.g -$(subst output,,$@) > $@              # 参数替换:$(subst output,,$@) 表示替换 $@ 中的 output 为空字符串

#等价于上
.RECIPEPREFIX = >
bigoutput: text.g
>  generate text.g -big > bigoutput

littleoutput: text.g
>  generate text.g -little > littleoutput

-------------------------------------静态模式规则
.RECIPEPREFIX = >
<targets ...> : <target-pattern> : <prereq-patterns ...>
>  <commands>

#eg
.RECIPEPREFIX = >
objects = foo.o bar.o

all: $(objects)

$(objects): %.o: %.c                    #匹配 %.o格式 的目标文件,由$@逐个取出;匹配目标集的 依赖集文件格式为%.c ,由$< 逐个取出
>  $(CC) -c $(CFLAGS) $< -o $@

#等同于上
foo.o: foo.c
>  $(CC) -c $(CFLAGS) foo.c -o foo.o

bar.o: bar.c
>  $(CC) -c $(CFLAGS) bar.c -o bar.o



命令行环境变量#

除了 **Makefile 内部定义的变量;makefile 能以 $(var) 的方式引用 1)os环境变量和 2)make 选项传递的命令行环境变量;

同名变量优先级:命令行传递的变量值 >Makefile 中定义的变量值 > os 环境变量
1)提升 ****Makefile**** 中定义的变量值优先级:override变量前缀
2)提升 os 环境变量优先级:
make -e**
**
eg:
make CFLAGS="-O2 -Wall"传递CFLAGS** 环境变量;export CC=gcc传递 os 环境变量 CC

# Makefile
all:
    echo $(CFLAGS)      #传递进来了-O2 -Wall
    echo $(CC)

$$bash 中代表当前进程的 PID(进程 ID)
$$$$ 会插入当前进程的 PID 的最后四个数字。通常用于生成唯一的临时文件名

在 makefile 间传递变量#

export                       # 传递所有变量
export variable += value;    # 传递变量variable到下级Makefile
unexport variable := value;  #不想让变量variable传递到下级Makefile

赋值#

变量的值可依赖于其他变量的值

-----------------=  递归赋值:以递归的方式展开依赖的变量值;这种赋值方法灵活,可推迟变量的定义,缺点是可以写出无限递归赋值,而且每次依赖的变量重新赋值都要重新计算
all:
    echo $(foo)
    
foo = $(bar)
bar = $(ugh)
ugh = Huh?

-----------------:=  立即赋值:在赋值时展开右侧表达式,并将结果存储到变量中,比递归赋值性能更高。更安全
x := foo
y := $(x) bar    # foo bar;由于立即展开了,下面一句不影响y的值
x := later

-----------------?=  条件赋值:只有未定义变量,赋值才生效;已定义变量则什么也不做,而非覆盖原定义
FOO ?= bar

-----------------# 不仅能用来注释,还可用来标示变量定义的结束位置
dir := /foo/bar    # directory to put the frobs in

-----------------+=  追加变量值,原变量用 := 定义,则 +=为立即赋值;变量用 = 定义,则 +=为递归赋值
objects = main.o foo.o bar.o utils.o
objects += another.o             #相当于objects = $(objects) another.o

-----------------指定makefile中定义的某些变量有最高优先级,不会被make选项传入的命令行变量覆写
override <variable> := <value>
override <variable> += <more text>
override <variable> = <value>

-----------------多行变量定义:跟命令包一样
define <variable>
<value>
endef

变量值替换#

-----------------简单替换
${var:a=b}   #将变量 var 中以 a 结尾的部分替换为 b

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

-----------------模式替换
$(var:%.suffix1=%.suffix2)   # 将变量var中 %.suffix1 替换为 %.suffix2

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

Target-specific Variable#

变量只对特定目标及其依赖有效,避免影响到其他目标的设置

-----------------------------------------变量表达式只对特定目标<target>及其依赖有效
<target> : <variable-assignment>              # 变量表达式只对特定目标<target>及其依赖有效
<target> : override <variable-assignment>
#eg:不论全局 CFLAGS 的值是什么,prog 目标及其相关的目标(prog.o、foo.o、bar.o)会使用 CFLAGS = -g
prog : CFLAGS = -g
prog : prog.o foo.o bar.o
    $(CC) $(CFLAGS) prog.o foo.o bar.o

prog.o : prog.c
    $(CC) $(CFLAGS) prog.c

foo.o : foo.c
    $(CC) $(CFLAGS) foo.c

bar.o : bar.c
    $(CC) $(CFLAGS) bar.c

---------------------------变量表达式只对特定格式目标有效
<pattern ...>; : <variable-assignment>;
<pattern ...>; : override <variable-assignment>;
%.o : CFLAGS = -O2           #变量表达式只对.o结尾的目标有效


动态变量名#

dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c)        #$(dir)_sources定义为$(dir)目录下所有c文件
define $(dir)_print                             #甚至是动态定义命令包
lpr $($(dir)_sources)                           #打印$(dir)_sources的值
endef

特殊值的转义#

$$Makefile 中,单个 $ 被用于变量引用。而任何 $$会视为$ 字符的转义

-----------------定义包含空格的变量
nullstring :=
space := $(nullstring)

文件路径搜索#

shell 通配符:
*(匹配任意长度字符)
? (匹配一个字符)
~ (家目录或环境变量)

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

VPATH = src:../headers       # 现在当前目录下寻找。再去src,以及../headers找

vpath <pattern> <directories>
vpath %.h ../headers     #当前目录没找到时;在../headers"目录下搜索所有以 .h 结尾的文件;..上级目录
vpath %.c foo            #.c 文件,按"foo""blish""bar"目录顺序搜索
vpath %   blish
vpath %.c bar

objects := $(wildcard *.o)                        # 展开通配符*.o通配符,列出所有 .o 文件

$(patsubst %.c,%.o,$(wildcard *.c))               # 将%.c字符串替换为%.o字符串,列出所有.c文件对应的 .o 文件

objects := $(patsubst %.c,%.o,$(wildcard *.c))    # 编译并链接所有 .c 和 .o 文件
foo : $(objects)
>  cc -o foo $(objects)

隐含规则#

只需编写项目链接环节的规则,而从源文件到目标文件的构建规则可由make 根据 1)内置规则库,用户自定义的 2)模式规则和 3)后缀规则 自动推导;

隐含规则优先级:规则按内置顺序匹配,优先级靠前的规则会覆盖靠后的;make 允许重载默认的隐含规则;一般以 Pattern Rules 的形式重载。

以下是常用内置自动推导规则:

targetprerequisitescommand
<n>.o<n>.c$(CC) -c $(CFLAGS) $(CPPFLAGS)
<n>.o<n>.cc / <n>.C$(CXX) -c $(CXXFLAGS) $(CPPFLAGS)
<n><n>.o$(CC) $(LDFLAGS) <n>.o $(LDLIBS)
<n>.c<n>.y$(YACC) $(YFLAGS)
<n>.c<n>.l$(LEX) $(LFLAGS)

以及默认变量:

命令变量描述默认值
AR函数库打包程序ar
AS汇编语言编译器as
CCC 语言编译器cc
CXXC++ 语言编译器g++
CO从 RCS 文件中扩展文件程序co
CPPC 程序的预处理器$(CC) -E
FCFortran 和 Ratfor 编译器f77
LEXLex 文法分析器(针对 C 或 Ratfor)lex
PCPascal 编译器pc
YACCYacc 文法分析器(针对 C 程序)yacc
MAKEINFO将 Texinfo 文件转换为 Info 格式的程序makeinfo
参数变量描述默认值
ARFLAGSAR 命令的参数rv
ASFLAGS汇编器的参数
CFLAGSC 语言编译器的参数
CXXFLAGSC++ 语言编译器的参数
COFLAGSRCS 命令的参数
CPPFLAGSC 预处理器的参数
FFLAGSFortran 编译器的参数
LDFLAGS链接器的参数
LFLAGSLex 文法分析器的参数
PFLAGSPascal 编译器的参数
RFLAGSRatfor 编译器的参数
YFLAGSYacc 文法分析器的参数

自动推导构建规则#

GNUmake 可自动推导某些 prerequisites 到 target 的构建规则,比如 C 项目每个 x.o 文件后必然有一个同名x.c依赖,必然有一条cc -c x.c

自动推导有个问题就是规则匹配优先级:比如你显式指定foo.o : foo.p; 但文件目录下存在foo.c;此时会自动推导出foo.c构建foo.o的规则;make -rmake --no-builtin-rules 能禁用所有内置隐含规则。添加明确规则可以覆盖隐含规则。

.RECIPEPREFIX = >
objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

edit: $(objects)
    cc -o edit $(objects)

# 自动推导规则,make 会推导出这些规则
main.o: defs.h
kbd.o: defs.h command.h
command.o: defs.h command.h
# 其他规则...

另一种风格:其实就是进一步把相同.h文件的目标合并表示;我不喜欢

.RECIPEPREFIX = >
objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

edit: $(objects)
    cc -o edit $(objects)

# 将多个文件的依赖合并
$(objects): defs.h
kbd.o command.o files.o: command.h
display.o insert.o search.o files.o: buffer.h

.PHONY: clean
clean:
    rm edit $(objects)

Pattern Rules#

用通用的格式描述一组目标和依赖文件的关系。在目标或依赖文件中用 % 代表任意字符或文件名的一部分

#eg:用Pattern Rules重载内建隐含的.c构建.o规则
%.o : %.c
    $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

%.o : %.s    #取消内建的隐含规则,只要不在后面写命令就行

后缀规则#

保持兼容的老规则不推荐,遇到这么写的项目再看

引用其它的 Makefile#

将其它 Makefile 文件的规则和变量加载到当前 Makefile ; 用 include 引入文件时,Make 会在以下几个位置查找文件:
1) 当前目录
2) 用 -I 指定的目录
3) 默认的系统目录(如 /usr/include

-include other.make *.mk $(othermake)         #可以是文件名,也可用通配符

sinclude other.make *.mk $(othermake)         #同上

伪目标#

指明 target 不是文件,而是 make 后跟的操作标签。用于打包 shell 命令;伪目标不能和文件同名,为确保这一点,用.PHONY显式声明,使存在同名文件时伪目标也能执行;

伪目标可依赖其他伪目标,形成分层结构。按深度优先序执行操作;

GNU 惯例:
1)all: 默认目标,编译所有内容。
2)clean: 删除所有生成的文件,包括目标文件和压缩文件。
3)install: 将编译好的程序安装到系统路径中。
4)print: 列出自上次构建以来被修改过的源文件。
5)tar: 打包源程序和相关文件,生成.tar文件。
6)dist: 压缩 tar 文件,生成.tar.gz文件。
7)TAGS: 一个占位规则,可以集成ctagsetags来生成代码索引。
8)check/test: 运行测试,通常可以链接到单元测试框架或脚本。

.RECIPEPREFIX = >
.PHONY: cleanall cleanobj cleandiff

cleanall: cleanobj cleandiff
>  rm -f program

cleanobj:
>  rm -f *.o

cleandiff:
>  rm -f *.diff


自动生成依赖关系#

make 可根据依赖关系判断哪些.c.h 文件修改了需要重新编译;自动生成依赖避免大型项目的手动管理依赖苦难;
gcc -MM file.c生成依赖关系:file.o: file.c defs.h gcc -M main.c生成依赖关系(含标准库头文件):main.o: main.c defs.h /usr/include/stdio.h /usr/include/features.h \ /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h \
GNU 组织建议为为每个 name.c 文件生成一个 name.d 的 Makefile, .d 存放对应 .c 的依赖关系。让 make 自动更新或生成 .d 文件,并把其包含在主 Makefile 中,自动化生成每个文件的依赖关系
sed 's,pattern,replacement,g':字符串替换命令
1)pattern :要匹配的内容;\($*\)\.o[ :]*$表示匹配零个或多个字符,* 表示前面元素出现零次或多次捕获组\(\)中键入$*表示字符串会被捕获到 \(\)\.o表示匹配.o[ :]*表示可匹配多个 :
2)replacement :正则匹配替换成的内容;\1.o $@ :\1表示第一个捕获组;$@**** 表示Makefile 规则中的 target;替换成main.o main.d :;于是, .d 文件也会自动更新和自动生成了
3)g :表示替换所有匹配的地方。

.RECIPEPREFIX = >

sources = foo.c bar.c

%.d: %.c
>  @set -e; rm -f $@; \                                     # @set -e 设置脚本一旦遇到错误(命令返回值非零)立即退出;rm -f $@删除已存在的中间文件
    $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \                    # 由%.c逐个构建依赖关系,重定向到一个临时文件$@.$$$$,相当于%.d.$$$$
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \   # < $@.$$$$表示从文件 %.d.$$$$ 中读取输入;替换后的内容输出到%.d;这一步是为子.d自动更新
    rm -f $@.$$$$
    
include $(sources:.c=.d)            #  把变量 $(sources) 所有 .c 的字串都替换成 .d;然后把依赖关系include进来


中间目标处理#

.INTERMEDIATE: mid         #显式声明mid文件是中间目标。make不会把它当最终目标处理,当生成mid文件后不会被自动删除
.SECONDARY: sec            #sec 文件是一个中间目标,但 make 不会在生成最终目标后删除它
.PRECIOUS: %.o             #告诉 make 保留所有 .o 文件,即使它们是中间目标

条件表达式#

<conditional-directive>
<text-if-true>             #
else
<text-if-false>
endif

-----------------ifeq 和 ifneq
.RECIPEPREFIX = >
foo = bar

ifeq ($(foo),bar)
>  echo "foo is bar"
endif

ifneq ($(foo),baz)
>  echo "foo is not baz"
endif

-----------------ifdef 和 ifndef
.RECIPEPREFIX = >
foo = bar

ifdef foo
>  echo "foo is defined"
else
>  echo "foo is not defined"
endif

函数#

函数调用语法#

$(<function> <arguments>)
${<function> <arguments>}

#<function> 是函数名;<arguments> 是参数,用逗号分隔;函数名和参数之间用空格隔开

字符串处理#

$(subst <from>,<to>,<text>)   # 字符串替换:把字符串<text>中的<from>替换为<to>;并返回
#eg: $(subst ee,EE,feet on the street) 

$(patsubst <pattern>,<replacement>,<text>)   # 字符串替换:把字符串<text>中的<pattern>替换为<replacement>;并返回
#eg: $(patsubst %.c,%.o,x.c.c bar.c) 

$(strip <string>)   # 去掉字符串开头和结尾的空格
#eg: $(strip  a b c  ) 

$(findstring <sub_text>,<text>)   # 在字符串<text>中查找子串<sub_text>
#eg: $(findstring a,a b c) # 返回值:a

$(filter <pattern...>,<text>)   # 过滤出<text>中符合格式 <pattern...>的部分
#eg: $(filter %.c %.s,bar.c baz.s ugh.h) 

$(filter-out <pattern...>,<text>)   # 过滤出<text>中不符合格式 <pattern...>的部分
#eg: $(filter-out main1.o main2.o,main1.o foo.o main2.o) 

$(sort <list>)   # 排序并去重
#eg: $(sort foo bar lose foo) 

$(word <n>,<text>)   # 提取<text>中第n个单词
#eg: $(word 2,foo bar baz)  #取出了bar

$(wordlist <start>,<end>,<text>)   # 提取单词范围
#eg: $(wordlist 2,3,foo bar baz)  # 返回值:bar baz

$(words <text>)   # 统计单词数
#eg: $(words foo bar baz)    # 返回值:3

$(firstword <text>)   #  获取第一个单词
#eg: $(firstword foo bar) 

文件名操作#

$(dir <path...>)            #提取目录部分
#eg: $(dir src/foo.c hacks) # 返回值:src/ ./

$(notdir <path...>)      #提取非目录部分
#eg: $(notdir src/foo.c hacks)      # 返回值:foo.c hacks

$(suffix <names...>)      # 提取文件后缀
#eg: $(suffix src/foo.c src-1.0/bar.c hacks) # 返回值:.c .c

$(basename <names...>)      # 提取文件名前缀
#eg: $(basename src/foo.c src-1.0/bar.c hacks)    # 返回值:src/foo src-1.0/bar hacks

$(addsuffix <suffix>,<names...>)      # 给<names...>中的文件添加后缀<suffix>
#eg: $(addsuffix .c,foo bar)          # 返回值:foo.c bar.c

$(addprefix <prefix>,<names...>)      #添加前缀
#eg: $(addprefix src/,foo bar)        # 返回值:src/foo src/bar

$(join <list1>,<list2>)      #连接两个列表
#eg: $(join aaa bbb,111 222 333)     # 返回值:aaa111 bbb222 333

高级函数#

$(foreach <var>,<list>,<text>)            # 循环处理列表:从<list>依次取出单词赋值给<var>,然后用表达式<text>进行计算,最终返回由每次计算结果用空格连起来的字符串
#eg: files := $(foreach n,a b c d,$(n).o)     # 返回值:a.o b.o c.o d.o

$(if <condition>,<then-part>,<else-part>)      #条件判断  
#eg: $(if 1,yes,no)     #yes

$(call <expression>,<parm1>,<parm2>,...)      #自定义函数调用:用占位符 $(1)、$(2) 等表示参数,传递到<expression>计算并返回
#eg: 
reverse = $(2) $(1)
foo = $(call reverse,a,b) 
# 返回值:b a

$(origin <variable>)      #检查变量来源
#返回值:
undefined:    未定义
default:      默认定义
environment:  环境变量
file:         Makefile 定义
command line: 命令行定义
override:     override定义
automatic:    自动变量

$(shell <command>)      #执行Shell命令
#eg: files := $(shell echo *.c) 

$(error <text...>)      #终止 make 的执行,并显示指定的错误消息
#eg: 检查变量是否为空
MY_VAR :=

ifeq ($(MY_VAR),)
$(error MY_VAR is not defined or is empty)
endif

$(warning <text...>)      #输出警告,但不会中断构建过程
#eg: 输出调试信息
MY_VAR :=

ifeq ($(MY_VAR),)
$(warning MY_VAR is not defined or is empty)
endif

更新函数库文件#

-j 并行构建库可能会影响 ar 打包

#eg:将所有的 .o 文件打包成 foolib 函数库
foolib(*.o) : *.o
    ar cr foolib *.o

模板#

tree#

<PROJECT_NAME >/
├── build/          # 存放编译生成的目标文件、依赖文件等
├── bin/            # 存放最终生成的可执行文件
├── inc/            # 存放头文件
├── lib/            # 存放库文件
├── src/            # 存放源代码文件 (.c)
├── tests/          # 存放测试代码
├── submodule1/     # 第一个子模块
│   └── Makefile    # 子模块的 Makefile
├── submodule2/     # 第二个子模块
│   └── Makefile    # 子模块的 Makefile
├── Makefile        # 顶层 Makefile
└── <其他文件>      # 可能包括其他文档、配置文件等

总控 makefile#

删除生成的目标文件,以便重新编译。更稳健的做法是使用 .PHONY 声明,并在删除命令前加上 -,这样即使某些文件不存在,也不会报错:

.RECIPEPREFIX = >
# -------------------------------
# General settings
# -------------------------------
PROJECT_NAME := 
VERSION := 1.0.0

INC_DIR := inc
SRC_DIR := src
LIB_DIR := lib
BIN_DIR := bin
TEST_DIR := tests
BUILD_DIR := build
SUBMODULES := submodule1 submodule2
INSTALL_DIR := /usr/local/bin
TAR_FILE := $(PROJECT_NAME)-$(VERSION).tar.gz

# -------------------------------
# Compiler and flags
# -------------------------------
CC := cc
CXX := g++
AR := ar
AS := as
CO := co
CPP := $(CC) -E
LEX := lex
YACC := yacc
MAKEINFO := makeinfo
      
CFLAGS := -Wall -Wextra -Og
LIBS := 
ARFLAGS := rv
ASFLAGS := 
CXXFLAGS := 
COFLAGS := 
CPPFLAGS := 
LDFLAGS := 
LFLAGS := 
YFLAGS :=

# -------------------------------
# 非独立构建加载子模块Makefile(即把子模块当静态库和源代码用);各模块独立构建则不需要
# -------------------------------
#-include $(foreach submodule, $(SUBMODULES), $(submodule)/Makefile)

# -------------------------------
# top level Files
# -------------------------------
SRCS := $(shell find $(SRC_DIR) -name '*.c')
OBJS := $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRCS))
DEPS := $(OBJS:.o=.d)

# -------------------------------
# Targets
# -------------------------------
.PHONY: all clean install print tar dist TAGS test $(SUBMODULES) #还应该加个检查是否安装相关工具的目标

all: $(PROJECT_NAME)

# Build the main target executable
$(PROJECT_NAME): $(OBJS) $(SUBMODULES)
> $(CC) $(OBJS) -o $(BIN_DIR)/$@ $(LDFLAGS) $(LIBS)

# 构建子模块:遍历子模块,进入其中执行make;如果找不到makefile,就打印skip
$(SUBMODULES):
> @$(foreach submodule, $(SUBMODULES), \
    $(MAKE) -C $(submodule) || echo "Skipping $(submodule)";)

# Automatically generate dependency files
-include $(DEPS)

# Clean target: remove build artifacts
clean:
> rm -rf $(BUILD_DIR) $(BIN_DIR) $(LIB_DIR) $(TAR_FILE)
> $(foreach submodule, $(SUBMODULES), $(MAKE) -C $(submodule) clean || echo "Skipping $(submodule)";)

# Install target: install the program
install: all
> install -m 755 $(BIN_DIR)/$(PROJECT_NAME) $(INSTALL_DIR)

# Print target: list modified source files
print:
> @echo "Modified source files since last build:"
> @find $(SRC_DIR) -type f -newer $(BIN_DIR)/$(PROJECT_NAME) -print || echo "No modified source files."

# Tarball target: create a source tarball
tar:
> tar -cvzf $(TAR_FILE) $(SRC_DIR) $(INC_DIR) $(LIB_DIR) Makefile $(SUBMODULES)

# Dist target: compress the tarball
dist: tar
> gzip $(TAR_FILE)

# TAGS target: generate ctags or etags
TAGS:
> ctags -R $(SRC_DIR) $(INC_DIR) $(SUBMODULES)

# Test target: run unit tests (assuming a separate test suite)
test:
> @echo "Running tests..."
> $(MAKE) -C $(TEST_DIR)

# -------------------------------
# Compilation rules
# -------------------------------
# Rule to create object files,这里妙在.c.h更新,则.o更新;.o更新引用这条规则,.d也更新
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
> @mkdir -p $(@D)
> $(CC) $(CFLAGS) -MD -MP -c $< -o $@

# Include all dependency files
-include $(DEPS)


独立构建 submodule makefile#



非独立构建 submodule makefile#

.RECIPEPREFIX = >

# 子模块配置
MODULE_NAME := submodule1
SRC_DIR := src
INC_DIR := inc
BUILD_DIR := build
LIB_DIR := ../lib

CC := cc
CFLAGS := -Wall -Wextra -Og -I$(INC_DIR)
AR := ar
ARFLAGS := rv

SRCS := $(shell find $(SRC_DIR) -name '*.c')
OBJS := $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRCS))
LIB := $(LIB_DIR)/lib$(MODULE_NAME).a
DEPS := $(OBJS:.o=.d)

.PHONY: all clean

# 默认目标
all: $(LIB)

# 生成静态库
$(LIB): $(OBJS)
> @mkdir -p $(LIB_DIR)
> $(AR) $(ARFLAGS) $@ $^

# 编译规则
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
> @mkdir -p $(@D)
> $(CC) $(CFLAGS) -MMD -MP -c $< -o $@

# 清理规则
clean:
> rm -rf $(BUILD_DIR) $(LIB)

# 自动包含依赖文件
-include $(DEPS)

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。