1. 基本规则
一般一个稍大的linux项目会有很多个源文件组成,最终的可执行程序也是由这许多个源文件编译链接而成的。编译是把一个.c或.cpp文件编译成中间代码.o文件,链接是就使用这些中间代码文件生成可执行文件。比如在当前项目目录下有如下源文件:
1 | # ls |
以上源代码可以这样编译:
1 | # gcc -o target_bin main.c debug.c ipc.c timer.c tools.c |
如果之后修改了其中某一个文件(如tools.c),再执行一下上一行代码即可,但如果有成千上万个源文件这样编译肯定是不够合理的。此时我们可以按下面步骤来编译:
1 | # gcc -c debug.c |
如果其中tools.c修改了,只需要编译该文件,再执行最后生成可执行文件的操作,也就是做如下两步操作即可:
1 | # gcc -c tools.c |
这样做看上去应该很合理了。但是如果修改了多个文件,就很可能忘了编译某一文件,那么运行时就很有可能出错。如果是common.h文件修改了,那么包含该头文件的所有.c文件都需要重新编译,这样一来的话就更复杂更容易出错了。看来这种方法也不够好,手动处理很容易出错。那有没有一种自动化的处理方式呢?有的,那就是写一个Makefile来处理编译过程。 下面给一个简单的Makefile,在源代码目录下建一个名为Makefile的文件:
1 | target_bin : main.o debug.o ipc.o timer.o tools.o |
然后在命令行上执行命令:
1 | # make |
可见在该目录下生成了.o文件以及target_bin可执行文件。现在我们只需要执行一个make命令就可以完成所有编译工作,无需像之前一样手动执行所有动作,make命令会读取当前目录下的Makefile文件然后完成编译步骤。从编译过程输出到屏幕的内容看得到执行make命令之后所做的工作,其实就是我们之前手动执行的那些命令。现在来说一下什么是Makefile? 所谓Makefile我的理解其实就是由一组组编译规则组成的文件,每条规则格式大致为:
1 | target ... : prerequisites ... |
其中target是目标文件,可以为可执行文件、*.o
文件或标签。Prerequisites是产生target所需要的源文件或*.o
文件,可以是另一条规则的目标。commond是要产生该目标需要执行的操作系统命令,该命令必须以tab(文中以>—标示tab字符)开头,不可用空格代替。
说白了就是要产生target,需要依赖后面的prerequisites文件,然后执行commond来产生来得到target。这和我们之前手动执行每条编译命令是一样的,其实就是定义好一个依赖关系,我们把产生每个文件的依赖文件写好,最终自动执行编译命令。
比如在我们给出的Makefile例子中target_bin main.o等就是target,main.o debug.o ipc.o timer.o tools.o是target_bin的prerequisites,gcc -o target_bin main.o debug.o ipc.o timer.o tools.o就是commond,把所有的目标文件编译为最终的可执行文件target,而main.c common.h是main.o的prerequisites,其gcc -c main.c命令生成target所需要的main.o文件。
在该例子中,Makefile工作过程如下:
- 首先查找第一条规则目标,第一条规则的目标称为缺省目标,只要缺省目标更新了就算完成任务了,其它工作都是为这个目的而做的。 该Makefile中第一条规则的目标target_bin,由于我们是第一次编译,target_bin文件还没生成,显然需要更新,但此时依赖文件main.o debug.o ipc.o timer.o tools.o都没有生成,所以需要先更新这些文件,然后才能更新target_bin。
- 所以make会进一步查找以这些依赖文件main.o debug.o ipc.o timer.o tools.o为目标的规则。首先找main.o,该目标也没有生成,该目标依赖文件为main.c common.h,文件存在,所以执行规则命令gcc -c main.c,生成main.o。其他target_bin所需要的依赖文件也同样操作。
- 最后执行gcc -o target_bin main.o debug.o ipc.o timer.o tools.o,更新target_bin。
在没有更改源代码的情况下,再次运行make:
1 | # make |
得到提示目标target_bin已经是最新的了。 如果修改文件main.c之后,再运行make:
1 | # vim main.c |
此时make会自动选择受影响的目标重新编译: 首先更新缺省目标,先检查target_bin是否需要更新,这需要检查其依赖文件main.o debug.o ipc.o timer.o tools.o是否需要更新。 其次发现main.o需要更新,因为main.o目标的依赖文件main.c最后修改时间比main.o晚,所以需要执行生成目标main.o的命令:gcc -c main.c更新main.o。 最后发现目标target_bin的依赖文件main.o有更新过,所以执行相应命令gcc -o target_bin main.o debug.o ipc.o timer.o tools.o更新target_bin。 总结下,执行一条规则步骤如下:
- 先检查它的依赖文件,如果依赖文件需要更新,则执行以该文件为目标的的规则。如果没有该规则但找到文件,那么该依赖文件不需要更新。如果没有该规则也没有该文件,则报错退出。
- 再检查该文件的目标,如果目标不存在或者目标存在但依赖文件修改时间比他要晚或某依赖文件已更新,那么执行该规则的命令。 由此可见,Makefile可以自动发现更新过的文件,自动重新生成目标,使用Makefile比自己手动编译比起来,不仅效率高,还减少了出错的可能性。
Makefile中有很多目标,我们可以编译其中一个指定目标,只需要在make命令后面带上目标名称即可。如果不指定编译目标的话make会编译缺省的目标,也就是第一个目标,在本文给出的Makefile第一个目标为target_bin。如果只修改了tools.c文件的话,我们可能只想看看我们的更改的源代码是否有语法错误而又不想重新编译这个工程的话可以执行如下命令:
1 | # make tools.o |
编译成功,这里又引出一个问题,如果继续执行同样的命令:
1 | # make tools.o |
我们先手动删掉tools.o文件再执行就可以了,怎么又是手动呢?我们要自动,要自动!!好吧,我们加一个目标来删除这些编译过程中产生的临时文件,该目标为clean。 我们在上面Makefile最后加上如下内容:
1 | clean: |
当我们直接make命令时不会执行到该目标,因为没有被默认目标target_bin目标或以target_bin依赖文件为目标的目标包含在内。我们要执行该目标需要在make时指定目标即可。如下:
1 | # make clean |
可见clean目标被执行到了,再执行make时make就会重新生成所有目标对应的文件,因为执行make clean时,那些文件被清除了。 clean目标应该存在与你的Makefile当中,它既可以方便你的二次编译,又可以保持的源文件的干净。该目标一般放在最后,不可放在最开头,否则会被当做缺省目标被执行,这很可能不是你的意愿。 最后总结一下,Makefile只是告诉了make命令如何来编译和链接程序,告诉make命令生成目标文件需要的文件,具体的编译链接工作是你的目标对应的命令在做。 给一个今天完整的makefile:
1 | target_bin : main.o debug.o ipc.o timer.o tools.o |
2. 隐含规则自动推导
上一节的Makefile勉强可用,但还写的比较繁琐,不够简洁。对每一个.c源文件,都需要写一个生成其对应的.o目标文件的规则,如果有几百个或上千个源文件,都手动来写,还不是很麻烦,这也不够自动化啊。 这样,我们把生成.o目标文件的规则全部删除掉,就是这样一个Makefile文件:
1 | target_bin : main.o debug.o ipc.o timer.o tools.o |
这下简洁了不少,这样也能用吗?试试看吧先,make一下:
1 | # make |
原来酱紫都可以啊!!target_bin后面那一群依赖文件怎么生成呢?不是没有生成*.o目标文件的规则了吗?再看屏幕编译输出内容:
1 | cc -c -o main.o main.c |
怎么长的和之前不太一样呢,尤其是前面那个cc是何物? 其实make可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个*.o文件后都写上类似的命令,因为,我们的 make 会自动推导依赖文件,并根据隐含规则自己推导命令。所以上面.o文件是由于make自动推导出的依赖文件以及命令来生成的。 下面来看看make是如何推导的。 命令make –p可以打印出很多默认变量和隐含规则。Makefile变量可以理解为C语言的宏,直接展开即可(后面会讲到)。取出我们关心的部分:
1 | # default |
其中cc是一个符号链接,指向gcc,这就可以解释为什么我们看到的编译输出为cc,其实还是使用gcc在编译。
1 | # ll /usr/bin/cc |
变量$(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH)
都为空。所以%.o: %.c
规则命令展开为:
1 | cc -c -o $@ $< |
再看屏幕输出编译内容,摘取一条:
1 | cc -c -o main.o main.c |
不是看出点什么?$@
和main.o对应,$<
和main.c对应。其实$@
和$<
是两个变量。$@
为规则中的目标,$<
为规则中的第一个依赖文件。%.o:%.c
是一种称为模式规则的特殊规则。因为main.o符合该模模式,再推导出依赖文件main.c,最终推导出整个规则为:
1 | main.o : main.c: |
其余几个目标也同样推导。make自动推导的功能为我们减少了不少的Makefile代码,尤其是对源文件比较多的大型工程,我们的Makefile可以不用写得那么繁琐了。 最后,今天的Makefile相对于上一节进化成这个样子了:
1 | target_bin : main.o debug.o ipc.o timer.o tools.o |
3. 变量的使用
仔细研究我们的之前Makefile发现,我们还有改进的地方,就是此处:
1 | target_bin : main.o debug.o ipc.o timer.o tools.o |
如果增加一个源文件xx.c的话,需要在两处或多处增加xx.o文件。我们可以使用变量来解决这个问题。之前说过,Makefile的变量就像C语言的宏一样,使用时在其位置上直接展开。变量在声明时赋予初值,在引用变量时需要给在变量名前加上“$”符号,但最好用小括号“()”或是大括号“{}”把变量给包括起来。 默认目标target_bin也在多处出现了,该文件也可以使用变量代替。 修改我们的Makefile如下:
1 | SRC_OBJ = main.o debug.o ipc.o timer.o tools.o |
这样每次有新增的文件是只需要在SRC_OBJ变量里面增加一个文件即可。要修改最终目标的名字是可以只修改变量SRC_BIN。 其实在之前还说过特殊变量:
$@
,表示规则中的目标。$<
,表示规则中的第一个依赖文件。$?
,表示规则中所有比目标新的条件,组成一个列表,以空格分隔。$^
:,表示规则中的所有条件,组成一个列表,以空格分隔。 上一节我们看到make -p有很多自定义的变量,比如CC。其中很多变量我们可以直接使用或修改其变量值或增加值。我们的Makefile中可以使用CC(默认值为cc)、RM(默认值为rm -f)。
由此可见我们的Makefile还可以进一步修改:
1 | SRC_OBJ = main.o debug.o ipc.o timer.o tools.o |
这样的Makefile编译也是可用的。
但是这样的Makefile还是需要我们手动添加文件,还是不够自动化,最好增删文件都要修改Makefile。伟大的人类真是太懒了!!于是乎,他们发明了一个函数wilcard
(函数后面会讲到),它可以用来获取指定目录下的所有的.c文件列表。这样的话我们可以自动获取当前目录下所有.c源文件,然后通过其他方法再得到.o文件列表,这样的话就不需要在每次增删文件时去修改Makefile了。所谓其他方法这里给出两种:
- 使用patsubst函数。在
$(patsubst %.c,%.o,$(dir) )
中,patsubst把$(dir)中的变量符合后缀是.c的全部替换成.o。 - 变量值的替换。 我们可以替换变量中的共有的部分,其格式是
“$(var:a=b)”
或“${var:a=b}”
,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。
修改后的Makefile如下:
1 | # SRC_OBJ = $(patsubst %.c, %.o, $(wildcard *.c)) |
其中# 后面的内容为注释。 这样终于满足了那些懒人的想法了。可见在使用变量时,的确可以是编译变得更自动化。
其实变量的定义有三种运算符=、:=、?=、+=
。
=
运算符可以读取到后面定义的变量。比如:1
2
3
4
5VAR = $(VAR2)
VAR2 = hello_make
all:
>---@echo =====$(VAR)=====
运行结果为:
1 | # |
但是这种定义可能会导致并非我们意愿的事发生,并不是很符合C语言的编程习惯。
:=
运算符在遇到变量定义时立即展开。1
2
3
4
5VAR := $(VAR2)
VAR2 = hello_make
all:
>---@echo =====$(VAR)=====
运行结果为:
1 | # |
?=
运算符在复制之前先做判断变量是否已经存在。例如var1 ?= $(var2)
的意思是:如果var1没有定义过,那么?=
相当于=
,如果var1先前已经定义了,则什么也不做,不会给var重新赋值。+=
运算符是给变了追加值。如果变量还没有定义过就直接用+=赋值,那么+=
相当于=
如何使用这几个运算符要看实际情况,有时一个大的工程可能有许多Makefile组成,变量可能在多个Makefile中都在使用,这时可能使用+=
比较好。使用:=
有时可能比要好。
有时在编译程序时,我们需要编译器给出警告,或加入调试信息,或告知编译器优化可执行文件。编译时C编译器的选项CFLAGS使用的较多,默认没有提供值,我们可以给该变量赋值。有时我们还需要使用链接器选项LFLAGS告诉链接器链接时需要的库文件。可能我们还需要给出包含头文件的路径,因为头文件很可能和源文件不再同一目录。所以,我们今天的Makefile加上部分注释又更新了:
1 | # A commonMakefile for c programs, version 1.0 |
编译:
1 | # make |
可见我们的预编译选项,编译选项都用到了,之前我们说过make的使用隐含规则自动推导:
1 | COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) –c |
其中变量CFLAGS 和 CPPFLAGS均是我们给出的,变量$(TARGET_ARCH)未给,所以在编译输出可以看到-c前面有2个空,最早未给变量是有四个空。 目前给出的Makefile基本上可以适用于那些源代码全部在同一目录下的简单项目,并且基本上在增删文件时不需要再去手动修改Makefile代码。在新的一个项目只需要把该Makefile拷贝到源代码目录下,再修改一下你需要编译的可执行文件名称以及你需要的编译连接选项即可。 后面章节将会讲到如何写多目录源代码工程下的Makefile。 最后,今天的最终Makefile是这样的:
1 | # A commonMakefile for c programs, version 1.0 |
3. 伪目标
一般情况下,Makefile都会有一个clean目标,用于清除编译过程中产生的二进制文件。我们在第一节的Makefile就用到了这个 clean目标,该目标没有任何依赖文件,并且该目标对应的命令执行后不会生产clean文件。 像这种特点目标,它的规则所定义的命令不是去创建文件,而仅仅通过make指定目标来执行一些特定系统命令或其依赖为目标的规则(如all),称为伪目标。 一个Makefile一般都不会只有一个伪目标,如果按Makefile的“潜规则”以及其约定俗成的名字来说的话,在较大的项目的Makefile中比较常用的为目标有这些:
- all:执行主要的编译工作,通常用作缺省目标,放在最前面。
- Install:执行编译后的安装工作,把可执行文件、配置文件、文档等分别拷到不同的安装目录。
- clean:删除编译生成的二进制文件。
- distclean:删除除源文件之外的所有中间生成文件,如配置文件,文档等。
- tags:为vim等编辑器生成tags文件。
- help:打印当前Makefile的帮助信息,比如有哪些目标可以有make指定去执行。 等。
make处理Makefile时,首先读取所有规则,建立关系依赖图。然后从缺省目标(第一个目标)或指定的目标开始执行。像clean,tags这样的目标一般不会作为缺省目标,也不会跟缺省目标有任何依赖关系,所以 make 无法生成它的依赖关系和决定它是否要执行。所以要执行这样的目标时,必须要显示的指定make该目标。就像前面我们清楚便已产生的中间二进制文件一样,需要显示执行命令:make clean。 伪目标也可以作为默认目标(如all),并且可以为其指定依赖文件。 我们先将version 1.0的Makefile完善下,我们可以加入帮助信息,tags等功能。
1 | # A common Makefile for c programs, version 1.1 |
make会把执行的命令打印在屏幕上,如果我们不想把命令打印在屏幕上,只显示命令结果时,直接在命令前面加上符号“@”就可以实现。如上面help目标一样,只显示命令结果。一般我们会在make时都会输出“Compiling xxx.c…”,不输出编译时的命令。我们在后面写Makefile时可以模仿。 如果当前目录下存在一个和伪目标同名的文件时(如clean),此时如果执行命令make clean后出现如下结果:
1 | # touch clean |
这是因为clean文件没有依赖文件,make认为目标clean是最新的不会去执行规则对应的命令。为了解决这个问题,我们可以明确地将该目标声明为伪目标。将一个目标声明为伪目标需要将它作为特殊目标.PHONY”的依赖。如下:
1 | .PHONY : clean |
这条规则写在clean:规则的后面也行,也能起到声明clean是伪目标的作用 这样修改一下之前Makefile,将所有伪目标都作为.PHONY的依赖:
1 | .PHONY : all obj tag help clean disclean |
这样在当前目录下存在文件clean时执行:
1 | # make clean |
发现问题解决。 最后,给出今天最终的Makefile:
1 | # A common Makefile for c programs, version 1.1 |
5. 嵌套执行
在大一些的项目里面,所有源代码不会只放在同一个目录,一般各个功能模块的源代码都是分开的,各自放在各自目录下,并且头文件和.c源文件也会有各自的目录,这样便于项目代码的维护。这样我们可以在每个功能模块目录下都写一个Makefile,各自Makefile处理各自功能的编译链接工作,这样我们就不必把所有功能的编译链接都放在同一个Makefile里面,这可使得我们的Makefile变得更加简洁,并且编译的时候可选择编译哪一个模块,这对分块编译有很大的好处。 现在我所处于工程目录树如下:
1 | . |
这样组织项目源码要比之前合理一些,那这样怎么来写Makefile呢?我们可以在每个目录下写一个Makefile,通过最顶层的Makefile一层一层的向下嵌套执行各层Makefile。那么我们最顶层的Makefile简单点的话可以这样写:
1 | # top Makefile for xxx |
命令:
1 | >---$(MAKE) -C src |
就是进入src目录继续执行该目录下的Makefile。然后src目录下的Makefile在使用同样的方法进入下一级目录tools、main、ipc,再执行该目录下的Makefile。其实这样有些麻烦,我们可以直接从顶层目录进入最后的目录执行make。再加入一些伪目标完善下,我们的顶层Makefile就出来了:
1 | # Top Makefile for C program |
当我们这样组织源代码时,最下面层次的Makefile怎么写呢?肯定不可以将我们上一节的Makefile(version 1.1)直接拷贝到功能模块目录下,需要稍作修改。不能所有的模块都最终生成各自的可执行文件吧,我们目前是一个工程,所以最后只会生成一个可执行程序。我们这样做,让主模块目录生成可执行文件,其他模块目录生成静态库文件,主模块链接时要用其他模块编译产生的库文件来生成最终的程序。将上一节Makefile稍作修改得出编译库文件Makefile和编译可执行文件Makefile分别如下:
1 | # A Makefile to generate archive file |
====================
1 | # A Makefile to generate executive file |
最后在顶层执行:
1 | # make clean |
最后生成了可执行程序文件。这样的话一个工程的各个模块就变得独立出来了,不但源码分开了,而且各自有各自的Makefile,并且各个功能模块是可独立编译的。 我们发现顶层Makefile还有可以改进的地方,就是在进入下一层目录是要重复写多次,如下:
1 | >---$(MAKE) -C src/ipc |
每增加一个目录都要在多个伪目标里面加入一行,这样不够自动化啊,于是我们想到shell的循环语 句,我们可以在每条规则的命令处使用for循环。如下:
1 | DIR = src |
这样懒人有可以高兴很久了。不过还有问题: 上面for循环会依次进入系统命令ls列出的目录,但我们对每个目录的make顺序可能有要求,在该项目当中,main目录下的Makefile必须最后执行,因为最终的链接需要其他目录编译生成的库文件,否则会执行失败。并且在当前的Makefile中,当子目录执行make出现错误时,make不会退出。在最终执行失败的情况下,我们很难根据错误的提示定位出具体是是那个目录下的Makefile出现错误。这给问题定位造成了很大的困难。为了避免这样的问题,在命令执行错误后make退出。 所以将刚才的Makefile修改为如下
1 | DIR = src |
这样在执行出错时立马退出,但这样还是没有解决问题,编译错误还是会出现。那怎么解决呢? 我们可以通过增加规则来限制make执行顺序,这样就要用到伪目标,对每一个模块我们都为他写一条规则,每个模块名称是目标,最后需要执行的模块目标又是其他模块的目标,这样就限制了make顺序。在执行到最后需要执行的目标时,发现存在依赖,于是先更新依赖的目标,这样就不会出错了。并且这样的话,我们还可以对指定模块进行编译,比如我只修改了tools模块,我只想看看我修改的这个模块代码是否可以编译通过,我可以在编译时这样:
1 | # make tools |
还有另外一种方法也可以解决此问题,就是手动列出需要进入执行的模块名称(这里就是目录了),把最后需要执行的模块放在最后,这样for循环执行时最后需要编译链接的模块就放在最后了,不会像我们之前那样make是按照使用系统命令ls列出模块目录的顺序来执行。ls列出目录是按照每个目录的名称来排序的,我们总不能要求写代码的时候最后执行的模块的名称必须是以z开头的吧,总之不现实。
我们的顶层Makefile又进化了,也是这一节最终Makefile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49# Top Makefile for C program
# Copyright (C) 2014 shallnew \at 163 \dot com
DIR = src
MODULES = $(shell ls $(DIR))
# MODULES = ipc main tools
all : $(MODULES)
$(MODULES):
>---$(MAKE) -C $(DIR)/$@
main:tools ipc
obj:
>---@for subdir in $(MODULES); \
>---do $(MAKE) -C $(DIR)/$$subdir $@; \
>---done
clean :
>---@for subdir in $(MODULES); \
>---do $(MAKE) -C $(DIR)/$$subdir $@; \
>---done
distclean:
>---@for subdir in $(MODULES); \
>---do $(MAKE) -C $(DIR)/$$subdir $@; \
>---done
tags:
>---ctags -R
help:
>---@echo "===============A common Makefilefor c programs=============="
>---@echo "Copyright (C) 2014 liuy0711 \at 163\dot com"
>---@echo "The following targets aresupport:"
>---@echo
>---@echo " all - (==make) compile and link"
>---@echo " obj - just compile, withoutlink"
>---@echo " clean - clean target"
>---@echo " distclean - clean target and otherinformation"
>---@echo " tags - create ctags for vimeditor"
>---@echo " help - print help information"
>---@echo
>---@echo "To make a target, do 'make[target]'"
>---@echo "========================= Version2.0 ======================="
.PHONY : all clean distclean tags help
6.参数传递、条件判断、include
在多个Makefile嵌套调用时,有时我们需要传递一些参数给下一层Makefile。比如我们在顶层Makefile里面定义的打开调试信息变量DEBUG_SYMBOLS,我们希望在进入子目录执行子Makefile时该变量仍然有效,这是需要将该变量传递给子Makefile,那怎么传递呢?这里有两种方法:
在上层Makefile中使用”export”关键字对需要传递的变量进行声明。比如:
1
2DEBUG_SYMBOLS = TRUE
export DEBUG_SYMBOLS当不希望将一个变量传递给子 make 时,可以使用指示符 “unexport”来声明这个变量。 export一般用法是在定义变量的同时对它进行声明。如下:
1
export DEBUG_SYMBOLS = TRUE
- 在命令行上指定变量。比如:
1
$(MAKE) -C xxx DEBUG_SYMBOLS = TRUE
这样在进入子目录xxx执行make时该变量也有效。
像编程语言一样,Makefile也有自己的条件语句。条件语句可以根据一个变量值来控制make的执行逻辑。比较常用的条件语句是ifeq –else-endif、ifneq-else-endif、ifdef-else-endif。 ifeq关键字用来判断参数是否相等。 比如判断是否生成调试信息可以这么用:
1 | ifeq ($(DEBUG_SYMBOLS), TRUE) |
Ifneq和ifeq作用相反,此关键字是用来判断参数是否不相等。 ifdef关键字用来判断一个变量是否已经定义。 后两个关键字用法和ifeq类似。
现在我们继续改进我们上一节的Makefile,上一节的Makefile完成Makefile的嵌套调用,每一个模块都有自己的Makefile。其实每个模块的Makefile都大同小异,只需要改改最后编译成生成的目标名称或者编译链接选项,规则都差不多,那么我们是否可以考虑将规则部分提取出来,每个模块只需修改各自变量即可。这样是可行的,我们将规则单独提取出来,写一个Makefile.rule,将他放在顶层Makefile同目录下,其他模块内部的Makefile只需要include该Makefile就可以了。如下:
1 | include $(SRC_BASE)/Makefile.rule |
include类似于C语言的头文件包含,你把它理解为为本替换就什么都明白了。 这样以后规则有修改的话我们直接修改该Makefile就可以了,就不用进入每一个模块去修改,这样也便于维护。 这样我们今天顶层Makefile稍作修改:
1 | # Top Makefile for C program |
目前我们顶层目录下的目录树为:
1 | . |
每个子模块下的Makefile删除规则后修改为如下:
1 | SRC_BASE = ../.. |
而处于顶层目录下的Makefile.rule专门处理各模块编译链接时需要的规则。内容如下:
1 | # Copyright (C) 2014 shallnew \at 163 \dot com |
我们将Makefile.rule放在顶层有可能会一不小心在命令行上面执行了该Makefile,如下:
1 | # make -f Makefile.rule |
由于我们没有定义变量$(SRC_BIN)
和$(SRC_LIB)
,伪目标all没有任何依赖,所以编译是无法成功的。这里我们我们应该禁止直接执行该Makefile。
在make里面有这样一个变量:MAKELEVEL,它在多级调用的 make 执行过程中。变量代表了调用的深度。在 make 一级级的执行过程中变量MAKELEVEL的值不断的发生变化,通过它的值我们可以了解当前make 递归调用的深度。顶层的MAKELEVEL的值为“0” 、下一级时为“1” 、再下一级为“2”…….,所以我们希望一个子目录的Makefile必须被上层 make 调用才可以执行,而不允许直接执行,我们可以判断变量MAKELEVEL来控制。所以我们这一节最终的Makefile.rule为:
1 | # Copyright (C)2014 shallnew \at 163 \dot com |
此时再直接执行该Makefile:
1 | # make -f Makefile.rule |
7. 统一目标输出目录
上一节我们把规则单独提取出来,方便了Makefile的维护,每个模块只需要给出关于自己的一些变量,然后再使用统一的规则Makefile。这一节我们继续改进我们的Makefile,到目前为止我们的Makefile编译链接输出的目标都在源文件同目录下或模块Makefile同一目录下,当一个项目大了之后,这样会显得很乱,寻找编译输出的文件也比较困难。既然Makefile本身就是按照我们的的规则来编译链接程序,那么我们就可以指定其编译链接目标的目录,这样,我们可以清楚输出文件的地方,并且在清除已编译的目标时直接删除指定目录即可,不需要一层一层的进入源代码目录进行删除,这样又提高了效率。
既然要统一目标输出目录,那么该目录就需要存在,所以我们可以增加一条规则来创建这些目录,包括创建可执行文件的目录、链接库文件的目录以及.o文件的目录。并且目录还可以通过条件判断根据是否产生调试信息来区分开相应的目标文件。一般一个工程的顶层目录下都会有一个build目录来存放编译的目标文件结果,目前我的工程目录下通过Makefile创建的目录build的目录树如下:
1 | build/ //build根目录 |
ifeq ($(DEBUG_SYMBOLS), TRUE)
—BUILDDIR = ./build/$(PLATFORM)_dbg
else
—BUILDDIR = ./build/$(PLATFORM)
endif
all : $(BUILDDIR) $(MODULES)
$(BUILDDIR):
—@echo “ Create directory $@ …”
—mkdir -p $(BUILDDIR)/bin $(BUILDDIR)/lib
1 | 我们在all目标里面增加了其依赖目标BUILDDIR,该目标对应的规则为创建bin目录和lib目录。这样每次编译之前都会创建目录。 |
……
define a root build directory base on the platform
if without a SRC_BASE defined, just use local src directory
ifeq ($(SRC_BASE),)
—BUILDDIR = $(MOD_SRC_DIR)
—OBJDIR = $(MOD_SRC_DIR)
—LIBDIR = $(MOD_SRC_DIR)
—BINDIR = $(MOD_SRC_DIR)
else
—ifeq ($(DEBUG_SYMBOLS), TRUE)
—>—BUILDDIR = $(SRC_BASE)/build/$(PLATFORM)_dbg
—else
—>—BUILDDIR = $(SRC_BASE)/build/$(PLATFORM)
—endif
—OBJDIR = $(BUILDDIR)/obj/$(MODULE)
—LIBDIR = $(BUILDDIR)/lib
—BINDIR = $(BUILDDIR)/bin
endif
……
ifeq ($(MAKELEVEL), 0)
all : msg
else
all : lib bin
endif
lib : $(OBJDIR) $(SRC_LIB)
bin : $(OBJDIR) $(SRC_BIN)
$(OBJDIR) :
—@echo “ MKDIR $(notdir $@)…”
—@mkdir -p $@
……
1 | 此时我们编译一下后查看build目录: |
build/
└── unix_dbg
├── bin
├── lib
└── obj
├── ipc
├── main
└── tools
7 directories, 0 files
1 | 由于我们是开启了调试信息,所以创建了unix_dbg目录,并且该目录下创建了bin、lib、obj目录及其模块目录,但我们没有发现有文件存放在里面。 |
lib : $(OBJDIR) $(LIBDIR)/$(SRC_LIB)
bin : $(OBJDIR) $(BINDIR)/$(SRC_BIN)
$(OBJDIR) :
—@echo “ MKDIR $(notdir $@)…”
—@mkdir -p $@
ifneq ($(SRC_BIN),)
$(BINDIR)/$(SRC_BIN) : $(SRC_OBJ)
—$(CC) -o $@ $^ $(LDFLAGS)
endif
ifneq ($(SRC_LIB),)
$(LIBDIR)/$(SRC_LIB) : $(SRC_OBJ)
—$(AR) rcs $@ $^
—cp $@ $(SRC_BASE)/libs
endif
1 | 此时再执行make,完成后查看build目录树: |
build/
└── unix_dbg
├── bin
│ └── target_bin
├── lib
│ ├── libipc.a
│ └── libtools.a
└── obj
├── ipc
├── main
└── tools
1 | 可以看到,生成的目标是在对应目录下。我们乘胜追击,把.o文件也将其修改了。我们之前的每个模块Makefile大致是这样写的: |
SRC_BASE = ../..
CFLAGS +=
CPPFLAGS += -I. -I./inc -I$(SRC_BASE)/include
SRC_OBJ = $(patsubst %.c, %.o, $(wildcard *.c))
SRC_FILES = $(wildcard src/*.c)
SRC_OBJ = $(SRC_FILES:.c=.o)
SRC_LIB = xx.a
include $(SRC_BASE)/Makefile.rule
1 | 其中SRC_OBJ在此处给出,然后再在Makefile.rule中使用,此处的.o文件会在.c文件相同目录下生成,所以我们现在需要将.o文件加上路径,由于取得路径是在Makefile.rule里面,所以我们可以统一在Makefile.rule里面给变量SRC_OBJ赋值,大致如下: |
SRC_OBJ = $(patsubst %.c, $(OBJDIR)/%.o, $(notdir $(SRC_FILES)))
1 | 这里用到函数patsubst、notdir,关于函数会在后面讲到。这样.o文件作为目标生成之后就会生成到相应目录里面了。 |
make
make[1]: Entering directory /home/Myprojects/example_make/version-2.9/src/ipc'
make[1]: *** No rule to make target
../../build/unix_dbg/obj/ipc/ipc.o’, needed by ../../build/unix_dbg/lib/libipc.a'. Stop.
make[1]: Leaving directory
/home/Myprojects/example_make/version-2.9/src/ipc’
make: *** [ipc] Error 2
1 | 发现出错了,并且是在生成目标文件ipc.o时没有成功,查看build目录树也没有生成.o文件。为什么会生成失败呢? |
%.o: %.c
commands to execute (built-in):
—$(COMPILE.c) $(OUTPUT_OPTION) $<
1 | 该模式规则中目标文件是$(OBJDIR)/%.o,那么现在有了符合生成我们需要的.o文件的规则了,编译一下: |
make
make[1]: Entering directory /home/Myprojects/example_make/version-2.9/src/ipc'
make[1]: *** No rule to make target
../../build/unix_dbg/obj/ipc/ipc.o’, needed by ../../build/unix_dbg/lib/libipc.a'. Stop.
make[1]: Leaving directory
/home/Myprojects/example_make/version-2.9/src/ipc’
make: *** [ipc] Error 2
#
1 | 发现还是不对,不是已经增加了模式规则了吗,为何还是没有生成.o文件。 |
<targets …>:
….
1 | 比如下面是一个静态模式规则: |
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
1 | 该规则描述了所有的.o文件的依赖文件为对应的.c文件,对于目标“foo.o” ,取其茎“foo”替代对应的依赖模式“%.c”中的模式字符“%”之后可得到目标的依赖文件“foo.c”。这就是目标“foo.o”的依赖关系“foo.o: foo.c”,规则的命令行描述了如何完成由“foo.c”编译生成目标“foo.o” 。命令行中“$<”和“$@”是自动化变量,“$<” 表示规则中的第一个依赖文件, “$@” 表示规则中的目标文件。上边的这个规则描述了以下两个具体的规则: |
foo.o : foo.c
—$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
—$(CC) -c $(CFLAGS) bar.c -o bar.o
1 | 注:该示例与其相关描述摘抄于互联网,描述很不错,估计比我讲的详细) |
$(SRC_OBJ) : $(OBJDIR)/%.o : %.c
—$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
1 | 执行后: |
make
make[1]: Entering directory /home/Myprojects/example_make/version-2.9/src/ipc'
make[1]: *** No rule to make target
ipc.c’, needed by ../../build/unix_dbg/obj/ipc/ipc.o'. Stop.
make[1]: Leaving directory
/home/Myprojects/example_make/version-2.9/src/ipc’
make: *** [ipc] Error 2
#
1 | 发现提示没有文件ipc.c,这说明没有生成.o的原因是没有.c文件,我很好奇的是为何使用非静态模式为何不提示呢?(还没搞懂,再研究研究,知道的可以给个提示哈~~) |
VPATH += ./src
1 | 指定了依赖搜索目录为当前目录下的src目录,我们可以在Makefile.rules里面添加给VPATH变量赋值,而在包含该Makefile.rules之前给出当前模块.c文件所在目录。 |
$(SRC_OBJ) : $(OBJDIR)/%.o : $(MOD_SRC_DIR)/%.c
—$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
1 |
|
VPATH += ./src
1 | 可以写为: |
vpath %.c ./src
1 | 现在给一个我们的Makefile.rules: |
Copyright (C) 2014 shallnew \at 163 \dot com
if without a platform defined, give value “unknow” to PLATFORM
ifndef PLATFORM
—PLATFORM = unknow
endif
define a root build directory base on the platform
if without a SRC_BASE defined, just use local src directory
ifeq ($(SRC_BASE),)
—BUILDDIR = $(MOD_SRC_DIR)
—OBJDIR = $(MOD_SRC_DIR)
—LIBDIR = $(MOD_SRC_DIR)
—BINDIR = $(MOD_SRC_DIR)
else
—ifeq ($(DEBUG_SYMBOLS), TRUE)
—>—BUILDDIR = $(SRC_BASE)/build/$(PLATFORM)_dbg
—else
—>—BUILDDIR = $(SRC_BASE)/build/$(PLATFORM)
—endif
—OBJDIR = $(BUILDDIR)/obj/$(MODULE)
—LIBDIR = $(BUILDDIR)/lib
—BINDIR = $(BUILDDIR)/bin
endif
update compilation flags base on “DEBUG_SYMBOLS”
ifeq ($(DEBUG_SYMBOLS), TRUE)
—CFLAGS += -g -Wall -Werror -O0
else
—CFLAGS += -Wall -Werror -O2
endif
VPATH += $(MOD_SRC_DIR)
SRC_OBJ = $(patsubst %.c, $(OBJDIR)/%.o, $(notdir $(SRC_FILES)))
ifeq ($(MAKELEVEL), 0)
all : msg
else
all : lib bin
endif
lib : $(OBJDIR) $(LIBDIR)/$(SRC_LIB)
bin : $(OBJDIR) $(BINDIR)/$(SRC_BIN)
$(OBJDIR) :
—mkdir -p $@
ifneq ($(SRC_BIN),)
$(BINDIR)/$(SRC_BIN) : $(SRC_OBJ)
—$(CC) -o $@ $^ $(LDFLAGS)
endif
ifneq ($(SRC_LIB),)
$(LIBDIR)/$(SRC_LIB) : $(SRC_OBJ)
—$(AR) rcs $@ $^
—cp $@ $(SRC_BASE)/libs
endif
$(SRC_OBJ) : $(OBJDIR)/%.o : %.c
—$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
msg:
—@echo “You cannot directily execute this Makefile! This Makefile should called by toplevel Makefile.”
clean target
clean:
ifneq ($(SRC_LIB),)
—>—$(RM) $(SRC_OBJ) $(LIBDIR)/$(SRC_LIB)
endif
ifneq ($(SRC_BIN),)
—>—$(RM) $(SRC_OBJ) $(BINDIR)/$(SRC_BIN)
endif
.PHONY : all clean
1 |
|
SRC_FILES = $(wildcard src/*.c)
1 | 返回src目录下所有.c文件列表。 |
SRC_OBJ = $(patsubst %.c, %.o, $(SRC_FILES))
1 | 将SRC_FILES中所有.c文件替换为.o返回给变量SRC_OBJ。 |
$(objects:.c=.o)
$(patsubst %.c,%.o,$( src_files))
1 | 4. 过滤函数—filter。 |
make
make[1]: Entering directory /home/Myprojects/example_make/version-3.1/src/ipc'
make[1]: Leaving directory
/home/Myprojects/example_make/version-3.1/src/ipc’
make[1]: Entering directory /home/Myprojects/example_make/version-3.1/src/tools'
make[1]: Leaving directory
/home/Myprojects/example_make/version-3.1/src/tools’
make[1]: Entering directory /home/Myprojects/example_make/version-3.1/src/main'
make[1]: Leaving directory
/home/Myprojects/example_make/version-3.1/src/main’
1 | 发现只有进入目录和退出目录的显示,这样很难知道目前编译过程。其实我们可以在规则命令处加入一行类似打印: |
$(OBJDIR) :
—@echo “ MKDIR $(notdir $@)…”
—@mkdir -p $@
ifneq ($(SRC_BIN),)
$(BINDIR)/$(SRC_BIN) : $(SRC_OBJ)
—@echo “ LINK $(notdir $@)…”
—@$(CC) -o $@ $^ $(LDFLAGS)
endif
ifneq ($(SRC_LIB),)
$(LIBDIR)/$(SRC_LIB) : $(SRC_OBJ)
—@echo “ ARCHIVE $(notdir $@)…”
—@$(AR) rcs $@ $^
—@echo “ COPY $@ to $(SRC_BASE)/libs”
—@cp $@ $(SRC_BASE)/libs
endif
$(SRC_OBJ) : $(OBJDIR)/%.o : %.c
—@echo “ COMPILE $(notdir $<)…”
—@$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
1 | 编译输出如下: |
make
make[1]: Entering directory /home/Myprojects/example_make/version-3.1/src/ipc'
COMPILE ipc.c...
ARCHIVE libipc.a...
COPY ../../build/unix_dbg/lib/libipc.a to ../../libs
make[1]: Leaving directory
/home/Myprojects/example_make/version-3.1/src/ipc’
make[1]: Entering directory /home/Myprojects/example_make/version-3.1/src/tools'
COMPILE base64.c...
COMPILE md5.c...
COMPILE tools.c...
ARCHIVE libtools.a...
COPY ../../build/unix_dbg/lib/libtools.a to ../../libs
make[1]: Leaving directory
/home/Myprojects/example_make/version-3.1/src/tools’
make[1]: Entering directory /home/Myprojects/example_make/version-3.1/src/main'
COMPILE main.c...
LINK target_bin...
make[1]: Leaving directory
/home/Myprojects/example_make/version-3.1/src/main’
1 | 其中目录切换的输出仍然很多,我们可以将其关闭,这需要使用到make的参数,在make -C是指定--no-print- |
$(BUILDDIR):
—@echo “ Create directory $@ …”
—mkdir -p $(BUILDDIR)/bin $(BUILDDIR)/lib
$(MODULES):
—@$(MAKE) -C $(DIR)/$@ MODULE=$@ –no-print-directory
main:tools ipc
clean :
—@for subdir in $(MODULES); \ —do $(MAKE) -C $(DIR)/$$subdir MODULE=$$subdir $@ –no-print-directory; \ —done
编译输出:
make
COMPILE ipc.c...
ARCHIVE libipc.a...
COPY ../../build/unix_dbg/lib/libipc.a to ../../libs
COMPILE base64.c...
COMPILE md5.c...
COMPILE tools.c...
ARCHIVE libtools.a...
COPY ../../build/unix_dbg/lib/libtools.a to ../../libs
COMPILE main.c...
LINK target_bin…
make clean
rm -f ../../build/unix_dbg/obj/ipc/ipc.o ../../build/unix_dbg/lib/libipc.a
rm -f ../../build/unix_dbg/obj/main/main.o ../../build/unix_dbg/bin/target_bin
rm -f ../../build/unix_dbg/obj/tools/base64.o ../../build/unix_dbg/obj/tools/md5.o
../../build/unix_dbg/obj/tools/tools.o ../../build/unix_dbg/lib/libtools.a
#
这样看上去输出清爽多了。其实我们也可以使用make -s 来全面禁止命令的显示。
>【版权声明:转载请保留出处:http://blog.csdn.net/shallnet/article/details/37358655】