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

nightly ninja-parse

Ninja 构建文件的分析器和解析器

1 个不稳定版本

0.1.0 2020 年 8 月 29 日

#754 in 开发工具


2 crates 中使用

Apache-2.0

96KB
2K SLoC

ninja-rs

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

动机

ninja-rs 的创建有多个原因

  1. 教育 - Build Systems a la Carte 是对我影响巨大的论文,因为它正式和精炼了构建系统。这是尝试使用该论文中的想法实现一个真实构建系统的一次尝试。关于设计的更多内容,请参阅下面的设计说明。Ninja 的原始作者 Evan Martin 也 希望 他们有这篇论文。

  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,它是静态链接的,可以复制到您需要的任何地方。

贡献

欢迎贡献。请理解,我查看拉取请求的时间有限,所以我可能无法及时回复。对于错误修复,只需提交一个PR,最好包括对该错误的回归测试。

如果您正在考虑实现一个主要功能,请提交一个问题讨论该功能以及您打算如何实现它。可能有多种解决方案和权衡。如果不在讨论之前提交,可能行的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中的过度泛型代码可能会很快变得陈旧。在Rust中,如果需要在每个地方放置特质界限或泛型,则编写过多泛型代码可能会很快变得陈旧。首先,解析ninja文件并将这些转换为构建描述。然后,将其转换为论文中提到的键和任务集合。然后,有一个调度器(拓扑)和重建器(基于mtime)的实现,它实现了ninja语义。

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

尽量拥有可用的独立crate

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

主要的 ninja 包仅仅是将所有这些部件组合在一起。理论上,可以使用 tasksbuild 包来创建另一个构建系统。目前存在许多不干净的 API 边界,但这些都可以被清理。

启用性能优化

我开始专注于尽量减少分配和复制,但由于 Rust 强大的所有权要求,这很快变得难以处理。因此,我现在走向了另一个极端,大量使用 clone()。词法分析器仍然是“零复制”,分配给单个 &[u8]。规范化字符串和字符串内部化可以处理路径,只要愿意传播更多的生命周期,就可以从词法分析器直接到任务描述而无需复制。当然,变量评估始终需要分配新的字节。

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

可读/有文档

这通常是通过坚持原始操作实现的,但我也在不太明显的地方指出了部分内容,当不清楚如何将“ninja 是如何做的”转化为“如何用论文中的原始操作表达”时。一个特定的例子是重建器对伪规则和脏度的处理。同样,调度器执行了一个非常有意向的深度优先拓扑排序,与 ninja 不同,ninja 中的类似操作与解析和处理动态依赖结合在一起。

可测试

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

依赖项

~300–760KB
~18K SLoC