从入门到精通makefile,手写实现make命令-下篇

380 阅读4分钟

大家好,我是春哥,一名拥有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.写在最后

今天分享的内容就到这里,『如果通过本文你有新的认知和收获,记得关注我,下期分享不迷路,我将持续在掘金上分享技术干货』。

硬核爆肝码字,跪求一赞!!!