大家好,我是春哥,一名拥有10多年Linux后端研发经验的BAT互联网老兵。
在上一篇文章中,我们通过4个版本的迭代介绍了如何编写通用的makefile文件。
我们已经掌握了makefile的编写和make命令的使用。为了加深大家对这个知识点的理解,下篇将实现一个简易的make程序,我们将其命名为mymake。
mymake将会实现make命令以下的功能。
- 解析makefile文件并进行基本语法的校验。
- 支持指定特定的目标执行关联的命令。
- 分析目标之间的依赖关系,并递归地完成目标的编译。
- 根据源文件的变更来决定哪些目标需要重新编译,实现源码变更时的增量编译。
下面我们来看一下具体的代码实现,一共有5个源代码文件,它们分别是:mymake.cpp、makefileparser.cpp、makefileparser.h、makefiletarget.cpp和makefiletarget.h。其中mymake.cpp为程序执行的入口文件,makefileparser.cpp和makefileparser.h实现了makefile的解析,makefiletarget.cpp和makefiletarget.h实现了目标之间依赖关系的分析并实现了目标的构建。
1.makefile文件解析
makefileparser.h和makefileparser.cpp实现了makefile文件的解析,我们使用通用的词法分析算法,即有限状态机,makefile文件的解析分为5个状态。相应的代码如代码清单1-1和代码清单1-2所示。
代码清单1-1 头文件makefileparser.h
#pragma once
#include <assert.h>
#include <iostream>
#include <string>
#include <vector>
namespace MyMake {
enum ParserStatus {
INIT = 1, // 初始化状态
COLON = 2, // 冒号
IDENTIFIER = 3, // 标识符(包括关键字)-> .PHONY mymake.cpp mymake.o
TAB = 4, // tab键
CMD = 5, // 要执行的命令
};
typedef int TokenType;
typedef struct Token {
int32_t line_pos; // 在行中的位置
int32_t line_number; // token在makefile文件中的第几行
std::string text; // token的文本内容
TokenType token_type; // token类型,使用ParserStatus枚举赋值
void Print() {
std::cout << "line_number[" << line_number << "],line_pos[" << line_pos << "],token_type[" << GetTokenTypeStr()
<< "],text[" << text << "]" << std::endl;
}
std::string GetTokenTypeStr() {
if (token_type == COLON) {
return "COLON";
}
if (token_type == IDENTIFIER) {
return "IDENTIFIER";
}
if (token_type == TAB) {
return "TAB";
}
if (token_type == CMD) {
return "CMD";
}
assert(0);
}
} Token;
class Parser {
public:
bool ParseToToken(std::string file_name, std::vector<std::vector<Token>>& tokens);
private:
void parseLine(std::string line, int32_t line_number, std::string& token, ParserStatus& parseStatus,
std::vector<Token>& tokens_list);
};
} // namespace MyMake
代码清单1-2 源文件makefileparser.cpp
#include "makefileparser.h"
#include <fstream>
using namespace std;
namespace MyMake {
bool Parser::ParseToToken(string file_name, vector<vector<Token>>& tokens_list) {
if (file_name == "") return false;
ifstream in;
in.open(file_name);
if (not in.is_open()) return false;
string token = "";
string line;
ParserStatus parse_status = INIT;
int32_t line_number = 1;
while (getline(in, line)) {
line += '\n';
vector<Token> tokens;
parseLine(line, line_number, token, parse_status, tokens);
line_number++;
if (tokens.size() > 0) {
tokens_list.push_back(tokens);
}
}
return true;
}
void Parser::parseLine(std::string line, int32_t line_number, std::string& token, ParserStatus& parse_status,
std::vector<Token>& tokens) {
auto getOneToken = [&token, &tokens, &parse_status, line_number](ParserStatus new_status, int32_t pos) {
if (token != "") {
Token t;
t.line_pos = pos;
t.line_number = line_number;
t.text = token;
t.token_type = parse_status;
tokens.push_back(t);
}
token = "";
parse_status = new_status;
};
int32_t pos = 0;
for (size_t i = 0; i < line.size(); i++) {
char c = line[i];
if (parse_status == INIT) {
if (c == ' ' || c == '\n') continue;
if (c == '\t') { // tab键
parse_status = TAB;
} else if (c == ':') { // 冒号
parse_status = COLON;
} else { // 其他字符
parse_status = IDENTIFIER;
}
token = c;
pos = i;
} else if (parse_status == COLON) {
if (c == ' ' || c == '\n') {
getOneToken(INIT, pos);
continue;
}
if (c == '\t') {
getOneToken(TAB, pos);
} else if (c == ':') {
getOneToken(COLON, pos);
} else {
getOneToken(IDENTIFIER, pos);
}
token = c;
pos = i;
} else if (parse_status == IDENTIFIER) {
if (c == ' ' || c == '\n') {
getOneToken(INIT, pos);
continue;
}
if (c == '\t') {
getOneToken(TAB, pos);
token = c;
pos = i;
} else if (c == ':') {
getOneToken(COLON, pos);
token = c;
pos = i;
} else {
token += c;
}
} else if (parse_status == TAB) {
if (isblank(c)) { // 过滤掉tab键之后的空白符(space + tab)
continue;
}
getOneToken(CMD, pos); // 其他非空白符的部分就是命令
token = c;
pos = i;
} else if (parse_status == CMD) {
if (c == '\n') {
getOneToken(INIT, pos);
} else {
token += c;
}
} else {
assert(0);
}
}
}
} // namespace MyMake
2.makefile目标管理
makefile本质上是目标之间的依赖管理,通过分析目标之间的依赖关系来实现增量编译,我们通过makefiletarget.cpp和makefiletarget.h实现了目标之间依赖关系的分析并实现了目标的构建。对应的代码如代码清单2-1和代码清单2-2所示。
代码清单2-1 makefiletarget.h
#pragma once
#include <map>
#include <string>
#include <vector>
#include "makefileparser.h"
namespace MyMake {
class Target {
public:
Target(std::string name) : name_(name), is_real_(false) {}
void SetIsReal(bool is_real) { is_real_ = is_real; }
void SetRelateCmd(std::string relate_cmd) { relate_cmds_.push_back(relate_cmd); }
void SetLastUpdateTime(int64_t last_update_time) { last_update_time_ = last_update_time; }
void SetRelateTarget(Target* target) { relate_targets_.push_back(target); }
bool IsNeedReBuild();
int ExecRelateCmd();
int64_t GetLastUpdateTime();
std::string Name() { return name_; }
static Target* QueryOrCreateTarget(std::map<std::string, Target*>& name_2_target, std::string name);
static Target* GenTarget(std::map<std::string, Target*>& name_2_target, std::vector<std::vector<Token>>& tokens_list);
static int BuildTarget(Target* target);
private:
int execCmd(std::string cmd);
private:
std::string name_; // 目标名称
bool is_real_; // 是否位真正的目标,例如:mymake.o : mymake.cpp 中,mymake.o是真正的目标,mymake.cpp是一个虚拟的目标
int64_t last_update_time_; // 目标最后更新的时间
std::vector<std::string> relate_cmds_; // 目标关联的命令
std::vector<Target*> relate_targets_; // 依赖的目标列表
};
} // namespace MyMake
代码清单2-2 makefiletarget.cpp
#include "makefiletarget.h"
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
namespace MyMake {
bool Target::IsNeedReBuild() {
if (not is_real_) { // 非真实的目标不用重建
return false;
}
if (name_ == ".PHONY") { // 伪目标声明的关键字,则需要重新构建
return true;
}
if (relate_cmds_.size() <= 0) { // 真实目标且不是.PHONY,没有关联命令的目标,直接报错
std::cout << "mymake: Nothing to be done for `" << name_ << "'." << std::endl;
exit(4);
}
if (relate_targets_.size() <= 0) { // 依赖的目标为空,则需要重新构建,例如:clean
return true;
}
last_update_time_ = GetLastUpdateTime();
for (size_t i = 0; i < relate_targets_.size(); i++) {
relate_targets_[i]->last_update_time_ = relate_targets_[i]->GetLastUpdateTime();
// 文件不存在(last_update_time_值为-1)或者依赖的目标已经更新了
if (relate_targets_[i]->last_update_time_ == -1 || relate_targets_[i]->last_update_time_ > last_update_time_) {
return true;
}
if (relate_targets_[i]->IsNeedReBuild()) { // 需要再检查依赖的目标是否需要重新构建
return true;
}
}
return false; // 所有依赖的目标的最后更新时间都小于当前的目标的更新时间,则当前目标不用重建
}
int Target::ExecRelateCmd() {
if (not IsNeedReBuild()) { // 不用构建,则直接返回0
return 0;
}
int result = 0;
for (auto& cmd : relate_cmds_) {
result = execCmd(cmd);
if (result) {
std::cout << "mymake: *** [" << name_ << "] Error " << result << std::endl;
exit(3);
}
}
return 0;
}
int64_t Target::GetLastUpdateTime() {
struct stat file_stat;
std::string target_name = "./" + name_;
if (stat(target_name.c_str(), &file_stat) == -1) {
return -1;
}
return file_stat.st_mtime;
}
int Target::execCmd(std::string cmd) {
pid_t pid = fork();
if (pid < 0) {
perror("call fork failed.");
return -1;
}
if (0 == pid) {
std::cout << cmd << std::endl;
execl("/bin/bash", "bash", "-c", cmd.c_str(), nullptr);
exit(1);
}
int status = 0;
int ret = waitpid(pid, &status, 0); // 父进程调用waitpid等待子进程执行子命令结束,并获取子命令的执行结果
if (ret != pid) {
perror("call waitpid failed.");
return -1;
}
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
return 0;
}
return WEXITSTATUS(status);
}
Target* Target::QueryOrCreateTarget(std::map<std::string, Target*>& name_2_target, std::string name) {
if (name_2_target.find(name) == name_2_target.end()) {
name_2_target[name] = new Target(name);
}
return name_2_target[name];
}
Target* Target::GenTarget(std::map<std::string, Target*>& name_2_target, std::vector<std::vector<Token>>& tokens_list) {
Target* root = nullptr;
Target* target = nullptr;
for (auto& tokens : tokens_list) {
if (tokens[0].token_type == IDENTIFIER) { // 一个目标
if (tokens.size() <= 1 || tokens[1].token_type != COLON) { // 目标之后必须是冒号
// 语法错误
std::cout << "makefile:" << tokens[0].line_number << ": *** missing separator. Stop." << std::endl;
exit(1);
}
target = QueryOrCreateTarget(name_2_target, tokens[0].text);
for (size_t i = 2; i < tokens.size(); i++) { // 创建依赖的target
Target* relate_target = QueryOrCreateTarget(name_2_target, tokens[i].text);
target->SetRelateTarget(relate_target);
}
target->SetIsReal(true);
if (nullptr == root) {
root = target; // 第一个target就是多叉树根节点
}
continue;
}
if (tokens[0].token_type == TAB) { // 一条命令
if (tokens[0].line_pos != 0) { // tab键必须在开头位置
std::cout << "makefile:" << tokens[0].line_number << ": *** tab must at line begin. Stop." << std::endl;
exit(3);
}
if (tokens.size() == 1) { // 一条空命令,直接过滤
continue;
}
assert(tokens.size() == 2);
if (target == nullptr) {
std::cout << "makefile:" << tokens[0].line_number << ": *** recipe commences before target. Stop."
<< std::endl;
exit(2);
}
assert(tokens[1].token_type == CMD);
target->SetRelateCmd(tokens[1].text);
continue;
}
// 语法错误
std::cout << "makefile:" << tokens[0].line_number << ": *** missing separator. Stop." << std::endl;
exit(1);
}
return root;
}
int Target::BuildTarget(Target* target) {
if (target->relate_targets_.size() <= 0) { // 叶子节点,直接执行自身关联的命令,并返回,例如mymake.cpp这个目标
return target->ExecRelateCmd();
}
for (auto& relate_target : target->relate_targets_) { // 执行依赖的目标的构建
BuildTarget(relate_target);
}
return target->ExecRelateCmd(); // 构建完依赖的目标,最后执行目标自身管理的命令
}
} // namespace MyMake
3.mymake程序启动入口
最后就是mymake程序的入口文件,如代码清单3-1所示。
代码清单3-1 源文件mymake.cpp
#include <fstream>
#include <iostream>
#include "makefileparser.h"
#include "makefiletarget.h"
using namespace std;
bool GetMakeFileName(string &makefile_name) {
ifstream in;
in.open("./makefile"); // 优先判定makefile文件
if (in.is_open()) {
makefile_name = "./makefile";
return true;
}
in.open("./Makefile");
if (in.is_open()) {
makefile_name = "./Makefile";
return true;
}
return false;
}
int main(int argc, char *argv[]) {
MyMake::Parser parser;
vector<vector<MyMake::Token>> tokens_list;
string makefile_name;
// 判断makefile或者Makefile是否存在
if (not GetMakeFileName(makefile_name)) {
cout << "mymake: *** No targets specified and no makefile found. Stop." << endl;
return -1;
}
// 词法分析
if (not parser.ParseToToken(makefile_name, tokens_list)) {
cout << "ParseToToken failed" << endl;
return -1;
}
// 语法分析,生成编译依赖树(多叉树),并完成简单的语法校验
map<string, MyMake::Target *> name_2_target;
MyMake::Target *build_target = MyMake::Target::GenTarget(name_2_target, tokens_list);
if (argc >= 2) {
string build_name = string(argv[1]);
if (name_2_target.find(build_name) == name_2_target.end()) {
cout << "mymake: *** No rule to make target `" << build_name << "'. Stop." << endl;
exit(-1);
}
build_target = name_2_target[build_name];
}
// 执行编译操作,深度优先遍历多叉树,编译过程需要判断目标是否需要重新构建
if (build_target->IsNeedReBuild()) {
return MyMake::Target::BuildTarget(build_target); // 一个深度优先遍历的构建过程
}
cout << "mymake: `" << build_target->Name() << "' is up to date." << endl;
return 0;
}
mymake程序一开始会先判断makefile或Makefile文件是否存在,如果存在,则执行解析操作完成词法分析。接着,程序会执行语法分析,生成目标编译的依赖树,并完成简单的语法校验。最后,程序使用深度优先遍历算法遍历编译依赖树,来完成目标的编译。
4.程序效果
我们对上面的程序进行编译,并执行命令来测试mymake程序相关的功能。相关的命令和输出如下所示。
执行make命令完成mymake程序的编译
[root@VM-114-245-centos mymake]# make
g++ -g -std=c++11 -c mymake.cpp -o mymake.o
g++ -g -std=c++11 -c makefileparser.cpp -o makefileparser.o
g++ -g -std=c++11 -c makefiletarget.cpp -o makefiletarget.o
g++ -g -std=c++11 -o mymake mymake.o makefileparser.o makefiletarget.o
执行mymake程序编译自己
[root@VM-114-245-centos mymake]# ./mymake
mymake: `mymake' is up to date.
[root@VM-114-245-centos mymake]# rm -rf *.o
删除所有的.o文件之后,重新执行mymake程序
[root@VM-114-245-centos mymake]# ./mymake
g++ -g -std=c++11 -c mymake.cpp -o mymake.o
g++ -g -std=c++11 -c makefileparser.cpp -o makefileparser.o
g++ -g -std=c++11 -c makefiletarget.cpp -o makefiletarget.o
g++ -g -std=c++11 -o mymake mymake.o makefileparser.o makefiletarget.o
[root@VM-114-245-centos mymake]# rm -rf makefileparser.o
单独删除一个.o文件之后,重新执行mymake程序
[root@VM-114-245-centos mymake]# ./mymake
g++ -g -std=c++11 -c makefileparser.cpp -o makefileparser.o
g++ -g -std=c++11 -o mymake mymake.o makefileparser.o makefiletarget.o
指定clean目标,执行关联的清理操作
[root@VM-114-245-centos mymake]# ./mymake clean
rm -rf mymake mymake.o makefileparser.o makefiletarget.o
[root@VM-114-245-centos mymake]#
从执行结果来看,mymake成功实现了我们预期的所有功能。有趣的是,『mymake可以用于生成自己,这一点和自举非常相似』。
5.完整项目
春哥已将这部分代码开源并托管在GitHub上,项目页面包含了详细的使用说明和编译脚本。传送门:github.com/wanmuc/MyMa…。欢迎大家积极参与,进行fork、star以及提出issue。如果因为网络问题导致无法下载,可以评论加关注,我会直接将项目完整源码发送给你。
6.写在最后
今天分享的内容就到这里,『如果通过本文你有新的认知和收获,记得关注我,下期分享不迷路,我将持续在掘金上分享技术干货』。
硬核爆肝码字,跪求一赞!!!