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

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

再来看一个简单的例子:

[root@localhost linux_c]# cat Makefile
foo = $(bar)
bar = Huh?
all:
@echo $(foo)
[root@localhost linux_c]# make
Huh?

  

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

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

main.o: main.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c $<
CC = gcc
CFLAGS = -O -g
CPPFLAGS = -Iinclude

  

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

A = $(B)
B = $(A)

  

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

[root@localhost linux_c]# cat Makefile
x := foo
y := $(x) bar
all:
@echo "-$(y)-"
[root@localhost linux_c]# make
-foo bar-

  

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

[root@localhost linux_c]# cat Makefile
y := $(x) bar
x := foo
all:
@echo "-$(y)-"
[root@localhost linux_c]# make
- bar-

  

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

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

  

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

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

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

[root@localhost linux_c]# cat Makefile
objects = main.o
objects += $(foo)
foo = foo.o bar.o
all:
@echo "$(objects)"
[root@localhost linux_c]# make
main.o foo.o bar.o

  

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

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

例如下面的这条规则:

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

  

可以改写成:

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

  

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

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

all: main
main: main.o stack.o maze.o
gcc $^ -o $@
main.o: main.h stack.h maze.h
stack.o: stack.h main.h
maze.o: maze.h main.h
clean:
-rm main *.o
.PHONY: clean

  

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

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

  

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

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

  

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

all: main
main: main.o stack.o maze.o
gcc $^ -o $@
clean:
-rm main *.o
.PHONY: clean
sources = main.c stack.c maze.c
include $(sources:.c=.d)
%.d: %.c
set -e; rm -f $@; \
$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$

    

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

include main.d stack.d maze.d

  

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

[root@localhost linux_c]# make
Makefile:8: main.d: No such file or directory
Makefile:8: stack.d: No such file or directory
Makefile:8: maze.d: No such file or directory
set -e; rm -f maze.d; \
cc -MM maze.c > maze.d.$$; \
sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \
rm -f maze.d.$$
set -e; rm -f stack.d; \
cc -MM stack.c > stack.d.$$; \
sed 's,\(stack\)\.o[ :]*,\1.o stack.d : ,g' < stack.d.$$ > stack.d; \
rm -f stack.d.$$
set -e; rm -f main.d; \
cc -MM main.c > main.d.$$; \
sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.$$ > main.d; \
rm -f main.d.$$
cc -c -o main.o main.c
cc -c -o stack.o stack.c
cc -c -o maze.o maze.c
gcc main.o stack.o maze.o -o main

  

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

set -e; rm -f maze.d; \
cc -MM maze.c > maze.d.$$; \
sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \
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中添加了三条规则:

main.o main.d: main.c main.h stack.h maze.h
maze.o maze.d: maze.c maze.h main.h
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的规则有两条:

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

  

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

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

  

常用make命令行选项:

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

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

[root@localhost linux_c]# cat ./test/Makefile
objects = main.o
objects += $(foo)
foo = foo.o bar.o
all:
@echo "$(objects)"
[root@localhost linux_c]# make -C ./test/
make: Entering directory `/home/lf/linux_c/test'
main.o foo.o bar.o
make: Leaving directory `/home/lf/linux_c/test'

  

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

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

[root@localhost linux_c]# cat Makefile
main: main.o stack.o maze.o
gcc main.o stack.o maze.o -o main
main.o stack.o: main.h
main.o maze.o: maze.h
main.o stack.o: stack.h
clean:
-rm main *.o
.PHONY: clean
[root@localhost linux_c]# make CFLAGS=-g
cc -g -c -o main.o main.c
cc -g -c -o stack.o stack.c
cc -g -c -o maze.o maze.c
gcc main.o stack.o maze.o -o main

  

我们再来看一段代码:

[root@localhost linux_c]# cat Makefile
foo = 1
all:
@echo $(foo)
[root@localhost linux_c]# make
1
[root@localhost linux_c]# export foo=2
[root@localhost linux_c]# make -e
2

  

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

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

[root@localhost linux_c]# make foo=3
3
[root@localhost linux_c]# foo=3 make
1
[root@localhost linux_c]# foo=3 make -e
3

  

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

[root@localhost linux_c]# cat Makefile
foo = str1
bar = str2
export foo
all:
$(MAKE) -C subdir
[root@localhost linux_c]# cat ./subdir/Makefile
all:
@echo $(foo) $(bar)
[root@localhost linux_c]# make
make -C subdir
make[1]: Entering directory `/home/lf/linux_c/subdir'
str1
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. 1126 数字统计 2010年NOIP全国联赛普及组

    1126 数字统计 2010年NOIP全国联赛普及组 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 白银 Silver         题目描述 Description 请统计某个 ...

  2. Kendo DataSource 概述

    Kendo DataSource 概述 Kendo 的数据源支持本地数据源( JavaScript 对象数组),或者远程数据源(XML, JSON, JSONP),支持 CRUD 操作(创建,读取,更 ...

  3. cmd_ping命令

    ping命令是网络命令里的核心命令,同时也是黑客入侵的基础命令.下面和大家分享一下ping命令的基础知识和一般用法. 以ping百度公司域名为例,介绍ping命令相关内容. 一.ping命令基础知识 ...

  4. selenium3+webdriver学习笔记1(访问常用请求)

    #!/usr/bin/env python# -*- coding:utf-8 -*- from selenium import webdriverimport osimport time url1= ...

  5. 参考消息 Android 读报

    <参考消息>是新华通讯社主办,参考消息报社编辑出版的日报,创刊于1931年,历史长达80年.<参考消息>每天及时选载世界各国(地区)通讯社.报刊及因特网上的最新消息.评论的精华 ...

  6. C基础的练习集及测试答案(31-39)

    31.读懂以下程序,说明程序的功能#include<stdio.h>int main(){ int m,n,r,m1,m2; printf("请输入2个正整数:"); ...

  7. iOS消息体系架构详解-融云即时通讯云

    iOS SDK 体系架构 本文档将详细介绍融云的 SDK 产品架构和消息体系,以便于您更深入的了解融云并更快速的开发自己的产品. 融云 SDK 系统架构 IMKit IMKit 的功能主要是封装各种界 ...

  8. Nuget~管理自己的包包

    很久很久以前,自己就想有个包包,最近又从网上淘了一个,价格不便宜呢,99块,还是个小腰包,不过作工还算精良,我喜欢的类型,帆布休闲包,可以将我的手机,耳机,水,小烟,小酒,小伞都放里,方便至极,哈哈!

  9. [VC]listctrl的基本用法

    1   添加listctrl的头 m_list.setextendedstyle(LVS_EX_FULLROWSELECT||LVS_EX_GRIdLINES); m_list.insertcolum ...

  10. CodeForces 77C Beavermuncher-0xFF (树形dp)

    不错的树形dp.一个结点能走多次,树形的最大特点是到达后继的路径是唯一的,那个如果一个结点无法往子结点走,那么子结点就不用考虑了. 有的结点不能走完它的子结点,而有的可能走完他的子节点以后还会剩下一些 ...