Makefile


前言:make是Linux平台的一个应用软件,这个软件根据Makefile规则来完成编译代码的工作,所以我们主要学习两点,一点就是Makefile的规则,另一点则是make这个软件如何使用。
这个跟学习C/C++语言规则和C/C++代码编译工具如何使用是类似的道理。一个项目中的文件一般非常多,我们按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。
makefile带来的好处就是“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
一、首先要学习C/C++代码编译工具和一些其他工具的使用
1、预处理(展开头文件)
# gcc -E xxx.c -o xxx.i
运行后会将xxx.c里包含的所有头文件的内容原地展开然后写入xxx.i文件里。
2、编译(生产汇编代码)
# gcc -S xxx.i -o xxx.s
运行后会将xxx.i文件里的内容编译成汇编代码写入xxx.s文件。
3、汇编(生成目标文件)
# gcc -c xxx.s -o xxx.o
运行后会将xxx.s汇编代码编译成二进制目标文件xxx.o。
4、链接(生成可执行文件)
# gcc main.c xxx.o -o xxx
运行后会将带主函数的main.c以及需要的.o编译为一个可执行程序xxx。
5、直接将.c文件编译为.o文件
# gcc -c xxx.c -o xxx.o
这样即可跳过生成.i和.s文件。
6、指定头文件目录(-I后面直接加路径)
我们的.c文件包含头文件.h时,需要告诉编译工具在哪里找这些.h。当然编译工具有默认的寻找头文件的目录,这个不同的平台是不一样的。当我们要指定其他目录为寻找头文件的路径时,运行下面命令:
# gcc -c xxx.c -o xxx.o -I./include
7、定义宏(-D后面直接加宏)

# gcc -c xxx.c -o xxx.o -DPLATEFORM_TYPE=0x001`
# gcc -c xxx.c -o xxx.o -DUSEOPENSSL`

8、编译release程序要加的参数
# gcc -c xxx.c -o xxx.o -Wall -Os
release程序是直接发布使用的。
9、编译debug程序要加的参数
# gcc -c xxx.c -o xxx.o -Wall -g
debug程序是定位问题使用的,一般需要配合gdb来使用
10、生成静态库,将多个.o打包成一个.a文件后,程序链接的时候只要加一个.a文件即可
生成静态库:
# ar -rv libxxx.a xxx1.o xxx2.o
链接静态库:
# gcc main.c -o xxx -lxxx -L. -static
(-lxxx可替换为libxxx.a,-L.表示在当前目录下找libxxx.a)
11、生成动态库,将多个.o打包成一个.so文件后,程序链接的时候只要加一个.so文件即可,但是程序运行的时候,需要把该.so或者优化后的.so放在系统动态库查找路径下(linux平台一般是/usr/lib/或者/lib/)。
与静态库相比的优点就是更新代码时,不需要重新编译我们的可执行程序,直接将新代码编译为so文件,替换原先的so文件即可。
生成动态库:
# gcc -fPIC -shared -o libxxx.so xxx1.o xxx2.o
链接动态库:
# gcc main.c -o xxx -lxxx -L.
(-lxxx可替换为libxxx.so,-L.表示在当前目录下找libxxx.so)
12、生成依赖关系.d文件(文件里将会记录该源文件依赖的所有头文件)
# gcc -I./include -MM -MD -c test.c -o test.d
13、nm
nm命令可以查看一个库或者可执行程序的符号信息,nm后面直接跟文件名即可查看该文件的符号信息。
14、strip
strip 命令减少 XCOFF 对象文件的大小。strip 命令从 XCOFF 对象文件中有选择地除去行号信息、重定位信息、调试段、typchk 段、注释段、文件头以及所有或部分符号表。
一旦您使用该命令,则很难调试文件的符号;因此,通常应该只在已经调试和测试过的生成模块上使用 strip 命令。使用 strip 命令减少对象文件所需的存储量开销。
例子:(假如out为未strip过的可执行程序),执行以下3条命令即可查看strip前后的out符号信息。
# nm out
# strip out
# nm out
15、rm
# rm -rf xxx
(强制删除)
16、cp
# cp -rf xxx xx
(强制拷贝)
17、sed
(1)简介
sed是非交互式的编辑器。它不会修改文件,除非使用shell重定向来保存结果。默认情况下,所有的输出行都被打印到屏幕上。
sed编辑器逐行处理文件(或输入),并将结果发送到屏幕。具体过程如下:首先sed把当前正在处理的行保存在一个临时缓存区中(也称为模式空间),然后处理临时缓冲区中的行,完成后把该行发送到屏幕上。
sed每处理完一行就将其从临时缓冲区删除,然后将下一行读入,进行处理和显示。处理完输入文件的最后一行后,sed便结束运行。sed把每一行都存在临时缓冲区中,对这个副本进行编辑,所以不会修改原文件。
(2)定址
定址用于决定对哪些行进行编辑。地址的形式可以是数字、正则表达式、或二者的结合。如果没有指定地址,sed将处理输入文件的所有行。
地址是一个数字,则表示行号;是“$"符号,则表示最后一行。
例如:
只打印第三行:
# sed -n '3p' datafile
只查看文件的第100行到第200行:
# sed -n '100,200p' mysql_slow_query.log
地址是逗号分隔的,那么需要处理的地址是这两行之间的范围(包括这两行在内)。范围可以用数字、正则表达式、或二者的组合表示。 删除第二到第五行:
# sed '2,5d' datafile
删除包含"My"的行到包含"You"的行之间的行:
# sed '/My/,/You/d' datafile
删除包含"My"的行到第十行的内容:
# sed '/My/,10d' datafile
sed命令的详细使用可单独学习。
18、patsubst、subst
(1)patsubst

格式:$(patsubst <pattern>,<replacement>,<text>)`
名称:模式字符串替换函数——patsubst。
功能:查找<text>中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式<pattern>,如果匹配的话,则以<replacement>替换。
这里,<pattern>可以包括通配符“%”,表示任意长度的字串。如果<replacement>中也包含“%”,那么,<replacement>中的这个“%”将是<pattern>中的那个“%”所代表的字串。(可以用“\”来转义,以“\%”来表示真实含义的“%”字符)
返回:函数返回被替换过后的字符串。

例子:
# $(patsubst %.c,%.o, test.c bar.c)
把字串”test.c bar.c”符合模式[%.c]的单词替换成[%.o],返回结果是”test.o bar.o”
(2)subst

格式:$(subst <from>,<to>,<text>)
名称:字符串替换函数——subst。
功能:把字串<text>中的<from>字符串替换成<to>。
返回:函数返回被替换过后的字符串。

例子:
# $(subst hello, hi, hello world !!!)
把字串“hello world !!!”中的”hello”替换成”hi”,返回结果是”hi world !!!”
二、makefile基本概念(注意下面使用的[tab]代表键盘按下Tab键,并且不可用空格替换[tab])
1、make和makefile的关系
make命令本身并不知道该如何做,需要有人告诉它如何来做。通常这些都写在一个叫做Makefile(makefile)的文件中,make命令依赖这个文件进行工作。
Makefile(makefile)文件是make命令缺省使用的文件,当然也可以用命令行参数指定其他文件。
比如:
# make -f Rules.make
2、makefile基本规则
makefile由一些列的规则构成,每条规则如下:

target : prerequisites
[tab] recipe

解释:
target :目标,通常是输出文件名(可以是多个),也可以是某个特定操作的名字(伪目标)。
prerequisites :前提条件,通常是文件名,是用于创建目标的输入文件,可以理解为依赖关系。
recipe :动作,可以使多条命令,前面必须加tab,另外每一行对应一个命令。
例子:
makefile文件内容如下:

test : test1.o test2.o test3.o
[tab]gcc -o test test1.o test2.o test3.o 
test1.o: test1.c test.h
[tab]gcc -c test1.c 
test2.o: test2.c test.h
[tab]gcc -c test2.c 
test3.o: test3.c
[tab]gcc -c test3.c -o test3.o 
clean: 
[tab]rm -f test test1.o test2.o test3.o

注意:
(1)、依赖关系寻找测试
修改 test3.c后make
修改 test1.c后make
修改 test.h 后make
make clean 后make
(2)、执行make命令时,默认生成makefile中的第一个目标test
例子:调换规则test和test1.o后执行make
(3)、make后面可以加上目标名字,目标可以是多个。
例子:
# make test
# make test1.o test3.o
# make clean
3、makefile静态模式规则

targets : target-pattern: prereq-patterns 
[tab] recipe

解释:
targets :目标
target-pattern:目标的模式
prereq-patterns:前提条件的模式
recipe :命令
例子

objects = test1.o test2.o
$(objects): %.o: %.c
[tab]gcc -c $< -o $@

注意:$< 表示第一个前提条件 ,$@表示目标,后面会讲到。
例子相当于下面的内容

test1.o:test1.c
[tab]gcc -c $< -o $@
test2.o:test2.c
[tab]gcc -c $< -o $@

4、makefile隐含规则

target : prerequisites
没有recipe

例子:

test.o: test.c

相当于下面的内容(C语言编译规则)

test.o:test.c
[tab]gcc -c $< -o $@

例子:

test.o: test.cpp

相当于下面的内容(C++语言编译规则)

test.o:test.cpp
[tab]g++ -c $< -o $@

当然还有很多其他语言的隐含规则,这里就不细展开了,总之看到一个规则没有相应的动作也没有看到依赖关系,就要注意隐含规则了另外,隐含规则是和make相关的,不同版本的make是有差异的。
5、递归调用make

subsystem:
[tab]cd subdir && $(MAKE)

或者

subsystem:
[tab]$(MAKE) -C subdir

例子:

BASEDIR= .
SUBDIRS = $(BASEDIR)/test1 $(BASEDIR)/test2
all: $(SUBDIRS)
[tab]@for i in $(SUBDIRS); do make -C $$i || exit; done
[tab]#要注意上面必须是一行, 使用;分开多个命令

三、makefile语法
1、include/-include(sinclude)
include:包含文件,如果文件不存在将会有错误提示。
-include(sinclude:GNU支持sinclude代替-include):包含文件,如果文件不存在则不会错误提示。
2、PHONY
.PHONY : clean
申明 clean是伪目标.
主要防止如果当前目录中,正好有一个文件叫做clean,执行make clean不能得到想要的动作。
3、 #
注释,和C++中的//一样
4、echo
显示。
5、@
make执行时不显示当前行
例子:

sample:
[tab]# sample
[tab]echo "sample" 
[tab]@ # sample 
[tab]@echo "sample"

# make sample
6、* (通配符, 还包括? /[…]/ ~)
例子:

clean:
[tab]rm -f *.o
[tab]rm -f *.d

# make clean
7、%
%.o:%.c
表示有一系列的规则,规则的个数等于当前目录下.c文件的个数,而每个规则的目标都是以一个.c文件的文件名去掉c加上o构成,前提条件为该.c文件名。
等价于下面语句:
.c.o:
8、$@
规则中目标的名字
9、$<
规则中第一个前提条件的名字
10、$^
所有的前提条件
11、其他还有$?, $*,$(@D), $(@F),$(<D) , $(<F)...
以上这些又称为自动变量(Automatic Variables),具体代表什么就不展开了。
12、变量及赋值
VARIABLE = value:变量的值在执行时赋
VARIABLE := value:变量的值在定义时赋
VARIABLE ?= value:只有在该变量为空时才赋值
VARIABLE += value:将值追加到变量的尾端
例子:

BASEDIR = .
SUBDIRS = $(BASEDIR)/test1 $(BASEDIR)/test2
SUBDIRS += $(BASEDIR)/test3

13、export
例子

WORKDIR = /home/work
export WORKDIR

14、条件

ifeq(arg1, arg2)/ifneq(arg1, arg2)
[tab] 
else 
[tab] 
endif 

例子:

libs_for_gcc = 
normal_libs = 
ifeq ($(CC),gcc)
[tab]libs=$(libs_for_gcc) 
else 
[tab]libs=$(normal_libs) 
endif 
foo: $(objects)
[tab]$(CC) -o foo $(objects) $(libs)

四、完整示例
示例文件下载:MakefileExample.7z
一个test库与主函数的工程make示例。该示例中test库目录下有一个makefile,负责编译test库。根目录下有一个makefile,负责编译主函数及链接test库。test库的源文件修改头文件时不需要clean,直接make即可,因为makefile中已经添加了.d依赖文件。
(1)、文件结构如下:





文件内容如下:
Print1.h:

#ifndef _PRINT1_H_
#define _PRINT1_H_

#ifdef __cplusplus
extern "C"{
#endif

#define TESTD 2

void print1();

#ifdef __cplusplus
}
#endif

#endif

Print2.h:

#ifndef _PRINT2_H_
#define _PRINT2_H_

#ifdef __cplusplus
extern "C"{
#endif

void print2();

#ifdef __cplusplus
}
#endif

#endif

Print1.c:

#include <stdio.h>
#include "Print1.h"

void print1()
{
    int a = PRINT;
    printf("print1:%d %d\n", a, TESTD);
}

Print2.cpp:

#include <stdio.h>
#include "Print2.h"

void print2()
{
    printf("print2-OUT\n");
}

Test目录下的Makefile:

BASEDIR=.

CPP   =   @echo " g++ $@"; g++
CC    =   @echo " gcc $@"; gcc
LD    =   @echo " ld  $@"; ld
AR    =   @echo " ar  $@"; ar
STRIP =   @echo " strip $@"; strip
RM    =   @rm -rf
CP    =   @cp -rf

#定义宏PRINT 5
CFLAGS += -DPRINT=5
#release编译选项
CFLAGS += -Wall -Os
#debug编译选项
#CFLAGS += -Wall -g
#指定头文件目录
CFLAGS += -I$(BASEDIR)/Include
#64位系统需要此参数
CFLAGS += -fPIC

#静态库编译参数
AFLAGS = -rv

SRCS = $(BASEDIR)/Src/Print1.c $(BASEDIR)/Src/Print2.cpp

#字符串替换获取.o目标文件
TMPOBJS = $(patsubst $(BASEDIR)/%.cpp, $(BASEDIR)/%.o, $(SRCS))
OBJS = $(patsubst $(BASEDIR)/%.c, $(BASEDIR)/%.o, $(TMPOBJS))

#字符串替换获取.d文件
OBJD = $(patsubst $(BASEDIR)/%.o, $(BASEDIR)/%.d, $(OBJS))

LIBTEST = $(BASEDIR)/Lib/libtest.a
LIBTESTSO = $(BASEDIR)/Lib/libtest.so

all: $(LIBTEST) $(LIBTESTSO)

$(LIBTEST): $(OBJS)
    $(RM) $@
    $(AR) $(AFLAGS) $@ $^

$(LIBTESTSO): $(OBJS)
    $(RM) $@
    $(CC) -fPIC -shared -o $@ $^

%.o: %.c
    $(CC)  -c $(CFLAGS) $< -o $@
%.o: %.cpp
    $(CPP)  -c $(CFLAGS) $< -o $@

#sed的作用:给.d文件里的.o添加路径
%.d: %.c
    $(CC) $(CFLAGS) -MM -MD -c $< -o $@
    @sed 's/.*\.o/$(subst /,\/,$(dir $@))&/g' $@ >$@.tmp
    @sed 's/ /\t/g' $@.tmp >$@
    @rm $@.tmp
%.d: %.cpp
    $(CPP) $(CFLAGS) -MM -MD -c $< -o $@
    @sed 's/.*\.o/$(subst /,\/,$(dir $@))&/g' $@ >$@.tmp
    @sed 's/ /\t/g' $@.tmp >$@
    @rm $@.tmp

#clean时不生成.d文件
ifneq ($(MAKECMDGOALS), clean)
-include $(OBJD)
endif

clean:
    $(RM) $(OBJS)
    $(RM) $(OBJD)
    $(RM) $(LIBTEST)
    $(RM) $(LIBTESTSO)

Main.c:

#include <stdio.h>
#include "Print1.h"
#include "Print2.h"

int main(int argc, char *argv[])
{
    print1();
    print2();

    return 0;
}

根目录下的Makefile:

BASEDIR = .
SUBDIRS = $(BASEDIR)/Test

CPP   =   @echo " g++ $@"; g++
CC    =   @echo " gcc $@"; gcc
LD    =   @echo " ld  $@"; ld
AR    =   @echo " ar  $@"; ar
STRIP =   @echo " strip $@"; strip
RM    =   @rm -rf
CP    =   @cp -rf

#release编译选项
CFLAGS += -Wall -Os
#debug编译选项
#CFLAGS += -Wall -g
#指定头文件目录
CFLAGS += -I$(BASEDIR)/Test/Include
#64位系统需要此参数
CFLAGS += -fPIC

#静态库编译参数
AFLAGS = -rv

SRCS = $(BASEDIR)/Main/Main.c
LIBS = $(BASEDIR)/Test/Lib/libtest.a

DIST = $(BASEDIR)/out

$(DIST): subdir
    $(CPP) $(SRCS) -o $@ $(CFLAGS) $(LIBS)
    $(STRIP) $@

subdir:
    @for i in $(SUBDIRS); do make -C $$i || exit; done

clean: subdirclean
    $(RM) $(DIST)

subdirclean:
    @for i in $(SUBDIRS); do make clean -C $$i || exit; done

这样,我们在根目录下执行make就可以生成对应的库和可执行程序,并且修改任何一个源码文件都不需要clean,直接make就可以。当然如果想clean的话,直接执行make clean即可。