linux学习笔记-makefile的写法

前言

  一个项目,拥有成百上千的源程序文件,那如何组织这些源码文件的编译和链接呢?此时就需要确定整个工程的编译链接规则,Makefile就是用来指定规则的。而make是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。

Makefile介绍

  当我们编译工程时,只要输入make就会去编译了,那么这个make命令执行的时候一定需要一个Makefile文件,通过这个Makefile告诉make命令需要怎么样的去编译和链接程序。

  make的工作规则是:

1.如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。
2.如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。
3.如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。

  只要我们的Makefile写得够好,所有的这一切,我们只用一个make命令就可以完成,make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自己编译所需要的文件和链接目标程序。

Makefile规则

  在讲述这个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中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。

make是如何工作的

  在默认的方式下,也就是我们只输入make命令。那么,

  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会被重链接。

Makefile使用变量

  在上面的例子中,我们可以看到[.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有很多的雷同,我们有没有更好的维护办法呢?请看下一小节:

让make自动推导

  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],能不能把其收拢起来?

另类风格的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变量的定义,那怎么办呢?

make中的函数

先来看我们的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如何写呢?

文件搜索

  在一些大的工程中,有大量的源文件,我们通常的做法是把这许多的源文件分类,并存放在不同的目录中。所以,当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中可以包含别的Makefile,被包含的Makefile一般是定义了一些变量,让通过这些变量控制其他的Makefile的编译。也可以再包含多个包,从顶层的Makefile一直调用到所有包的Makefile,完成编译工作。那这到底是如何完成的,
我们看到顶级目录的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