本文假设你使用过makefile, 了解最基本的一些概念, 比如makefile是干什么用的, makefile中注释是怎么写的...
变量
推荐在makefile内部使用小写字母作为变量名, 以区分make中的内建变量和环境变量.
要注意赋值等号后面的, 空格是有效的字符. 也不要将注释写在和变量定义的同一行, 注释最好独占一行.
变量定义
makefile中变量的定义有以下几种方式:
var:=value
: 简单扩展型变量
var=value
: 递归扩展型变量
var?=value
: 变量为空则赋值
var+=value
: 变量追加内容
要了解这四种变量复制的方式有何不同, 则需要了解make处理makefile文件的方式, make串行处理makefile文件文本, 对其做两编解析, 其中:
第一遍读取makefile和include的文件内容, 变量和规则被加载到make内部数据库, 生成依赖图
第二遍通过依赖图判断哪些目标需要更新, 执行相应的脚本
所以对于上述四种赋值方式:
:=
赋值: 变量在第一遍解析时直接赋值
=
赋值: 变量在第二遍解析时才进行赋值, 若value本身是其他该类型变量, 则变量递归展开, 此之谓递归扩展性变量
?=
赋值: 变量在第二遍解析时才进行赋值, 因为它需要第一遍解析来判断该变量是否存在
+=
赋值: 变量何时赋值, 取决于变量最初定义时是何种变量, 默认是递归扩展性变量
# ------ file:makefile ------
simple_value:=10
simple_var:=$(simple_value) # simple_var立即赋值为 10
recursion_var=$(simple_value) # recursion_var为何值待决议
simple_value=20 # simple_value发生改变, recursion_var延迟决议值为 20
$(warning $(simple_var)) # 输出10
$(warning $(recursion_var)) # 输出20
# ------ file:makefile ------
simple_value:=10
simple_var:=$(simple_value) # simple_var立即赋值为 10
recursion_var=$(simple_value) # recursion_var为何值待决议
simple_var+=$(simple_value) # simple_var立即追加值为 10 10
recursion_var+=$(simple_value) # recursion_var延迟追加
null_var?=$(simple_value) # null_var为何值待决议
simple_value=20 # simple_value发生改变, recursion_var延迟决议值为 20 20, null_var延迟决议值为 20
$(warning $(simple_var)) # 输出10 10
$(warning $(recursion_var)) # 输出20 20
$(warning $(null_var)) # 输出20
变量取值
通过上方例子可观察到, makefile中变量的取值使用过$(...)
获取的.
$(simple_value)
获取simple_value变量的值, 取值时便会根据变量的类型判断此时是否展开(取值).
需要注意, 当变量取值发生在赋值操作左边时, 变量总是立即展开. 在makefile中这样的操作时允许的, 在变量定义的时候可能遇到的少, 作为目标时则很常见.
simple_value=10
recursion_var=$(simple_value) # recursion_var为何值待决议
$(warning $(recursion_var)) # 但此处要求立即获取recursion_var的值为 10
var=recursion_var # var为何值待决议
$(var)=$(simple_value) # $(var)出现在等号左边, 则立即展开, 为 recursion_var=$(simple_value), recursion_var为何值待决议
simple_value=20 # simple_value发生改变, recursion_var延迟决议值为 20
$(warning $(recursion_var)) # 输出20
环境变量
make使用的变量可以来自make的运行环境, 任何make能够看见的环境变量, 在make开始运行时都转变为同名同值(变量名全大写)的make变量(通过make -p能够看到这些环境变量), 此处列举一些环境变量, 它们可以直接使用, 也可以被修改, 约定俗成的使用这些名称的环境变量能够降低在阅读makefile时的理解
AR=ar
ARFLAGS=rv
CXX=g++
CXXFLAGS
LDFLAGS
LFLAGS
自动变量
自动变量在规则每次执行时都基于当前的目标和依赖自动取值.
$@
: 规则的目标文件名
$<
: 第一个依赖的文件名
$^
: 所有依赖的名字, 名字之间用空格隔开
(还有一些其他的自动变量: $%
$?
$+
$*
, 相对来说用的较少, 需要的自行查阅资料了解.)
对于自动变量都有两种变体: 分别用于获取 目标文件名中的路径部分 和 目标文件名中的文件名部分.
例如: 对于$@
的值是dir/foo.o
, 则有:
$(@D)
: 值为dir(不含最后的斜杠). 若dir不存在则为.
$(@F)
: 值为foo.o
# 此处 $< 为 %.cpp, $@ 为 $(obj_output_dir)/release/%.o
$(obj_output_dir)/release/%.o : %.cpp
$(CXX) -c $(RELEASE_FLAGS) $(CXXFLAGS) $(INCLUDE_FLAGS) $(src_dir)/$< -o $@
目标和规则
显示规则
下面是makefile中规则的定义方式, 一条规则由目标,依赖,命令组成, 此类规则为显式规则.
# 一般情况下target只有一个, 但也可以是有多目标的
target: prerequiries; command # 如果只有一个目标则也可以和规则放在同一行, 用;分开
<tab>command # 单行命令前面是一个<tab>
目标(target): 一般会生成文件, 如果不是实际的文件, 则最好将其设置成假想目标(假想目标是一类特殊目标)
依赖(prerequiries): 生成目标的前置条件, 如果该前置条件作为目标需要被生成, 则转而先生成该依赖目标
命令(command); 生成目标所使用的命令, 命令的行首总应该是一个<tab>
占位, 每行命令在单独的shell环境中执行
- make在执行命令之前首先打印命令行, 以
@
起始的命令则不会回显 - 如果命令出现错误(非零退出状态), make将放弃当前的规则, 以
-
起始的命令会忽略一个命令执行产生的错误
隐含规则
隐含规则虽然很方便, 但了解这些隐含规则实在让人头疼, 并且大多数时候这些隐含规则也不完全适用. 即使在不了解隐含规则的情况下也能写好makefile(并且可读性更高), 所以关于隐含规则此处不做多介绍, 仅了解隐含规则的存在即可.
(通过make -p能够看到这些隐含规则)
# 这是make数据库中内建的一条隐含规则, 默认的一种情况.o文件可以从.cpp文件编译得到
%.o: %.cpp
# recipe to execute (built-in):
$(COMPILE.cpp) $(OUTPUT_OPTION) $<
特殊目标
.PHONY
用于指定假想目标, 假想目标用于解决这样一个问题, 对于实际的目标, make会根据目标的状态判断是否需要执行相关操作来更新目标, 若目标(及依赖)并无改动则不需要生成对应的目标. 例如makefile中一般都有clean目标, 若恰好目录中有名为clean的文件, 则根据clean文件的状态, make clean可能不会按预期执行. 将其设定为假想目标, 则能够保证, 无论clean文件是否存在, clean目标下的命令都会执行.
# 将all, debug, release, clean目标设定为假想目标
.PHONY: all debug release clean
.LIBPATTERNS
用于指定库的展开方式, 缺省值是lib%.so
lib%.a
, 这是很多系统库,开源库库名的默认格式. 有了这条指示, 在链接库时就不用写库全名了, make会自动按格式展开库名.
# 此处在链接过程中会展开为 libpthread.so 和 libdl.so, 他们是
LIBRARY_FLAGS+= -lpthread -ldl
(还有其他特殊目标, 有需要的自行了解, 因不常用则此处不提)
特定目标变量/特定格式变量
这些变量只能在一个目标的命令脚本的上下文起作用(实际环境中用到的不多)
# 此CFLAGS=-g仅对生成prog有效
prog : CFLAGS=-g
# 此CFLAGS=-O仅对生成.o有效
%.o : CFLAGS=-O
目录搜寻过程
如果目标没有具体的规则, make则使用隐含规则, 如目标隐含规则存在, make使用内置的规则编译它
如果依赖的目标不在当前目录下, 就搜寻适当的目录
make特别地在当前目录下,与vpath匹配的目录下,VPATH指定的目录下,/lib
,/usr/lib
和prefix/lib
目录下搜寻依赖的文件, 这里面需要说明的是vpath执行和VPATH变量.
VPATH
值指定了make搜寻的目录
vpath pattern directories
vpath指令, 对一定格式类型的文件名指定一个搜寻路径
pattern是一个包含一个'%'的字符串, 字符%可和任何字符串匹配
directories可以指定多项, 目录的名字由冒号或空格分开
# 对于所需要的文件, 若没有找到就去这些目录找. 目录的名字由冒号或空格分开
VPATH = src ../headers
# 在当前目录没有找到.h文件就去../headers目录中找
vpath %.h ../headers
需要注意的是, vpath在编译阶段为依赖的目标指定相关目录, 以便能够找到这些依赖的文件, 这和链接器执行链接操作时, 需要指定包含头文件性质是不一样的, 链接还是需要通过-I指定包含头文件.
项目管理
一般来说一个makefile文件管理当前目录下的源码, 当项目源码较多后, 可能期望能有一些方式对项目源码进行管理, 这时就涉及到源码的拆分,编译链接上的问题(实际就是组织makefile的问题), 这涉及到以下知识点.
include包含
include file
用于包含其它的makefile文件, 当一个项目中包含了很多makefile时, 并且它们的内容大同小异, 则可以考虑创建一个公用的makefile, 以减少实际项目中makefile中的内容. 并且, 通过约定公用的makefile文件, 可以达到一些规范的目录, 例如文件生成目录之类.
变量重载
override var=...
当决定使用include包含其他makefile文件后, 则需要考虑如何使用公用makefile文件中的一些变量, 其中最常见的一种需要是重载变量, 即公用的makefile中存在的一些变量, 需要在项目中的的makefile文件中覆盖其内容, 以达到某种处理, makefile中的override指令可以完成这个任务.
override指令可以理解为提供了这样一种机制, 用override修饰后的变量不会在后续的赋值操作中被覆盖!(或者说对于后续出现赋值操作, 都会被由override修饰的变量所覆盖)
override target_file=program
# 此时, 即使../../share/makefile存在target_file变量, 其值仍为program
include ../../share/makefile
递归调用make
递归调用make的命令总是使用变量MAKE, 而不是明确的命令名make, 在文章最后也提及了, 对于要使用的命令最好为其创建一个大写的同名变量(MAKE作为环境变量则已经存在).
这里使用了--directory(或-C)选项, 指示make将先进入指定目录后再执行.
# 递归子目录执行make
# MAKECMDGOALS变量为在执行make命令时附带的参数(目标)列表, 例如make debug, 参数时debug
makesubdirs:
@for subdir in $(src_subdirs); do \
$(MAKE) --directory $$subdir $(MAKECMDGOALS); \
done
clean :
@echo "make clean"
@for subdir in $(src_subdirs); do \
$(MAKE) --directory $$subdir clean; \
done
链接
递归编译后, obj文件放在哪的, 如何方便的链接? 如果使用公共的makefile指定了obj目录在当前目录下, 则在每个子目录都创建自己的obj文件夹, 链接收集这些obj文件会比较麻烦. 如果将所有的obj都输出到同一目录, 则链接就方便了.
可以在公共的makefile中做花招, 让obj的输出目录总是在src/obj这个目录(假设src就是源码根目录), 对于src下的子目录, 则对其中的makefile, 通过公共的makefile文件, 能够自行计算到src的相对路径, 以便找到最终的obj文件夹.
# 默认为src目录下的obj目录, 关于subdir_relative_src的值可以在本文搜索一下
obj_output_dir:=$(subdir_relative_src)/obj
# 进行链接
obj_debug_files:=$(addprefix $(obj_output_dir)/debug/, $(notdir $(obj_files)))
# 拿到所有的.o文件: $(obj_output_dir)/debug/*.o
$(target_debug_file) : $(obj_debug_files)
$(CXX) $(DEBUG_FLAGS) $(CXXFLAGS) $(INCLUDE_FLAGS) $(obj_output_dir)/debug/*.o -o $@ $(DEBUG_LIBRARY_FLAGS)
条件语句
ifeq/ifneq/-else-endif
判断条件是否成立
ifdef/ifndef-else-endif
判断变量是否定义
ifeq (arg1, arg2)
ifeq 'arg1' 'arg2'
ifeq "arg1" "arg2"
参数的写法上面三种均可
# 若没有指定目标, 则不生成目标输出目录
ifneq "$(target_file)" ""
@mkdir -p $(target_output_dir)/debug
@mkdir -p $(target_output_dir)/release
endif
内建函数
以下对于常用的函数有示例, 其他仅供了解.
字符串处理
$(patsubst pattern,replacement,text)
寻找text中符合格式pattern的字, 用replacement替换它们. pattern和replacement中可包含通配符%
($(foo:%.o=%.c)
patsubst的简写形式, 等价于$(patsubst .o, .c, $(foo))
)
$(findstring find,in)
在字符串in中搜寻find, 如果找到, 则返回值是find, 否则返回值为空
$(subst from,to,text)
在文本text中使用to替换每一处from
$(strip string)
去掉前导和结尾空格, 并将中间的多个空格压缩为单个空格
$(filter pattern...,text)
返回在text中由空格隔开且匹配格式pattern...的字, 对于不符合格式pattern...的则移出
$(sort list)
将list中的字按字母顺序排序, 并取掉重复的字, 输出由单个空格隔开
文件名处理
$(wildcard pattern)
获取匹配的文件名, pattern是一个可以含通配符的文件名格式
$(dir names...)
抽取names中每一个文件名中的路径部分, 含最后的斜杠
$(notdir names...)
抽取names中每一个文件名中的文件名
$(suffix names...)
抽取names中每一个文件名的后缀
$(basename names...)
抽取names中每一个文件名中除后缀外一切字符
$(addsuffix suffix,names...)
为names中的每一个文件名添加后缀
$(addprefix prefix,names...)
为names中的每一个文件名添加前缀
其他
$(foreach item,list,$(item))
对于list中的每一个元素item, 对item执行操作
$(if condition,then-part[,else-part])
if函数
$(origin variable)
变量的来源(它是内建变量的还是makefile中定义的之类)
$(shell cat foo)
shell函数, 在shell环境执行命令
$(warning text...)
错误信息但不退出
$(error text...)
错误信息并退出
# 获取目标文件后缀
target_suffix:=$(suffix $(target_file))
# 生成一份与源码文件名同名的.o文件名
obj_files:=$(patsubst %.cpp, %.o, $(src_files))
# 为obj文件名添加执行前缀
obj_debug_files:=$(addprefix $(obj_output_dir)/debug/, $(notdir $(obj_files)))
obj_release_files:=$(addprefix $(obj_output_dir)/release/, $(notdir $(obj_files)))
# 从当前路径找是否存在src/名的目录
is_subdir=$(findstring src/,$(CURDIR))
# 获取所有的cpp文件名, 剔除目录部分
src_files+=$(notdir $(wildcard $(src_dir)/ *.cpp))
# 当前目录下svn版本
svn_version:=$(shell svnversion ../)
宏? 自定义函数?
在makefile中除了内建函数似乎并不支持直接创建函数, 能够实现的是定义一个宏, 它封装了一些操作, 以便在其它位置使用, 在调用宏的时候也完全是做的宏替换.
宏本身不会返回任何结果, 所以不能将它看做是一个函数调用, 但可以通过$(shell ...)
内建函数可以做到想要的任何事情(shell脚本能做的都能在makefile中做到), $(shell ...)
是可以有返回值的.
宏调用通过$(call ...)
函数, 它支持向宏传递参数.
通过$(shell ...)
来获取返回值, 通过$(call ...)
来调用宏及传参, 也就可以看作是自定义了一个函数.
# 定义宏, 通过shell脚本当前目录到父目录的相对路径
# 可指定一个参数($1), 指定父目录名
# 例如当前路径为: /home/user/trunk/project, 指定父目录为trunk, 则得到 ..
define get_relative_path
$(shell dir_flag="$1"; \
current_path=`pwd`; \
offset=`echo $$current_path | grep -bo $$dir_flag | cut -d: -f1`; \
offset=$$[$$offset + $${#dir_flag}]; \
dir=$${current_path:0:$$offset}; \
echo `realpath --relative-to="." "$$dir"`)
endef
# 下面获取当前目录$(CURDIR)到src目录的相对路径
is_subdir=$(findstring src/,$(CURDIR))
ifneq "$(is_subdir)" ""
# 调用宏, 传递了一个参数"src/"
subdir_relative_src:=$(call get_relative_path,src/)
endif
make选项
-C dir, --directory=dir
在将makefile读入之前, 把路径切换到dir下. 此选项在递归make时常用
-j [N], --jobs[=N]
同时运行N个任务. 有可能多线程编译会因为依赖顺序出现问题, 暂无比较好的解决办法, 只好单线程编译
-k, --keep-going
当目标无法创建时继续执行
-n, --just-print
仅输出执行命令, 而不执行实际动作
-p, --print-data-base
make内部数据库
(关于make参数, 实际中用的貌似并不多, 若想了解更多, 参考make --help)
makefile文件的通用惯例
makefile中为特定命令,选项等提供变量
例如使用程序ssh_scp, 则最好定义一个SSH_SCP变量, 在使用$(SSH_SCP)在命令中调用
makefile中一些约定俗成的变量名
这些变量在开源软件中常见, 它们安规范编译安装到相关目录.
在实际项目中多半不会用到这些目录, 所以仅供了解吧.
prefix
设置安装文件的根目录(如果使用过./configure --prefix=...则对此不会陌生). 缺省值是/usr/local
srcdir
安装要编译的原文件. 该变量正常的值由configure命令设定.
bindir
安装用户可以运行的可执行程序. 缺省值是/usr/local/bin
sbindir
安装从shell中调用执行的可执行程序, 它仅仅对系统管理员有作用. 缺省值是/usr/local/sbin
libexecdir
安装其它程序调用的可执行程序. 缺省值是/usr/local/libexec
datadir
安装只读型体系无关数据文件. 缺省值是/usr/local/share
includedir
安装头文件. 缺省值是/usr/local/include
libdir
安装库文件. 不要在这里安装可执行文件, 它们可能应属于$(libexecdir)
. 缺省值是/usr/local/lib
infodir
安装软件包的Info文件. 缺省值是/usr/local/info
mandir
安装该程序的手册. 其正常的值是/usr/local/man
makefile中一些约定俗成的目标
这些目标在开源软件中常见, 它们安规范编译安装到相关目录.
对实际项目中如何命名这些目标具有一定的借鉴意义.
all
编译整个程序
check
执行自我检查. 编写一个自我测试程序, 在程序已建立但没有安装时执行
clean
删除生成的文件
distclean
删除生成的文件,包括可能存在的配置文件
installdirs
创建文件要安装的目录
install
编译程序并将可执行程序,库文件等拷贝到为实际使用保留的文件名下
uninstall
删除所有由install目标拷贝安装的文件
TAGS
更新该程序的标志表
info
使用makeinfo
程序产生的info文件
dist
为程序创建一个tar文件. 创建tar文件可以将其中的文件名以子目录名开始, 这些子目录名可以是用于发布的软件包名
参考
[1] 官方文档: https://www.gnu.org/software/make/manual/make.html
[2] 《Managing Projects with GNU Make, Third Edition》 by Robert Mecklenburg