Makefile学习

说明

是选择cmake还是make,这个问题不用纠结。make必须会,自己平时写小程序,一定要会make。而cmake有时候组织一些大工程会很有用,这里不是说make不能做大工程。
make涉及到源文件到目标文件,目标文件到可执行文件,如果这些概念不清楚,先将这些概念性东西理解清楚,更加方便下面的内容。

编写测试文件时小技巧:
如果在vim中编辑Makefile文件,可以在命令行模式下输入 ‘:! make target’ 快速测试,不需要再退出Makefile在shell终端执行make target。

本文档很多东西没有涵盖,也没有做过多的讲解,详细请参考官方make文档。如果已经对Makefile有一点基础,建议直接看实战项目。


Makefile语法规则

    target ... : prerequisites ...
        receipe 
        ...
        ...

target是可执行文件或目标文件的名字。(还有一种情况就是伪目标,后面讲)
prerequisites是一个文件,使用这些文件创建最后的目标。
recipe是一个make执行的命令。

注意:每一行的recipe之前一定要有一个tab字符。
说明:Makefile文件名不能更改,除非你指定make读取指定的文件(不建议这种做法)。


简单例子

main : main.o hello.o foo.o
    g++ -o main main.o hello.o foo.o

main.o : main.cpp hello.h foo.h
    g++ -c main.cpp

hello.o : hello.cpp hello.h
    g++ -c hello.cpp

foo.o : foo.cpp foo.h
    g++ -c foo.cpp

.PHONY : clean
clean : 
    rm main main.o hello.o foo.o

上面逻辑很清楚,用源文件创建出目标文件,然后将所有的目标文件创建出最后的可执行文件。clean是一个伪目标,执行make clean将清除可执行文件main,main.o,hello.h和foo.h。


简单例子升级(使用变量)

使用变量来缩减代码量。

objects = main.o hello.o foo.o

main : $(objects)
    g++ -o main $(objects)

main.o : main.cpp hello.h foo.h
    g++ -c main.cpp

hello.o : hello.cpp hello.h
    g++ -c hello.cpp

foo.o : foo.cpp foo.h
    g++ -c foo.cpp

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

说明:变量实际上就是进行了内容的替换。
变量不用声明,可以直接使用,使用$(variable)获取变量的值。


简单例子升级(make自动推断)

实际上make可以自动推断recipes,如果你的target是目标文件,prerequisite是源文件,那么可以省去g++ -o xxx.cpp。看下面代码:

objects = main.o hello.o foo.o

main : $(objects)
    g++ -o main $(objects)

main.o : main.cpp hello.h foo.h
hello.o : hello.cpp hello.h
foo.o : foo.cpp foo.h

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

以上代码省略了recipes,让make自动推断,编译出目标文件。

实际上,上面代码还可以省:

objects = main.o hello.o foo.o

main : $(objects)
    g++ -o main $(objects)

$(objects) : hello.h foo.h

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

上面将上面三条进行合并,这些目标文件并不需要按照顺序,make会根据源代码自动推断,生成目标文件。


Makefile组成

Makefile可以说非常像shell脚本,它里面有显式规则,隐式规则,变量,指令和注释。
下面将粗略讲讲各个组成部分
 显式规则:列出所有的prerequisite和recipes,参考:简单例子。
 隐式规则:用make自动推断,参考:简单例子升级(make自动推断)。
 变量:包括自定义变量和内置变量。
 指令:include,define等指令。include指令可以包含其它Makefile,define指定可以定义变量。
 注释:以 ‘#’ 开头的一行。

提示: 当一行内容特别长,可以使用反斜杠 ‘\’ 来分割一行为两行。
读取其他Makefile可以使用include指令。

recipes中可以使用shell变量,但是有时候并不是直接使用指令。例如创建一个子系统时,通常会使用如下:

substem:
    cd subdir && $(MAKE)

这里不能显式使用make,而要使用$(MAKE)。


变量

变量分为自定义变量和内置变量。
自定义变量不需要声明,可以直接使用,使用$(variable)表示variable中的值,内置变量就是make中的一些预留变量,代表一些固定的值,例如$(MAKE)。

可以使用define指令定义一个变量,变量名中不能包含 ‘:’, ‘#’, ‘=’和空格等符号。
变量名大小写敏感,foo和FOO表示不同的变量,可以使用$(foo)${foo}取出变量foo中值。变量能够在target,prerequisite和recipe等地方使用。

记住,变量遵循严格的文本替换。

foo = c
prog.o : prog.$(foo)
    g$(foo)$(foo) -$(foo) prog.$(foo)

上面代码可以转换为一下内容。

prog.o : prog.c 
    gcc -c prog.c

变量的赋值

变量名的赋值除了使用 = 外,还可以使用 :=, ::=, ?= 和 +=。
:= 符号用于覆盖变量值并写入新值。

whoami := $(shell whoami)
whoami := byy
show: 
    @echo $(whoami)

使用make show,会输出byy。

说明:可以使用$(shell command)来执行shell命令。

那么问题来了,为什么会同时存在 = 和 := 两个符号?每次赋值不是都可以更新值吗,看看下面例子:

x = foo
y = $(x) bar
x = abc
show:
    @echo $(y)

执行make show显示结果: abc bar。

x := foo
Y := $(x) bar
x := abc
show: 
    @echo $(y)

执行make show显示结果:foo bar。

总结::= 符号取决于在makefile中的位置,变量会最后在Makefile中展开,而 “=” 会用变量最后展开的值。

?= 表示条件变量赋值操作符。
如果左边的变量已经定义,那么就将右边的值赋给变量,否则,不会进行赋值操作。

+= 表示在变量原有的情况下,增加一个新的内容,在写Makefile会非常有用。
ojects += another.o

变量值中的替换操作

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

将.o全部替换为.c,显示结果为: a.c b.c c.c

多行变量

define two-lines = 
echo foo 
echo $(bar)
endef 

如果变量右边的内容太多,可以使用多行变量。上面的代码等同于以下代码:
two-lines = echo foo; echo $(bar)

取消变量定义

使用 undefine 变量名 可以将一个变量变为undefined。

特殊变量

$<  表示第一个prerequisite
$^  表示所有的prerequisite
$@  表示目标

VPATH指定prerequisite的搜索目录

VPATH = src:../headers
...
foo.o : foo.c 

如果当前目录没有foo.c,那么会到src目录下去搜foo.c文件是否存在,如果还是没有找到那么会到../headers。

vpath指定搜索路径下的指定类型的文件名

以下是vpath的用法:

    vpath pattern directories
        指定搜索目录下匹配的文件名
    vpath pattern 
        清除与pattern相关的搜索路径
    vpath 
         清除前面指定的所有查找路径
示例:vpath %.h ../headers
结果:如果当前目录下没有找到.h类型文件,那么会到../headers目录下进行查找。

说明: vpath是指令,在Makefile中,大写的一般是变量,小写一般是指令。


条件判断

ifeq ($(CC),gcc)
    $(CC) -o foo $(objects) $(libs_for_gcc)
else
    $(CC) -o foo $(objects) $(normal_libs)
endif

条件判断的种类:ifeq, ifneq, ifdef, ifndef。


内置函数

字符串操作

$(subst from, to, text)    
    作用:字符串中子字符串替换,返回替换之后的结果。
    例子:$(subst ee, EE, feet on the street)
    结果: ‘fEEt on the strEEt’

$(patsubst pattern, replacement, text)
    作用:字符串模式匹配,进行替换,并返回替换之后的结果。
    例子:$(patsubst %.c, %.o, x.c.c bar.c)
    结果:‘x.c.o bar.o’
上面可以简写: $(objects:.c=.o)

$(strip string)
    作用:去掉字符串开头和结尾的空格。
    去掉字符串开头和结尾的空格可以让代码更加具有健壮性。

$(findstring find, in)
    作用:在字符串中查找字符串find,如果找到,则返回find,否则返回‘’
    例子:$(findstring a,a b c)
    结果:返回 ‘a’

还有很多关于文本替换的函数,这里不做过多的介绍。

foreach函数

$(foreach var,list,text)
    作用: 遍历list,执行text。

wildcard函数

通配符函数,可以出现在Makefile的任意位置。

小技巧,看下面例子:

$(wildcard *.c)

上面语句可以获取当前目录下所有的.c文件名。
下面语句可以将所有的.c文件变为.o文件。

$(wildcard %.c,%.o,$(wildcard *.c))

$(wildcard pattern…)
wildcard可以在任何地方使用。

文件名函数

$(basename names...)
    作用:去掉文件名后缀。
    例子:$(basename src/foo.c src-1.0/bar hacks)
    结果:src/foo src-1.0/bar hacks

$(addprefix prefix,names...)
    作用:增加前缀。
    例子:$(addprefix src/,foo bar)
    结果:src/foo src/bar 

$(addsuffix suffix,names...)
    作用:增加后缀。
    例子:$(addsuffix .c,foo bar)
    结果:foo.c bar.c

项目实战

看文件目录结构

├── bin
├── Makefile
├── obj
└── src
    ├── foo
    │   ├── foo.cpp
    │   └── foo.h
    ├── hello
    │   ├── hello.cpp
    │   └── hello.h
    └── main.cpp

实际项目中,需要分模块将hello和foo当做两个模块。现在如何编写Makefile?

CXXFLAGS = -Wall
LINK := $(CPP)

vpath %.cpp src
vpath %.cpp src/hello
vpath %.cpp src/foo

OBJ_PATH := obj
BIN_PATH := bin
SRC_PATH := src/hello src/foo src
INCS = -I src/hello -I src/foo

SRC = $(foreach x,$(SRC_PATH),$(wildcard $(addprefix $(x)/*,.c .cpp)))
OBJ = $(addprefix $(OBJ_PATH)/, $(addsuffix .o,$(notdir $(basename $(SRC)))))
TARGET = main

$(BIN_PATH)/$(TARGET): $(OBJ)
    $(CXX) $(CXXFLAGS) -o $@ $^

$(OBJ_PATH)/%.o: %.cpp
    $(CXX) -c $(CXXFLAGS) $(INCS) -o $@ $<

.PHONY: clean 
clean:
    rm -f $(OBJ_PATH)/* $(BIN_PATH)/* 

总结

Makefile中尽量显示出模块化的思想,尽量使用变量带代替重复的内容,不会导致改变一处而全局改变。Makefile的编写中,最重要是当做一门语言来学,学会看错误提示,不断调试。


FAQ

Q: 如何指定目录放指定文件?

A: 在编写Makefile时指定前缀。

    $(OBJ_PATH)/$(TARGET):$(OBJ)
        $(CPP) -o $@ @^

参考

[1] gnu make官网:http://www.gnu.org/software/make/manual/make.pdf
[2] Makefile模板 :http://www.oschina.net/code/snippet_113073_38647


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值