天天看点

《C程序设计新思维》一1.4 使用Makefile

本节书摘来自异步社区《c程序设计新思维》一书中的第1章,第1.4节,作者 【美】ben klemens,更多章节内容可以访问云栖社区“异步社区”公众号查看

makefile提供一个解决所有以上这些麻烦的方案。它基本上可以看作一组有组织的变量和shell脚本。posix标准的make程序输入makefile作为指令和变量,然后自动化处理那些冗长繁琐的命令行。在这部分讲解之后,就没有什么必要去直接调用编译器了。

在“3.2 makefile还是shell脚本”中,会讲述关于makefile的更多细节;这里,先给出一个最小的、实用的,并且能够编译一个依赖于一个库的基本程序的makefile:

《C程序设计新思维》一1.4 使用Makefile

用法:

曾经的用法:将这段存为一个名为makefile的文件,与.c文件放在同一个目录中。如果你在试用gnu make,如果你感觉需要将该文件与其他文件区别开,可以将首字母大写,即命名为makefile。把你的程序的名字放在第一行(用progname,而不是progname.c)。

每次你需要重新编译的时候,输入make命令。

《C程序设计新思维》一1.4 使用Makefile

自己动手:下面是编程世界著名的hello.c程序,就两行:

《C程序设计新思维》一1.4 使用Makefile

把这个源文件和前面的makefile存在同一个目录中,然后试着按前面的步骤编译和运行程序。成功后,修改makefile使其能编译erf.c。

很快我们会介绍makefile的实际应用,但你可能注意到前述makefile里6行中有5行是关于变量设定的(目前多数被设定为空),意味着我们还需要多花一点时间在环境变量的细节上。

《C程序设计新思维》一1.4 使用Makefile

历史上,曾经出现过两种主流的shell语法:一种基本上是基于bourne shell的,另一种主要基于c shell。c shell的变量语法稍有不同,例如,采用set cflags=”-g –wall –o3”来设定cflags的值。但是posix标准是写成bourne类型的语法,也就是我这本书余下部分所采用的。

shell和make用$来指代变量的值,但是shell用$var,而make假设任何变量的名字多于一个字母:$(var)。所以,前面的makefile中,$(p):$(objects)将相当于

《C程序设计新思维》一1.4 使用Makefile

有几种办法来让make识别变量:

首先,在调用make之前从shell中设定变量,并且使用export命令导出这些变量,也就是说当shell产生一些子进程的时候,它有自己的环境变量列表。从一个posix标准的命令行中设定cflags,可以这样:

《C程序设计新思维》一1.4 使用Makefile

我自己经常忽略这个makefile中的第一行,p=program_name,取而代之的是在每个会话中通过export p=program_name来设定,这意味着我免不了有时还得编辑一下makefile。

你也可以将那些export命令放在你的shell启动脚本中,比如.bashrc或.zshrc。这可以确保每次你登录或开始一个新的shell,这个变量会被设定并导出。如果你很确信cflags每次都会是一样的,你可以在脚本中设定它们然后再也不用惦记这个问题了。

你可以通过在命令前放一个赋值操作来为命令导出变量。env命令列出它所知道的有环境变量,所以当你运行:

《C程序设计新思维》一1.4 使用Makefile

你将看到正确的变量及其值。这是为什么shell不让你在等号附近放空格的原因:空格是用来区分命令行中的赋值操作的。

在这个方法中设定和导出变量应该在一行实现。如果你在命令行中执行了上面这条命令,再次运行env | grep pants,就会发现pants不再是一个被导出的变量了。

只要你愿意,你可以指定任意数量的变量:

《C程序设计新思维》一1.4 使用Makefile

这个技巧出现在shell规范的“简单命令”描述部分,也就是说赋值必须在一个实际的命令之前。这在你使用非命令的shell构造时很有意义。编写:

《C程序设计新思维》一1.4 使用Makefile

将失败并伴随一个晦涩的语法错误。正确的方式是:

《C程序设计新思维》一1.4 使用Makefile

就像前面介绍的那个makefile一样,你可以在makefile的头部设定变量,类似:cflags=…。在makefile中,你可以在等号周围放上空格,这不会引发任何中断。

make将让你在命令行中设定变量,并独立于shell。那么,下面两行基本是对等的:

《C程序设计新思维》一1.4 使用Makefile

对makefile而言,上面所有这些手段都是相等的;例外之处在于,被make调用的子程序只知道新的环境变量,而不知道任何makefile变量。

《C程序设计新思维》一1.4 使用Makefile

在c代码中,可以用getenv函数来得到环境变量。getenv非常简单易用,在c中快速设定变量时是非常容易的,所以你可以从命令行中尝试设定不同的值。

例1-2是一个打印示例程序,只要用户需要,随时打印一个信息到屏幕。环境变量msg用于设定要打印的信息,而通过reps设定重复的次数。请注意我们是如何设定它们的默认值10次和“hello.”的,这些默认值一般在调用getenv时返回null(典型的含义是这个环境变量没有被设定)。

例1-2 环境变量提供了一个改变程序细节的快速方式(getenv.c)

《C程序设计新思维》一1.4 使用Makefile

就像之前看到的,我们可以用一行命令导出一个变量,这样可以使得向程序发送变量更加方便。用法:

《C程序设计新思维》一1.4 使用Makefile

你可能觉得这个用法很奇怪——程序的输入应该跟在程序名后面才对,真是可恨——先不管这些奇怪的事情,你可以看到程序自身进行了一些设置工作,我们几乎没费什么精力立即就得到了来自命令行的命名参数。

当你的程序能够跑得远一点以后,可以试一下配置getopt来按照通常的方法设定输入参数。

make也提供一些内置的变量。下面是其中一些(posix标准)变量的介绍,你可能在随后针对规则的学习中用到。

$@

返回完整的目标文件名。所谓目标(target),我的意思是指需要被生成的文件,比如从一个a.c文件中编译而得到的a.o文件,或者一个通过连接.o文件生成的程序。

$*

不带文件名后缀的输出文件。如果输出文件是prog.o,$就是prog,而$.c就成为prog.c。

$<

触发和制作该目标的文件的名称。如果我们正在制作prog.o,有可能是prog.c文件刚被修改,所以$<就是prog.c。

现在让我们专注地了解一下makefile的执行过程,并了解变量是如何影响这个过程的。

先不讨论变量的事情,来看一下makefile的代码片段,一般有以下形式:

《C程序设计新思维》一1.4 使用Makefile

如果目标被调用,通过命令make target,那么dependencies(支持项)将被检查。如果target是一个文件,dependencies也都是文件,并且target是比dependencies时间上更新的文件,那就是说这个文件已经是最新的,所以系统也不会再做什么。否则,针对target的处理将被执行,所有的dependencies将被运行或重新产生,target段落的script(脚本)部分也会被执行。

例如,在本文成书之前,我的博客上贴出了一系列的文章(在http://modelingwithdata.org)。每篇博客都是用html和pdf格式上传的,也都是用latex产生的。我忽略了很多这个简单例子里的很多细节(比如latex2html的很多配置选项),但这是一个人们经常编写和运行的makefile。

《C程序设计新思维》一1.4 使用Makefile

如果你将这些makefile片段从屏幕或者书本上复制到makefile文件中,不要忘记每行代码开头的留白部分必须是制表符(tab键)而不是空格(space键)。要怪就怪posix标准吧。

《C程序设计新思维》一1.4 使用Makefile

我们通过类似export f=tip-make的命令来设定f。然后在命令行输入make的时候,第一个目标all被检测到。就是说,make命令自身相当于make“第一个目标”。这个依赖于html、doc和publish,所以那些目标也被依次调用。如果我知道这还没有准备好递交给这个世界,我可以调用make html doc并仅完成这些步骤。

在之前那个简单的makefile中,我们仅有一组target/dependency/script。例如:

《C程序设计新思维》一1.4 使用Makefile

这个和我的博客里的makefile遵循同样的支持项和脚本执行次序,但是脚本是隐含的。这里,p=domath是被编译的程序,并且依赖于目标文件addition.o和subtraction.o。因为addition.o没有被列出来当作一个处理目标,所以make用一条如下所述的隐含的规则来从.c文件编译.o文件。对subtrction.o和domath.o也是一样的操作(因为gnu make隐含假设domath依赖于这里给出设置的domath.o)。一旦所有的目标文件被建立了,我们没有在$(p) target运行的脚本,那么gnu make填充它的默认脚本来连接.o文件而成一个可执行文件。

posix标准的make有一个特殊的从a.c源文件到a.o的编译方法:

《C程序设计新思维》一1.4 使用Makefile

这里$(cc)变量代表你的c编译器;posix标准规定一个默认的cc=c99,但是gnu make的当前版本设定为cc=cc,并且一般连接给gcc。在本段最早的那个最小的makefile中,$(cc)被明确设定为c99,$(cflags)被按照之前的选项列表设定,$(ldflags)没有被设定因此没有被任何事物替代。所以如果make认为它必须产生your_program.o,下面就是你需要运行的命令行,在给定的makefile中:

《C程序设计新思维》一1.4 使用Makefile

当gnu make觉得你需要从目标文件编译出一个可执行文件时,它用下面的方法实现:

《C程序设计新思维》一1.4 使用Makefile

如果想起在连接器中的次序问题,那么我将需要两个连接器变量。在前面的例子中,我们需要:

《C程序设计新思维》一1.4 使用Makefile

作为连接的相关操作。相较这个方法中的正确的汇编命令,我们可以看到我们需要设定ldlibs=-lbroad –lgeneral。如果我们已经设定了ldflags=-lboard –lgeneral,那么这个方法将产生cc –lbroad –lgenerl specifix,o,这个方法看起来是有问题的。请注意ldflags也经常出现在从.c文件编译到.o文件的过程中。

《C程序设计新思维》一1.4 使用Makefile

如果你想看到你的make内置的全部默认规则和变量的列表,可以尝试:

《C程序设计新思维》一1.4 使用Makefile

所以,这个游戏就是:找到合适的变量并把它们设置在makefile中。你还是要探究一下哪个才是正确的选项,但是最少你可以把它们写在makefile中,之后便再也不必去考虑。

如果你用一个ide,或者cmake,或者任何posix标准make的替代品,你都可以做这个“找到合适的变量”的游戏。我还会继续讨论前面的最小makefile,在你的ide中应该不难找到对应的变量。

cflags是根深蒂固的习惯,但是你需要为不同的系统的连接器设定不同的变量。甚至ldlibs都不是posix标准的,但是被gnu make使用。

cflags和ldlibs变量是我们将用来串起所有的定位和识别库文件的编译器选项。如果你有pkg-config,可以把反引号调用放在这里。例如,我的系统中的makefile,也就是我几乎把apophenia和glib用在所有程序中,看起来是这样的:

《C程序设计新思维》一1.4 使用Makefile

或者,手工指定-i、-l和-l变量,如:

《C程序设计新思维》一1.4 使用Makefile

当你在libs和cflags行中添加了一个库的路径,并确定这个配置在你的系统上起了作用时,一般很少有理由去移除这个配置。你真的在乎最终的可执行文件可能会比你为每个程序都做一个个性化makefile所配置的大10kb么?这意味着,你可以把你机器里常用的所有库都总结在一个makefile中,并把它从一个目录复制到另一个目录中而不需要重写。

如果你有第二个(或者更多)c文件,加入类似second.o third.o的内容到makefile objects段落头部的行中(不要加逗号,名字之间仅用空格)。make将用这些来决定哪个文件需要被制作以及以何种方式制作。

如果你的程序只有一个.c文件,你可能压根就不需要makefile。假设目录中现在有一个erf.c文件且没有makefile,可以用你的shell:

《C程序设计新思维》一1.4 使用Makefile

并可以欣赏make如何用他的c编译知识来做余下的工作。

《C程序设计新思维》一1.4 使用Makefile

说实话,我根本不知道。不同的操作系统中是不同的,无论是类型不同还是年份不同,甚至在同一个系统中,那些规则也经常有点混乱。

不过,我们将在第3章中介绍的工具libtool知道每个操作系统中的每个共享库的每个制作过程的细节。我建议你花点时间去了解autotools,那么就可以一举解决共享目标文件的编译问题,而不是花时间在了解每种系统的正确编译器选项和连接过程上。