一、简介

  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. 开源共享一个训练好的中文词向量(语料是维基百科的内容,大概1G多一点)

    使用gensim的word2vec训练了一个词向量. 语料是1G多的维基百科,感觉词向量的质量还不错,共享出来,希望对大家有用. 下载地址是: http://pan.baidu.com/s/1boPm ...

  2. c# winform 窗体起始位置 设置

    窗体起始位置为顶部中间,WinForm居中显示: ; ; this.StartPosition = FormStartPosition.Manual; //窗体的位置由Location属性决定 thi ...

  3. XPath Checker和Firebug安装与使用

    一.XPath Checker和Firebug简介 XPath Checker和Firebug是写爬虫过程中提取数据的非常有用的插件工具,直接打开火狐浏览器的菜单就可以下载 二.XPath Check ...

  4. Windows消息机制详解

    消息是指什么?      消息系统对于一个win32程序来说十分重要,它是一个程序运行的动力源泉.一个消息,是系统定义的一个32位的值,他唯一的定义了一个事件,向 Windows发出一个通知,告诉应用 ...

  5. es let2

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  6. python(28)获得网卡的IP地址

    获得第几块网卡的ip地址: def get_ip_address(self,ifname): # ifname = 'eth0' s = socket.socket(socket.AF_INET, s ...

  7. sql 中 left join 的使用

    left join .是以左表为基础,查询右表的值.如果在右表中没用没有数据,则为NULL. 这里有三张表. 线路bs_line:id,name(id主键) 线路段bs_seg:id,l_id,nam ...

  8. Flask 的 Context 机制

    转自https://blog.tonyseek.com/post/the-context-mechanism-of-flask/ Flask 的 Context 机制 2014 年 07 月 21 日 ...

  9. LeetCode 397. Integer Replacement

    397. Integer Replacement QuestionEditorial Solution My Submissions   Total Accepted: 5878 Total Subm ...

  10. linux journel

    http://www.linuxjournal.com/article/8545 http://www.linuxjournal.com/article/8093 http://www.linuxjo ...