入门C/C++自动构建利器之Makefile

Posted 易水南风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了入门C/C++自动构建利器之Makefile相关的知识,希望对你有一定的参考价值。

更多博文,请看音视频系统学习的浪漫马车之总目录

Makefile简介

上一篇浅析C/C++编译本质已经比较详细地介绍了C/C++编译的流程和初步探讨了编译过程中底层的一些细节,可能各位已经发现,即使文章中的Demo很小,但是每次修改源码之后都要调用g++命令再重新链接一次,改了几次之后就有点吃力了,假如遇到大型项目,这样一个个调用g++(gcc)编译命令,那有点一顿操作猛如虎,结果什么的那味么。。

我们程序员总是很喜欢偷懒的,总是希望能交给机器的事情觉得不自己亲自处理,所以编译也是一样,我们要的是告诉机器一个编译流程,然后每次机器都帮我们一键编译好,于是,Makefile就应运而生了。

法国数学家laplace才会说,对数的发明延长了科学家生命,那么Makefile也可以说是延长了程序员的寿命。

什么是Makefile呢?首先要先了解下make。make 是一个命令工具,,是一个解释 makefile 中指令的命令工具,一般来说,大多数的 IDE 都有这个命令,比如:Visual C++ 的 nmake,QtCreator 的 qmake ,只是IDE帮我们封装好了,所以一般不需要我们亲自编写。

这里专门讲GNU的make,先看下官方文档的叙述:Overview of make

The make utility automatically determines which pieces of a large program need to be recompiled, and issues commands to recompile them. ……Our examples show C programs, since they are most common, but you can use make with any programming language whose compiler can be run with a shell command. Indeed, make is not limited to programs. You can use it to describe any task where some files must be updated automatically from others whenever the others change.

To prepare to use make, you must write a file called the makefile that describes the relationships among files in your program and provides commands for updating each file. In a program, typically, the executable file is updated from object files, which are in turn made by compiling source files.

Once a suitable makefile exists, each time you change some source files, this simple shell command:

make
suffices to perform all necessary recompilations. The make program uses the makefile data base and the last-modification times of the files to decide which of the files need to be updated. For each of those files, it issues the recipes recorded in the data base.

You can provide command line arguments to make to control which files should be recompiled, or how.

简单来说,make就是一个构建工具,通过make的shell命令,去跑Makefile脚本,而Makefile脚本就指定了项目中哪些文件需要编译,文件的依赖关系以及最终的编译目标产物。所以通过make命令加上Makefile脚本就可以实现一键编译整个项目的梦想,而熟悉Makefile,也为将来编译FFmpeg等C/C++开源库打好基础。

Makefile规则

显示规则

Makefile最基本的规则可用以下语句表示:

# 每条规则的语法格式:
target1,target2...: depend1, depend2, ...
	command
	......
	......

target:当前规则的生成目标,比如需要生成一个C语言的目标文件
depend:生成目标文件的依赖文件,比如要生成一个C语言的目标文件,需要一个汇编文件(.s后缀的文件)
command:表示具体从依赖文件生成目标文件的方法。比如要从汇编文件生成目标文件,则使用gcc -c命令。

Makefile中第一条规则的目标就是当前Makefile文件的终极目标,而第一条规则中的依赖如果不存在,则会往下寻找生成这个依赖的规则,依次递归执行直到生成终极(第一条规则)目标的依赖全部存在,再生成终极目标。

Makefile就是通过这样一条条这样的规则语句的组合,就实现了对一个大型项目的构建。

举个栗子?

还是用上一篇浅析C/C++编译本质里面的动物例子吧:

这是当前目录各个文件:

我们的目标是产生一个main的可执行文件,为了得到main,就需要Cat、Dog、main的目标文件,而为了得到这些目标文件,就要从它们的源文件进行预处理、编译、汇编流程,首先我们先根据上面的规则写出产生Cat.o的Makefile语句吧:

#终极目标是Cat.o,依赖是Cat.s
Cat.o:Cat.s
		#为了得到Cat.o的具体命令
        g++ -c Cat.s -o Cat.o
#目标是Cat.s,依赖是Cat.i
Cat.s:Cat.i
		#为了得到Cat.s的具体命令
        g++ -S Cat.i -o Cat.s
#目标是Cat.i,依赖是Cat.cpp
Cat.i:Cat.cpp
		#为了得到Cat.i的具体命令
        g++ -E Cat.cpp -o Cat.i

这里终极目标是Cat.o,为了得到Cat.o,就要得到Cat.s,为了得到Cat.s,就需要往下寻找得到Cat.s的规则语句,依次类推找到直到找到最后一条规则语句,找到依赖项Cat.cpp是已经存在的,然后倒过来执行生成每个步骤的依赖,直到生成最后的Cat.o。

在src目录中执行make命令:

可以看到make命令很体贴,都把整个过程打印出来了,可以清晰看到整个依赖链生成的全过程。Cat.i、Cat.s、Cat.o文件都已经生成:

现在已经初步实现了一键生成目标文件。

伪目标

如果想删除掉Makefile产生的文件(Cat.i、Cat.s、Cat.o),传统做法是:

这样子当然很不自动化,如果要删除的文件成百上千,那基本就是gg的节奏。既然Makefile帮我们生成了这些文件,那么它也有责任将这些文件删除。

是的,Makefile可以做到,它可以封装命令来达到一键执行一些命令,通过伪目标来实现,例如删除构建产生的文件:

Cat.o:Cat.s
        g++ -c Cat.s -o Cat.o
Cat.s:Cat.i
        g++ -S Cat.i -o Cat.s
Cat.i:Cat.cpp
        g++ -E Cat.cpp -o Cat.i

#.PHONY表示这是个没有目标的规则语句,即伪目标,clear表示伪目标命令名称
.PHONY:
clear:
		//具体的执行命令
		rm -rf Cat.i Cat.s Cat.o

.PHONY表示这是个没有目标的规则语句,clear表示伪目标的命令,换行的是具体的伪目标命令。之所以要加使用.PHONY,是因为要防止当前目录刚好有文件名叫clear,从而以为目标是clear而产生的冲突。

怎么使用呢,直接make后面跟着命令名:

可以看到rm -rf Cat.i Cat.s Cat.o已经被执行,文件也顺利被删除。

有了伪目标,我们就开始可以自由地搞一些动作了,只要对命令足够熟悉~

变量

只是产生Cat.o那还没完成项目构建呢,根据上述内容,依葫芦画瓢写出完整Makefile吧:

#终极目标是main可执行文件,所以依赖是3个目标文件
main:Cat.o Dog.o main.o
        g++ Cat.o Dog.o main.o -o main
Cat.o:Cat.s
        g++ -c Cat.s -o Cat.o
Cat.s:Cat.i
        g++ -S Cat.i -o Cat.s
Cat.i:Cat.cpp
        g++ -E Cat.cpp -o Cat.i
Dog.o:Dog.s
        g++ -c Dog.s -o Dog.o
Dog.s:Dog.i
        g++ -S Dog.i -o Dog.s
Dog.i:Dog.cpp
        g++ -E Dog.cpp -o Dog.i
main.o:main.s
        g++ -c main.s -o main.o
main.s:main.i
        g++ -S main.i -o main.s
main.i:main.cpp
        g++ -E main.cpp -o main.i

可以看到哗啦啦执行了一堆命令后,main可执行文件出现了。

可执行文件是出现了,但是是个人都能看到这也麻烦了吧,这么个小项目还要写一堆东西,算个什么自动构建系统?

接下来,就是开始大刀阔斧精简Makefile文件的时候了~

1.上篇文章已经说了,gcc命令可以一步到位从源文件生成目标文件,所以可以改为:

main:Cat.o Dog.o main.o
        g++ Cat.o Dog.o main.o -o main
Cat.o:Cat.cpp
        g++ -c Cat.cpp -o Cat.o
Dog.o:Dog.cpp
        g++ -c Dog.cpp -o Dog.o
main.o:main.cpp
        g++ -c main.cpp -o main.o

.PHONY:
clear:
        rm -rf Cat.o Dog.o main.o

执行下:

ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ make
g++ -c Cat.cpp -o Cat.o
g++ -c Dog.cpp -o Dog.o
g++ -c main.cpp -o main.o
g++ Cat.o Dog.o main.o -o main
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ls
Cat.cpp  Cat.h  Cat.o  Dog.cpp  Dog.h  Dog.o  main  main.cpp  main.o  Makefile
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ./main 
Cat::eat
externData:10
I am dog
Here is a Cat

当然这只是简化gcc命令,接下来才是重点。

很容易发现,Makefile文件中有重复的内容,比如“Cat.o Dog.o main.o”,作为程序员,应该就有这方面的敏感度了,那么类似抽取变量操作就是必须的了。

没错,Makefile也有变量的概念,定义类似其他语言,常用符号为:

  1. =:替换
  2. :=:恒等于
  3. +=:追加

其实很简单,将上面例子改为:

#定义变量
TAR := main
DEPEND := Cat.o Dog.o main.o
G = g++
#通过$进行变量的使用
$TAR:$DEPEND
        $G $DEPEND -o $TAR
Cat.o:Cat.cpp
        $G -c Cat.cpp -o Cat.o
Dog.o:Dog.cpp
        $G -c Dog.cpp -o Dog.o
main.o:main.cpp
        $G -c main.cpp -o main.o

.PHONY:
#删除中间产物
clear:
        rm -rf $DEPEND

执行结果:

ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ make
g++ -c Cat.cpp -o Cat.o
g++ -c Dog.cpp -o Dog.o
g++ -c main.cpp -o main.o
g++ Cat.o Dog.o main.o -o main
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ls
Cat.cpp  Cat.h  Cat.o  Dog.cpp  Dog.h  Dog.o  main  main.cpp  main.o  Makefile

也成功生成main可执行文件

再试试清理中间产物的伪目标:

ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ make clear
rm -rf Cat.o Dog.o main.o
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ls
Cat.cpp  Cat.h  Dog.cpp  Dog.h  main  main.cpp  Makefile

同样执行成功。

其实Makefile中已经内置了预定义的变量了:

这些预定义的变量也经常使用在一些主流开源项目中,所以多记记才能避免遇到主流开源项目的Makefile一脸懵逼。

所以其实上面的例子的对g++定义的变量也可以改为直接使用CXX:

TAR := main
DEPEND := Cat.o Dog.o main.o
#g++使用内置变量
#G = g++

$TAR:$DEPEND
        $CXX $DEPEND -o $TAR
Cat.o:Cat.cpp
        $CXX -c Cat.cpp -o Cat.o
Dog.o:Dog.cpp
        $CXX -c Dog.cpp -o Dog.o
main.o:main.cpp
        $CXX -c main.cpp -o main.o

.PHONY:
clear:
        rm -rf $DEPEND

执行下:

ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ make clear
rm -rf Cat.o Dog.o main.o
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ make
g++ -c Cat.cpp -o Cat.o
g++ -c Dog.cpp -o Dog.o
g++ -c main.cpp -o main.o
g++ Cat.o Dog.o main.o -o main
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ls
Cat.cpp  Cat.h  Cat.o  Dog.cpp  Dog.h  Dog.o  main  main.cpp  main.o  Makefile

一样也是ok的。

隐藏规则

上面的Makefile看起来还不错,挺简介,但是Makefile可不是搞这个麻雀型项目的,燕雀安知鸿鹄志~~如果一个项目有成百上千个源文件,那岂不是要写上百行以上的gcc命令?即使有了变量,那定义起来一个个源文件列举出来也是要命,该如何是好呢?

为什么要列出具体文件名呢?有shell经验的估计已经想到了,当然就是通配符,Makefile 是可以使用 shell 命令的,所以 shell 支持的通配符在 Makefile 中也是同样适用的。

:匹配0个或者是任意个字符,比如 .c表示所有以.c结尾的文件。
%:也是匹配任意个字符,一般用于遍历上一层的依赖文件列表并匹配到目标对应的依赖文件

除了通配符,Makefile还有自动化变量来简化脚本,所谓的自动变量也是一种类似通配符的东西,常见的自动化变量:

通过通配符和自动变量这些隐藏规则,就可以大大简化脚本了。还是上面的栗子,一看就懂:

TAR := main
DEPEND := Cat.o Dog.o main.o

$TAR:$DEPEND
        $CXX $DEPEND -o $TAR
#以上命令改为以下语句。
#相当于遍历DEPEND中每个.o文件名,然后从当前目录中找到同名的.cpp文件,每次执行一次gcc命令生成对应的.o文件
%.o:%.cpp
		#$^会用依赖对应的具体.cpp文件替换,$@会用目标对应的具体.o文件替换
        $CXX -c $^ -o $@

%.o:%.cpp相当于遍历DEPEND中每个.o文件名,然后从当前目录中找到同名的.cpp文件,每次执行一次gcc命令生成对应的.o文件,而命令中的$^会用依赖对应的具体.cpp文件替换,$@会用目标对应的具体.o文件替换

执行下:

ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ make clear
rm -rf Cat.o Dog.o main.o
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ls
Cat.cpp  Cat.h  Dog.cpp  Dog.h  main  main.cpp  Makefile
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ make
g++ -c Cat.cpp -o Cat.o
g++ -c Dog.cpp -o Dog.o
g++ -c main.cpp -o main.o
g++ Cat.o Dog.o main.o -o main

是不是有种胖子突然减肥成功的感觉呢?

编译动态链接库

现在尝试用Makefile完成上一篇浅析C/C++编译本质所列举的编写动态链接库的栗子。

先把Cat.cpp和Dog.cpp编成动态链接库,这里将生成动态库的资源放到特定目录libSrc中:

ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ mkdir libSrc
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ mv Cat.cpp Dog.cpp libSrc/
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ls
Cat.h  Dog.h  libSrc  main  main.cpp  Makefile
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ls libSrc/
Cat.cpp  Dog.cpp
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ mv Cat.h Dog.h libSrc/
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ls
libSrc  main  main.cpp  Makefile
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ls libSrc/
Cat.cpp  Cat.h  Dog.cpp  Dog.h
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$
//移动头文件放到include目录下
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src/libSrc$ mkdir include
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src/libSrc$ ls
Cat.cpp  Cat.h  Dog.cpp  Dog.h  include
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src/libSrc$ mv Cat.h Dog.h include/
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src/libSrc$ ls
Cat.cpp  Dog.cpp  include
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src/libSrc$ ls include/
Cat.h  Dog.h

创建新的Makefile文件,根据上面的语法,很容易写出Makefile脚本:

LIBTARGET:=libAnimal.so
LIBDEPEND:=Cat.cpp Dog.cpp
#指定是生成动态库
LDFFLAGS:=-shared
#C++编译选项指定生成pic代码以及头文件位置
CXXFLAGS:=-fpic -Iinclude

$LIBTARGET:$LIBDEPEND
        $CXX $CXXFLAGS $LDFFLAGS $^ -o $@
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src/libSrc$ make
g++ -fpic -Iinclude -shared Cat.cpp Dog.cpp -o libAnimal.so
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src/libSrc$ ls
Cat.cpp  Dog.cpp  include  libAnimal.so  Makefile

可以看出动态库libAnimal.so已经生成~~

嵌套执行make

现在已经生成动态链接库了,就差和主工程的目标文件一起生成可执行文件了。

这里先调整下目录结构:

ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ls
app  libSrc  Makefile
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ls app/
include  main.cpp Makefile
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ls libSrc/
Cat.cpp  Dog.cpp  Makefile

项目目录下有app和libSrc 2个目录,app主要放主项目代码,libSrc放动态链接库代码,2个目录下都有各自的Makefile,项目根目录有根Makefile。

app下的Makfile:

TAR := main
CXXFLAGS := -Iinclude
DEPEND := main.o
#.o文件和动态链接库生成可执行文件
$TAR:$DEPEND
        $CXX $^ *.so -o $@
%.o:%.cpp
        $CXX $CXXFLAGS -c $^ -o $@

.PHONY:
clear:
        $RM main

libSrc目录Makefile:

#动态库生成输出到app目录下
LIBTARGET=../app/libAnimal.so
LIBDEPEND=Cat.cpp Dog.cpp
LDFFLAGS=-shared
CXXFLAGS=-fpic -I../app/include
#生成动态库
$LIBTARGET:$LIBDEPEND
        $CXX $CXXFLAGS $LDFFLAGS $^ -o $@

.PHONY:
clear:
        $RM $LIBTARGET *.o *.i *.s

clearTemp:
        $RM *.o *.i *.s

主要是看下根目录的Makefile,主要用于执行2个子目录的Makefile

LIB := libSrc
APP := app

.PHONY : all $(APP) $(LIB)
#这是遍历文件夹语句,每次遍历就执行下面的命令,即make命令
$(APP) $(LIB) :
#-C表示在当前目录下执行make
        $(MAKE) -C $@
#指定APP文件Makefile执行依赖LIB中的Makefile,即先执行LIB中的Makefile,再执行APP文件Makefile
$(APP):$(LIB)

主要就是遍历子目录执行对应的Makefile,这里要注意的就是子目录Makefile依赖关系,在最后一行已经指明。

执行根目录Makefile:

ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ make
make -C libSrc
make[1]: Entering directory '/home/ubuntu/study/projects/CatTest/src/libSrc'
g++ -fpic -I../app/include -shared Cat.cpp Dog.cpp -o ../app/libAnimal.so
make[1]: Leaving directory '/home/ubuntu/study/projects/CatTest/src/libSrc'
make -C app
make[1]: Entering directory '/home/ubuntu/study/projects/CatTest/src/app'
g++ main.o *.so -o main
make[1]: Leaving directory '/home/ubuntu/study/projects/CatTest/src/app'
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ls
app  libSrc  Makefile
//执行生成的main可执行文件
ubuntu@VM-20-7-ubuntu:~/study/projects/CatTest/src$ ./app/main 
Cat::eat
externData:10
I am dog
Here is a Cat

可以看到Makefile依旧很贴心,整个在各个目录间游动的路径都打印出来了,一切非常顺利~~麻雀虽小五脏俱全,一个像模像样的迷你Makefile系统就这样搭建完成了,相信以后大家看到大的C/C++项目的Makefile就不会一脸懵逼了哈哈。

最后想说的就是,Makfile是增量更新的,不会每次都无脑全部编译一遍,如果目标文件已经存在,则每次make都会比较依赖和目标的生成时间,如果依赖比目标文件新才会执行生成指令。

本文主要就介绍了Makefile入门相关的内容,在本文的基础上,后面将开始讲述cmake以及ndk的知识。

以上是关于入门C/C++自动构建利器之Makefile的主要内容,如果未能解决你的问题,请参考以下文章

入门C/C++自动构建利器之Makefile

一篇文章入门C/C++自动构建利器之Makefile

升级构建工具,从Makefile到CMake

升级构建工具,从Makefile到CMake

升级构建工具,从Makefile到CMake

浅析C/C++编译流程