#build-system #ninja-build #ninja #tooling

nightly ninja-builder

Rust 中 ninja 构建系统的构建逻辑。基于论文 Build Systems a la Carte。

1 个不稳定版本

0.1.0 2020 年 8 月 29 日

#1422 in 开发工具


用于 ninjars

Apache-2.0 协议

160KB
3.5K SLoC

ninja-rs

ninja-rs (像忍者一样,但发音时加上一个突兀的 "r" ;-)) 是 ninja 构建系统的克隆。

动机

ninja-rs 是出于几个不同的原因创建的

  1. 教育 - Build Systems a la Carte 是对我影响极大的论文,因为它正式化和提炼了构建系统。这是尝试使用该论文中的思想实现一个真正的构建系统的尝试。有关更多设计信息,请参阅下面的设计笔记。

  2. 在我日常工作之外使用 Rust 实际项目的一个练习。

  3. 我以前从未编写过真正的解析器。

ninja 是一个相当简单的构建系统(它被明确设计为构建系统的“汇编语言”),因此我认为这是一个好的起点。它相对较小,原始代码可读性很好。它不涉及任何网络或打包。正如 Evan Martin 所说

Ninja 的前 20% 很容易实现,剩下的 80% 只是些琐碎的细节。

这是一个正在进行中的项目。随着我对 Ninja 和 Rust 理解的不断提高,各种事情都在变化,并会继续变化。

功能完整性

到目前为止,解析器功能相对完整,ninja-rs 能够构建这个仓库中的简单 CMake hello-world。要完全支持 ninja 所支持的所有功能,它还有很长的路要走。

  • 工作解析器和基于拓扑排序的构建器
  • 基于 mtime 的重建
  • 与 Ninja 基本兼容的命令行
  • 隐式和有序依赖关系
  • 变量和作用域
  • 正确处理失败命令
  • 路径规范化
  • Windows 支持(没有故意阻止,但也没有测试过。)
  • Ninja 日志
  • 生成构建文件
  • C 编译器包含解析(GCC/clang 的 -M,MSVC 的 /showIncludes)和依赖关系日志
  • 动态依赖关系
  • 更好的美化打印
  • 额外工具
  • 提供版本
  • 使 ninjars 可用为 cargo install 命令。

用法

请确保您至少安装了Rust 1.46-nightly版本(这是我唯一测试过的版本)。克隆这个仓库后,请执行以下操作。

cargo build --release -p ninjars --bin ninja

这将生成一个静态链接的二进制文件 ./target/release/ninja,您可以将它复制到您需要的地方。

贡献

欢迎贡献。请理解我查看pull请求的时间有限,因此可能无法及时回复。对于错误修复,只需提交一个PR,最好附上对该错误的回归测试。

如果您正在考虑实现一个主要功能,请提交一个issue,讨论该功能以及您打算如何实现它。可能存在多种解决方案和权衡。如果一个包含主要功能的PR没有事先讨论,很可能会被拒绝。

测试

在工作区目录中运行此命令将运行所有测试。请在提交PR时确保所有测试都通过。请为任何新引入的代码添加测试。带有测试的PR更容易审查和接受。

RUST_BACKTRACE=full cargo test

格式化

在提交代码之前,请运行 cargo fmt

设计

我在思考各种事情时编写了DESIGN.mdparse/DESIGN.md文档,并不打算让其他人理解。本节总结了实际的设计。

此设计基于以下目标

尽量遵循《Build Systems a la Carte》论文中的原语

大多数生产构建系统在论文之前就已经存在,并且它们大多数都没有严格的各个阶段之间的界限。我尽量在可能的情况下让原语发挥其作用。例如,重构器和调度器非常明确地使用图,使用键和任务进行讨论,并尽可能避免直接I/O(或考虑现实世界中的事物,如Unix时间)。当然,由于Rust没有Haskell那样的完整泛型和高阶类型支持,因此在Rust中编写过于通用的代码会很快变得繁琐。如果需要在任何地方放置trait界限或泛型,那么编写过多通用代码很快就会变得陈旧。首先有一个解析ninja文件并将其转换为构建描述的阶段。然后,将其转换为论文中描述的键和任务集。然后,有一个调度器(拓扑)和重构器(基于mtime)的实现,它实现了ninja语义。

在某种意义上,这几乎是一个ninja语言的编译器管道,但最终阶段是动作图的解释器而不是代码生成器。

尽量有可用的独立crates

实现是一个crates集合。解析器可以单独使用。词法分析器保留有关标记的相对完整信息(与ninja不同),这可能允许像ninja-fmt这样的功能。最初,我设想解析器会生成每个文件的AST,但ninja处理跨includesubninja规则的作用域的方式,使得这变得复杂。具体来说,包含文件中的变量会在当前环境中立即评估,因此无法并行化,并且一旦发生包含,每个文件的AST就变得没有意义。

主要的ninja crate将这些组件组合在一起。理论上,可以使用tasksbuild crates创建另一个构建系统。目前存在一些不干净的API边界,但它们适合清理。

启用优化性能

我最初非常专注于尝试避免分配和复制,但由于Rust对所有权有严格的要求,这很快就变得难以处理。因此,我目前采取了另一种极端做法,即大量使用clone()。词法分析器仍然是“零复制”,提供单个&[u8]的引用。规范化(Canonicalization)和字符串国际化可以处理路径,只要愿意传播更多生命周期,就可以从词法分析器直接到任务描述而不需要复制。当然,变量评估始终需要分配新的字节。

目前还不清楚这样做是否可以在保持可读性的同时达到ninja的性能水平。

可读性/文档化

这通常源于坚持原语,但在不清楚如何将“ninja如何做”转化为“如何用论文中的原语表达”时,我也指出了更多片段。一个特定的例子是重建器对伪规则和脏度的处理。同样,调度器执行了非常有意向的深度优先拓扑排序,而ninja则不同,其中也执行了类似操作,但图构建与解析和处理动态依赖耦合在一起。

可测试性

Ninja实际上没有规范。它有一个手册,广泛解释了行为,但原始实现是唯一可信的来源。这意味着许多规范行为只能通过运行特定的构建文件通过ninja来确定。我已经尽可能为代码库添加了测试。特别是,解析器有一系列验收测试,以确保与ninja的兼容性,同时也作为回归测试。这些验收测试只是.ninja文件,可以用ninja快速运行,以确定它们是否遵循“规范”。

依赖项

~8MB
~124K SLoC