Make与Makefile

使用 Make 命令管理项目

Make 工具可以帮助我们管理项目的构建:它的主要任务是方便重建一个多文件项目中只需要重新编译或者重建的部分。这可以节省大量的时间,因为它可以用一个文件的编译来取代长达几分钟的完整安装。Make 还可以帮助维护一个程序在一台机器上的多个安装,例如用多个编译器编译一个库,或者在调试和优化模式下编译一个程序。

Make 是一个历史悠久的 Unix 工具,传统上有一些行为略有不同的变种,例如在不同的 Unix 版本上,如 HP-UXAUXIRIX。现在,无论在什么平台上,都建议使用 GNU 版的 Make,它有一些非常强大的扩展。它在所有的 Unix平台上都可用(在 Linux 上它是唯一可用的变体),而且它是一个事实的标准。该手册可在http://www.gnu.org/software/make/manual/make.html,或者我们可以阅读这本书。

还有其他的构建系统,最引人注目的是 SconsBjam 。我们不在这里讨论这些。本教程中的例子是针对 CFortran语言的,但 Make 可以在任何语言中工作,而事实上,像 TEX 这样的根本就不能算得上一种语言;见第22.6节。

22.1.1 C

Make 下面的文件:

foo.c

  1. #include "bar.h"
  2. int c=3;
  3. int d=4;
  4. int main()
  5. {
  6. int a=2;
  7. return(bar(a*c*d));
  8. }

bar.c

  1. #include "bar.h"
  2. int bar(int a)
  3. {
  4. int b=10;
  5. return(b*a);
  6. }

bar.h

  1. extern int bar(int);

还有一个 makfile

Makefile

  1. fooprog : foo.o bar.o
  2. cc -o fooprog foo.o bar.o
  3. foo.o : foo.c
  4. cc -c foo.c
  5. bar.o : bar.c
  6. cc -c bar.c
  7. clean :
  8. rm -f *.o fooprog

makefile有许多规则,如:

  1. foo.o : foo.c
  2. <TAB>cc -c foo.c

其一般形式为:

  1. target : prerequisite(s)
  2. <TAB>rule(s)

其中规则行以TAB字符缩进。

如上所述的规则指出一个 “目标” 文件 foo.o 是由 “先决条件” foo.c 生成的,即通过执行 cc -c foo.c 命令。该规则的精确定义是:

  • 如果目标文件 foo.o 不存在或者比先决条件文件 foo.c 存在时间更早。
  • 则执行该规则的命令部分:cc -c foo.c
  • 如果先决条件本身是另一条规则的目标,那么该规则将首先被执行。

也许解释规则的最好方法是:

  • 如果任何先决条件发生了变化,
  • 那么目标就需要重新make,
  • 而这是通过执行该规则的命令来完成的,
  • 检查先决条件需要递归应用make:
    • 如果先决条件不存在,找到一个规则来创建它;
    • 如果前提条件已经存在,检查适用的规则,看是否需要重新制作。

练习:调用 make 命令

预期结果:应用上述规则:不带参数的 make 命令尝试构建第一个目标,fooprog,建立这个目标需要原先不存在的先决条件 foo.obar.o 。然而,有一些可以生成它们的规则,make会递归地调用这些规则。因此我们可以看到两个编译,分别为 foo.obar.o ,以及一个针对fooprog 的链接命令。注意事项: makefile 或文件名中的错别字会导致各种错误,特别是要确保我们在规则行中使用制表符而不是空格。调试一个makefile文件并不简单,make 会给我们发送 make 文件中发现错误的行号信息。

练习:执行 make clean,然后执行 mv foo.c boo.c 后再执行 make 。解释一下错误信息,并恢复原来的文件名。

预期结果:make 会报错说没有按照规则来构建 foo.c,这个错误是由于 foo.c 是构建 foo.o 的先决条件,但是 foo.c 并不存在。make 后去寻找构建 foo.c 的规则,但不存在创建 .c 文件的规则。

现在给函数 bar 添加第二个参数,这需要我们编辑 bar.cbar.h :编辑后并 make 它们。然而,这也要求我们编辑 foo.c,但现在让我们 “忘记”这样做,我们将看到 make 如何帮助我们找到所产生的错误。

练习: 调用 make 来重新编译我们的程序,它是否重新编译了 foo.c

预期结果:即使在概念上 foo.c 需要被重新编译,因为它使用了 bar 函数,但 make 并没有这样做,因为 makefile 中没有任何规则强制它。

在makefile中,改变这一行

  1. foo.o : foo.c

  1. foo.o : foo.c bar.h

这意味着在这种情况下,foo.o 已经存在。make 将检查 foo.o 是否比它的任何先决条件都要早,由于 bar.h 已经被编辑过,它比 foo.o 更早,所以 foo.o 需要被重构。

练习:确认新的 makefile 确实使 foo.obar.h 被改变时被重新编译。这个编译现在会出现错误,因为我们已经”忘记”编辑 bar 函数的使用。

Fortran

make 以下文件:

foomain.F

  1. program test
  2. use testmod
  3. call func(1,2)
  4. end program

foomod.F

  1. module testmod
  2. contains
  3. subroutine func(a,b)
  4. integer a,b
  5. print *,a,b,c
  6. end subroutine func
  7. end module

和一个 makefile:

Makefile

  1. fooprog : foomain.o foomod.o
  2. gfortran -o fooprog foo.o foomod.o
  3. foomain.o : foomain.F
  4. gfortran -c foomain.F
  5. foomod.o : foomod.F
  6. gfortran -c foomod.F
  7. clean :
  8. rm -f *.o fooprog

调用 makemakefile 中的第一条规则被执行,解释会发生什么。

练习:调用 make

预期结果:应用上述规则:调用没有参数的 make 构建第一个目标,即 foomain ,构建它需要先前不存在的先决条件 foomain.ofoomod.o。然而,有一些规则可以用来生成它们,make 会递归地调用这些规则。因此,我们可以看到两个编译,分别为 foomain.ofoomod.o,还有一个链接命令为 fooprog 的链接命令。

注意事项:makefile 或文件名中的错别字会导致各种错误,调试一个makefile并不简单,我们将不得不理解并改正这些错误。

练习:先执行 make clean,然后执行 mv foomod.c boomod.c 后再执行 make。解释一下错误的信息并恢复原来的文件名。

预期结果:make会报错说没有构建 foomod.c 的规则,由于 foomod.c 作为 foomod.o 的先决条件不存在。make 后去寻找构建它的规则,但不存在构建 .F 文件的规则。

现在在 foomod.F 中给 func 添加一个额外的参数,然后重新编译。

练习:调用 make 来重新编译我们的程序,它是否重新编译了 foomain.F

预期结果:即使从概念上讲,foomain.F 也需要被重新编译。但 make 并没有这样做,因为makefile 中没有任何规则强迫它。

更改这一行

  1. foomain.o : foomain.F

  1. foomain.o : foomain.F foomod.o

foomod.o 添加为 foomain.o 的先决条件。这意味着,在这种情况下,foomain.o 已经存在的情况下,make 将检查 foomain.o 是否比它的任何先决条件都早。然后,make 将递归地检查 foomode.o 是否需要更新。在重新编译 foomode.F 后,foomode.ofoomain.o 更晚存在,所以 foomain.o 将被重构。

练习:确认修正后的 makefile 确实使 foomain.F 被重新编译。

关于 make 文件

make 文件被称为 makefileMakefile ;名称的文件在同一目录下是不可取的。如果我们想让 Make 使用一个不同的文件作为 make 文件,请使用语法 make -f My_Makefile

变量和模板规则

在我们的 makefile 中引入变量是很方便。例如,不需要每次都明确说明编译器的明确说明,而是在makefile中引入一个变量:

  1. CC = gcc
  2. FC = gfortran

并在编译行中使用 ${CC}${FC}

  1. foo.o : foo.c
  2. ${CC} -c foo.c
  3. foomain.o : foomain.F
  4. ${FC} -c foomain.F

练习:按照指示编辑我们的 makefile 文件。首先执行 make clean,然后执行 make foo (C) 或make fooprog (Fortran)。

预期结果:我们应该看到和以前完全一样的编译和链接行。

注意事项:不像在shell中,大括号是可选的,makefile中的变量名必须在大括号或小括号中。试验一下,如果我们忘记了变量名周围的大括号,会发生什么?

使用变量的一个好处是,我们现在可以从命令行上改变编译器:

  1. make CC="icc -O2"
  2. make FC="gfortran -g"

练习:按照建议调用 make(在 make clean 之后),我们看到我们的屏幕输出有什么不同吗?

预期结果:编译行现在显示添加的编译器选项 -O2-g

make 也有局部作用域变量:

$@ 目标。在主程序的链接行中使用这个。 先决条件的列表。在程序的链接行中也使用这个。 $< 第一个先决条件。在各个对象文件的编译命令中使用。 $* 在模板规则中(第22.2.2节),这与模板部分相匹配,即与%对应的部分。

使用这些变量,fooprog 的规则变为:

  1. fooprog : foo.o bar.o
  2. ${CC} -o $@ $ˆ

而一个典型的编译行变成了:

  1. foo.o : foo.c bar.h
  2. ${CC} -c $<

我们也可以定义一个变量:

  1. THEPROGRAM = fooprog

并在我们的makefile中使用这个变量而不是程序名称,这使我们以后更容易改变可执行文件的名称。

练习:编辑我们的 makefile,添加这个变量定义,并使用它来代替字面的程序名。构建一个命令行,使我们的 makefile 能够构建可执行的 fooprog v2

预期结果:我们需要在命令行中指定 THEPROGRAM 变量,使用语法 make VAR=value

注意事项:确保我们的命令行中的等号周围没有空格。

要查看全部局部作用域变量的列表可以登录: https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html

模板规则

到目前为止,我们为每个需要编译的文件写了一个单独的规则,而各种 .c 文件的规则是非常相似的。

  • 规则头( foo.o : foo.c )指出,源文件是对象文件的先决条件,具有相同的基名。

  • 而我们正在使用 make 的内置变量编译的指令( ${CC} -c $< )甚至在字符上都是一样的。

  • 规则唯一有区别的是

    1. foo.o : foo.c bar.h
    2. ${CC} -c $<

    我们可以把这些共性总结为一个模板规则:

  1. %.o : %.c
  2. ${CC} -c $<
  3. %.o : %.F
  4. ${FC} -c $<

这说明任何对象文件都依赖于具有相同基础名称的 CFortran 文件。要重新生成对象文件,请用 -c 标志调用 CFortran 编译器。这些模板规则可以作为替代上述 makefile 中的多个特定目标,但 foo.o 的规则除外。

foo.obar.h 的依赖,或者 foomain.ofoomod.o 的依赖,可以通过添加一个规则来处理:

  1. # C
  2. foo.o : bar.h
  3. # Fortran
  4. foomain.o : foomod.o

这条规则指出,如果文件 bar.hfoomod.o 改变了,文件 foo.ofoomain.o 也需要更新”。然后,make 会在 makefile 中搜索另一条规则,说明如何进行更新的不同规则,如何找到模板规则。

练习:修改我们的 makefile 文件以纳入这些想法,并进行测试。

通配符

我们的 makefile 现在使用一个一般规则来编译任何源文件。通常,我们的源文件是我们目录中所有的 .c.F 文件,那么是否有办法说明 “编译此目录中的所有文件”?实际上是有的。

在我们的 makefile 中添加以下几行,并在适当的地方使用变量 COBJECTSFOBJECTSwildcard 命令给出了 ls 的结果,我们可以用以下方法来处理 patsubst 的文件名:

  1. # wildcard: find all files that match a pattern
  2. CSOURCES := ${wildcard *.c}
  3. # pattern substitution: replace one pattern string by another
  4. COBJECTS := ${patsubst %.c,%.o,${SRC}}
  5. FSOURCES := ${wildcard *.F}
  6. FOBJECTS := ${patsubst %.F,%.o,${SRC}}

条件式

有多种方法可以使 makefile 的操作动态化。例如,我们可以把一个 shell 条件放在一个操作行中。然而,这可能会使 makefile 变得杂乱无章;有一个更简单的方法是使用 makefile 条件。有两种类型的条件:对字符串相等的测试和对环境变量的测试。 第一种类型如下形式:

  1. ifeq "${HOME}" "/home/thisisme"
  2. # case where the executing user is me
  3. else
  4. # case where it’s someone else
  5. endif

而在第二种情况下,形式如下:

  1. ifdef SOME_VARIABLE

truefalse 部分的文本可以是 makefile 中的大多数部分。例如,可以让规则中的一个规则中的一个动作行被有条件地包括在内。然而,在大多数情况下,我们将使用条件式来使变量的定义依赖于某些条件。

练习:假设我们想在家里和工作中使用我们的 makefile ,在工作中,我们的雇主有一个付费的许可证的英特尔编译器 icc,但在家里我们使用开源的 Gnu 编译器 gcc。写一个 makefile 在两个地方都能使用,并为 CC 设置适当的值。

杂录

makefile* 是做什么的?

上面我们已经知道,发出 make 命令会自动执行 makefile 中的第一条规则。这在某种意义上是很方便的,但在另一种意义上是不方便的:要想知道 makefile 允许哪些可能的操作的唯一方法是阅读 makefile 本身,否则阅读文档通常是不够充分的。

一个更好的方式是用一个目标来启动 makefile

  1. info :
  2. @echo "The following are possible:"
  3. @echo " make"
  4. @echo " make clean"

没有明确目标的 make 会告知我们 makefile 的能力。

如果我们的 makefile 变长了,我们可能想这样记录每个部分。这就遇到了一个问题。我们不能有两条目标相同的规则,上述的 info 就是这种情况。然而,如果我们使用双冒号,则是可以的。我们的makefile 将有如下结构:

  1. info ::
  2. @echo "The following target are available:"
  3. @echo " make install"
  4. install :
  5. # ..... instructions for installing
  6. info ::
  7. @echo " make clean"
  8. clean :
  9. # ..... instructions for cleaning

Phony 目标

makefile 包含一个 clean 目标的例子。它使用 make 机制来完成一些与文件创建无关的操作:调用 make clean 导致 make 推理出 “ 没有名为 clean 的文件所以需要执行下面的指令 “。然而,这实际上并没有使 clean 文件出现,所以再次调用 make clean 会使同样的指令被执行。

为了表明这个规则实际上并产生成为目标,我们可以使用 .PHONY 关键字:

  1. .PHONY : clean

大多数情况下,没有声明 makefile 实际上也能正常工作,但声明 phony 目标的的主要好处是,即使我们有一个名为 “ clean “的文件(或文件夹),make规则仍然可以工作。

预定义的变量和规则

调用 make -p yourtarget 会使 make 输出它的所有操作,以及所有变量和规则的值,包括我们的makefile 中的和预定义的。如果我们在一个没有 makefile 的目录下做这个动作,我们会看到 make 实际上已经知道如何编译 .c.F 文件。找到这个规则,并找到其中的变量定义。

我们会看到,我们可以通过设置 CFLAGSFFLAGS 等变量来自定义 make ,通过一些实验来证实这一点。如果我们想为相同的源代码 make 第二个 makefile ,我们可以调用 make -fothermakefile来代替默认的 makefile

顺便注意,makefileMakefile 都是默认 makefile 的合法名称。在我们的目录中尽量不要同时拥有 makefileMakefile

将目标作为先决条件

假设我们有两个同等对待的不同目标,我们会想这样写:

  1. PROGS = myfoo other
  2. ${PROGS} : $@.o # this is wrong!!
  3. ${CC} -o $@ $@.o ${list of libraries goes here}

然后说 make myfoo 会导致

  1. cc -c myfoo.c
  2. cc -o myfoo myfoo.o ${list of libraries}

同样地,对于 make other 也是如此。这里出问题的是使用 $@.o 作为先决条件。在 Gnu Make 中,我们可以按以下方式修复它:

  1. .SECONDEXPANSION:
  2. ${PROGS} : $$@.o
  3. ${CC} -o $@ $@.o ${list of libraries goes here}

练习:编写第二个主程序 foosecond.cfoosecond.F ,并修改我们的 makefile ,以便使调用 make foomake foosecond 都使用同一规则。

Makefile 中的 shell 脚本

目的:在这一节中,我们将看到一个出现在 makefile 规则中较长的 shell 脚本例子。

在我们到目前为止看到的 makefile 中,命令部分是单行的。实际上,只要我们想可以有许多行。例如,让我们制定一个为我们正在构建的程序做备份的规则。

在我们的 makefile 中添加一个备份规则,它需要做的第一件事是建立一个备份目录:

  1. .PHONY : backup
  2. backup :
  3. if [ ! -d backup ] ; then
  4. mkdir backup
  5. fi

我们输入这个了吗?但这并不会奏效:makefile 规则的命令部分的每一行都会作为一个单独的程序被执行。因此,我们需要把整个命令写在一行上。

  1. backup :
  2. if [ ! -d backup ] ; then mkdir backup ; fi

否则 if 行变得太长:

  1. backup :
  2. if [ ! -d backup ] ; then \
  3. mkdir backup ; \
  4. fi

接下来我们进行实际的复制:

  1. backup :
  2. if [ ! -d backup ] ; then mkdir backup ; fi
  3. cp myprog backup/myprog

但这种备份方案只保存一个版本。让我们做一个其名称中的日期为保存的程序名称中的日期的版本。

Unixdate 命令可以通过接受一个格式字符串来定制其输出。输入以下内容 :date 这可以在 makefile 中使用。

练习:编辑cp 命令行,使备份文件的名称包括当前日期。

预期结果:提示:需要反引号。如果我们不记得反引号的作用,请查阅 Unix 教程第 20.3.3 节。

如果我们在 makefile 规则的命令部分定义 shell 变量,我们需要注意的是以下几点。用一个循环来扩展我们的备份规则,复制对象文件:

  1. #### This Script Has An ERROR!
  2. backup :
  3. if [ ! -d backup ] ; then mkdir backup ; fi
  4. cp myprog backup/myprog
  5. for f in ${OBJS} ; do \
  6. cp $f backup ; \
  7. done

(这不是最好的复制方式,但我们为了演示而使用它)。这导致了一个报错信息,原因是Make$f 解释为外部进程的一个环境变量。解决办法是:

  1. backup :
  2. if [ ! -d backup ] ; then mkdir backup ; fi
  3. cp myprog backup/myprog
  4. for f in ${OBJS} ; do \
  5. cp $$f backup ; \
  6. done

(在这种情况下,Make 在扫描命令行时将两个连续的美元符号替换为单个的美元符号,在执行命令行时,$f 会扩展为适当的文件名)。

使用Make的实用技巧

这里有几个实用的提示。

  • 调试一个 makefile 通常是令人沮丧的困难,唯一的工具就是 -p 选项。它可以根据当前的makefile 打印出 Make 正在使用的所有规则。

  • 我们经常会发现自己先输入一个 make 命令,然后再调用程序。大多数 Unix shell 允许我们通过使用向上的箭头键来使用 shell 的历史命令。不过,这可能会让人感到厌烦,所以我们可能会想写以下内容:

    1. make myprogram ; ./myprogram -options

    然后不断地重复这一点。这样做有一个危险:如果 make 失败,例如因为编译问题,我们的程序仍然会被执行。相反,如果写

    1. make myprogram && ./myprogram -options

    该程序执行的条件是 make 成功结束。

LATEXMakefile

Make工具通常用于编译程序,但其他用途也是可能的。在本节中,我们将讨论LATEX文档的Makefile。

我们从一个非常基本的makefile开始:

info : @echo "Usage: make foo" @echo "where foo.tex is a LaTeX input file" %.pdf : %.tex pdflatex $<

命令 make myfile.pdf 将调用 pdflatex myfile.tex,如果需要的话,就调用一次。接下来我们重复调用 pdflatex ,直到日志文件不再报告说需要进一步运行。

%.pdf : %.tex pdflatex $< while [ ‘cat ${basename $@}.log | grep "Rerun to get" \ | wc -l‘ -gt 0 ] ; do \ pdflatex $< ; \ done

我们使用 ${basename fn} 宏来从目标名称中提取不带扩展名的基名。

如果该文件有书目或索引,我们就运行 bibtexmakeindex

%.pdf : %.tex pdflatex ${basename $@} -bibtex ${basename $@} -makeindex ${basename $@} while [ ‘cat ${basename $@}.log | grep "Rerun to get" \ | wc -l‘ -gt 0 ] ; do \ pdflatex ${basename $@} ; \ done

行首的减号意味着如果这些命令失败,make 不会中止。

最后,我们想使用 make 的工具来考虑到依赖关系,我们可以写一个有通用规则的 makefile :

mainfile.pdf : mainfile.tex includefile.tex

但我们也可以明确地发现包含文件,下面的 makefile 是用:

make pdf TEXTFILE=mainfile

然后,pdf 规则使用一些 shell 脚本来发现 include 文件(但不是递归的),并再次调用 make,调用另一个规则,并明确地传递依赖性。

pdf : export includes=‘grep "ˆ.input " ${TEXFILE}.tex \ | awk ’{v=v FS $$2".tex"} END {print v}’‘ ; \ ${MAKE} ${TEXFILE}.pdf INCLUDES="$$includes" %.pdf : %.tex ${INCLUDES} pdflatex $< ; \ while [ ‘cat ${basename $@}.log \ | grep "Rerun to get" | wc -l‘ -gt 0 ] ; do \ pdflatex $< ; \ done

这种 shell 脚本也可以在 makefile 之外进行,动态地生成 makefile

Cmake

制作 makefile 可能很麻烦,它们可能需要为任何特定的安装进行自定义。由于这个原因,Cmake 是一个构建系统,它首先生成 makefiles,然后可以用正常的方式使用。

这里是主要的组成部分:

  • 源文件目录中有一个描述应用程序结构的文件 CMakeLists.txt
  • 生成的文件可以与源文件分开保存:
  1. sourcedir=.... # source location
  2. builddir=.... # place for temporaries
  3. installdir=... # here goes the finished stuff
  4. cd ${builddir}
  5. cmake \
  6. -DCMAKE_PREFIX_PATH=${installdir} \
  7. ${sourcedir} # do the setup
  8. make # compile
  9. make install # move finished stuff in place

依赖性分析

上面我们学到了 make 如何用于具有复杂依赖关系的项目,我们可以用子句告诉 Cmake :

  1. include_directories(include)
  2. file(GLOB SOURCES "*/*.c")
  3. file(GLOB HEADERS "*/*.h")
  4. add_executable(hello_world hello.c ${SOURCES} )

当被调用时,Cmake 会报告

  1. Scanning dependencies of target hello_world

并相应地生成 makefile 文件。

这种设置的一个好处是,只有当应用程序的结构发生变化时,才需要重新进行 Cmake 阶段,例如增加了文件。对于程序开发过程中的任何普通编辑,只需重复 makemake install 部分即可。

例子:考虑一个主文件 hello.c ,它使用了两个辅助文件 compute.coutput.cCmake 后调用 makemake install 给出的输出是:

  1. Scanning dependencies of target hello_world
  2. [ 25%] Building C object CMakeFiles/hello_world.dir/hello.c.o
  3. [ 50%] Building C object
  4. CMakeFiles/hello_world.dir/compute/compute.c.o
  5. [ 75%] Building C object CMakeFiles/hello_world.dir/output/output.c.o
  6. [100%] Linking C executable hello_world
  7. [100%] Built target hello_world
  8. [100%] Built target hello_world
  9. Install the project...

在编辑完 compute.c 文件后,只有该文件被重新编译,并且项目被重新连接:

  1. Scanning dependencies of target hello_world
  2. [ 25%] Building C object
  3. CMakeFiles/hello_world.dir/compute/compute.c.o
  4. [100%] Built target hello_world
  5. [100%] Built target hello_world
  6. Install the project...

显然,cmake 已经生成了具有正确结构的 makefile,而使用 cmake 的缺点是自动生成的 makefile 几乎是不可读的。

Re-Cmaking

刚才概述包括独立的构建和安装的目录结构,意味着完全删除这些库就足够从没有历史记录的开始。对于一个更温和的方法,我们可以删除位于构建目录中的 CMakeCache.txt 文件。

系统依赖性

我们可以给 Cmake 提供选项,使其进入 makefile 结构。例如,在编写本书的苹果笔记本上,直接调用 Cmake 就能找到本地的 C 编译器:

  1. -- The C compiler identification is AppleClang 9.1.0.9020039

然而,指定:

  1. cmake -DCMAKE_C_COMPILER=gcc

会给出:

  1. -- The C compiler identification is GNU 7.4.0

详情

CMake 有的变量:

  1. set(myvar myvalue)
  2. message(STATUS "my variable has value ${myvar}")