GNU Make 学习系列一:怎样写一个简单的Makefile

  编程通常遵循一个相当简单的程序:编辑源文件,编译源代码成可执行的格式,调试结果。尽管将源代码翻译成可执行程序是常规的过程,如果做的不正确,程序员可能会浪费大量的时间去追踪问题。大多数的开发者都经历过这样的挫折:修改一个函数并运行新代码却发现他们的修改并没有修正bug。后来他们发现他们再也不能执行这个修改过的函数,由于一些程序的错误,如未能重新编译源代码、未能重新链接成可执行文件、未能重建成一个jar包。由于不同版本程序的开发,或由于其他平台和支撑库的其他版本,随着程序复杂性的增加,这些寻常的工作越来越容易出错。

  Make的目的是自动将源代码编译成可执行文件。Make的优点是通过脚本你能规范程序中文件间的关系去make,并且通过它们间的关系和时间戳,每次就准确地知道采取什么步骤来生成所需的程序。使用这些信息,Make就可以优化建造过程从而可以避免一些不必要的步骤。

  GNU make是这样做的,它定义了一个语言来描述源代码、中间文件、可执行文件间的依赖关系,还提供了管理配置功能来实现规范库的重用以及用户自定义宏的参数化过程。简而言之,通过提供一个应用程序组装的一个路线图以及它们是怎样组装的,make已经被考虑作为开发过程的中心了。

  make所使用的规范通常被保存为一个文件,名字为makefile。下面是一个用来生成“Hello,World”程序的makefile:  

hello: hello.c
  gcc hello.c -o hello

  为了生成可执行程序,在你喜欢的shell命令行需要键入:

  $ make

  这将引起make程序去读makefile文件并且生成它所发现的第一个目标:

  $ make   gcc hello.c -o hello

  如果给出一个目标被作为命令行参数,那么这个目标将被更新。如果没有给出命令行参数,makefile文件中的第一个目标即默认目标将被作为参数使用。大多数的makefile都是使用默认目标来生成一个程序。这经常涉及许多步骤。通常程序中的源码是不完整的并且源码必须使用工具如flex或bison产生。接下来,源码被编译成二进制目标文件(C/C++为.o文件,Java为.class文件等)。最后,对于C/C++,目标文件被链接器(通常涉及编译器,gcc)链接来产生一个可执行程序。修改源文件中任何一个并调用make将引起这些命令中的一些而不是所有的命令去重复执行,所以改变后的源码被恰当地编译成可执行程序。规范文件或makefile描述了源代码、中间文件、可执行程序间的关系,所以make可以执行最少量的必需工作去更新可执行程序。

  所以,make的首要价值是:按复杂顺序执行命令去生成一个应用程序等能力以及优化这些操作去减少编辑、编译、调试循环所占用的时间。而且make是足够的灵活,而被使用在一种文件依赖于另一种文件的任何地方,如从传统的程序C/C++到Java、TEX、数据库管理等。

  目标和先决条件(或依赖条件)

  makefile本质上包含一组用来生成一个应用程序的规则。第一个规则是作为默认规则。一个规则由三部分组成:目标target,目标的先决条件prereq,执行的命令commands:

target:prereq1  prereq2
  commands

  目标target是makefile文件必须要做的。先决条件prereq或依赖条件是在目标能被成功创建前必须存在的哪些文件。命令commands是那些从先决条件prereq来创建目标target的shell命令。

  下面是一个将c文件foo.c编译成目标文件foo.o的规则:

foo.o: foo.c  foo.h
  gcc -c foo.c

  目标文件foo.o位于冒号前,先决条件foo.c和foo.h位于冒号后,命令脚本位于下一行并且前面有一个tab制表符。

  当make计算一个规则时,它就开始寻找文件中先决条件prereq和目标target的标示了。如果任何的先决条件有一个相关的规则,make就尝试首先更新它们,接下来就是更新目标文件。如果先决条件比目标文件更新,目标文件就会通过执行命令被重新制作。每一个命令行都传递给shell,并且在它的子shell里执行。如果命令中的任何一个产生一个错误,目标的生成就会被终止并且make也会退出。如果一个文件被修改距离现在更近,那么这个文件就比另一个文件更新。

  下面是一个程序,它在它的输入中计算单词“fee”、“fie”、“foe”、“fum”的出现次数。它使用被一个简单的main函数驱动的一个flex扫描器:

#include <stdio.h>
extern int fee_count, fie_count, foe_count, fum_count;
extern int yylex( void );
int main( int argc, char ** argv )
{
yylex();
printf( "%d %d %d %d\n", fee_count, fie_count, foe_count, fum_count );
exit( 0 );
}

flex扫描器文件lexer.l是非常简单:

    int fee_count = 0;
int fie_count = 0;
int foe_count = 0;
int fum_count = 0;
%%
fee fee_count++;
fie fie_count++;
foe foe_count++;
fum fum_count++;

这个程序的makefile也是相当简单:

count_words: count_words.o lexer.o -lfl
gcc count_words.o lexer.o -lfl -ocount_words
count_words.o: count_words.c
gcc -c count_words.c
lexer.o: lexer.c
gcc -c lexer.c
lexer.c: lexer.l
flex -t lexer.l > lexer.c

  当这个makefile被首次执行时,显示如下:

$ make
gcc -c count_words.c
flex -t lexer.l > lexer.c
gcc -c lexer.c
gcc count_words.o lexer.o -lfl -ocount_words

  我们现在已经生成了一个可执行程序。当然,实际的程序通常由比这个更多的模块组成。而且,后面将会发现:这个makefile没有使用make的大多数特性,所以它显得更加冗长。然而,这是一个有效并有用的makefile。例如,在写这个例子期间,当以这个程序做实验时,我执行了几十次的makefile。当你查看makefile和样例的执行时,你可能注意到:被make执行的命令的顺序与makefile中这些命令出现的顺序是相反的。在makefile中,这种自顶向下的设计方式是寻常的。通常大多数目标的一般形式在makefile中首先被指定,而细节留待以后指定。在许多方面,make支持这种风格。其中最主要的是make的两阶段执行模式和递归变量。我们将在后面讨论详细的细节。

  依赖检查

  make是如何做决定去做什么的?让我们重温以前执行make时的更多细节来找到答案。

  首先,make注意到:命令行没有包含目标参数targets,所以它决定去make默认目标,即count_words。它检查先决条件并发现了三个:count_words.o、lexer.o、-lfl。make现在考虑如何生成count_words.o并发现了与此相关的一个规则。它检查此规则的先决条件,注意到count_words.c没有规则但那个文件已存在,所以make执行命令将count_words.c编译成count_words.o,通过执行如下命令:

gcc -c count_words.c

  从目标到先决条件、再到目标、再到先决条件的链接,这是典型的make如何分析一个makefile文件来决定命令的执行。

  make考虑的下一个先决条件是lexer.o。规则的链接指向lexer.c但这次此文件不存在。make寻找到从lexer.l产生lexer.c的规则,所以它运行flex程序。现在lexer.c已生成,make就可以运行gcc程序。

  最后,make检查到-lfl。gcc的-l选项指明一个系统库,它必须链接到应用程序。实际的库名通过“fl”指明,即libfl.a。GNU make包含这种句法的专门支持。当形如-l<NAME>的先决条件出现时,make就搜索一个形如libNAME.so的文件;如果未找到匹配项,它就搜索libNAME.a。make此处找到的是/usr/lib/libfl.a然后继续做最终的动作,即链接。

  最小化重建

  当我们运行我们的程序时,我们发现除了打印fees、fiesta、foes和fums外,它还打印输入文件的文本。打印输入文件的文本不是我们想要的。这个问题是我们已经忘记了我们的lexical分析器的一些规则,并且flex传递了这些未识别的文本到它的输出。为了解决这个问题,我们简单地加上一个任意字符的规则并且一个新的行规则为:

    int fee_count = 0;
int fie_count = 0;
int foe_count = 0;
int fum_count = 0;
%%
fee fee_count++;
fie fie_count++;
foe foe_count++;
fum fum_count++;
.
\n

  在编辑了这个文件后,我们需要重新生成应用程序去测试我们的改正:

$ make
flex -t lexer.l > lexer.c
gcc -c lexer.c
gcc count_words.o lexer.o -lfl -ocount_words

  注意到这次文件count_words.c没有重新编译。当make分析那个规则时,它发现count_words.o已存在并且此文件比它的先决条件count_words.c更新,所以没有必要去重新编译来更新此文件。当分析lexer.c时,当然,make发现先决条件lexer.l比它的目标lexer.c更新,所以make必须更新lexer.c。这样依次引起lexer.o更新,然后count_words更新。现在我们的单词计数程序形成了:

$ count_words < lexer.l
3 3 3 3

  调用make

  之前的例子假定:

  • 所有的工程源代码和make描述文件都保存在一个目录里。
  • make描述文件称为makefile、Makefile、或GNUMakefile。
  • 当执行make命令时,makefile位于用户的当前目录。

  当在这些条件下调用make时,它就自动创建第一个目标。为了更新一个不同的目标需要包含将目标名字包含在命令行里:

$ make lexer.c

  当make被执行时,它将读取描述文件并识别那些需要更新的目标。如果目标或任何它的先决条件文件是过时的(或丢失),那么对应的规则命令脚本里的shell命令将被一个一个执行。在命令被运行后,make假定目标是最新的并移动到下一个目标或退出。

  如果你指定的目标也是最新的,make将显示如下所示并立即退出,而不会做任何事。

$ make lexer.c
make: `lexer.c' is up to date.

  如果你指定的目标不在makefile里并且也没有隐含的规则在makefile里,make将有如下的响应:

$ make non-existent-target
make: *** No rule to make target `non-existent-target'. Stop.

  make有许多命令行选项。最有用选项中的一个是--just-print (或-n),它告诉make去显示它将为一个特别的目标而执行的命令,实际上并没有执行这些命令。当写makefile时,这是特别重要的。在命令行里设置几乎任何makfile变量来覆盖默认变量值或那些在makefile中设置的变量值。

  基本的makefile句法

  现在你对make有了一个基本的理解了,你也几乎能写你自己的makefile了。 此处我们将覆盖足够的makefile句法和结构知识,为了让你开始使用make。

  Makefile经常是自顶向下构建的,如此以至于最一般的目标,经常称为all,被默认更新。由于程序维护,紧跟其后的是越来越多的细节目标,如删除所有不想要的临时文件的clean目标。你能从这些目标名称上猜出:目标并不需要有实际的文件,任何名称都可以。

  在上面的例子中,我们看见了一个规则的简化形式。一个规则的更完整形式为:

target1 target2 target3 : prerequisite1 prerequisite2
command1
command2
command3

  一个或更多的目标位于冒号的左边,零个或更多的先决条件位于冒号的右边。如果冒号右边没有先决条件,那么仅仅当目标不存在时,此目标才会被更新。为了更新一个目标而执行的命令集有时被称为命令脚本,但经常它们仅仅是命令。

  每一个命令必须以一个tab制表符开始。这个句法告诉make:紧跟tab后的字符串传给一个子shell去执行。如果你突然插入一个tab制表符作为一个非命令行的第一个字符,在大多数环境下make将接下来的文本解释为一个命令。如果你是幸运的,你的错误的tab制表符被识别为一个句法错误的话,你将收到信息为:

$ make
Makefile:6: *** commands commence before first target. Stop.

  我们将在下一节讨论tab字符的复杂性。

  make的注释字符是#。从这个#字符到这行的结束的所有文本都将被忽略。注释可能是缩进的并且前面的空格将被忽略。注释字符#没有引进作为命令文本的make注释。整行包括#和后续的字符将被传给shell执行。如何处理这些依赖于你的shell。

  长行能被延续,使用标准的Unix换码符后划线(\)。以这种方式,命令行被延续是寻常的。以后划线延续先决条件列表也是常见的。后面我们将介绍其他方式处理长的先决条件列表。

  你现在已经有足够的背景知识去写简单的makefile文件。第2章将详细介绍规则,第3章是make变量,第5章是命令。现在你应该避免使用变量、宏以及多行的命令序列。    

 

  (待续2013.11.17)

 
 
分类: Linux

MAKE gnu的更多相关文章

  1. 感悟 GNU C 以及将 Vim 打造成 C/C++ 的半自动化 IDE

    C 语言在 Linux 系统中的重要性自然是无与伦比.不可替代,所以我写 Linux 江湖系列不可能不提 C 语言.C 语言是我的启蒙语言,感谢 C 语言带领我进入了程序世界.虽然现在不靠它吃饭,但是 ...

  2. 使用 GCC 和 GNU Binutils 编写能在 x86 实模式运行的 16 位代码

    不可否认,这次的标题有点长.之所以把标题写得这么详细,主要是为了搜索引擎能够准确地把确实需要了解 GCC 生成 16 位实模式代码方法的朋友带到我的博客.先说一下背景,编写能在 x86 实模式下运行的 ...

  3. 在 Linux 中使用 Eclipse 和 Gnu Autotools 管理 C/C++ 项目

    在我该系列的之前的所有随笔中,都是采用 Linux 发行版自带的包管理工具(如 apt-get.yum 等)进行软件的安装和卸载,从来没有向大家展示使用源代码自行编译安装软件的方法.但是长期混迹于 U ...

  4. GNU Readline 库及编程简介

    用过 Bash 命令行的一定知道,Bash 有几个特性: TAB 键可以用来命令补全 ↑ 或 ↓ 键可以用来快速输入历史命令 还有一些交互式行编辑快捷键: C-A / C-E 将光标移到行首/行尾 C ...

  5. GNU Radio Radar Toolbox

    GNU Radio Radar Toolbox Install guide Change to any folder in your home directory and enter followin ...

  6. gnu coreutils-8.25 for win32 static - Beta

    gnu.win32-coreutils-8.25.7z 2.7 Mb bc-1.06.tar.gz coreutils-8.25.tar.xz diffutils-3.5.tar.xz gawk-4. ...

  7. window下搭建c开发环境(GNU环境的安装)

    一.在windows平台上安装GNU环境 windows操作系统不自带GNU环境,如果需要开发跨平台的C语言程序,那么需要给windows安装GNU环境 windows下的两款GNU环境:MinGW和 ...

  8. GNU make使用变量⑤变量的引用、定义等

    在 Makefile 中,变量是一个名字(像是 C 语言中的宏),代表一个文本字符串(变量的值).在 Makefile 的目标.依赖.命令中引用变量的地方,变量会被它的值所取代(与 C 语言中宏引用的 ...

  9. (转)完全用GNU/Linux工作 by 王珢

    完全用GNU/Linux工作 王珢      (看完这篇博文,非常喜欢王珢的这篇博客,也我坚定了学gnu/linux的决心,并努力去按照国外的计算机思维模式去学习编程提高自己.看完这篇文章令我热血沸腾 ...

  10. Gnu/Linux的学习探索

    1.Gnu/Linux是一个基于POSIX和UNIX的多用户多任务 支持多线程多CPU的类UNIX的操作系统. 继承了UNIX以网络为核心的设计思想 是性能稳定的多用户网络操作系统. 1991年10月 ...

随机推荐

  1. I2C驱动程序框架probe道路

    基于Linux的I2C驱动器.采纳probe道路.根据这个框架,如下面就可以写任何支持I2C总线设备Linux驱动器. I2C设备连接到cpu具体i2c接口.被安装在cpu的i2c适配器.i2c设备和 ...

  2. hdu-4419-Colourful Rectangle-段树区,并寻求

    这个问题很有趣的项目,写麻烦.它预计将有写了很长的时间. 好在,我想开了一个比较简单的方法.. . 使用位计算,颜色RGB分别1,2,4,代表. 状态的长度了. #include<stdio.h ...

  3. linux_vim_最佳快捷键

    如何使用vi文本编辑器     vi由比尔·乔伊(Bill Joy)撰写,所有UNIX like均默认安装此文本编辑器.详细简介请点击维基中文. 1.首先复制一个文件到/tmp目录(本例中为复制根目录 ...

  4. POJ 3233 Matrix Power Series(矩阵高速功率+二分法)

    职务地址:POJ 3233 题目大意:给定矩阵A,求A + A^2 + A^3 + - + A^k的结果(两个矩阵相加就是相应位置分别相加).输出的数据mod m. k<=10^9.     这 ...

  5. CSS3 制作向左、向右及关闭图标的效果 (另一种思路)

    最终效果 制作步骤 1.边框 CSS及Html代码 显示效果 2.向左的标志 CSS及Html代码,增加的代码在黄色范围内 显示效果 方向不对了,马上修改一下方向,逆时针旋转45度调整一下 CSS及H ...

  6. easyui datagrid datagrid-filter bug

    问题描述:空字符串.数字过滤 过滤异常 修改js源码: $.fn.datagrid.defaults.operators = { nofilter: { text: 'No Filter' }, co ...

  7. uploadfiy 动态传递Form 参数

    参见 百度 http://jingyan.baidu.com/article/a3a3f8118b1c4d8da3eb8a60.html @{    ViewBag.Title = "Ind ...

  8. WPF中嵌入WinForm中的webbrowser控件

    原文:WPF中嵌入WinForm中的webbrowser控件 使用VS2008创建WPF应用程序,需使用webbrowser.从工具箱中添加WPF组件中的webbrowser发现其中有很多属性事件不能 ...

  9. Linux常用命令汇总-速查

    对Linux新手有用的20个命令 对中级Linux用户有用的20个命令 对Linux专家非常有用的20个命令 20个最受欢迎的Linux命令 20个有趣的Linux命令

  10. Android中利用Handler实现消息的分发机制(三)

    在第二篇文章<Android中利用Handler实现消息的分发机制(一)>中,我们讲到主线程的Looper是Android系统在启动App的时候,已经帮我们创建好了,而假设在子线程中须要去 ...