Makefile学习笔记

一、关于Makefile的一些Tips

1. 整个Makefile是按照“依赖关系树”来执行的,Makefile中的所有依赖根据各种规则定义出来,Makefile识别这些规则,并建立一个树状的依赖关系数据结构,并根据该数据结构的指导,有序地进行各项操作,从“依赖关系树”的各个叶子节点汇集到根节点,即Makefile的最终target,Build出target。这里的“依赖关系”实际上就是用了说明“目标文件是由哪些文件生成的”。当“依赖关系树”中的某个节点发生了改变,那么再次执行make时,从该节点往上到根节点,所有直接或间接依赖于该节点的中间节点和根节点都会被重新执行。

2. 在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个Tab键作为开头。记住,make并不管命令是怎么工作的,他只管执行所定义的命令。make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。

3. Makefile中,一个标签的“:”后面(同一行)如果没有其他内容,则该标签只是一个动作的名字,有点像C语言中的lable,make就不会自动去找什么依赖性,而是去执行该标签的下一行所定义的命令(以一个Tab开头)。

4. make命令通常以在Makefile文件中找到的第一个标签作为其build的目标。而对于其他标签,如果根据依赖关系,能遍历到,则该标签对应的内容就会被执行,否则不会被执行。对于这种默认不会被执行到的标签,也可以显式地让它被执行。方法就是,在make命令后面显式地加上该标签。

5. 对于下面的四行,第三行和第四行中都有$(objects),看上去貌似重复定义了,但实际上二者有着不同的含义。第三行实际上是定义了“依赖关系树”中的edit节点和它的所有子节点之间的依赖关系数据结构,$(objects)在这里充当依赖树中的edit节点的子节点。而第四行定义了“为了生成edit节点,需要做什么动作”,$(objects)在这里为make提供了生成edit所需要的参数。即,第三行和“创建树状数据结构”相关,第四行定义的是“生成某个节点的具体参数”。

objects = main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o
edit : $(objects)
    cc -o edit $(objects)

6. Makefile 文件一般都以 Makefile 或 makefile 作为文件名,这两个文件名任何make 命令都能识别。如果以其他文件名命名Makefile文件,比如 Make.Linux,可以这样让make命令加载它:make -f Make.Linux

7. 在 Makefile 中,规则的顺序是很重要的,因为,Makefile 中只应该有一个最终目标,其它的目标都是被这个目标所连带出来的,所以一定要让 make 知道你的最终目标是什么。一般来说,定义在 Makefile 中的目标可能会有很多,但是第一条规则中的目标将被确立为最终的目标。 如果第一条规则中的目标有很多个, 那么, 第一个目标会成为最终的目标。make所完成的也就是这个目标。

8. 在 Makefile 中的命令, 必须要以[Tab]键开始。

9. Makefile 规则包含两个部分,一个是依赖关系,一个是生成目标的方法

10. 标签的下一行可以不是命令,比如:

kbd.o : defs.h command.h

其实,它包含隐含的生成目标的方法的命令:

    cc -c kbd.c

再比如:

objects = main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o
all : edit
edit : $(objects)
    cc -o edit $(objects)

这里的 all : edit 一行后面就没有定义什么命令,但它会间接调用 cc -o edit $(objects)

11. 关于通配符

对于 objects = *.o 这样的变量定义,并不是说[*.o]会展开,事实上,objects的值就是“*.o”。Makefile中的变量其实就是C/C++中的宏。如果你要让通配符在变量中展开,也就是让objects的值是所有[.o]的文件名的集合,那么,你可以这样:objects := $(wildcard *.o),其中 wildcard 是Makefile 的关键字。可以尝试用这种方法在Makefile中打印出各种Makefile 自动变量的值。

Makefile 中的"%"的意思是匹配零或若干字符,例如,“%.h”表示所有以“.h”结尾的文件。

12. 带问号的变量赋值

CC ?= gcc

的意思是,如果CC没有被赋过值,则 CC=gcc,否则不做任何操作,即CC取先前已赋的值。

13. Makefile.in中的 cc=@cc@ 是autoconf的语法,在生成Makefile时自动将@cc@匹配成系统的C编译器

14. g++/gcc的 -c 选项的意思是,编译当前源文件,但不做 link 操作,源文件的输出为 object 文件。默认情况下,输出的 object 文件的文件名就是把源文件的后缀改成 ".o"

15. link 操作是在 有link选项的命令 中做的,例如下面的以 $(CC) 开头的命令

LNK_OPT = -ldl -lrt -lpthread  -lexpat -lssl -lcrypto -L$(SNMP_LIBS) -lecomshm $(EB_LIBS) $(SAFEC_LIBS) -lz
$(TARGET) : $(OBJS)
    $(CC) -o $@ $(OBJS) $(LNK_OPT)

16. Makefile 中的编译器如果使用Makefile 预定义的变量 CXX,则默认使用 g++ 编译器,除非显式地给CXX 赋了别的值,比如gcc

17. Makefile 中的自动变量

$@ 代表规则中的目标文件名。如果目标是一个文档(Linux中,一般称.a文件为文档),那么它代表这个文档的文件名。在多目标的模式规则中,它代表的是哪个触发规则被执行的目标文件名

$% 规则的目标文件是一个静态库文件时,代表静态库的一个成员名。例如,规则的目标是“foo.a(bar.o)”,那么,“$%”的值就为“bar.o”,“$@”的值为“foo.a”。如果目标不是函数库文件,其值为空。

$< 规则的第一个依赖文件名。如果是隐含规则,则它代表通过目标指定的第一个依赖文件。


二、用一个例子说明Makefile的一些写法

1. 下面是一个简单的Makefile例子:

edit : main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o

cc -o edit main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o
main.o : main.cpp defs.h
    cc -c main.cpp
kbd.o : kbd.cpp defs.h command.h
    cc -c kbd.cpp
command.o : command.cpp defs.h command.h
    cc -c command.cpp
display.o : display.cpp defs.h buffer.h
    cc -c display.cpp
insert.o : insert.cpp defs.h buffer.h
    cc -c insert.cpp
search.o : search.cpp defs.h buffer.h
    cc -c search.cpp
files.o : files.cpp defs.h buffer.h command.h
    cc -c files.cpp
utils.o : utils.cpp defs.h
    cc -c utils.cpp
clean :
    rm edit main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o

“依赖关系树”如下图所示:


图1

2. Makefile改进一:使用变量

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.cpp defs.h
    cc -c main.cpp
kbd.o : kbd.cpp defs.h command.h
    cc -c kbd.cpp
command.o : command.cpp defs.h command.h
    cc -c command.cpp
display.o : display.cpp defs.h buffer.h
    cc -c display.cpp
insert.o : insert.cpp defs.h buffer.h
    cc -c insert.cpp
search.o : search.cpp defs.h buffer.h
    cc -c search.cpp
files.o : files.cpp defs.h buffer.h command.h
    cc -c files.cpp
utils.o : utils.cpp defs.h
    cc -c utils.cpp
clean :
    rm edit $(objects)

这样改进,避免了众多目标文件名的三处拷贝,以后只要在一个地方维护。并且,如果有新的 .o 文件加入,我们只需简单地修改一下 objects 变量就可以了。


3. Makefile改进二:让make自动推导

可以自动推导文件以及文件依赖关系后面的命令。只要make看到一个[xxx.o]文件,它就会自动的把[xxx.c]文件加在依赖关系中,如果make找到一个whatever.o,那么whatever.c,就会是whatever.o的依赖文件。并且 cc -c whatever.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂(事实上,省略了图1中的跟节点往下的第四层)。这种方法,也就是make的“隐晦规则”。

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 : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h
clean :
    rm edit $(objects)

4. Makefile改进三:简洁的目标文件依赖描述

如果觉得2中的一堆[xxx.o]和[xxx.h]不爽,可以再次利用make的自动推导功能,进一步简化makefile的依赖描述,即,把所有依赖某个[xxx.h]的[xxx.o]文件汇集到一起。

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

clean :
    rm edit $(objects)

这种风格,让makefile变得很简单,但文件依赖关系就显得有点凌乱了。一是文件的依赖关系看不清楚,二是如果文件一多,要加入几个新的.o文件,那就理不清楚了。一般不建议使用这种风格的依赖描述。

5. Makefile改进四(基于3):对于clean的改进

clean:
    rm edit $(objects)

上面的clean工作是可以的,但更为稳健的做法是

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

.PHONY意思表示clean是一个“伪目标”(即通过不加参数的make命令不能执行到的目标)。在rm命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。另外,clean的标签不要放在文件的开头,不然,clean就会变成make的默认目标。一条不成文的规矩是——“clean从来都是放在文件的最后”,当然,“做rpm build”之类的标签可以放在clean的后面。使用.PHONY标签还有一个原因,“clean”有可能和某个真实的目标重名,使用了.PHONY标签,可以显示地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。“伪目标”不是一定没有依赖,它也可以定义依赖,只不过,通过不加参数的make命令不能执行到“伪目标”。还有一点需要说明,可以通过下面的方式一次性生成多个最终目标文件,其中all 是Makefile 中的第一个目标,prog1、prog2 和prog3 是真正的最终目标:

all : prog1 prog2 prog3
.PHONY : all

6. Makefile改进五(基于3):使用自动变量、使用子目录、普遍使用变量、include路径、link选项、自动创建销毁子目录、使用CXX变量

(前提:首先创建 h 和 src 两个子文件夹,并将所有 [.h] 文件放进 h 文件夹,将所有 [.cpp] 文件放进 src 文件夹,Makefile 文件也放入src 文件夹)

OBJ_DIR = ../obj
BIN_DIR = ../bin
INC_DIR = ../h
SRC_DIR = ./
OBJS = \
    $(OBJ_DIR)/main.o \
    $(OBJ_DIR)/command.o \
    $(OBJ_DIR)/display.o \
    $(OBJ_DIR)/insert.o \
    $(OBJ_DIR)/search.o \
    $(OBJ_DIR)/files.o \
    $(OBJ_DIR)/utils.o
TARGET = $(BIN_DIR)/edit
INC_OPT = -I$(INC_DIR)
LNK_OPT =


$(TARGET) : chkobjdir chkbindir $(OBJS)
    $(CXX) -o $@ $(OBJS) $(LNK_OPT)


$(OBJ_DIR)/%.o : $(SRC_DIR)/%.cpp
    $(CXX) $(INC_OPT) -c -o $@ $<


chkobjdir 
:
    @if test ! -d $(OBJ_DIR) ; \
    then \
        mkdir $(OBJ_DIR) ; \
    fi


chkbindir :
    @if test ! -d $(BIN_DIR) ; \
    then \
        mkdir $(BIN_DIR) ; \
    fi


.PHONY : clean
clean :
    -rm -f $(TARGET)
    -rm -rf $(OBJ_DIR)

说明如下:

1) 定义并使用了大量的变量,如OBJ_DIR、BIN_DIR、OBJS、INC_OPT、TARGET 等,使得各个配置项更容易维护。

2) 使用了子目录,如h、src、bin、obj 等子文件夹,用来放置不同类型的文件,使得项目在目录层次上更清爽。

3) 增加了include 选项,使得[.h] 文件的查找更具一致性

INC_OPT = -I$(INC_DIR)
$(OBJ_DIR)/%.o : $(SRC_DIR)/%.cpp
    $(CXX) $(INC_OPT) -c -o $@ $<

4) 使用CXX 预定义变量,代替了CC 预定义变量。对于C++ 代码,前者默认使用g++ 编译器,后者从Makefile 输出看是 cc,可能是 gcc编译器,编译C++ 代码时,可能会遇到 类似 “undefined reference to `std::cout'” 之类的报错。建议使用CXX 预定义变量。

5) 加了link 选项,虽然还没有定义具体的值,但在需要用到时,只要修改 LNK_OPT 变量的值即可

LNK_OPT =
$(TARGET) : chkobjdir chkbindir $(OBJS)
    $(CXX) -o $@ $(OBJS) $(LNK_OPT)

6) 在往 OBJ_DIR、BIN_DIR 等子目录写入文件之前,先检查子目录是否存在,不存在,则先创建,避免“向不存在的目录写入文件”。

$(TARGET) : chkobjdir chkbindir $(OBJS)
...
chkobjdir :
    @if test ! -d $(OBJ_DIR) ; \
    then \
        mkdir $(OBJ_DIR) ; \
    fi
chkbindir :
    @if test ! -d $(BIN_DIR) ; \
    then \
        mkdir $(BIN_DIR) ; \
    fi

7) 利用Makefile 的自动推导和自动变量,进一步简化了目标文件的依赖关系和生成规则

$(TARGET) : chkobjdir chkbindir $(OBJS)
    $(CXX) -o $@ $(OBJS) $(LNK_OPT)
$(OBJ_DIR)/%.o : $(SRC_DIR)/%.cpp
    $(CXX) $(INC_OPT) -c -o $@ $<

其中,自动变量$@ 是目标文件名,自动变量$< 是“当前规则的第一个依赖文件的文件名”,例如,对于search.cpp,

$(CXX) $(INC_OPT) -c -o $@ $<

的输出是 

g++ -I../h -c -o ../obj/search.o search.cpp

即 $@ 翻译成了search.o,而$< 翻译成了search.cpp,从而所有的 [.o] 文件的依赖和生成规则由两行定义就足够了,使得Makefile 更加清爽,并且删除或添加某个 [.cpp] 文件时,不用修改这两行。

注意,对于search.cpp,完整的依赖和生成规则事实上应该是这样的:

search.o : search.cpp defs.h buffer.h
    g++ -I../h -c -o ../obj/search.o search.cpp

但是,下面的写法中,省略了第一行中所有的 [.h],这个信息是由make 分析源文件自动获取的。同时,第一行中也没有明确指明 search.cpp,而是列举了src 目录下的所有 [.cpp] 文件,这也是make 根据 “[.h] 文件和 [.cpp] 文件同名” 的原则自动获取的,这就要求源文件的命名方式遵循一个“潜规则”,即 [.h] 文件和 [.cpp] 文件尽量同名!否则,make 可能会给出 类似 “ No rule to make target `../obj/dmd.o', needed by `../bin/edit” 的错误信息,make失败。

$(OBJ_DIR)/%.o : $(SRC_DIR)/%.cpp
    $(CXX) $(INC_OPT) -c -o $@ $<

7. 同时生成多个最终目标文件(基于3)

例子工程在这里,可以作为zip包下载

这里使用了目标all,all 中就会生成两个目标target1 和target2

OBJ_DIR = ../obj
BIN_DIR = ../bin
INC_DIR = ../h
SRC_DIR = ./
OBJS = \
    $(OBJ_DIR)/main.o \
    $(OBJ_DIR)/command.o \
    $(OBJ_DIR)/display.o \
    $(OBJ_DIR)/insert.o \
    $(OBJ_DIR)/search.o \
    $(OBJ_DIR)/files.o \
    $(OBJ_DIR)/utils.o
TARGET1 = $(BIN_DIR)/edit
TARGET2 = $(BIN_DIR)/edit2

INC_OPT = -I$(INC_DIR)
LNK_OPT =

all : target1 target2

target1: $(TARGET1)

target2: $(TARGET2)

$(TARGET1) : chkobjdir chkbindir $(OBJS)
    $(CXX) -o $@ $(OBJS) $(LNK_OPT)

$(TARGET2) : $(OBJS)
    $(CXX) -o $@ $(OBJS) $(LNK_OPT
)

$(OBJ_DIR)/%.o : $(SRC_DIR)/%.cpp
    $(CXX) $(INC_OPT) -c -o $@ $<

chkobjdir :
    @if test ! -d $(OBJ_DIR) ; \
    then \
        mkdir $(OBJ_DIR) ; \
    fi

chkbindir :
    @if test ! -d $(BIN_DIR) ; \
    then \
        mkdir $(BIN_DIR) ; \
    fi

.PHONY : clean
clean :
    -rm -f $(TARGET1)
    -rm -f $(TARGET2)

    -rm -rf $(OBJ_DIR)


附:

有时写个小demo大可不必分出许多文件夹,这时写个两行文字的Makefile足矣,甚至直接在命令行敲一个命令就可以了,比如下面这一小段用于测试openssl消息摘要算法的代码。


#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>

int main(int argc, char* argv[])
{
EVP_MD_CTX mdctx;
const EVP_MD *md;
char mess1[] = "Test Message\n";
char mess2[] = "Hello World\n";
unsigned char md_value[EVP_MAX_MD_SIZE];
unsigned int md_len;
int i;

OpenSSL_add_all_digests();

if(!argv[1]) {
       printf("Usage: mdtest digestname\n");
       exit(1);
}

md = EVP_get_digestbyname(argv[1]);

if(!md) {
       printf("Unknown message digest %s\n", argv[1]);
       exit(1);
}

EVP_MD_CTX_init(&mdctx);
EVP_DigestInit_ex(&mdctx, md, NULL);
EVP_DigestUpdate(&mdctx, mess1, strlen(mess1));
EVP_DigestUpdate(&mdctx, mess2, strlen(mess2));
EVP_DigestFinal_ex(&mdctx, md_value, &md_len);
EVP_MD_CTX_cleanup(&mdctx);

printf("Digest is: ");
for(i = 0; i < md_len; i++) printf("%02x", md_value[i]);
printf("\n");

return 0;
}

假设我们知道openssl 的头文件位于 /yasi/code/openssl-1.0.1c/include,而openssl 库文件位于 /usr/lib64,于是Makefile文件中只需要两行即可:

all:
        g++ -o mdtest mdtest.cpp -I/yasi/code/openssl-1.0.1c/include -lssl -lcrypto -L/usr/lib64

或者可以直接在命令行运行这条命令:

g++ -o mdtest mdtest.cpp -I/yasi/code/openssl-1.0.1c/include -lssl -lcrypto -L/usr/lib64

可执行文件就build出来了


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值