linux学习笔记-makefile的写法

<h3 id="1">前言</h3>
  一个项目,拥有成百上千的源程序文件,那如何组织这些源码文件的编译和连接呢?此时就须要肯定整个工程的编译连接规则,Makefile就是用来指定规则的。而make是一个解释makefile中指令的命令工具,通常来讲,大多数的IDE都有这个命令,好比:Delphi的make,Visual C++的nmake,Linux下GNU的make。git

<h3 id="1">Makefile介绍</h3>
  当咱们编译工程时,只要输入make就会去编译了,那么这个make命令执行的时候必定须要一个Makefile文件,经过这个Makefile告诉make命令须要怎么样的去编译和连接程序。 github

  make的工做规则是:编程

1.若是这个工程没有编译过,那么咱们的全部C文件都要编译并被连接。
2.若是这个工程的某几个C文件被修改,那么咱们只编译被修改的C文件,并连接目标程序。
3.若是这个工程的头文件被改变了,那么咱们须要编译引用了这几个头文件的C文件,并连接目标程序。

  只要咱们的Makefile写得够好,全部的这一切,咱们只用一个make命令就能够完成,make命令会自动智能地根据当前的文件修改的状况来肯定哪些文件须要重编译,从而本身编译所须要的文件和连接目标程序。socket

<h3 id="3">Makefile规则</h3>
  在讲述这个Makefile以前,仍是让咱们先来粗略地看一看Makefile的规则。函数

target... : prerequisites ...
    command
     ...
     ...
  • target:也就是一个目标文件,能够是Object File,也能够是执行文件,也能够是标签。
  • prerequisites:就是,要生成那个target所须要的文件或是目标。
  • command:也就是make须要执行的命令。(任意的Shell命令)

  这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说,prerequisites中若是有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。
  说到底,Makefile的东西就是这样一点,好像接下来不用再讲任何东西了。呵呵。还不尽然,这是Makefile的主线和核心,但要写好一个Makefile还不够,我会之后面一点一点地结合个人工做经验给你慢慢到来。内容还多着呢。:)
  咱们来看一个示例:工具

nty_http_server : nty_coroutine.o nty_epoll.o nty_schedule.o nty_socket.o nty_http_server.o
        gcc -o nty_http_server nty_http_server.o nty_coroutine.o nty_epoll.o nty_schedule.o nty_socket.o -lpthread
nty_coroutine.o:nty_coroutine.c nty_coroutine.h
        gcc -c nty_coroutine.c
nty_epoll.o:nty_epoll.c nty_coroutine.h
        gcc -c nty_epoll.c
nty_schedule.o:nty_schedule.c nty_coroutine.h
        gcc -c nty_schedule.c
nty_socket.o:nty_socket.c nty_coroutine.h
        gcc -c nty_socket.c
nty_http_server.o:nty_http_server.c nty_coroutine.h
        gcc -c nty_http_server.c

clean :
        rm -rf *.o

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

这里要说明一点的是,clean不是一个文件,它只不过是一个动做名字,有点像C语言中的lable同样,其冒号后什么也没有,那么,make就不会自动去找文件的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显得指出这个lable的名字。这样的方法很是有用,咱们能够在一个makefile中定义不用的编译或是和编译无关的命令,好比程序的打包,程序的备份,等等。

<h3 id="4">make是如何工做的</h3>
  在默认的方式下,也就是咱们只输入make命令。那么,ui

  1. make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
  2. 若是找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“nty_http_server”这个文件,并把这个文件做为最终的目标文件。
  3. 若是nty_http_server文件不存在,或是nty_http_server所依赖的后面的 .o 文件的文件修改时间要比nty_http_server这个文件新,那么,他就会执行后面所定义的命令来生成nty_http_server这个文件。
  4. 若是nty_http_server所依赖的.o文件也存在,那么make会在当前文件中找目标为.o文件的依赖性,若是找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程)
  5. 固然,你的C文件和H文件是存在的啦,因而make会生成 .o 文件,而后再用 .o 文件声明make的终极任务,也就是执行文件nty_http_server了。

  这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程当中,若是出现错误,好比最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,若是在我找了依赖关系以后,冒号后面的文件仍是不在,那么对不起,我就不工做啦。
  经过上述分析,咱们知道,像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,咱们能够显示要make执行。即命令——“make clean”,以此来清除全部的目标文件,以便重编译。
  因而在咱们编程中,若是这个工程已被编译过了,当咱们修改了其中一个源文件,好比nty_coroutine.c,那么根据咱们的依赖性,咱们的目标nty_coroutine.o会被重编译(也就是在这个依性关系后面所定义的命令),因而nty_coroutine.o的文件也是最新的啦,因而nty_coroutine.o的文件修改时间要比nty_http_server要新,因此nty_http_serve也会被从新连接了(详见nty_http_serve目标文件后定义的命令)。
  而若是咱们改变了“nty_coroutine.h”,那么,nty_coroutine.o、nty_epoll.o、nty_schedule.o、nty_socket.o和nty_http_server.o都会被重编译,而且,nty_http_server会被重连接。操作系统

<h3 id="5">Makefile使用变量</h3>
  在上面的例子中,咱们能够看到[.o]文件的字符串被重复了两次,若是咱们的工程须要加入一个新的[.o]文件,那么咱们须要在两个地方加(应该是三个地方,还有一个地方在clean中)。固然,咱们的makefile并不复杂,因此在两个地方加也不累,但若是makefile变得复杂,那么咱们就有可能会忘掉一个须要加入的地方,而致使编译失败。因此,为了makefile的易维护,在makefile中咱们可使用变量。makefile的变量也就是一个字符串,理解成C语言中的宏可能会更好。

  好比,咱们声明一个变量,叫objects, OBJECTS, objs, OBJS, obj, 或是 OBJ,反正无论什么啦,只要可以表示obj文件就好了。咱们在makefile一开始就这样定义:

OBJECTS=nty_coroutine.o nty_epoll.o nty_schedule.o nty_socket.o nty_http_server.o

  因而咱们的Makefile能够修改为这样子:

OBJECTS=nty_coroutine.o nty_epoll.o nty_schedule.o nty_socket.o nty_http_server.o
nty_http_server : $(OBJECTS)
        gcc -o nty_http_server $(OBJECTS) -lpthread
nty_coroutine.o:nty_coroutine.c nty_coroutine.h
        gcc -c nty_coroutine.c
nty_epoll.o:nty_epoll.c nty_coroutine.h
        gcc -c nty_epoll.c
nty_schedule.o:nty_schedule.c nty_coroutine.h
        gcc -c nty_schedule.c
nty_socket.o:nty_socket.c nty_coroutine.h
        gcc -c nty_socket.c
nty_http_server.o:nty_http_server.c nty_coroutine.h
        gcc -c nty_http_server.c

clean :
        rm -rf $(OBJECTS) nty_http_server

  因而若是有新的 .o 文件加入,咱们只需简单地修改一下 OBJECTS 变量就能够了。但是聪明的你应该也发现了这个Makefile仍是很差维护,由于每一个.o文件都须要显示地写出依赖和生成的命令,致使这个Makefile有不少的雷同,咱们有没有更好的维护办法呢?请看下一小节:

<h3 id="6">让make自动推导</h3>
  GNU的make很强大,它能够自动推导文件以及文件依赖关系后面的命令,因而咱们就不必去在每个[.o]文件后都写上相似的命令,由于,咱们的make会自动识别,并本身推导命令。只要make看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,若是make找到一个whatever.o,那么whatever.c,就会是whatever.o的依赖文件。而且 cc -c whatever.c 也会被推导出来,因而,咱们的makefile不再用写得这么复杂。咱们的是新的makefile又出炉了。

OBJECTS=nty_coroutine.o nty_epoll.o nty_schedule.o nty_socket.o nty_http_server.o
nty_http_server : $(OBJECTS)
        gcc -o nty_http_server $(OBJECTS) -lpthread
nty_coroutine.o:nty_coroutine.h
nty_epoll.o:nty_coroutine.h
nty_schedule.o:nty_coroutine.h
nty_socket.o:nty_coroutine.h
nty_http_server.o:nty_coroutine.h

.PHONY : clean
clean :
        rm -rf $(OBJECTS) nty_http_server

  这种方法,也就是make的“隐晦规则”。上面文件内容中,“.PHONY”表示,clean是个伪目标文件。
  即然咱们的make能够自动推导命令,那么我看到那堆[.o]和[.h]的依赖就有点不爽,那么多的重复的[.h],能不能把其收拢起来?预知后事如何,请听下回分解。
  
<h3 id="7">另类风格的Makefile</h3>
  上一小节提到的问题是没有问题,这个对于make来讲很容易,谁叫它提供了自动推导命令和文件的功能呢?来看看最新风格的makefile吧。

OBJECTS=nty_coroutine.o nty_epoll.o nty_schedule.o nty_socket.o nty_http_server.o
nty_http_server : $(OBJECTS)
        gcc -o nty_http_server $(OBJECTS) -lpthread
$(OBJECTS):nty_coroutine.h

.PHONY : clean
clean :
        rm -rf $(OBJECTS) nty_http_server

  这种风格,让咱们的makefile变得很简单,但咱们的文件依赖关系就显得有点凌乱了。鱼和熊掌不可兼得。还看你的喜爱了。
  另外即便这个Makefile比较简单,可是它依然比较笨拙,好比若是咱们须要添加新的源码文件,都要来修改OBJECTS变量的定义,那怎么办呢?

<h3 id="8">make中的函数</h3>
先来看咱们的Makefile:

SOURCES=$(wildcard *.c)
OBJECTS=$(patsubst %.c,%.o,$(SOURCES))
PROGRAM=nty_http_server
CC=gcc
CFLAGS=-lpthread

$(PROGRAM) : $(OBJECTS)
        $(CC) -o $(PROGRAM) $(OBJECTS) $(CFLAGS)
$(OBJECTS):$(SOURCES)

.PHONY : clean
clean :
        rm -rf $(OBJECTS) $(PROGRAM)

  在 GNU Make 里有一个叫 'wildcard' 的函 数,它有一个参数,功能是展开成一列全部符合由其参数描述的文 件名,文件间以空格间隔。你能够像下面所示使用这个命令:

SOURCES= $(wildcard *.c)

  这行会产生一个全部以 '.c' 结尾的文件的列表,而后存入变量 SOURCES 里。固然你不须要必定要把结果存入一个变量。
  notdir把展开的文件的路径去掉,只显示文件名而不包含其路径信息,例如:

FILES =$(notdir $(SOURCES))

  这行的做用是把上面以'.c'结尾的文件的文件列表中附带的路径去掉,只显示符合条件的文件名。
  patsubst( patten substitude, 匹配替换的缩写)函数。它须要3个参数:第一个是一个须要匹配的式样,第二个表示用什么来替换它,第三个是一个须要被处理的由空格分隔的字列。例如,处理那个通过上面定义后的变量,

OBJS = $(patsubst %.c,%.o,$(SOURCES))

  这行将处理全部在 SOURCES列个中的字(一列文件名),若是它的 结尾是 '.c' ,就用'.o' 把 '.c' 取代。注意这里的 % 符号将匹配一个或多个字符,而它每次所匹配的字串叫作一个‘柄’(stem) 。在第二个参数里, % 被解读成用第一参数所匹配的那个柄。
make还有不少的函数,好比foreach,origin,call,if then else,addprefix,dir,notdir

  到这里,咱们已经把Makefile改了5个版本,这个应该是能够用于大型工程了吧???非也,非也,非也。好比在大型工程中,咱们通常会划分模块,不一样的目录管理不一样的源码文件,此时Makefile如何写呢?

<h3 id="9">文件搜索</h3>
  在一些大的工程中,有大量的源文件,咱们一般的作法是把这许多的源文件分类,并存放在不一样的目录中。因此,当make须要去找寻文件的依赖关系时,你能够在文件前加上路径,但最好的方法是把一个路径告诉make,让make在自动去找。
  Makefile文件中的特殊变量“VPATH”就是完成这个功能的,若是没有指明这个变量,make只会在当前的目录中去找寻依赖文件和目标文件。若是定义了这个变量,那么,make就会在当当前目录找不到的状况下,到所指定的目录中去找寻文件了。

VPATH = src:../headers

  上面的的定义指定两个目录,“src”和“../headers”,make会按照这个顺序进行搜索。目录由“冒号”分隔。(固然,当前目录永远是最高优先搜索的地方)。
  另外一个设置文件搜索路径的方法是使用make的“vpath”关键字(注意,它是全小写的),这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很相似,可是它更为灵活。它能够指定不一样的文件在不一样的搜索目录中。这是一个很灵活的功能。它的使用方法有三种:

1. vpath < pattern> < directories>    为符合模式< pattern>的文件指定搜索目录<directories>。

2. vpath < pattern>                              清除符合模式< pattern>的文件的搜索目录。

3. vpath                                                 清除全部已被设置好了的文件搜索目录。

  vapth使用方法中的< pattern>须要包含“%”字符。“%”的意思是匹配零或若干字符,例如,“%.h”表示全部以“.h”结尾的文件。< pattern>指定了要搜索的文件集,而< directories>则指定了的文件集的搜索的目录。例如:

vpath %.h ../headers

  该语句表示,要求make在“../headers”目录下搜索全部以“.h”结尾的文件。(若是某文件在当前目录没有找到的话)。
  咱们能够连续地使用vpath语句,以指定不一样搜索策略。若是连续的vpath语句中出现了相同的< pattern>,或是被重复了的< pattern>,那么,make会按照vpath语句的前后顺序来执行搜索。如:

vpath %.c foo

   vpath %   blish

   vpath %.c bar

  其表示“.c”结尾的文件,先在“foo”目录,而后是“blish”,最后是“bar”目录。

vpath %.c foo:bar

   vpath %   blish

  而上面的语句则表示“.c”结尾的文件,先在“foo”目录,而后是“bar”目录,最后才是“blish”目录。
  到这里,咱们已经把Makefile改了6个版本,这个应该是能够用于大型工程了吧???非也,非也,非也。好比在大型工程中,咱们的目标文件通常会有多个,那如何实现一键式编译,好比咱们在编译thrift就用过,还好比咱们不少同窗搞过嵌入式开发,好比用于路由器开发的openwrt系统,那个系统一键make命令,就能把全部的包里的目标编译完,那这个是多麽神奇啊,Oh my god, what should i do?

<h3 id="9">绝对顶级的大型工程Makefile</h3>
  其实在Makefile中能够包含别的Makefile,被包含的Makefile通常是定义了一些变量,让经过这些变量控制其余的Makefile的编译。也能够再包含多个包,从顶层的Makefile一直调用到全部包的Makefile,完成编译工做。那这究竟是如何完成的,你们先移步到
https://github.com/zhiyong0804/RTSP_PullerModule.git
咱们看到顶级目录的Makefile,内容以下:

LIVEMEDIA_DIR = liveMedia
GROUPSOCK_DIR = groupsock
USAGE_ENVIRONMENT_DIR = UsageEnvironment
BASIC_USAGE_ENVIRONMENT_DIR = BasicUsageEnvironment

PULLER_MODULE_DIR = PullerModule

all:
    cd $(LIVEMEDIA_DIR) ; $(MAKE)
    cd $(GROUPSOCK_DIR) ; $(MAKE)
    cd $(USAGE_ENVIRONMENT_DIR) ; $(MAKE)
    cd $(BASIC_USAGE_ENVIRONMENT_DIR) ; $(MAKE)

install:
    mkdir -p $(PULLER_MODULE_DIR)/lib
    cd $(LIVEMEDIA_DIR) ; $(MAKE) install
    cd $(GROUPSOCK_DIR) ; $(MAKE) install
    cd $(USAGE_ENVIRONMENT_DIR) ; $(MAKE) install
    cd $(BASIC_USAGE_ENVIRONMENT_DIR) ; $(MAKE) install

module:
    cd $(PULLER_MODULE_DIR) ; $(MAKE)
    cd $(PULLER_MODULE_DIR) ; $(MAKE) test

clean:
    cd $(LIVEMEDIA_DIR) ; $(MAKE) clean
    cd $(GROUPSOCK_DIR) ; $(MAKE) clean
    cd $(USAGE_ENVIRONMENT_DIR) ; $(MAKE) clean
    cd $(BASIC_USAGE_ENVIRONMENT_DIR) ; $(MAKE) clean
    cd $(PULLER_MODULE_DIR) ; $(MAKE) clean

cleanall:
    cd $(PULLER_MODULE_DIR) ; $(MAKE) cleanall

  从该文件咱们看到在all里,有cd $(LIVEMEDIA_DIR) ; $(MAKE),这就是要进入到liveMedia子目录执行make操做,固然在该Makefile还须要编译groupsock、UsageEnvironment和BasicUsageEnvironment目录,在module这个伪目标里,再去编译了PullerModule和PullerModule的测试工程。那么咱们进入到groupsock目录看看它的Makefile内容以下:

INCLUDES = -Iinclude -I../UsageEnvironment/include

include ../inc.mk

NAME = libgroupsock
ALL = $(NAME).$(LIB_SUFFIX)
all:    $(ALL)

.$(C).$(OBJ):
    $(C_COMPILER) -c $(C_FLAGS) $<
.$(CPP).$(OBJ):
    $(CPLUSPLUS_COMPILER) -c $(CPLUSPLUS_FLAGS) $<

GROUPSOCK_LIB_OBJS = GroupsockHelper.$(OBJ) GroupEId.$(OBJ) inet.$(OBJ) Groupsock.$(OBJ) NetInterface.$(OBJ) NetAddress.$(OBJ) IOHandlers.$(OBJ)

GroupsockHelper.$(CPP):    include/GroupsockHelper.hh
include/GroupsockHelper.hh:    include/NetAddress.hh
include/NetAddress.hh:    include/NetCommon.h
GroupEId.$(CPP):    include/GroupEId.hh
include/GroupEId.hh:    include/NetAddress.hh
inet.$(C):        include/NetCommon.h
Groupsock.$(CPP):    include/Groupsock.hh include/GroupsockHelper.hh include/TunnelEncaps.hh
include/Groupsock.hh:    include/groupsock_version.hh include/NetInterface.hh include/GroupEId.hh
include/NetInterface.hh:    include/NetAddress.hh
include/TunnelEncaps.hh:    include/NetAddress.hh
NetInterface.$(CPP):    include/NetInterface.hh include/GroupsockHelper.hh
NetAddress.$(CPP):    include/NetAddress.hh include/GroupsockHelper.hh
IOHandlers.$(CPP):    include/IOHandlers.hh include/TunnelEncaps.hh

libgroupsock.$(LIB_SUFFIX): $(GROUPSOCK_LIB_OBJS) \
    $(PLATFORM_SPECIFIC_LIB_OBJS)
    $(LIBRARY_LINK)$@ $(LIBRARY_LINK_OPTS) \
        $(GROUPSOCK_LIB_OBJS)

clean:
    -rm -rf *.$(OBJ) $(ALL) core *.core *~ include/*~

install: install1 $(INSTALL2)
install1: libgroupsock.$(LIB_SUFFIX)
      install -m 644 libgroupsock.$(LIB_SUFFIX) $(DESTDIR)$(LIBDIR)
install_shared_libraries: libgroupsock.$(LIB_SUFFIX)
      ln -s libgroupsock.$(LIB_SUFFIX) $(DESTDIR)$(LIBDIR)/libgroupsock.$(SHORT_LIB_SUFFIX)
      ln -s libgroupsock.$(LIB_SUFFIX) $(DESTDIR)$(LIBDIR)/libgroupsock.so

##### Any additional, platform-specific rules come here:

第三行include ../inc.mk包含了上一级目录的inc.mk(对,没有错,咱们通常的include的Makefile习惯以.mk命名),为何要include这个文件呢,这是由于C_FLAGS还有其它的变量都是定义在inc.mk里,而这些变量对于其它的包(好比groupsock、UsageEnvironment和BasicUsageEnvironment)也是须要的,避免重复定义,咱们在inc.mk统必定义。OK,inc.mk文件内容以下:

PREFIX = ../PullerModule
LIBDIR = $(PREFIX)/lib

Debug = -g
Release = 

#### Change the following for your environment:
COMPILE_OPTS =    $(Debug)    $(INCLUDES)  -fPIC -I. -O2 -DSOCKLEN_T=socklen_t -D_LARGEFILE_SOURCE=1 -D_FILE_OFFSET_BITS=64
C =        c
C_COMPILER =        cc  # replace with "mipsel-linux-cc" for mipsel platform
C_FLAGS =        $(COMPILE_OPTS)
CPP =            cpp
CPLUSPLUS_COMPILER =    g++ # replace with "mipsel-linux-g++" for mipsel platform
CPLUSPLUS_FLAGS =    $(COMPILE_OPTS) -Wall -DBSD=1
OBJ =            o
LINK =            g++ -o  # replace with "mipsel-linux-g++ -o" for mipsel platform
LINK_OPTS =        -L.
CONSOLE_LINK_OPTS =    $(LINK_OPTS)
LIBRARY_LINK =        ar cr #replace with "mipsel-linux-ar cr" for mipsel platform
LIBRARY_LINK_OPTS =    
LIB_SUFFIX =            a
LIBS_FOR_CONSOLE_APPLICATION =
LIBS_FOR_GUI_APPLICATION =
EXE =
##### End of variables to change
相关文章
相关标签/搜索