Redo Build System

前一阵子想找一个简单的 build system,看到了 redo,就去研究了一下。一开始还有点迷糊,但最后发现,虽然 redo 看起来只做了一件事(当文件变化时重新执行脚本),但它其实还解决了另一件更重要的事:如何在一个命令式语言中描述结构信息?

声明式语言是擅长描述结构信息的,谁是谁的父亲、谁是谁的兄弟、谁依赖了谁、谁的属性是什么等等。Makefile 就是一种典型的声明式语言,大多数 build system 也都使用声明式语言来描述构建流程(也有的使用了同时掺杂了命令式和声明式的语言,比如 Meson)。

回到一开始的问题,如何在命令式语言中方便地描述这种结构信息?熟悉函数式编程的人大概一下子就会想到使用 Method Chaining:

target("all")
    .depends_on("other_target")
    .source_files("a.c", "b.c")
    .call("...")
    ....;

虽然很多语言都能模仿出这种写法(你甚至可以用 bash 写 fp),但这已经不算是典型的命令式语言的用法了。同时,它仍然是“提前声明”式的做法(即在 build target 之前声明好这个 target 的所有属性、依赖等等),不够灵性。Redo 的做法同时也解决了 Makefile 的一些其他问题,例如依赖的其他 Make 文件是在编译过程中产生的情况、或是 Recursive Make Considered Harmful

Redo 的做法其实很简单,就是让脚本一边跑,一边收集脚本或文件之间的依赖关系。这是它与其他声明式语法的 build system 的最大不同之处。这样的好处是,build 过程仍然用 bash 描述(比起用 Method Chaining 自然很多),而且依赖关系可以随时添加——甚至在编译结束之后。在命令式语言里看起来这是再正常不过的事了:

# 依赖 $2.c
redo-ifchange $2.c
gcc -MD -MF $2.d -c -o $3 $2.c
read DEPS <$2.d

# 依赖 gcc 生成的头文件列表中的所有文件
redo-ifchange ${DEPS#*:}

稍微跑个题,我很久之前其实也想过类似的问题:如何在 C++ 中描述一个图形界面?这个问题与虽然跟 Redo 半毛钱关系没有,但解决的思路其实完全一样。最后我做到了这样:

这是完全合法的 C++ 代码,扔进编译器就能编过。它描述了 main_form、button1 和 button2 之间的结构关系(父子关系),看起来像是声明式的语法,但仍然不丧失命令式语言的特点。最后只要:

int main()  
{
    yz::app app;
    yz::window* w = yz::ui["main_form"];
    app.exec();
    return 0;
}

就能启动。实现细节请看 https://zhuanlan.zhihu.com/p/24196189


至于如何看待 Redo,我觉得它小众自然有它的原因。一个 build 脚本,目标的依赖关系方面使用声明式语言更容易表达,而每个目标的产出过程是一个命令式的过程。所以路无非三条:可以在声明式的语法中加上点命令元素,或是在命令式的语法中加上点声明元素,或是干脆搞一个半声明半命令式的语言。Make 走的第一条路,Redo 是第二条,Meson 是第三条。

从结果来看,大家明显认为解决目标之间的依赖问题更重要。