dbt 项目中用 MiniJinja 构建脚本(Build Script)生成 Rust 代码的实战指南
发布时间:2026/9/15 0:07:47 作者:尧图编辑部 阅读量:1,286
生成 Rust 代码的实战指南)
dbt 项目中用 MiniJinja 构建脚本Build Script生成 Rust 代码的实战指南【免费下载链接】dbtdbt enables data analysts and engineers to transform their data using the same practices that software engineers use to build applications.项目地址: https://gitcode.com/GitHub_Trending/db/dbt导读本文以 dbt 仓库中crates/dbt-jinja/examples/build-script示例为骨架系统讲解如何把 MiniJinja 模板引擎用于 Rust 构建脚本build script在编译期渲染模板、生成.rs源码文件再由main.rs通过include!内联编译。你将掌握自定义 Formatter、|safe过滤器、OUT_DIR输出机制等关键技能并理解这套模板即代码生成器模式在 dbt 工程中的落地方式。一、示例概览一个完整的构建期代码生成闭环crates/dbt-jinja/examples/build-script/README.md开篇即点明这个示例的核心目的演示如何将 MiniJinja 用于构建脚本。它通过一个自定义 Formatter自动把模板中的所有 Rust 值以Debug格式输出模板本身无需关心格式化细节随后再借助|safe过滤器让值以合法的 Rust 表达式形式嵌入生成的代码。整个示例由四个文件构成一个完整闭环文件职责build.rs构建脚本配置 MiniJinja 环境、渲染模板、写入OUT_DIRsrc/example.rs.jinja模板生成 Rust 源码的骨架src/main.rs主程序include!引入生成的文件并使用其中的常量Cargo.toml声明minijinja为构建依赖build-dependency运行方式很简单在示例目录下执行$ cargo run二、Cargo 配置把 MiniJinja 变成构建依赖构建脚本要能使用模板引擎首先需要在 Cargo.toml 中把minijinja声明为build-dependencies而非普通 dependencies并显式启用两个 feature[package] name build-script version 0.1.0 edition 2021 publish false [build-dependencies] minijinja { path ../../minijinja, default-features false, features [ serde, builtins, ] }这里的要点path ../../minijinja表示直接引用 dbt 仓库内嵌的 MiniJinja 源码即crates/dbt-jinja/minijinja属于工作区内联依赖default-features false关闭默认特性仅按需启用serde提供值序列化/反序列化能力与builtins内置过滤器、测试与函数|safe过滤器即来自这里publish false表明这是仓库内部的示例工程不会发布到 crates.io。Cargo 会先编译 build-dependencies再执行build.rs最后才编译主 crate因此构建脚本阶段使用 MiniJinja 不会污染最终产物的运行时依赖。三、build.rs自定义 Formatter 是灵魂build.rs 是整套模式的发动机全文如下use std::path::Path; use std::{env, fs}; use minijinja::{render, Environment}; fn main() { // This environment has a formatter that formats unsafe values in Rusts // debug format, and safe values as normal strings. let mut env Environment::new(); env.set_formatter(|out, _state, value| { if !value.is_safe() { write!(out, {value:?})?; } else { write!(out, {value})?; } Ok(()) }); // render the template and write it into the file that main.rs includes. fs::write( Path::new(env::var(OUT_DIR).unwrap()).join(example.rs), render!( in env, include_str!(src/example.rs.jinja), struct_name Point, points vec![ (1.0, 2.0), (2.0, 2.5), (4.0, 1.0), ], build_cwd env::current_dir().unwrap() ), ) .unwrap(); }3.1 自定义 Formatter 做了什么MiniJinja 默认的 Formatter 会依据值的类型做格式化。而这个示例通过Environment::set_formatter替换了默认行为其核心逻辑是非安全值unsafe用 Rust 的Debug格式化{value:?}输出——例如1.0会输出为1.0字符串/build/...会输出为带引号与转义的合法 Rust 字符串字面量安全值safe按普通字符串原样输出。从源码看set_formatter的签名要求一个闭包Fn(mut Output, State, Value) - Result(), Error它被存入环境的ArcF在每次值输出时被调用。这正是模板不需要关心 Rust 转义的关键——格式化职责被整体上移到了构建脚本里。3.2is_safe()与|safe过滤器的底层机制Formatter 里调用的value.is_safe()并非黑魔法。在 value/mod.rs 中可以看到它的实现/// Returns true if this value is safe. pub fn is_safe(self) - bool { matches!(self.0, ValueRepr::String(_, StringType::Safe)) }也就是说只有被标记为StringType::Safe的字符串才被认为是安全值。而把普通字符串标记为 Safe 的入口正是模板中使用的|safe过滤器。于是二者形成默契的分工模板中{{ build_cwd }}不安全→ Formatter 走{value:?}分支 → 自动得到带引号的合法 Rust 字符串字面量模板中{{ struct_name|safe }}安全→ Formatter 走普通输出分支 → 原样输出Point这个合法的 Rust 标识符。这样一来哪些内容需要被当作代码原样输出如类型名、哪些内容需要被当作数据转义如路径字符串这个语义被清晰地编码在了模板的过滤器选择里。3.3render!宏与模板装载渲染入口使用了 MiniJinja 的render!宏render!( in env, include_str!(src/example.rs.jinja), struct_name Point, points vec![(1.0, 2.0), (2.0, 2.5), (4.0, 1.0)], build_cwd env::current_dir().unwrap() )in env指定使用上面配置好自定义 Formatter 的环境模板文本通过include_str!在编译期直接嵌入到构建脚本二进制中无需运行时读取文件、也不依赖模板文件是否部署到目标机器后面的key value语法会被宏展开为context! { ... }再调用env.render_str(...)因此这里传入的是任意实现了相应序列化约定的 Rust 值——str、Vec(f32, f32)、PathBuf均可直接传入这正是serdefeature 的价值所在。3.4 输出到 OUT_DIR渲染结果最终被写入OUT_DIRfs::write( Path::new(env::var(OUT_DIR).unwrap()).join(example.rs), ... )OUT_DIR是 Cargo 为每个 crate 构建提供的唯一输出目录main.rs可以稳定地通过env!(OUT_DIR)引用它。构建脚本在编译期把模板渲染结果落地为.rs文件主程序在编译期把这个文件include!进来两段代码在编译期完成了交接。四、模板源码把 Rust 代码写进 Jinja 骨架src/example.rs.jinja 是生成 Rust 代码的模板与 README 中展示的模板内容完全一致// This file is auto generated from a MiniJinja template struct {{ struct_name|safe }} { pub x: f32, pub y: f32, } const BUILD_CWD: str {{ build_cwd }}; const POINTS: [{{ struct_name|safe }}; {{ points|length }}] [ {% for x, y in points %} {{ struct_name|safe }} { x: {{ x }}, y: {{ y }} }, {% endfor %} ];逐段解读其中的模板语法{{ struct_name|safe }}输出结构体名Point|safe告诉 MiniJinja 这是可信代码片段原样输出不转义、不被 Formatter 的 Debug 分支处理{{ build_cwd }}不加|safe因此走自定义 Formatter 的 Debug 分支自动转义为合法的 Rust 字符串字面量{{ points|length }}使用length过滤器输出数组长度用来声明const POINTS: [Point; N]的数组大小{% for x, y in points %}Jinja 的元组解构循环逐个展开Point { x: ..., y: ... }字面量。注意points的元素是(f32, f32)元组模板里直接用x, y解构体现 MiniJinja 对 Rust 元组的原生支持。这里的模板化设计思路值得借鉴把数据与代码形状分离——数据结构坐标点变化时只需改构建脚本传入的数据模板与生成代码的骨架保持稳定。五、main.rs消费生成代码src/main.rs 展示了主程序如何消费生成的文件// include the generated file include!(concat!(env!(OUT_DIR), /example.rs)); fn main() { println!(build cwd: {BUILD_CWD}); for point in POINTS { println!(({}, {}), point.x, point.y); } }include!(concat!(env!(OUT_DIR), /example.rs))在编译期把build.rs生成的文件展开进当前 crate生成文件中的struct Point、const BUILD_CWD: str、const POINTS: [Point; 3]全部对main.rs可见直接以普通 Rust 标识符使用。六、运行结果模板渲染后的产物README 给出了 build.rs 传入上述数据后渲染出的完整输出对应于运行cargo run前生成文件的实际内容struct Point { pub x: f32, pub y: f32, } const BUILD_CWD: str /build/minijinja/examples/build-script; const POINTS: [Point; 3] [ Point { x: 1.0, y: 2.0 }, Point { x: 2.0, y: 2.5 }, Point { x: 4.0, y: 1.0 }, ];与模板对比可以直观看到 Formatter 与|safe的分工成果BUILD_CWD对应的/build/minijinja/examples/build-script被自动加上了双引号并转义为合法字符串字面量Formatter 的 Debug 分支Point作为安全值原样输出|safe分支[Point; 3]中的3由{{ points|length }}计算得到三个Point字面量由{% for %}循环生成。运行cargo run后程序会打印build cwd: /build/minijinja/examples/build-script以及三个坐标点证明生成的常量确实参与了程序逻辑。七、从示例到实战构建期代码生成的可复用模式这个 40 行左右的示例实际上概括了一套在 Rust 工程中广泛适用的构建期代码生成模式依赖声明在[build-dependencies]中引入 MiniJinja含serde、builtinsfeatures模板引擎只存在于构建期环境配置在build.rs中创建Environment按需用set_formatter定制值输出规则——这是解决Rust 代码转义问题的通用手段模板与数据分离模板用include_str!内嵌数据以render!宏的key value形式传入数据变化不触碰模板编译期交接渲染结果写入OUT_DIR/example.rs主程序用include!(concat!(env!(OUT_DIR), /example.rs))引入安全值语义用|safe标记本就是要输出成代码的片段其余值交给 Formatter 自动转义杜绝手写转义导致的低级错误。八、延伸阅读示例 READMEcrates/dbt-jinja/examples/build-script/README.md构建脚本实现build.rs生成代码模板src/example.rs.jinja主程序消费方式src/main.rsMiniJinja 环境与 Formatter APIenvironment.rsrender!/context!宏定义macros.rs安全值Safe String判定实现value/mod.rsMiniJinja 整体文档与更多示例crates/dbt-jinja/minijinja/README.md如果希望深入了解 MiniJinja 在普通应用而非构建脚本中的渲染、继承与测试能力dbt 仓库中的 examples 目录还提供了大量可直接运行的示例可供对照学习。【免费下载链接】dbtdbt enables data analysts and engineers to transform their data using the same practices that software engineers use to build applications.项目地址: https://gitcode.com/GitHub_Trending/db/dbt创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考