一、简介

  How to make a "make"?在进行实现前,应该先对make有一个最基本的了解。这里稍作简介:当一个程序的源文件较少时,对其进行修改并重新生成可执行文件并不复杂,只要将这些文件名作为参数传递给编译器即可;当一个项目的源文件越来越多,对于源文件的修改,必然要重新生成一些中间文件。这时,如果把没有修改的源文件也重新编译,势必会浪费很多时间。make可以根据makefile文件提供的文件依赖,决定哪些中间文件需要重新编译,哪些不需要,从而节约了大量的时间。

  因此,实现make,需要提供的功能是:通过处理读入的makefile文件的内容,梳理文件依赖、并执行相应指令。以下分别介绍。包括自己编写的hash表以及一个测试用例,全部代码已托管至github:https://github.com/vvy/wmake

二、功能实现:makefile的分析和获得文件依赖

(1)makefile的基本格式

    

    图片来源:Makefiles in Linux: An Overview

  上图是一个makefile文件的一个单元,不考虑makefile中的变量,每个makefile都由这样的单元组成。其中:

  第一行,目标文件名,一个分隔的:号,以空格分隔的一连续的文件名。目标文件依赖于后面的所有文件。

  第二行至第N+1行,对应需要执行的命令。

  可以看出,文件分析的重点是这部分的第一行;后续的行直接执行对应的命令即可。第一行中指出了target是依赖于file1...fileN的,这个依赖关系是判断是否需要重新编译target的依据。如果filex比target新,那么意味着filex在生成target之后进行了改动,必须重新编译target。对于target不存在的情况,可以认为target是最旧的,也需要进行编译。

(2)文件新旧的依据:Linux时间戳

  正如(1)中提到的,判断时需要一个文件新旧的指标。makefile使用了时间戳(timestamp)的概念, 利用时间戳的先后判断哪个文件比较新,具体使用的就是修改时间这个指标,可以获得指定文件的修改时间。对于不存在的文件,则认为它的修改时间是最老的,也即0,总是比其他文件旧。这个函数可以写成:

time_t GetModifiedTimestamp(char *path)
{
struct stat attr;
if(stat(path,&attr) == -)
return ;
return attr.st_mtime;
}

  更多关于Linux时间戳的信息,可以参考:linux Makefile时间戳

(3)文件依赖的分析

  假如依赖只有一行,那么很简单,依次检查各个文件是否比目标文件新,然后就可以决定是否需要重新编译了。但实际中往往比较复杂,举个稍微简单点的例子:

#忽略依赖行下面的命令行
something : x y z
x : a b
y : b c
z : d e

  如果a更新了,make something时只需要重新编译x就行了;如果b更新了,make something时不仅需要重新编译x,还要重新编译y。上面的文件依赖可以表示为:

  

  可以看出,make时,需要检查所有与其有依赖的文件的时间戳,而这个过程是递归的。在这个图示的启发下,很容易想到使用图这一数据结构来表示文件依赖。结合实际情况,邻接链表表示的有向图比较合适。图中的结点代表了一个源文件或目标文件,也有可能是“clean”这样的单纯的命令。为了加快结点的插入和查找,使用hash表来存放各个结点是一个合适的选择。这相当于把哈希表和邻接表结合在了一起,即:哈希表存放代表文件的结点,结点的邻接表指向文件依赖中的其他结点。

  这时回到时间戳先后的分析问题,使用深度优先搜索算法(DFS),就可以递归地判断当前顶点的时间戳是否是最新的,如果不是,那么需要重新编译。在DFS这个递归过程中,所有需要更新的结点都会通过重新编译变成最新的,而源文件代表的结点没有邻接结点,不必更新。同时DFS还能找出这个有向图中是否有环,有环时,文件依赖非法,不执行任何动作。使用DFS判断有向图是否有环可以参考《算法导论(第二版)》22.3节“深度优先搜索”中“边的分类”和22.4节“拓扑排序”的引理22.11。同时要注意,这里用了DFS的一个特性:在退出一个结点时才标记为BLACK,这时才与它的后续结点中时间戳最新的进行比较。

  有向图中需要区分两种结点:目标文件结点(含clean)和源文件结点。前者存在文件依赖,并且需要执行一行或多行命令;后者不存在文件依赖,不需要执行命令。因此结点的结构体为:

struct vertex_t{
char* filename;
char** command; //lines of command(s)
time_t timestamp;
int isbase;
int color; //for dfs
struct adjlist_t *adj;
};
typedef struct vertex_t vertex_s;

  而邻接表为:

struct adjlist_t{
struct vertex_t *v;
struct adjlist_t* next;
};
typedef struct adjlist_t adjlist_s;

  对于hash表的数据结构这里不详细解释了,我为wmake编写的hash表可以直接作为库来使用。

三、功能实现:执行命令

  这里的命令,是指输入"make XXX"时执行"XXX : ..."的下一行或多行命令。一开始我本想使用与手把手教你编写一个具有基本功能的shell(已开源)一文中类似的方法对命令行进行分析,不过发现了如果不提供对正则表达式的支持,有个致命的缺点:形如*.c这样的文件名无法通过exec()族函数执行,这将导致make clean中常见的"rm *.o"命令无法运行。因此,这里直接使用system()系统调用来执行对应的文本行即可。

四、测试

(1)基本测试

  测试的内容是多行命令、“make clean”

  为了避免冲突,我把这个程序所使用的“makefile”设定为"wmakefile",其内容为

total : .o .c .h .h
gcc .o .c -o total .o : .h .c hello.c
gcc -c .c -o .o
gcc hello.c -o hello clean :
rm -f *.o total

  执行“./wmake”以及ls,可以看到相关的文件已经生成,并能正确执行。

  执行“./wmake clean”,相应地执行了rm命令。

(2)有环的文件依赖

  使用有环的wmakefile,wmake提示有环,退出。

(3)不存在生成规则

  执行“./wmake XXX”,提示不存在生成规则,退出。

参考资料:

Makefiles in Linux: An Overview

linux Makefile时间戳

编写一个make的更多相关文章

  1. 编写一个通用的Makefile文件

    1.1在这之前,我们需要了解程序的编译过程 a.预处理:检查语法错误,展开宏,包含头文件等 b.编译:*.c-->*.S c.汇编:*.S-->*.o d.链接:.o +库文件=*.exe ...

  2. CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL

    CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL +BIT祝威+悄悄在此留下版了个权的信息说: 开始 本文用step by step的方式,讲述如何使 ...

  3. .NET Core RC2发布在即,我们试着用记事本编写一个ASP.NET Core RC2 MVC程序

    在.NET Core 1.0.0 RC2即将正式发布之际,我也应应景,针对RC2 Preview版本编写一个史上最简单的MVC应用.由于VS 2015目前尚不支持,VS Code的智能感知尚欠火候,所 ...

  4. 网络爬虫:使用Scrapy框架编写一个抓取书籍信息的爬虫服务

      上周学习了BeautifulSoup的基础知识并用它完成了一个网络爬虫( 使用Beautiful Soup编写一个爬虫 系列随笔汇总 ), BeautifulSoup是一个非常流行的Python网 ...

  5. 作业二:个人编程项目——编写一个能自动生成小学四则运算题目的程序

    1. 编写一个能自动生成小学四则运算题目的程序.(10分)   基本要求: 除了整数以外,还能支持真分数的四则运算. 对实现的功能进行描述,并且对实现结果要求截图.   本题发一篇随笔,内容包括: 题 ...

  6. 用Java语言编写一个简易画板

    讲了三篇概博客的概念,今天,我们来一点实际的东西.我们来探讨一下如何用Java语言,编写一块简易的画图板. 一.需求分析 无论我们使用什么语言,去编写一个什么样的项目,我们的第一步,总是去分析这个项目 ...

  7. 22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表。然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法showB输出大写的英文字母表。最后编写主类C,在主类的main方法 中测试类A与类B。

    22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表.然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法sh ...

  8. 编写一个简单的C++程序

    编写一个简单的C++程序 每个C++程序都包含一个或多个函数(function),其中一个必须命名为main.操作系统通过调用main来运行C++程序.下面是一个非常简单的main函数,它什么也不干, ...

  9. 35.按要求编写Java程序: (1)编写一个接口:InterfaceA,只含有一个方法int method(int n); (2)编写一个类:ClassA来实现接口InterfaceA,实现int method(int n)接口方 法时,要求计算1到n的和; (3)编写另一个类:ClassB来实现接口InterfaceA,实现int method(int n)接口 方法时,要求计算n的阶乘(n

      35.按要求编写Java程序: (1)编写一个接口:InterfaceA,只含有一个方法int method(int n): (2)编写一个类:ClassA来实现接口InterfaceA,实现in ...

随机推荐

  1. Windows2003 IIS6.0支持32位和64位两种模式的设置方法

    IIS 6.0 可支持 32 位和 64 位两种模式.但是,IIS 6.0 不支持在 64 位版本的 Windows 上同时运行这两种模式.ASP.NET 1.1 只在 32 位模式下运行.而 ASP ...

  2. 转 strace

    转自http://www.cnblogs.com/ggjucheng/archive/2012/01/08/2316692.html strace常用来跟踪进程执行时的系统调用和所接收的信号. 在Li ...

  3. Python实现支持并发、断点续传的FTP

    参考网上一个FTP程序,重写了一遍,并稍加扩展 一.要求 1. 支持多用户同时登录 2. 可以注册用户,密码使用md5加密 3. 可以登录已注册用户 4.  支持cd切换目录,ls查看目录子文件 5. ...

  4. 查看MS SQL SERVER 错误日志

    查看目的: 错误日志的查看是确保过程已成功完成(例如,备份和恢复操作,批处理命令,或其他脚本和过程).这可以帮助检测任何当前或潜在的问题,包括自动恢复信息(尤其是如果SQL Server实例已停止并重 ...

  5. CM添加kafka服务

    下载所需的包: 在http://archive.cloudera.com/kafka/parcels/latest/ 选择合适parcel下载: ubuntu14.04的cdh5.5.1对应包 htt ...

  6. Windows动态库学习心得

    最近在工作中需要给项目组其他成员提供调用函数,决心抛弃以前“拷贝头文件/源文件”的简陋方法,采用动态库的方式对自己开发的接口进行模块化管理.因之前一直没有机会从事Windows动态库的开发,现借助这个 ...

  7. SplendidCRM中给来自EditView中的listbox控件设置选中值或数据源

    DropDownList list = this.findContol("aas") as DropDownList;list.DataSource = new DataTable ...

  8. c# 文件及目录操作类

    18位长度的计时周期数: DateTime.Now.Ticks.ToString() 多数是收集而来,加上测试感觉很不错,分享一下或许有些帮助吧: 引用: using System; using Sy ...

  9. 查看SQLServer最耗资源时间的SQL语句

    执行最慢的SQL语句 SELECT (total_elapsed_time / execution_count)/1000 N'平均时间ms' ,total_elapsed_time/1000 N'总 ...

  10. oracle中having的用法

    having的用法,having一般是用在group的后面,用在前面查询结果相同,但是语法上不通顺.答:having是对行组进行过滤.一般是先用where 过滤返回行,用group by 对过滤后的行 ...