linux学习笔记makefile的写法

6次阅读

共计 11522 个字符,预计需要花费 29 分钟才能阅读完成。

  • 前言
  • Makefile 介绍

    • Makefile 规则
    • make 是如何工作的
    • Makefile 使用变量
    • 让 make 自动推导
    • 另类风格的 Makefile
    • make 中的函数
    • 文件搜索
    • 绝对顶级的大型工程 Makefile

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

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

  make 的工作规则是:

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

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

<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 命令。那么,

  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

正文完
 0