March 3, 2015 8:19 PM

Makefile 文件的编写

学习前的准备

需要准备的工程目录结构如下:

.
├── add
│   ├── add_float.c
│   ├── add.h
│   └── add_int.c
├── main.c
└── sub
├── sub_float.c
├── sub.h
└── sub_int.c

文件编译为可执行文件cacu

NOTE:需要的源代码:MakefileExample.tar

Makefile的介绍

使用 GCC 的命令行进行程序编译在单个文件下是比较方便的,当工程中的文件逐渐增多,甚至变得十分庞大的时候,使用 GCC 命令编译就会变得力不从心。Linux 中的 make 工具提供了一种管理工程的功能,可以方便的进行程序的编译,对更新的文件进行重编译。

Makefile的基本格式为:

TARGET... : DEPENDEDS...
COMMAND
...
...
  • TARGET:规则所定义的目标,通常是最后生成的文件,也可以是一个“动作”,称之为“伪目标”。
  • DEPENDEDS:执行此规则所必须的依赖条件。
  • COMMAND:规则所执行的命令。命令可以是多个,每个命令占一行,以 Tab 开头。

动手编写多文件工程的Makefile

1.命令行编译程序

如果在命令行下手动编译该程序比较麻烦,需要先编译每个文件,生成目标文件,最后再将5个目标文件编译成可执行文件。

#get add_int.o target
gcc -c add/add_int.c -o add/add_int.o -ggdb #get add_float.o target
gcc -c add/add_float.c -o add/add_float.o -ggdb #get sub_float.o target
gcc -c sub/sub_float.c -o sub/sub_float.o -ggdb #get sub_int.o target
gcc -c sub/sub_int.c -o sub/sub_int.o -ggdb #get main.o target
gcc -c main.c -o main.o -Iadd -Isub -ggdb #get cacu bin file
gcc -o cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o -ggdb #get main.S target
gcc -S add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o

2.多文件的Makefile

使用make进行项目管理,需要编写Makefile。在编译时,make程序按照顺序从Makefile文件中读取指令,依次执行!

#get cacu bin file
cacu:add_int.o add_float.o sub_int.o sub_float.o main.o
gcc -o cacu add/add_int.o add/add_float.o \
sub/sub_int.o sub/sub_float.o main.o -ggdb #get add_int.o target
add_int.o:add/add_int.c add/add.h
gcc -c -o add/add_int.o add/add_int.c -ggdb #get add_float.o target
add_float.o:add/add_float.c add/add.h
gcc -c -o add/add_float.o add/add_float.c -ggdb #get sub_int.o target
sub_int.o:sub/sub_int.c sub/sub.h
gcc -c -o sub/sub_int.o sub/sub_int.c -ggdb #get sub_float.o target
sub_float.o:sub/sub_float.c sub/sub.h
gcc -c -o sub/sub_float.o sub/sub_float.c -ggdb #get main.o target
main.o:main.c add/add.h sub/sub.h
gcc -c -o main.o main.c -Iadd -Isub -ggdb #clean project
clean:
rm -f cacu add/add_int.o add/add_float.o \
sub/sub_int.o sub/sub_float.o main.o

当需要编译工程时,直接在工程目录中执行make即可。如果想清除编译过程中生成的目标文件和cacu,执行make clean即可。

3.使用用户自定义变量的Makefile

在Makefile文件中,用户可以自定义变量,方便用户修改参数。

使用变量后,原本冗长的文件可以化简为:

CC = gcc
CFLAGS = -Iadd -Isub -O2
OBJS = add/add_int.o add/add_float.o sub/sub_float.o sub/sub_int.o main.o
TARGET = cacu
RM = rm -f $(TARGET):$(OBJS)
$(CC) -o $(TARGET) $(OBJS) $(CFLAGS) $(OBJS):%o:%c
$(CC) -c $(CFLAGS) $< -o $@ clean:
$(RM) $(TARGET) $(OBJS)

NOTE:$(OBJS):%.o:%.c中 %.o:%.c 是将 $(OBJS) 中以 .o 结尾的文件替换成以 .c 结尾的文件。

其中 $< 和 $@ 是自动化变量,下一节会介绍。

4.使用预定义变量的Makefile

在Makefile中还有一些变量是系统预定义的,用户可以直接使用。

Makefile中经常使用的变量及含义

变量名 含 义 默 认 值
AR 生成静态库库文件的程序名称 ar
AS 汇编编译器的名称 as
CC C语言编译器的名称 cc
CPP C语言预编译器的名称 $(CC) -E
CXX C++语言编译器的名称 g++
FC FORTRAN语言编译器的名称 f77
RM 删除文件程序的名称 rm -f
ARFLAGS 生成静态库库文件程序的选项 无默认值
ASFLAGS 汇编语言编译器的编译选项 无默认值
CFLAGS C语言编译器的编译选项 无默认值
CPPFLAGS C语言预编译器的编译选项 无默认值
CXXFLAGS C++语言编译器的编译选项 无默认值
FFLAGS FORTRAN语言编译器的编译选项 无默认值

因此,前面的Makefile文件可以改写成:

CFLAGS = -Iadd -Isub -O2
OBJS = add/add_int.o add/add_float.o \
sub/sub_int.o sub/sub_float.o main.o
TARGET = cacu $(TARGET):$(OBJS)
$(CC) -o $(TARGET) $(OBJS) $(CFLAGS) clean:
-$(RM) $(TARGET) $(OBJS)

其中变量$(CC) $(RM)可以直接使用,默认值分别是ccrm -f。另外CFLAGS等变量是调用编译器时的默认选项配置,在生成main.o时没有指定编译选项,make程序自动调用了文件中定义的CFLAGS选项来增加头文件的搜索路径。

5.使用自动变量的Makefile

还记得上面出现的 $< 和 $@ 吗?它们是Makefile中的自动变量,分别代表依赖项和目标项。下面是一些常见的自动变量及其含义:

Makefile 中常见的自动变量和含义

变量 含义
* 表示目标文件的名称,不包含目标文件的扩展名
+ 表示所有的依赖文件,这些依赖文件之间以空格分开,按照出现的先后为顺序,其中可能包含重复的依赖文件
< 表示依赖项中第一个依赖文件的名称
? 依赖项中,所有目标文件时间戳晚的依赖文件,依赖文件之间以空格分开
@ 目标项中目标文件的名称
^ 依赖项中,所有不重复的依赖文件,这些文件之间以空格分开

由此,对上面的Makefile文件进行重写,代码如下:

CFLAGS = -Iadd -Isub -O2
OBJS = add/add_int.o add/add_float.o \
sub/sub_int.o sub/sub_float.o main.o
TARGET = cacu $(TARGET):$(OBJS)
$(CC) $^ -o $@ $(CFLAGS) $(OBJS):%o:%c
$(CC) -c $< -o $@ $(CFLAGS) clean:
-$(RM) $(TARGET) $(OBJS)

6.设置搜索路径

在大的系统中,通常存在很多目录,手动添加目录的方法不仅十分笨拙而且容易造成错误。Make 的目录搜索功能提供了一个解决此问题的方法,指定需要搜索的目录, make 会自动找到指定文件的目录并添加到文件上, VPATH 变量可以实现此目的。VPATH 变量的使用方法如下:

VPATH = path1:path2:...

VPATH 右边是冒号(:)分割路径名称,例如下面的指令:

VPATH = add:sub
add_int.o:%.o:%.c
$(CC) -c -o $@ $<

Make 的搜索路径包含 add 和 sub 目录。add_int.o 规则自动扩展成如下代码:

add_int.o:add/add_int.c
cc -c -o add_int.o add/add_int.c

用添加路径的方法改写上面的 Makefile 文件,代码如下:

CFLAGS = -Iadd -Isub -O2
OBJDIR = objs
VPATH = add:sub:.
OBJS = add_int.o add_float.o sub_int.o sub_float.o main.o
TARGET = cacu $(TARGET):$(OBJSDIR) $(OBJS)
$(CC) -o $(TARGET) $(OBJDIR)/*.o $(CFLAGS) $(OBJDIR):
mkdir -p ./$@ $(OBJS):%.o:%.c
$(CC) -c $(CFLAGS) $< -o $(OBJDIR)/$@ clean:
-$(RM) $(TARGET)
-$(RM) $(OBJDIR)/*.o -r

7.自动推导规则

使用命令 make 编译扩展名为 .c 的 C 语言文件的时候,源文件的编译规则不用明确地给出。这是因为 make 进行编译的时候会使用一个默认的编译规则,按照默认规则完成对 .c 文件的编译,生成对应的 .o 文件。它执行命令 cc -c 来编译 .c 源文件。在 Makefile 中只需要给出需要重建的目标文件(一个 .o 文件),make 会自动为这个 .o 文件寻找合适的依赖文件(对应的 .c 文件),并且使用默认的命令来构建这个目标文件。

对于上边的例子,默认规则是使用命令cc -c main.c -o main.o来创建文件 main.o 。对一个目标文件是“文件名.o“,依赖文件是”文件名.c“的规则,可以省略其编译规则的命令行,由 make 命令决定如何使用编译命令和选项。此默认规则称为 make 的隐含规则。

这样,在书写 Makefile 时,就可以省略掉描述 .c 文件和 .o 依赖关系的规则,而只需要给出那些特定的规则描述(.o 目标所需要的 .h 文件)。因此上面的例子可以使用更加简单的方式书写, Makefile 文件的内容如下:

CFLAGS = -Iadd -Isub -O2
VPATH = add:sub
OBJS = add_int.o add_float.o sub_int.o sub_float.o main.o
TARGET = cacu
$(TARGET):$(OBJS)
$(CC) -o $(TARGET) $(OBJS) $(CFLAGS) clean:
-$(RM) $(TARGET)
-$(RM) $(OBJS)

8.递归 make

对于规模比较大的程序,需要多个人在多个目录下进行开发。如果只用一个 Makefile 来维护就会比较麻烦,因此可以在每个目录下建立自己的 Makefile ,然后在总控 Makefile 中调用子目录的 Makefile 文件。

目录结构如下:

.
├── add
│   ├── add_float.c
│   ├── add.h
│   ├── add_int.c
│   └── Makefile
├── main.c
├── Makefile
└── sub
├── Makefile
├── sub_float.c
├── sub.h
└── sub_int.c

1.递归调用的方式

add:
cd add && $(MAKE)

它等价于

add:
$(MAKE) -C add

2.总控Makefile

CC = gcc
CFLAGS = -O2
TARGET = cacu
export OBJSDIR = $(shell pwd)/objs $(TARGET):$(OBJSDIR) main.o
$(MAKE) -C add
$(MAKE) -C sub
$(CC) -o $(TARGET) $(OBJSDIR)/*.o $(OBJSDIR):
mkdir -p $@ main.o:%.o:%.c
$(CC) -c $< -o $(OBJSDIR)/$@ $(CFLAGS) -Iadd -Isub clean:
-$(RM) $(TARGET)
-$(RM) $(OBJSDIR)/*.o

如果总控 Makefile 中的一些变量需要传递给下层的 Makefile,可以使用 export 命令。如:export OBJSDIR = ./objs

3.子目录Makefile的编写

Add 目录下的 Makefile 如下:

OBJS = add_int.o add_float.o
all:$(OBJS) $(OBJS):%.o:%.c
$(CC) -c $< -o $(OBJSDIR)/$@ $(CFLAGS) clean:
$(RM) $(OBJS)

Sub 目录下的 Makefile 如下:

OBJS = sub_int.o sub_float.o
all:$(OBJS) $(OBJS):%.o:%.c
$(CC) -c $< -o $(OBJSDIR)/$@ $(CFLAGS) clean:
$(RM) $(OBJS)

Makefile 中的函数

1.获取匹配模式的文件名wildcard

这个函数的功能是查找当前目录下所有符合模式 PATTERN 的文件名,其返回值是以空格分割的、当前目录下的所有符合模式 PATTERN 的文件名列表。其原型如下:

$(wildcard PATTERN)

例如,如下模式返回当前目录下所有扩展名位 .c 的文件列表。

$(wildcard *.c)

2.模式替换函数patsubst

这个函数的功能是查找字符串 text 中按照空格分开的单词,将符合模式 pattern 的字符串替换成 replacement。 Pattern 中的模式可以使用通配符, % 代表 0 个到 n 个字符,当 pattern 和 replacement 中都有 % 时,符合条件的字符将被 replacement 中的替换。函数的返回值是替换后的新字符串。其原型如下:

$(patsubst pattern, replacement, text)

例如,需要将 C 文件替换为 .o 的目标文件可以使用如下模式:

$(patsubst %.c, %.o, add.c)

上面的模式将 add.c 字符串作为输入,当扩展名为 .c 时符合模式 %.c ,其中 % 在这里代表 add,替换为 add.o,并作为输出字符串。

$(patsubst %.c, %.o, $(wildcard *.c))

输出的字符串将当前扩展名为 .c 的文件替换成 .o 的文件列表。

3.循环函数foreach

这个函数的原型为:

$(foreach VAR, LIST, TEXT)

函数的功能为 foreach 将 LIST 字符串中一个空格分割的单词,先传给临时变量 VAR ,然后执行 TEXT 表达式, TEXT 表达式处理结束后输出。其返回值是空格分割表达式 TEXT 的计算结果。

例如,对于存在 add 和 sub 的两个目录,设置 DIRS 为 "add sub ./" 包含目录 add、sub 和当前目录。表达式 $(wildcard $(dir)/*.c) ,可以取出目录 add 和 sub 及当前目录中的所有扩展名为 .c 的C语言源文件:

DIRS = sub add ./
FILES = $(foreach dir, $(DIRS), $(wildcard $(dir)/*.c))

利用上面几个函数对原有的 Makefile 文件进行重新编写,使新的 Makefile 可以自动更新各个目录下的C语言源文件:

CC = gcc
CFLAGS = -O2 -Iadd -Isub
TARGET = cacu
DIRS = sub add .
FILES = $(foreach dir, $(DIRS), $(wildcard $(dir)/*.c))
OBJS = $(patsubst %.c, %.o, $(FILES))
$(TARGET):$(OBJS)
$(CC) -o $(TARGET) $(OBJS) clean:
-$(RM) $(TARGET)
-$(RM) $(OBJS)

总结

至此,已经可以阅读大部分软件的 Makefile 了~~~

一个工程实例来学习 Makefile的更多相关文章

  1. 如何编写一个工程文件夹下通用的Makefile

    新建工程文件夹,在里面新建 bsp.imx6ul.obj 和project 这 3 个文件夹,完成以后如图所示: 新建的工程根目录文件夹 其中 bsp 用来存放驱动文件:imx6ul 用来存放跟芯片有 ...

  2. 一个不错的linux学习资料下载的网址

    本文比较完整的讲述GNU make工具,涵盖GNU make的用法.语法.同时重点讨论如何为一个工程编写Makefile.作为一个Linux程序员,make工具的使用以及编写Makefile是必需的. ...

  3. 一个 XSD 实例

    一个 XSD 实例 本节会为您演示如何编写一个 XML Schema.您还将学习到编写 schema 的不同方法. XML 文档 让我们看看这个名为 "shiporder.xml" ...

  4. C++ 容器的综合应用的一个简单实例——文本查询程序

    C++ 容器的综合应用的一个简单实例——文本查询程序 [0. 需求] 最近在粗略学习<C++ Primer 4th>的容器内容,关联容器的章节末尾有个很不错的实例.通过实现一个简单的文本查 ...

  5. Web框架本质及第一个Django实例 Web框架

    Web框架本质及第一个Django实例   Web框架本质 我们可以这样理解:所有的Web应用本质上就是一个socket服务端,而用户的浏览器就是一个socket客户端. 这样我们就可以自己实现Web ...

  6. 前端框架之Vue(1)-第一个Vue实例

    vue官方文档 知识储备 es6语法补充 let 使用 var 声明的变量的作用域是全局. { var a = 1; } console.info(a); 例1: var arr = []; for ...

  7. 如何在IDEA启动多个Spring Boot工程实例

    在我讲解的案例中,经常一个工程启动多个实例,分别占用不同的端口,有很多读者百思不得其解,在博客上留言,给我发邮件,加我微信询问.所以有必要在博客上记录下,方便读者. step 1 在IDEA上点击Ap ...

  8. Spring-boot原理(附带实现一个spring-boot-starter实例和代码下载)

    ​ (我就是个封面) Spring-boot自出现后,到现在火的很,大家貌似都在用,连招聘里面也要求会这个.但是说实话,spring-boot无外乎想实现一种可插拔的编程方式,说是简化配置,其实并没有 ...

  9. 《C#并发编程经典实例》学习笔记—2.7 避免上下文延续

    避免上下文延续 在默认情况下,一个 async 方法在被 await 调用后恢复运行时,会在原来的上下文中运行. 为了避免在上下文中恢复运行,可让 await 调用 ConfigureAwait 方法 ...

随机推荐

  1. 使用C语言扩展Python提供性能

    python底层是用c写的,c本身是一个非常底层的语言,所以它做某些事情的效率肯定会比上层语言高一些. 比如有些自动化测试用的python库,会对系统的UI进行一些捕获,点击之类的操作,这必然要用到c ...

  2. netbeans等宽字体却不支持中文

    一直用netbeans,各方面都很满意,就是这字体十分不爽,如用等宽字体却不支持中文,百度了一下,找到了解决办法,贴出来,给需要的朋友. 01.找到自己java字体目录.我的目录是[C:\Progra ...

  3. 初涉Rx套餐 之RxBinding(让你的事件流程更清晰)

    转载请注明出处:王亟亟的大牛之路 最近下班回家都在WOW,周末就爆肝,感觉人都要GO DIE了,昨天下午看了看RxBinding相关的功能感觉还是蛮强大的,所提供的API也是相当丰富(基本Rx套餐都是 ...

  4. LeetCode第[46]题(Java):Permutations(求所有全排列) 含扩展——第[47]题Permutations 2

    题目:求所有全排列 难度:Medium 题目内容: Given a collection of distinct integers, return all possible permutations. ...

  5. 在oracle中插入数据报错:ORA-00984列在此处不允许

    这里报错的原因就是当数据类型varchar2时没有使用单引号. 没写单引号,不管是双引号还是什么都没写都会报这个错误.

  6. Linux软件安装(二)

    1. 安装软件时,如果依赖的文件是 .so 类型的文件(so文件是谋个文件的小功能模块,如果php.ini中设置的模块引用就是 .so文件) ,这时被依赖的软件要安装完整的软件,一般可以根据 .so ...

  7. Spring获取bean的几种方式

    工作中需要对一个原本加载属性文件的工具类修改成对数据库的操作当然,ado层已经写好,但是需要从Spring中获取bean,然而,工具类并没有交给Spring来管理,所以需要通过方法获取所需要的bean ...

  8. charles抓包工具的使用:概述

    一. 什么是包 用户和后台客户端之间的请求数据,都是以包的形式来传递的,具体要深究,可以去看看这方面的网络知识 二. 为何要抓包 1) 可以用来分析网络流量 2) 可以用来破译抓来的数据,比如密码之类 ...

  9. react 总结

    1.React 里直接修改 this.state 和调用 setState() 修改 state 的值有什么区别? 使用对this.state赋值并没有什么作用,官方提醒,应该把this.state当 ...

  10. YUI笔记 1 模块加载

    我们通常开发js程序就是使用<script>标签把脚本引入到页面中进行开发,如果是简单的逻辑还好,但是如果是比较庞大的大规模js开发,可能会出现下面的问题: 1.  <script& ...