《跟我一起写Makefile》摘要 1-3


0. 前言

  • 想成为C++工程师,需要系统学习开发工具,就从Makefile和cmake入手吧。
  • 跟我一起写Makefile
  • 目录:
    1. 概述:简单介绍makefile的来源、作用,以及介绍程序编译与链接的基本过程
    2. makefile介绍:简单介绍Makefile的基本功能
    3. 书写规则:介绍规则相关的内容,即targets/commands/prerequisites相关

1. 概述

1.1. Makefile 基本介绍

  • 作用:关系到整个项目的编译规则:
    • 指定哪些文件先编译,哪些文件后编译,哪些重新编译。
    • 类似于shell脚本,执行操作系统的命令。
  • 好处:自动化编译
    • 通过make命令,使得整个工程自动编译,提高软件开发效率
    • 大多数IDE都自带make。
  • 其他:不同厂商的make略有不同,本书选用的是 RedHat Linux 8.0,make版本3.80。

1.2. C++程序的编译与链接

  • 总过程:源文件先编译成为中间目标文件,再由中间目标文件生成执行文件。
  • 编译(compile):将原文件编译成中间代码文件,即.obj.o文件,即object file。
    • 主要工作:确认语法正确、函数与变量的声明正确。对于后者,主要是确认头文件单的位置。
    • 一般来说,每个原文件对应一个中间目标文件。
  • 链接(link):将大量的object file合成执行文件。
    • 主要是链接函数与全局变量,会用到中间目标文件。
    • 链接器不管函数所在的源文件,只管中间目标文件。
    • 有时候,由于原文件太多 ,编译生成的中间文件太多,而在链接时需要明显地之处中间目标文件名称,这很不方便,所以需要对中间文件打包,生成库文件/Archive File,win下名为.lib,UNIX下名为.a

2. makefile介绍

2.1. makefile基本规则与举例

  • 基本规则:
    • 如果这个工程没有编译过,那么所有c文件都要编译并被链接。
    • 如果工程的某几个c文件被修改,那么只编译被修改的文件,并链接目标程序。
    • 如果工程的头文件被更改,那么需要重新编译引用了这几个头文件的c文件,并链接目标程序。
  • makefile 的基本格式
    • target:可以是目标文件、执行文件,还可以是标签(可能指的是 clean/install 这些吧)。
    • prerequisites:生成该target所依赖的文件或target。
    • command:该target要执行的命令(任意shell命令)
    • prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。
target ...: prerequisites...
    command ...
  • 举例
    • 执行make命令后,就会得到执行文件edit
    • 如果要删除执行文件和中间文件,可通过make clean实现。
    • command行必须以TAB开头
    • make会比较target文件和prerequisites文件的修改日期,如果prerequisites文件的日期比targets文件新,或target文件不存在,则会执行下面的command。
edit : main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o
    cc -o edit main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o

main.o : main.c defs.h
    cc -c main.c
kbd.o : kbd.c defs.h command.h
    cc -c kbd.c
command.o : command.c defs.h command.h
    cc -c command.c
display.o : display.c defs.h buffer.h
    cc -c display.c
insert.o : insert.c defs.h buffer.h
    cc -c insert.c
search.o : search.c defs.h buffer.h
    cc -c search.c
files.o : files.c defs.h buffer.h command.h
    cc -c files.c
utils.o : utils.c defs.h
    cc -c utils.c
clean :
    rm edit main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o

2.2. make的工作流程

  • 前提:在默认条件下,即只执行make命令时:
    • 在当前目录下找名字叫Makefilemakefile的文件。
    • 如果找到,它会找文件中的第一个目标文件(target)。
      • 在上面的例子中,会找到edit,并把这个target作为最终的目标文件。
    • 如果edit文件不存在,或是edit所依赖的后面的 .o 文件的文件修改时间要比 edit 这个文件新,那么,他就会执行后面所定义的命令来生成 edit 这个文件。
    • 如果 edit 所依赖的 .o 文件也不存在,那么make会在当前文件中找目标为 .o 文件的依赖性,如果找到则再根据那一个规则生成 .o 文件。(这有点像一个堆栈的过程)
    • 当然,你的C文件和H文件是存在的啦,于是make会生成 .o 文件,然后再用 .o 文件生成make的终极任务,也就是执行文件 edit 了。
    • 整个过程就是一层一层找文件的依赖关系,直到最终编译出第一个目标文件。
  • 类似于clean这些标签,没有被一个目标文件直接/间接关联,那么后面定义的命令也就不会被自动执行,但可以通过 make clean 来显示执行。
  • 当修改了其中一个源文件,那么对应的中间目标文件以及最初的edit都会重新编译、链接。

2.3. makefile中使用变量

  • makefile的变量就是一个字符串,也可理解为C语言中的宏。
  • 定义:
objects = main.o kbd.o command.o display.o \
     insert.o search.o files.o utils.o
  • 使用:$(objects)

2.4. make自动推导依赖关系

  • make可实现自动推导文件以及文件依赖关系,不在每一个.o文件后都加上 prerequisites。
  • make看到一个.o文件,会自动把.c文件加到依赖关系中。
    • 例如,有一个whatever.o,那么自动会把whatever.c作为其依赖文件。
    • 同时,cc -c wahtever.c也会被自动推导出来。
  • 举例
    • .PHONY 表示 clean 是个伪目标文件。
objects = main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o

edit : $(objects)
    cc -o edit $(objects)

main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h

.PHONY : clean
clean :
    rm edit $(objects)

2.5. 合并依赖项

  • 上面的例子中,.h文件非常重复,有方法可以实现将.h出现次数减少。
  • 举例
    • 这样简洁不少,但依赖关系也凌乱了(比如,要知道某个.o文件依赖哪些内容,就必须一个个看过去了)。
    • 鱼和熊掌不可兼得,没得法子。
objects = main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o

edit : $(objects)
    cc -o edit $(objects)

$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

.PHONY : clean
clean :
    rm edit $(objects)

2.6. 清空目标文件规则

  • 每个Makefile都应该应该对应一个clean,方便重新编译。
    • 感觉类似于写代码都应该写注释,大家都知道应该这样做,但……
  • 形式:
    • 第二种方式更为稳健(但也没写啥东西……)。
    • -rm中多了一个-主要是为了,如果某些文件除了问题也不要管,继续做后面的事。
clean:
    rm edit $(objects)

.PHONY : clean
clean :
    -rm edit $(objects)

2.7. makefile里有什么

  • 显式规则:说明了如何生成一个或多个目标文件。
    • 这是由Makefile的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。
  • 隐晦规则:由于make有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefile。
  • 变量的定义:变量一般都是字符串,类似于C语言中的宏。
  • 文件指示:包括了三个部分
      1. 在一个Makefile中引用另一个Makefile,就像C语言中的include一样
      1. 另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样
      1. 多行的命令,后面再介绍
  • 注释:Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用 # 字符。
  • 其他:Makefile中的命令必须以TAB开头。

2.8. Makefile 文件名

  • 默认情况下,会按“GNUmakefile”、“makefile”、“Makefile”这个循序寻找。
    • 建议使用Makefile,比较醒目。
    • 不推荐使用 GNUmakefile,因为只支持GNU的make。
    • 大多数make都支持makefileMakefile
  • 也可以指定文件名,通过-f, --file来指定,如make -f Make.Linux

2.9. 引用其它的Makefile

  • 基本格式:include <filename>
  • filename可以是当前操作系统Shell的文件格式,包括路径和通配符。
  • include前可以有空格,但不能是TAB。
  • 可以通过include多个文件,通过一个或多个空格隔开。
    • 例如include foo.make *.mk $(bar)
  • 原理:
    • 当make命令开始时,会寻找include所指出的其他Makefile,并把其内容安置在当前的位置,类似于C/C++中的#include
    • 如果文件都没有绝对路径,而是相对路径,那么首先会在当前目录下找。如果找不到,则:
      • 如果make有指定参数-I, --include-dir,则会在这些目录下去寻找。
      • 如果有<prefix>/include,例如/usr/local/bin/usr/include,make也会寻找。
    • 如果实在找不到:
      • 不会马上报错,而会生成warning信息。
      • 加载剩余其他文件,完成makefile读取,make会重新寻找,如果还是找不到,会出现致命错误。
      • 如果要跳过上面所说的致命信息,可通过-include <filename>实现。

2.10 环境变量MAKEFILES

  • 该环境变量的作用是:make将该变量中的值做一个类似于include的动作。
  • 该环境变量中的值是其他 Makefile,用空格分割。
  • 与普通include的区别:
    • 从这个环境变量中引入的target不会起作用。
    • 如果环境变量中定义的文件发现错误,make也不会理会。
  • 不建议使用
  • 放这是提醒大家,如果有错,不妨看看是不是有人设置了这个环境变量。

2.11 make的工作方式

  • GNU make工作时执行的步骤如下:
    • 读入所有的Makefile。
    • 读入被include的其它Makefile。
    • 初始化文件中的变量。
    • 推导隐晦规则,并分析所有规则。
    • 为所有的目标文件创建依赖关系链。
    • 根据依赖关系,决定哪些目标要重新生成。
    • 执行生成命令。
  • 1-5为第一阶段,6-7为第二阶段。
    • 第一个阶段中,如果定义的变量被使用了,那么make会把其展开在使用的位置。
    • make不会马上展开,使用的策略是,如果变量出现在依赖关系的规则中,只有当前依赖关系决定要使用了,才会内部展开。

3. 书写规则

3.1. 概述

  • 规则包括两部分:
    • 依赖关系
    • 生成目标的方法
  • 规则的顺序很重要
    • Makefile只有一个最终目标,其他目标都是被这个目标带出来,所以首先需要明确最终目标是什么。
    • Makefile中目标很多,但第一条规则将被确立为最终目标。
    • 如果第一条规则中目标数量很多,那第一个目标就是最终目标。
  • 规则举例
    • 前两章也有类似实例。
    • foo.o 是最终目标,foo.c defs.h 是目标依赖原文件,执行命令是cc -c -g foo.c
    • 由于foo.o依赖于foo.c defs.h,如果后两者的文件日期比目标要新,或者目标不存在,则会执行命令。
foo.o: foo.c defs.h       # foo模块
    cc -c -g foo.c

3.2. 规则详解

  • targets是文件名、label,以空格分开,可以使用通配符。
  • command是命令行,如果不与target:prerequisites在一行,那必须以TAB开头,如果在一行可以用分号分隔。
  • prerequisites是依赖文件或其他target,如果其中某个文件比当前target新,则当前target就被认为是过时的,需要重新生成。
  • 命令太长可以通过 \ 作为换行符。
targets : prerequisites
    command

# 或者

targets : prerequisites ; command
    command

3.3. 在规则中使用通配符

  • make支持三个通配符
    • *:任意长度任意字符,长度可以为0。
    • ?:任意单个字符,必须是1个。
    • ~:表示$HOME。在Windows下没有宿主目录,那么指的就是环境变量HOME的取值。
  • 如果要使用三个字符本身,而不是作为通配符使用,可以通过转义字符,\*, \?, \~
  • 通配符可以出现在command、依赖中。
  • 如果要在变量中使用通配符,不能直接通过 objects = *.o 的形式。
    • 如果通过上面这种形式,那变量就是 *.o 而不会展开。
    • 如果要定义所有.o文件的集合,可以使用 objects := $(wildcard *.o)
  • 有一个需求:想获得所有.c文件对应的.o文件合集,该如何处理?
    • $(patsubst %.c, %.o, $(wildcard *.c))
    • patsubst是个模式替换函数,格式为 $(patsubst <pattern>,<replacement>,<text> )
      • 查看<text>中的单词(以空格、TAB、回车分隔)是否符合<pattern>,如果匹配则用<replacement>替换。

3.4. 文件搜索

  • 需求:源码很多,分多个文件夹,make寻找依赖关系时可以加上搜索路径,但最好的方法是吧一个路径告诉make,让其自动寻找。
  • VPATH
    • 如果没有指定该变量,make只会在当前目录中寻找依赖文件和目标文件。
    • 如果定义了这个变量,那么make会在当前目录找不到的情况下,去指定的目录中寻找。
    • 举例:VPATH = src:../headers
      • 这里指定了两个目录src../headers,make会按这个顺序寻找。
      • 多个目录通过冒号分隔。
  • vpath
    • 这个与上面的不同,注意大小写。
    • vpath不是变量,而是关键字,可以指定不同的文件在不同的目录中搜索。
    • 基本用法:
      • vpath <pattern> <directories>:符合<pattern>的文件搜索目录<directories>
      • vapth <pattern>:清除符合模式<pattern>的文件的搜索目录。
      • vpath:清除所有已被设置好了的文件搜索目录。
    • <pattern>中包含%字符,用于表示零活若干匹配
    • 举例:
      • vpath %.h ../headers:如果在当前文件夹中找不到,make会在../headers目录下搜索所有以.h结尾的文件。
      • 如果vpath出现多次,同一文件被多次匹配,那么会按vpath的先后顺序执行。如下面代码中,如果搜索.c文件,则搜索顺序为 foo -> blish -> bar。
vpath %.c foo
vpath %   blish
vpath %.c bar

3.5. 伪目标

  • 所谓的伪目标,应该就是指target为标签的情况,例如clean
  • 特点
    • 伪目标不是一个文件,只是一个标签,make无法生成它的依赖关系和决定是否需要执行。
    • 只能通过显示地指明这个目标,才能让其生效。
    • 伪目标不能与文件重名,否则就没有意义了。
  • 为了避免和文件重名的情况,可以通过 .PHONY 来显示地指明一个目标是伪目标。
  • 伪目标一般没有依赖文件,但也可以指定依赖文件,也可作为默认目标。
  • 伪目标也可以作为依赖。

3.6. 多目标

  • 多个目标同时依赖一个文件,且生成的命令大体相同。
  • 但由于多个目标生成规则的执行命令不是同一个,可能会带来麻烦,但可以通过自动化变量 $@ 处理。
    • 自动化变量在后面的章节中有介绍。
  • 举例
    • 其中,-$(subst output,,$@) 中的 $ 表示执行一个Makefile函数,函数名为subst,后面的为参数。该函数用于替换字符串。
    • $@ 表示目标的集合,就像一个数组,依次读取目标并执行命令。
bigoutput littleoutput : text.g
    generate text.g -$(subst output,,$@) > $@

# 上面的命令等价于

bigoutput : text.g
    generate text.g -big > bigoutput
littleoutput : text.g
    generate text.g -little > littleoutput

3.7. 静态模式

  • 基本结构
    • targets 定义了一系列目标文件,可以有通用符,是目标的集合。
    • target-pattern指明了targets的模式。
    • prereq-patterns是目标依赖的模式,对target-pattern形成的模式再进行一次依赖目标的定义。
<targets ...> : <target-pattern> : <prereq-patterns ...>
    <commands>
    ...
  • 举个例子
    • $<$@ 都是自动化变量,前者表示第一个依赖文件,后者表示目标集合。
objects = foo.o bar.o

all: $(objects)

$(objects): %.o: %.c
    $(CC) -c $(CFLAGS) $< -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
  • 另外一个例子
    • filter 函数用于过滤 $files 集,只选择其中符合%.o模式的内容。
files = foo.elc bar.o lose.o

$(filter %.o,$(files)): %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
    emacs -f batch-byte-compile $<

3.8. 自动生成依赖性

  • 在不使用自动生成依赖时:
    • 如果是一个大型项目,必须清楚知道每个源文件包含哪些头文件,并且在加入/删除头文件时修改Makefile。
  • 编译器的-M/-MM选项
    • 作用:自动寻找源文件中包含的头文件,并生成一个依赖关系。
    • 在GNU的C/C++编译器中要使用-MM选项,不然会包含一些标准库的头文件。
  • 如何将编译器的功能与Makefile联系到一起
    • 让Makefile依赖于源文件的方案并不现实。
    • GNU建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,例如每个.c文件都对应一个.d文件。
  • 举例
    • 细节
      • 所有 .d 文件都依赖于 .c 文件。
      • 先删除所有目标,即.d文件。
      • 为每个依赖文件 $<(即.c文件)生成依赖文件,$@表示模式%.d文件,$$$$表示随机编号。
      • 第三行是一个替换命令,具体查看sed文件本身。
      • 第四行是删除临时文件。
    • 目标:在编译器生成的依赖关系中加入.d文件的依赖,即把依赖关系 main.o : main.c defs.h 转换为 main.o main.d: main.c defs.h
      • 这样.d文件会自动更新。
      • 之后吧这些生成的规则放到Makefile中,通过include命令。要注意顺序,不然可能就会作为make的默认target了。
%.d: %.c
    @set -e; rm -f $@; \
    $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
    rm -f $@.$$$$

# 下面是通过include引入.d文件
sources = foo.c bar.c
include $(sources:.c=.d)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章