编写一个make
一、简介
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
编写一个make的更多相关文章
- 编写一个通用的Makefile文件
1.1在这之前,我们需要了解程序的编译过程 a.预处理:检查语法错误,展开宏,包含头文件等 b.编译:*.c-->*.S c.汇编:*.S-->*.o d.链接:.o +库文件=*.exe ...
- CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL
CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL +BIT祝威+悄悄在此留下版了个权的信息说: 开始 本文用step by step的方式,讲述如何使 ...
- .NET Core RC2发布在即,我们试着用记事本编写一个ASP.NET Core RC2 MVC程序
在.NET Core 1.0.0 RC2即将正式发布之际,我也应应景,针对RC2 Preview版本编写一个史上最简单的MVC应用.由于VS 2015目前尚不支持,VS Code的智能感知尚欠火候,所 ...
- 网络爬虫:使用Scrapy框架编写一个抓取书籍信息的爬虫服务
上周学习了BeautifulSoup的基础知识并用它完成了一个网络爬虫( 使用Beautiful Soup编写一个爬虫 系列随笔汇总 ), BeautifulSoup是一个非常流行的Python网 ...
- 作业二:个人编程项目——编写一个能自动生成小学四则运算题目的程序
1. 编写一个能自动生成小学四则运算题目的程序.(10分) 基本要求: 除了整数以外,还能支持真分数的四则运算. 对实现的功能进行描述,并且对实现结果要求截图. 本题发一篇随笔,内容包括: 题 ...
- 用Java语言编写一个简易画板
讲了三篇概博客的概念,今天,我们来一点实际的东西.我们来探讨一下如何用Java语言,编写一块简易的画图板. 一.需求分析 无论我们使用什么语言,去编写一个什么样的项目,我们的第一步,总是去分析这个项目 ...
- 22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表。然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法showB输出大写的英文字母表。最后编写主类C,在主类的main方法 中测试类A与类B。
22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表.然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法sh ...
- 编写一个简单的C++程序
编写一个简单的C++程序 每个C++程序都包含一个或多个函数(function),其中一个必须命名为main.操作系统通过调用main来运行C++程序.下面是一个非常简单的main函数,它什么也不干, ...
- 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 ...
随机推荐
- Angular中的jsonp
1.一般我们使用Angualr中的jsonp值这样使用的:注入$http服务 这样使用jsonp的方式可以支持多数api,但是douban不支持无法使用 module.controller('InTh ...
- mfc MemoryAdressRead
内涵图 address
- Linux常见命令
du -sh 查看当前文件夹大小 tail -f /var/log/nginx/access.log 查看日志 vsFTPd Linux上面的ftp df -lh 查看磁盘 df -i 查看inode ...
- iOS5.0以上使用新浪微博开放平台OAuth 续(及解决登录无效问题)
新浪微博开放平台为第三方应用提供了简便的合作模式,满足了手机用户和平板电脑用户随时随地分享信息的需求.通过调用平台的api即可实现很多微博上的功能. 本篇主要目的是记录新浪微博移动SDK iOS版本的 ...
- 1、Android Studio集成极光推送(Jpush) 报错 java.lang.UnsatisfiedLinkError: cn.jpush.android.service.PushProtoco
Android studio 集成极光推送(Jpush) (华为手机)报错, E/JPush: [JPushGlobal] Get sdk version fail![获取sdk版本失败!] W/Sy ...
- KNN算法——python实现
二.Python实现 对于机器学习而已,Python需要额外安装三件宝,分别是Numpy,scipy和Matplotlib.前两者用于数值计算,后者用于画图.安装很简单,直接到各自的官网下载回来安装即 ...
- 64位.net调用32位com服务(c++)
说明: 因64位.net无法调用32位dll,才采用调用进程外com形式. 该项目必须为release时编译才不会报错. 项目调用时,添加引用->com中找到该com服务,添加即可. 部署: 启 ...
- 压测如何观测jvm,就是使用jmx来实现jvm监控
jps.jstack.jmap.jhat.jstat.hprof 基于jmx可以开发web版本,方便压测的时候观测jvm以及线程的信息 ================================ ...
- Objective-C之run loop详解[转]
做了一年多的IOS开发,对IOS和Objective-C深层次的了解还十分有限,大多还停留在会用API的级别,这是件挺可悲的事情.想学好一门语言还是需要深层次的了解它,这样才能在使用的时候得心应手,出 ...
- 使用微软CORS包不能跨域访问的问题
使用jquery的ajax异步调用的时候会出现不能跨域访问的问题,这个问题一般有两种方法. 1:使用jsonp跨域 2:使用html5的CORS 在这里只谈论第二种,微软对CORS提供的了支持,在Nu ...