问题描述

近日,笔者在使用 Buck2 构建 Libra 项目时遇到了以下问题:

编译器报告了 E0463 错误,提示编译器链接时找不到代码里声明的 libra 包,一般导致此问题的原因有两种:一是 crate 不存在(例如忘记添加依赖),二是“货不对版”(例如依赖名称不匹配或者元数据有误)。

排查过程

根据编译器报错,笔者初步怀疑 Buckal 工具生成的 BUCK 文件有误,可能没有正确处理 crate 自引用,但是经过反复比对 Buck2 和 Cargo 发射的 rustc 调用参数,最终排除了 Buckal 工具的问题。

为了进一步探明原因,笔者编写了以下代码来测试中间构建产物的正确性:

extern crate libra;

use libra::cli;

fn main() {}

在不使用构建系统的情况下,rustc 通过 --extern name[=path]-L dependency=... 参数指定当前 crate 所引用的依赖,其中前者主要用于声明直接依赖,后者则负责描述传递依赖的查找范围。

我们可以通过手动构造参数的方式使用以下命令进行测试,其中 liblibra-606a0e18.rlib 为 libra 库的中间构建产物,t.rs 为上述测试代码:

rustc --extern libra=buck-out/v2/gen/root/2c621926a02f7469/__liblibra__/LPPL/liblibra-606a0e18.rlib -Ldependency=buck-out/v2/gen/root/2c621926a02f7469/__libra__/XIPL-depslink-0 t.rs

正常情况下编译器应该发出 unused import 警告:

但是这里编译器直接报告了 E0460 错误,可以确定该问题由 turso_core 依赖导致:

然而,该问题在使用 Cargo 构建时无法复现,因此笔者推测其根本原因在于 Buck2 Prelude 对 Rust 编译所采用的底层策略。Buck2 可能会对同一个 target 使用不同的参数和策略进行多次编译,具体的参数组合如下表所示:

对于一般的 Rust 库,Buck2 默认会构建两次,分别编译 metadata-full 和 link 版本。其中,前者不实际生成机器代码,仅包含元数据,主要供其他依赖它的 crate 进行快速构建、依赖解析、类型检查等;后者则包含完整内容,在最后链接时使用。这种策略可以缩短单个 action 编译时间,同时提高整个构建任务的并行化程度。

正常情况下,rustc 通过 --emit link,metadata--emit link 参数产生的 rlib 文件当中的元数据应该保持一致。以 toml 库为例,解包后的 lib.rmeta 文件哈希值完全相同:

然而 turso_core 通过 built 动态注入构建时信息(时间戳),导致每次构建产物的哈希值都不一致,具体如下:

因此,turso_core 在构建时产生的元数据和链接时使用的并不一致,rustc 会拒绝链接。同时,因为这是一个传递依赖,编译器并不会给出明确的报错。

解决方案

为快速解决以上问题,我们选择修改 turso 源代码使之产生确定的构建产物。具体地,turso_core 通过 built 动态注入的构建时信息储存在 info::build 模块中:

// core/info.rs
pub mod build {
    include!(concat!(env!("OUT_DIR"), "/built.rs"));
}

在业务层面,主要是 turso_core 虚拟数据库引擎 (VDBE) 的 ScalarFunc::SqliteSourceId 函数用到了构建时间戳:

// core/vdbe/execute.rs:5326
ScalarFunc::SqliteSourceId => {
    let src_id = format!(
        "{} {}",
        info::build::BUILT_TIME_SQLITE,
        info::build::GIT_COMMIT_HASH.unwrap_or("unknown")
    );
    state.registers[*dest] = Register::Value(Value::build_text(src_id));
}

总结

本文所述问题具有一定的共性:任何构建产物中包含动态变化内容的 crate 都可能触发类似现象。其根本解决方案在于修改 Buck2 构建策略,在 Rust 构建规则中引入参数配置,支持为特定 target 启用“仅执行一次构建”的行为,从而避免因 rlib 元数据变动而导致构建失败。

然而,Buck2 Prelude 中涉及编译策略与参数控制的部分较为复杂,相关修改需投入大量时间和精力。为不影响 Libra 项目的当前进度,我们暂时采用了对依赖源码打补丁的临时方案。未来仍计划通过定制 Prelude 实现彻底、可持续的解决。