告别 Makefile 噩梦:redo 构建系统入门到精通指南
你是否还在为 Makefile 的复杂语法而头疼?是否曾因依赖管理混乱导致构建错误?是否渴望一个更简单、更可靠且并行构建效率更高的工具?本文将带你全面掌握 redo——这个比 make 更小、更易用、更强大的递归构建系统,让你的项目构建流程化繁为简。
读完本文后,你将能够:
- 理解 redo 的核心原理与优势
- 快速安装并配置 redo 环境
- 编写高效的 .do 脚本实现自动化构建
- 掌握依赖管理、并行构建等高级特性
- 解决实际开发中的常见构建难题
1. redo 简介:构建系统的新范式
1.1 什么是 redo?
redo 是一个基于 Daniel J. Bernstein 设计理念的递归构建系统(Recursive Build System),它旨在替代传统的 make 工具,提供更简洁的语法和更可靠的依赖管理。与 make 相比,redo 具有以下核心优势:
| 特性 | redo | make |
|---|---|---|
| 语法复杂度 | 极低(纯 shell 脚本) | 高(特殊语法规则) |
| 依赖管理 | 精确到文件内容变化 | 基于文件修改时间 |
| 并行构建 | 原生支持且高效 | 需要额外配置 |
| 递归构建 | 安全且推荐 | 被认为有害(Recursive Make Considered Harmful) |
| 学习曲线 | 平缓(熟悉 shell 即可) | 陡峭 |
1.2 redo 的工作原理
redo 的核心思想非常简单:每个目标文件都由对应的 .do 脚本构建。当你运行 redo target 时,redo 会自动查找并执行构建该目标所需的 .do 脚本。与 make 不同,redo 不使用特殊的语法规则,所有构建逻辑都通过标准 shell 脚本实现,这使得脚本更易于编写和维护。
redo 使用 SQLite 数据库跟踪文件依赖关系和内容变化,确保只有真正需要重建的文件才会被处理,从而提高构建效率。
2. 环境准备:安装与配置
2.1 系统要求
- Python 2.7(推荐安装 python-setproctitle 包以优化进程显示)
- SQLite3 模块(通常已包含在 Python 标准库中)
- GCC 或其他 C 编译器(用于构建测试用例)
2.2 安装步骤
2.2.1 从源码安装
# 克隆仓库
git clone https://gitcode.com/gh_mirrors/re/redo
cd redo
# 构建并测试
./do -j10 test
# 安装到系统
DESTDIR= PREFIX=/usr/local sudo -E ./do -j10 install
2.2.2 包管理器安装(推荐)
macOS(Homebrew):
brew install redo
Linux: 大多数发行版的软件仓库中已包含 redo,但可能不是最新版本。如需最新特性,建议从源码安装。
2.3 验证安装
# 检查版本
redo --version
# 查看帮助信息
redo --help
如果安装成功,你将看到类似以下的输出:
redo 0.42 (python)
Copyright (C) 2010-2020 Avery Pennarun <apenwarr@gmail.com>
...
3. 快速入门:第一个 redo 项目
3.1 经典 "Hello World" 示例
让我们通过一个简单的 C 程序来演示 redo 的基本用法。首先创建项目目录并编写源代码:
mkdir -p redo-demo && cd redo-demo
创建 hello.c 文件:
#include <stdio.h>
int main() {
printf("hello, world!\n");
return 0;
}
3.2 编写 .do 脚本
在 redo 中,每个目标都由对应的 .do 文件构建。创建 hello.do 文件:
#!/bin/sh
redo-ifchange hello.c
gcc -o $3 hello.c
让我们解析这个简单的脚本:
redo-ifchange hello.c:声明 hello.c 是当前目标的依赖项。如果 hello.c 发生变化,将重新构建当前目标。gcc -o $3 hello.c:使用 gcc 编译 hello.c,输出文件名为 $3(这是 redo 提供的临时文件路径)。
3.3 运行构建
# 构建目标
redo hello
# 运行生成的程序
./hello
输出结果:
hello, world!
3.4 理解文件命名规则
redo 会按照特定规则查找目标对应的 .do 文件。对于目标 hello,redo 会依次查找:
- hello.do
- default.do
这种灵活的命名规则允许你为不同类型的文件定义通用构建规则。例如,创建 default.c.do 可以为所有 .c 文件提供默认构建规则。
4. 核心命令详解
4.1 redo:无条件构建目标
redo [options] [targets...]
功能:无条件构建指定目标,即使该目标已经存在且依赖项未发生变化。
常用选项:
-j N:指定并行构建的最大作业数(推荐设置为 CPU 核心数)-d:显示依赖检查详情,用于调试-v: verbose 模式,显示 shell 脚本执行过程-x:xtrace 模式,显示脚本执行的详细命令(变量会被展开)
示例:
# 并行构建 all 目标,最多使用 4 个作业
redo -j4 all
# 调试模式构建 hello 目标
redo -d hello
4.2 redo-ifchange:条件构建目标
redo-ifchange [targets...]
功能:仅当目标不存在或依赖项发生变化时才构建目标。这是 redo 中最常用的命令,用于声明目标之间的依赖关系。
使用技巧:
- 可以在构建命令之后调用 redo-ifchange 声明依赖,这对于自动生成依赖非常有用:
# 编译 C 文件并自动检测头文件依赖
gcc -c $2.c -o $3 -MMD -MF $2.deps
read DEPS < $2.deps
redo-ifchange ${DEPS#*:} # 声明头文件依赖
- 尽量在一个 redo-ifchange 调用中声明多个依赖,以提高并行构建效率:
# 推荐:一次声明多个依赖
redo-ifchange a.o b.o c.o
# 不推荐:多次调用(会降低并行效率)
redo-ifchange a.o
redo-ifchange b.o
redo-ifchange c.o
4.3 其他常用命令
| 命令 | 功能 |
|---|---|
redo-ifcreate | 当指定文件被创建时重建目标 |
redo-always | 强制目标始终被重建 |
redo-stamp | 标记目标内容未变化,避免级联重建 |
redo-ood | 列出所有过时(Out Of Date)的目标 |
redo-targets | 列出所有已知目标 |
redo-whichdo | 显示指定目标使用的 .do 文件 |
5. 高级特性与最佳实践
5.1 并行构建优化
redo 的并行构建能力比 make 更高效,主要得益于其精细的依赖管理和锁机制。要充分利用多核 CPU,建议:
- 使用
-j参数指定合适的并行作业数(通常为 CPU 核心数 + 1) - 在 .do 脚本中一次性声明所有依赖,避免串行化构建步骤
- 对独立目标使用
redo-shuffle检测隐藏的依赖问题
# 随机顺序构建目标,检测依赖问题
redo --shuffle -j4 all
5.2 依赖管理高级技巧
5.2.1 自动依赖生成(C/C++ 项目)
结合编译器的自动依赖生成功能,可以轻松管理头文件依赖:
# hello.o.do
redo-ifchange $2.c
gcc -c $2.c -o $3 -MMD -MF $2.deps # 生成依赖文件
# 解析依赖文件并声明
if [ -f $2.deps ]; then
read DEPS < $2.deps
redo-ifchange ${DEPS#*:}
fi
5.2.2 处理动态生成的文件
对于需要动态生成的文件,可以使用 redo-ifcreate 声明潜在的依赖:
# docs.html.do
redo-ifchange docs.md
markdown docs.md > $3
# 如果有图片文件被创建,需要重建文档
redo-ifcreate images/*.png
5.3 项目组织结构
推荐的 redo 项目结构如下:
project/
├── all.do # 顶层构建目标
├── clean.do # 清理目标
├── install.do # 安装目标
├── src/ # 源代码目录
│ ├── hello.c
│ └── hello.o.do # 编译规则
├── docs/ # 文档目录
│ ├── index.md
│ └── index.html.do # 文档生成规则
└── test/ # 测试目录
├── test.do # 测试规则
└── hello_test.c
这种结构清晰分离了不同类型的文件和构建规则,便于维护和扩展。
5.4 与其他构建系统集成
redo 可以与其他构建系统无缝集成,例如:
- 与 autoconf/automake 集成:使用 redo 作为最终构建工具,保留 autoconf 进行系统检测
- 与 CMake 集成:让 CMake 生成 .do 文件,再用 redo 执行构建
- 与 npm/yarn 集成:在 .do 脚本中调用 npm 命令管理 JavaScript 依赖
# jsapp.do - 集成 npm 的示例
redo-ifchange package.json
npm install
browserify src/main.js -o $3
6. 实战案例:构建多文件 C 项目
让我们通过一个更复杂的示例来巩固所学知识。我们将构建一个包含多个源文件的 C 项目,演示如何组织 .do 脚本和管理依赖关系。
6.1 项目结构
mathapp/
├── all.do
├── clean.do
├── main.c
├── main.o.do
├── math/
│ ├── add.c
│ ├── add.h
│ ├── add.o.do
│ ├── multiply.c
│ ├── multiply.h
│ └── multiply.o.do
└── mathapp.do
6.2 实现代码
main.c:
#include <stdio.h>
#include "math/add.h"
#include "math/multiply.h"
int main() {
int a = 2, b = 3;
printf("%d + %d = %d\n", a, b, add(a, b));
printf("%d × %d = %d\n", a, b, multiply(a, b));
return 0;
}
math/add.c:
#include "add.h"
int add(int a, int b) {
return a + b;
}
math/multiply.c:
#include "multiply.h"
int multiply(int a, int b) {
return a * b;
}
6.3 编写 .do 脚本
all.do - 顶层目标:
redo-ifchange mathapp
mathapp.do - 链接可执行文件:
# 声明所有目标文件依赖
redo-ifchange main.o math/add.o math/multiply.o
# 链接生成可执行文件
gcc -o $3 main.o math/add.o math/multiply.o
main.o.do - 编译主程序:
redo-ifchange main.c
gcc -c main.c -o $3 -Imath -MMD -MF main.deps
# 自动处理头文件依赖
[ -f main.deps ] && read DEPS < main.deps && redo-ifchange ${DEPS#*:}
math/add.o.do - 编译加法模块:
redo-ifchange add.c add.h
gcc -c add.c -o $3 -MMD -MF add.deps
[ -f add.deps ] && read DEPS < add.deps && redo-ifchange ${DEPS#*:}
math/multiply.o.do - 编译乘法模块:
redo-ifchange multiply.c multiply.h
gcc -c multiply.c -o $3 -MMD -MF multiply.deps
[ -f multiply.deps ] && read DEPS < multiply.deps && redo-ifchange ${DEPS#*:}
clean.do - 清理目标:
rm -f mathapp *.o math/*.o *.deps math/*.deps
6.4 执行构建
# 构建项目
redo -j2 all
# 运行程序
./mathapp
# 清理构建产物
redo clean
输出结果:
2 + 3 = 5
2 × 3 = 6
7. 常见问题与解决方案
7.1 为什么 redo 总是重新构建我的目标?
这是初学者最常见的问题之一。原因在于 redo 命令总是重新构建指定的目标,无论其是否过时。要实现条件构建,应该使用 redo-ifchange。
解决方案:
- 顶层目标使用
redo-ifchange - 只有在确实需要无条件重建时才使用
redo
# 错误:总是重建
redo hello
# 正确:条件重建
redo-ifchange hello
7.2 如何处理跨平台构建?
使用 redo 的配置文件和环境变量可以轻松实现跨平台构建:
# config.do - 生成配置文件
os=$(uname)
if [ "$os" = "Darwin" ]; then
echo "CC=clang" > $3
else
echo "CC=gcc" > $3
fi
# 在其他 .do 脚本中包含配置
. ./config
$CC -c $2.c -o $3
7.3 如何迁移现有的 Makefile 项目到 redo?
迁移步骤:
- 识别 Makefile 中的目标和依赖关系
- 为每个目标创建对应的 .do 脚本
- 将 Makefile 中的规则转换为 shell 命令
- 使用 redo-ifchange 声明依赖关系
- 测试并优化新的构建流程
迁移工具:可以使用 make2do(第三方工具)自动转换简单的 Makefile 为 .do 脚本。
8. 总结与展望
8.1 redo 的优势回顾
- 简单易学:基于 shell 脚本,无需学习复杂语法
- 可靠高效:精确的依赖管理和并行构建
- 灵活强大:适用于各种规模和类型的项目
- 易于扩展:可与现有工具链无缝集成
8.2 进阶学习资源
- 官方文档:https://redo.rtfd.io
- 源码仓库:https://gitcode.com/gh_mirrors/re/redo
- 邮件列表:redo-list@googlegroups.com
8.3 后续学习建议
- 深入研究 redo 的高级命令(如
redo-stamp、redo-always) - 探索 redoconf 工具,实现自动配置功能
- 为常用编程语言编写通用 .do 脚本库
- 参与 redo 社区,贡献代码或文档
通过掌握 redo,你已经迈出了构建系统现代化的重要一步。无论是小型项目还是大型复杂系统,redo 都能帮助你实现更简单、更可靠的构建流程。现在就开始将你的项目迁移到 redo,体验构建系统的新范式吧!
如果你觉得本文对你有帮助,请点赞、收藏并关注,后续将带来更多关于 redo 高级技巧和最佳实践的内容!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



