make 构建工具

make是一种C/C++的构建工具,在Linux编写C/C++程序几乎一定会直接或间接用到它。make工具古老而且经典,Linux下存在大量工程使用make构建,不过它的缺点则是非常难以使用,make的语法十分怪异,仅仅看一遍make的文档是很难正确使用它的,配置Makefile需要一定的工程经验,否则很容易掉入陷阱,或者出现编写方式不是最佳实践的情况。不过好在make的功能比较单纯(这和恐怖的Gradle不同),参考一些例子简单学习后也是可以较快掌握其基本使用方法的,因此也不至于学习成本太高。

不过实际上,由于make还是比较难用,现在cmake其实才是现代C/C++工程构建工具的事实标准,有关cmake的内容将在其它章节介绍。至于Windows下不推荐也较少使用make,因为Windows下编写C/C++程序一般会采用微软的Visual Studio,图形界面的IDE管理工程构建更加方便直观,当然它的缺点则是强烈依赖IDE工具,几乎不具有可移植性。

为什么要使用构建工具

假设我们的工程中有这样一些C源代码文件:myfunc.hmeow.cbark.cmain.c

如果我们要使用GCC编译器编译这些文件,执行的命令应该是gcc meow.c bark.c main.c -I. -o main。对于C/C++工程来说,工程最终输出的内容通常都是由多个源代码文件编译链接而成的,如果每次都是手动输入这样一长串命令对每个源码文件编译并链接到一起这很不方便,规模越庞大的工程越不可能采用这种方式;此外每次重新构建时,对于那些没有修改的文件也重新编译无疑会浪费时间;即使我们自己编写一些Shell脚本来自动化这个过程,我们编写的脚本可能也不具备很好的可移植性和可复用性。

make等构建工具正是为了解决这些问题而诞生的,实际开发中我们通常使用一种构建工具来管理工程,而非每次手动使用命令编译工程。以make为例,我们只需要在工程根目录编写一个Makefile脚本文件,每次构建工程时执行对应的make命令即可自动构建整个工程,再也不用输入一长串命令或维护复杂的Shell脚本了。

Makefile使用例子

在具体学习make之前,我们先看一个简单的例子来理解make的使用方法,这里我们有一个C语言工程,它包含了若干文件:myfunc.hmeow.cbark.cmain.c,它的根目录下放置了Makefile脚本文件,其中包含了工程的编译和链接规则。Makefile脚本内容如下。

all: meow.o bark.o main.o
    gcc meow.o bark.o main.o -I. -o main

meow.o: meow.c myfunc.h
    gcc -c meow.c

bark.o: bark.c myfunc.h
    gcc -c bark.c

main.o: main.c myfunc.h
    gcc -c main.c

.PHONY: clean
clean:
    rm -r meow.o bark.o main.o main

如果需要编译工程,我们直接执行make命令;如果需要清理构建输出,执行make clean命令。执行make命令后,实际上发生了如下事情:

  1. make程序检查当前目录下是否有名字为Makefile的脚本文件
  2. make找到构建脚本后,寻找脚本中第一条构建规则并将其作为最终目标,也就是我们这里定义的all规则(当然这个目标的名字是我们自定义的,只不过将all作为最终目标是一种最佳实践)
  3. 根据all规则的依赖项寻找依赖文件,如果依赖文件不存在(或者它被修改过,即依赖的最后修改时间比已有的构建目标新),递归的寻找依赖文件的构建规则,直到所有依赖被满足并执行最终目标

注意:gcc -c编译源码文件到目标文件时,这一步不会用到头文件myfunc.h,但我们仍然将其加入了构建目标的依赖中,这是为什么呢?实际上,这样做能够在头文件修改时触发相关联的目标文件重新构建,避免出现头文件修改了而目标文件没有重新编译造成的问题,实际开发中我们也要注意这一点。

至于make clean命令,它是一个“伪目标”,简而言之clean就是一个标识而非实际文件,它执行一条rm命令,用于清理构建输出的内容,恢复工程目录结构的初始状态。

以上我们就实现了一个最基础的Makefile脚本,不过Makefile脚本的语法远不止这些,我们其实可以使用变量、自动推导等功能简化这个脚本,这里我们将继续学习Makefile语法并逐步介绍这些内容。

Makefile基本语法

规则

Makefile文件是make构建工具的脚本,使用make最重要的一步就是编写Makefile构建脚本。Makefile由若干条规则组成,每条规则的格式如下:

target ... : prerequisites ...
    command1
    command2
    ...

其中,冒号:前面的是目标,它是一个名称标识;后面的是依赖,规则具体执行前会检查并确保这些依赖目标已执行,依赖文件的改动将导致对其有依赖的目标过期;规则的具体内容则是若干条命令

下面是个简单的例子。

hello: meow.o bark.o main.o
    gcc meow.o bark.o main.o -I. -o main

上面一段代码简单来说就是我们有一个叫hello的目标,输入make hello命令就会执行该目标。如果它是最终目标我们在命令中可以省去目标名,直接输入make即可,此外这个目标要求必须存在3个文件meow.obark.omain.o,该目标具体执行的内容是gcc meow.o bark.o main.o -o hello命令。

注意:

  • Makefile语法十分严格,命令必须以Tab制表符即\t开头,不允许使用空格代替,make会调用Shell执行这些命令
  • 命令可以有多条,但注意所有命令都要以Tab制表符开头
  • 终端输入make命令后会自动寻找当前目录下的名为Makefile的文件并执行,这是一个约定的文件名,否则需要使用类似make -f xxx的方式指定Makefile脚本文件
  • make会自动选择哪些源文件需要重新编译,未更新的源文件则不必重新编译,这可以实现增量构建

注释

Makefile中,我们可以使用#开头表示注释。

# 这是测试规则
all: meow.o bark.o main.o
    gcc meow.o bark.o main.o -I. -o main

输出调试信息

Makefile中,规则中@开头的命令执行时,不会显示命令本身而只显示它的结果,因此我们可以用@echo来输出调试信息。

all: meow.o bark.o main.o
    @echo "开始链接啦"
    gcc meow.o bark.o main.o -I. -o main

变量

Makefile中我们可以定义变量以供复用,比如定义CC=gcc,表示定义一个名叫CC的变量其值为gcc,这样如果我们要改为使用其它编译器,直接修改脚本中的CC变量的值即可,而不必在具体的命令中到处修改了。下面例子中我们将构建中的编译器、编译器参数、依赖头文件、目标文件都定义成了变量。

CC=gcc
CFLAGS=-I.
DEPS=myfunc.h
OBJECTS=meow.o bark.o main.o

all: $(OBJECTS)
    $(CC) $(OBJECTS) $(CFLAGS) -o main

变量使用$()的形式引用。

模式规则

Makefile中我们可以定义模式规则来对匹配的一类文件(通常是多个)进行处理,我们直接看一个例子。

CC=gcc
CFLAGS=-I.
DEPS=myfunc.h
OBJECTS=meow.o bark.o main.o

all: $(OBJECTS)
    $(CC) $(OBJECTS) $(CFLAGS) -o main

%.o: %.c $(DEPS)
    $(CC) -c $< -o $@

我们主要关注%.o目标。这里我们定义目标为%.o依赖为%.c和头文件myfunc.h,它就是一个模式规则,它会匹配所有的.c文件并执行该规则中的命令。在文件的开头我们定义了一些变量,变量的作用之前已经介绍过了。注意这里我们将头文件myfunc.h加入了依赖中,这是为了在头文件修改时触发该规则的重新执行。

具体的命令中,我们用到了$<$@这两个变量,它们分别表示规则中的第一个依赖文件和规则中的目标文件。因此实际上,上述模式规则实际执行的命令是:

gcc -c meow.c -o meow.o
gcc -c bark.c -o bark.o
gcc -c main.c -o main.o

make的内置函数

make中有一些内置函数供我们使用,功能包括字符串处理、控制流程等。下面例子中,我们使用了wildcard函数匹配.h文件列表,省去了明确手写所有头文件的麻烦。

DEPS=$(wildcard include/*.h)

经过wildcard函数的处理,实际上DEPS就会被赋值为形如include/a.h include/b.h include/c.h的形式。除了wildcard以外make中还有很多其它的内置函数,这里就不多介绍了,具体可以参考相关文档。

伪目标

make中如果一个规则的目的不是为了生成目标,仅仅是执行一些命令,这种目标叫做伪目标,我们可以用.PHONY声明伪目标,下面是一个例子。

.PHONY:clean
clean:
    rm -r out/*

这里clean目标就非常适合声明为伪目标。当然,这里我们即使不声明clean是伪目标它也可以正常执行,声明伪目标主要有两个目的:

  1. 避免和真实文件冲突,比如上面例子如果工程中有个文件真的叫clean它就和这个目标冲突了,声明伪目标能避免这种冲突
  2. 提高执行效率,声明伪目标后make处理该目标时就是单纯的执行命令,不会再考虑其它

总之,我们应该将符合伪目标条件的目标都声明为伪目标,避免潜在的冲突以及提高执行效率。

约定

实际开发中,Makefile目标有一些约定俗成的名字:

  • all:执行主要的编译工作,通常用作默认目标
  • clean:删除编译生成的二进制文件
  • distclean:不仅删除编译生成的二进制文件也删除其它生成的文件,例如配置文件和格式转换后的文档等,只留下源代码文件
  • install:执行编译后的安装工作,用于把可执行文件、配置文件、文档等分别拷到不同的安装目录

在命令行中,如果仅输入make,默认会执行Makefile中的第一个目标,因此也推荐将all目标作为Makefile中的第一个目标。

隐式规则

隐式规则是make中的一个让人恼火的特性。make中有一些默认行为被称为“隐式规则”,如果你的Makefile缺少一些规则,可能它实际上执行了隐式规则,导致不完整的Makefile居然恰好也能正常运行或是莫名其妙报错的情况,我们可以使用以下命令查看make的隐式规则。

make -p

我们可以看到make的隐式规则非常庞大,我们的Makefile遇到莫名其妙的Bug时可以先考虑是否触发了隐式规则,我们编写自己的规则时也可以参考这些隐式规则的语法。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap