第一章:C语言之Makefile基础(一)

第二章:C语言之Makefile基础(二)

再来看一个简单的例子:

  1. [root@localhost linux_c]# cat Makefile
  2. foo = $(bar)
  3. bar = Huh?
  4. all:
  5. @echo $(foo)
  6. [root@localhost linux_c]# make
  7. Huh?

  

我们看到了,bar的值后于foo定义,但执行make的时候仍然打印出Huh?。这说明当make读到foo=$(bar)时,确定foo的值是$(bar),但并不立即展开$(bar),然后读到bar=Huh?,确定bar的值是Huh?,然后在执行规则all:的命令列表时才需要展开$(foo),得到$(bar),再展开$(bar),得到Huh?。因此,虽然bar的定义写在foo之后, $(foo)展开还是能够取到$(bar)的值

这样的特性有好处也有坏处,好处是我们可以把变量的定义推迟到后面定义,如:

  1. main.o: main.c
  2. $(CC) $(CFLAGS) $(CPPFLAGS) -c $<
  3. CC = gcc
  4. CFLAGS = -O -g
  5. CPPFLAGS = -Iinclude

  

编译命令展开成gcc -O -g -Iinclude -c main.c。通常把CFLAGS定义成一些编译选项,例如-O -g等,而把CPPFLAGS定义成一些预处理选项,如-D -I等,用等到定义变量的延迟展开特性也有坏处,如:

  1. A = $(B)
  2. B = $(A)

  

make有能力检查出这样的错误避免陷入死循环,但有时候我们希望make遇到变量立即展开时,可以用:=运算符,如:

  1. [root@localhost linux_c]# cat Makefile
  2. x := foo
  3. y := $(x) bar
  4. all:
  5. @echo "-$(y)-"
  6. [root@localhost linux_c]# make
  7. -foo bar-

  

我们尝试一下把x和y调换一下顺序:

  1. [root@localhost linux_c]# cat Makefile
  2. y := $(x) bar
  3. x := foo
  4. all:
  5. @echo "-$(y)-"
  6. [root@localhost linux_c]# make
  7. - bar-

  

当make读到y变量的时候,x尚未定义,展开即为空值,所以y的变量取值是 bar,注意bar前面有一个空格。一个变量的定义从=后面的第一个非空白字符开始(从$(x)的$开始),包括后面的所有字符,直到注释或换行之前结束。如果要定义一个变量的值是一个空格,可以这样:

  1. nullstring :=
  2. space := $(nullstring) # end of the line

  

nullstring的值为空,space的值是一个空格。

还有一个比较有用的运算符是?=,例如foo ?= $(bar)的意思是:如果foo没有定义过,那么?=相当于=,定义foo的值是$(bar),但不立即展开;如果先前已经定义了foo,则什么也不做,不会给foo重新赋值

+=运算符可以给变量追加值,例如:

  1. [root@localhost linux_c]# cat Makefile
  2. objects = main.o
  3. objects += $(foo)
  4. foo = foo.o bar.o
  5. all:
  6. @echo "$(objects)"
  7. [root@localhost linux_c]# make
  8. main.o foo.o bar.o

  

现在,让我们看几个常用的特殊变量:

  • $@,表示规则中的目标
  • $<,表示规则中的第一个条件
  • $?,表示规则中所有比目标新的条件,组成一个列表,以空格分隔
  • $^,表示规则中的所有条件,组成一个列表,以空格分隔

例如下面的这条规则:

  1. main: main.o stack.o maze.o
  2. gcc main.o stack.o maze.o -o main

  

可以改写成:

  1. main: main.o stack.o maze.o
  2. gcc $^ -o $@

  

这样即使以后往条件里追加了新的文件,编译命令也不用修改

现在,我们的Makefile写成这样:

  1. all: main
  2. main: main.o stack.o maze.o
  3. gcc $^ -o $@
  4. main.o: main.h stack.h maze.h
  5. stack.o: stack.h main.h
  6. maze.o: maze.h main.h
  7. clean:
  8. -rm main *.o
  9. .PHONY: clean

  

按照惯例,用all做缺省目标,但还有一点比较麻烦的是,在写main.o、stack.o和maze.o这三个目标时要检查源码,找出他们依赖于哪些头文件,这很容易出错。一是因为有的头文件包含在另一个头文件中,写规则的时候很容易遗漏,二是如果以后修改源代码改变了依赖关系,很可能忘记修改Makefile的规则。为了解决这个问题,可以用gcc -M选项自动生成目标文件和源文件额的依赖关系:

  1. [root@localhost linux_c]# gcc -M main.c
  2. main.o: main.c /usr/include/stdc-predef.h /usr/include/stdio.h \
  3. /usr/include/features.h /usr/include/sys/cdefs.h \
  4. /usr/include/bits/wordsize.h /usr/include/gnu/stubs.h \
  5. /usr/include/gnu/stubs-64.h \
  6. /usr/lib/gcc/x86_64-redhat-linux/4.8.5/include/stddef.h \
  7. /usr/include/bits/types.h /usr/include/bits/typesizes.h \
  8. /usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h \
  9. /usr/lib/gcc/x86_64-redhat-linux/4.8.5/include/stdarg.h \
  10. /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h main.h \
  11. stack.h maze.h

  

-M选项把stdio.h以及它包含的系统头文件也找出来,如果我们不需要输出系统头文件,的依赖关系,可以用-MM选项:

  1. [root@localhost linux_c]# gcc -MM *.c
  2. main.o: main.c main.h stack.h maze.h
  3. maze.o: maze.c maze.h main.h
  4. stack.o: stack.c stack.h main.h

  

接下来的问题是,怎么把这些规则包含到Makefile文件中,GNU make的官方手册建议这样写:

  1. all: main
  2. main: main.o stack.o maze.o
  3. gcc $^ -o $@
  4. clean:
  5. -rm main *.o
  6. .PHONY: clean
  7. sources = main.c stack.c maze.c
  8. include $(sources:.c=.d)
  9. %.d: %.c
  10. set -e; rm -f $@; \
  11. $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
  12. sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
  13. rm -f $@.$$$$

    

sources变量包含我们要编译的所有.c文件,$(source:.c=.d)是一个变量替换语法,把source变量中每一项的.c替换成.d,所以include这一句相当于:

  1. include main.d stack.d maze.d

  

类似于C语言中的#include指示,这里的include表示包含3个文件:main.d、stack.d和maze.d,这三个文件也应该符合Makefile的语法,如果现在你的工作目录是干净的,只有.c文件、.h文件和Makefile,运行make的结果是:

  1. [root@localhost linux_c]# make
  2. Makefile:8: main.d: No such file or directory
  3. Makefile:8: stack.d: No such file or directory
  4. Makefile:8: maze.d: No such file or directory
  5. set -e; rm -f maze.d; \
  6. cc -MM maze.c > maze.d.$$; \
  7. sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \
  8. rm -f maze.d.$$
  9. set -e; rm -f stack.d; \
  10. cc -MM stack.c > stack.d.$$; \
  11. sed 's,\(stack\)\.o[ :]*,\1.o stack.d : ,g' < stack.d.$$ > stack.d; \
  12. rm -f stack.d.$$
  13. set -e; rm -f main.d; \
  14. cc -MM main.c > main.d.$$; \
  15. sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.$$ > main.d; \
  16. rm -f main.d.$$
  17. cc -c -o main.o main.c
  18. cc -c -o stack.o stack.c
  19. cc -c -o maze.o maze.c
  20. gcc main.o stack.o maze.o -o main

  

一开始找不到.d文件,所以make会报警告,但是make会把include的文件名也当做目标来尝试更新,而这些目标适用规则%.d: %.c,所以执行它的命令列表,比如生成maze.d的命令:

  1. set -e; rm -f maze.d; \
  2. cc -MM maze.c > maze.d.$$; \
  3. sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \
  4. rm -f maze.d.$$

  

虽然在Makefile中这个命令写了四行,但其实是一条命令,make只创建了一个shell进程执行这条命令,这条命令分为5个子命令,用;号隔开,执行步骤为:

  1. set -e命令设置当前shell进程为这样的状态:如果它执行的任何一条命令退出状态非0则立刻终止,不再执行后续命令
  2. 把原来的maze.d删掉
  3. 重新生成maze.c的依赖关系,保存成文件maze.d.18006(假设当前shell进程的id是18006),在Makefile中$有特殊含义,如果要表示它的字面意思则需要两个$,所以Makefile中的四个$传给shell会变量两个$,两个$在shell中表示当前的进程id,一般用于给临时文件起名,以保证文件名唯一
  4. sed命令的主要作用就是查找替换,maze.d.18006的内容为:maze.o: maze.c maze.h main.h,经过sed处理后存为maze.d,其内容为:main.o main.d : main.c main.h stack.h maze.h
  5. 最后把临时文件maze.d.123删除

不管Makefile本身还是它包含的文件,只要有一个文件在make过程中被更新,make就会重新读取整个Makefile以及被它包含的所有文件,现在main.d、stack.d和maze.d都生成了,就可以正常包含进来了,相当于在Makefile中添加了三条规则:

  1. main.o main.d: main.c main.h stack.h maze.h
  2. maze.o maze.d: maze.c maze.h main.h
  3. stack.o stack.d: stack.c stack.h main.h

  

如果现在修改了main.c文件,根据规则:main.o main.d: main.c main.h stack.h maze.h要重新生成新的main.o和main.d。生成main.o的规则有两条:

  1. main.o: main.c main.h stack.h maze.h
  2. %.o: %.c
  3. # commands to execute (built-in):
  4. $(COMPILE.c) $(OUTPUT_OPTION) $<

  

第一条是把规则main.o main.d: main.c main.h stack.h maze.h拆开写得到的,第二条是隐含规则,因此执行cc命令重新编译main.o。生成main.d的规则也有两条:

  1. main.d: main.c main.h stack.h maze.h
  2. %.d: %.c
  3. set -e; rm -f $@; \
  4. $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
  5. sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
  6. rm -f $@.$$$$

  

常用make命令行选项:

-n选项只打印要执行的命令,而不会真的执行命令,这个选项有助于我们检查Makefile写得是否正确,由于Makefile不是顺序执行的,用这个选项可以查看命令执行的顺序。

-C选项可以切换另一个目录执行那个目录下的Makefile,比如:

  1. [root@localhost linux_c]# cat ./test/Makefile
  2. objects = main.o
  3. objects += $(foo)
  4. foo = foo.o bar.o
  5. all:
  6. @echo "$(objects)"
  7. [root@localhost linux_c]# make -C ./test/
  8. make: Entering directory `/home/lf/linux_c/test'
  9. main.o foo.o bar.o
  10. make: Leaving directory `/home/lf/linux_c/test'

  

一些规模较大的项目会把不同的模块或子系统的源代码放在不同的子目录中,然后在每个子目录下都写一个Makefile文件,然后再用一个总的Makefile文件中用make -C命令执行每个子目录下的Makefile

在make命令行也可以用=或:=定义变量,如果编译的时候我想加入-g,又不想每次编译的时候都加上-g,可以在命令行中定义CFLAGS变量而不必修改Makefile:

  1. [root@localhost linux_c]# cat Makefile
  2. main: main.o stack.o maze.o
  3. gcc main.o stack.o maze.o -o main
  4. main.o stack.o: main.h
  5. main.o maze.o: maze.h
  6. main.o stack.o: stack.h
  7. clean:
  8. -rm main *.o
  9. .PHONY: clean
  10. [root@localhost linux_c]# make CFLAGS=-g
  11. cc -g -c -o main.o main.c
  12. cc -g -c -o stack.o stack.c
  13. cc -g -c -o maze.o maze.c
  14. gcc main.o stack.o maze.o -o main

  

我们再来看一段代码:

  1. [root@localhost linux_c]# cat Makefile
  2. foo = 1
  3. all:
  4. @echo $(foo)
  5. [root@localhost linux_c]# make
  6. 1
  7. [root@localhost linux_c]# export foo=2
  8. [root@localhost linux_c]# make -e
  9. 2

  

我们首先在Makefile文件中定义foo为1,执行make命令,打印出1,这是没有任何问题的,然后我们在环境变量中定义了foo为2,再次执行的时候加上-e这个选项,文件中定义的foo = 1被环境变量的foo=2覆盖

如果我们把foo定义在make后面,会覆盖文件中的foo变量。如果把foo=3写在make前面,则foo=3是shell进程传给make进程的环境变量,而不是命令行选项,要区分第三行和第五行的写法

  1. [root@localhost linux_c]# make foo=3
  2. 3
  3. [root@localhost linux_c]# foo=3 make
  4. 1
  5. [root@localhost linux_c]# foo=3 make -e
  6. 3

  

刚才讲过在一些规模较大的项目中,每个目录底下会有一个Makefile文件,一般是由上层目录的Makefile里用make -C命令执行下层目录的Makefile。我们可以在上层目录的Makefile里用export声明一些变量,这些变量会自动传给make -C命令做环境变量,注意,这里的export不是shell命令,而是Makefile的声明:

  1. [root@localhost linux_c]# cat Makefile
  2. foo = str1
  3. bar = str2
  4. export foo
  5. all:
  6. $(MAKE) -C subdir
  7. [root@localhost linux_c]# cat ./subdir/Makefile
  8. all:
  9. @echo $(foo) $(bar)
  10. [root@localhost linux_c]# make
  11. make -C subdir
  12. make[1]: Entering directory `/home/lf/linux_c/subdir'
  13. str1
  14. make[1]: Leaving directory `/home/lf/linux_c/subdir'

  

  

Makefile基础(三)的更多相关文章

  1. Makefile基础(二)

    上一章:C语言之Makefile基础(一) 上一章的Makefile写的中规中矩,比较繁琐,是为了讲清楚基本概念,其实Makefile有很多灵活的写法,可以写的更简洁,同时减少出错的可能 一个目标依赖 ...

  2. Python全栈开发【基础三】

    Python全栈开发[基础三]  本节内容: 函数(全局与局部变量) 递归 内置函数 函数 一.定义和使用 函数最重要的是减少代码的重用性和增强代码可读性 def 函数名(参数): ... 函数体 . ...

  3. Bootstrap <基础三十二>模态框(Modal)插件

    模态框(Modal)是覆盖在父窗体上的子窗体.通常,目的是显示来自一个单独的源的内容,可以在不离开父窗体的情况下有一些互动.子窗体可提供信息.交互等. 如果您想要单独引用该插件的功能,那么您需要引用  ...

  4. Bootstrap <基础三十一>插件概览

    在前面布局组件中所讨论到的组件仅仅是个开始.Bootstrap 自带 12 种 jQuery 插件,扩展了功能,可以给站点添加更多的互动.即使不是一名高级的 JavaScript 开发人员,也可以着手 ...

  5. Bootstrap <基础三十>Well

    Well 是一种会引起内容凹陷显示或插图效果的容器 <div>.为了创建 Well,只需要简单地把内容放在带有 class .well 的 <div> 中即可.下面的实例演示了 ...

  6. Bootstrap<基础三> 排版

    Bootstrap 使用 Helvetica Neue. Helvetica. Arial 和 sans-serif 作为其默认的字体栈. 使用 Bootstrap 的排版特性,您可以创建标题.段落. ...

  7. makefile基础(GNU)

    makefile的核心 targets : prerequisites ; commands...   //不分行的情况 targets : prerequisites                 ...

  8. jdbc基础 (三) 大文本、二进制数据处理

    LOB (Large Objects)   分为:CLOB和BLOB,即大文本和大二进制数据 CLOB:用于存储大文本 BLOB:用于存储二进制数据,例如图像.声音.二进制文件 在mysql中,只有B ...

  9. Makefile基础---编译

    首先写一个自己的库: #include "../MyAPI.h" #include <cstdlib> #include <ctime> int getRa ...

随机推荐

  1. 策略模式和php实现

    策略模式: 策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换.策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy). 策略模式 ...

  2. 常用快捷键—Webstorm

    常用快捷键—Webstorm入门指南 提高代码编写效率,离不开快捷键的使用,Webstorm拥有丰富的代码快速编辑功能,你可以自由配置功能快捷键. 快捷键配置 点击“File”-> “setti ...

  3. null、undefined和NaN的区别

    未定义的值和定义未赋值的值是undefined: null是一种特殊的Object,可以给变量赋一个值null,来清除变量的值: NaN是一种特殊的number:

  4. 电话号码 马赛克*号 string类扩展

    /// <summary> /// 字符串马赛克 /// </summary> /// <param name="source"></pa ...

  5. jsp另外五大内置对象之-exception

    //有异常的页面 <%@ page language="java" contentType="text/html; charset=utf-8" page ...

  6. 你会如何给全局对象添加toString()方法

    首先,在讨论如何给所有方法window对象添加toString方法的时候,我们先来说说window的对象继承与对象实例,以及构造函数的this指针,还有变量的提升与方法的调用方式,最终一探window ...

  7. [OpenMP] 并行计算入门

    OpenMP并行计算入门 个人理解 OpenMP是一种通过共享内存并行系统的多处理器程序设计的编译处理方案,通过预编译指令告诉编译器哪些代码块需要被并行化,通过拷贝代码块实现并行程序.对于循环的并行化 ...

  8. 小技巧:unicode RLO

    unicode 控制字符 RLO 可以将位于其后的文字翻转. 于是可以被病毒利用. 如图 重命名文件,在gpj前插入unicode RLO,之后若不小心,可能会被欺骗,误以为是jpg文件. 如果修改程 ...

  9. wxWidgets:处理wxEVT

    我们仍然以继承于wxFrame的MyFrame作为例子. MyFrame.h: class MyFrame : public wxFrame { ...... private: ...... void ...

  10. 2018.3.16 Ubuntu 解决中文乱码问题

    一.乱码的样子类似: °²Àï¿ü ÒÁ¸ñÀ³Ï£ÑÇ˹,°²Àï¿ü ÒÁ¸ñÀ³Ï£ÑÇ˹ 这种乱码称为Gedit中文乱码 打开部分Windows下的txt文本文件的时候,中文显示为乱码.但 ...