使用 build.rs 生成文档并结合 Cuelang

发布: (2026年1月14日 GMT+8 17:06)
4 min read
原文: Dev.to

I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (excluding the source line you’ve already provided) here? Once I have the article text, I’ll translate it into Simplified Chinese while preserving the formatting, markdown, and any code blocks.

动机

我有一个小工具叫 tmplr,其目的是从人类可读的模板生成文件或文件集(去看看吧,挺不错的!)。
在 0.0.9 版时,我一直在多个地方手动更新版本信息:

  • 使用 -h / --help 显示的 HELP
  • README.md(其中重复了帮助文本)
  • Cargo.toml
  • 本地 git 标签(git tag v0.0.9

虽然这样可以工作,但会增加不必要的负担。由于我非常喜欢 CUE,并且已经用它生成了大部分配置/数据,我决定让 CUE 负责版本注入以及其他文档生成任务。

Source:

使用 build.rs 脚本

Rust 允许在编译之前运行一个预构建脚本(完整的 Rust 程序)。我利用它来:

  • 将帮助文本注入 src/main.rs
  • 生成完整的 README.md
  • 基于更智能的条目生成 CHANGELOG.md
  • 在其他位置复用文档的部分内容

下面是项目中使用的完整 build.rs

use std::env;
use std::fs;
use std::path::Path;
use std::process::Command;

fn main() {
    // 将 "HELP" 注入 src/main.rs
    prepare_documentation_code();

    generate_doc("README.md", "readme.full");
    generate_doc("CHANGELOG.md", "changelog.text");

    // 当 `cue/` 目录下的任何文件变化时重新运行脚本
    println!("cargo:rerun-if-changed=cue");
}

fn generate_doc(file_in_manifest: &str, eval_command: &str) {
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let out_path = Path::new(&manifest_dir).join(file_in_manifest);
    let output = Command::new("cue")
        .args([
            "export",
            "-e",
            eval_command,
            "--out",
            "text",
            "./cue:documentation",
        ])
        .output()
        .expect("Failed to execute cue command");

    if !output.status.success() {
        panic!(
            "Cue {} generation failed:\n{}",
            file_in_manifest,
            String::from_utf8_lossy(&output.stderr)
        );
    }
    fs::write(&out_path, &output.stdout).expect("Failed to write generated file");
}

fn prepare_documentation_code() {
    let out_dir = env::var_os("OUT_DIR").unwrap();
    let dest_path = Path::new(&out_dir).join("generated_docs.rs");

    let output = Command::new("cue")
        .args([
            "export",
            "-e",
            "help.code",
            "--out",
            "text",
            "./cue:documentation",
        ])
        .output()
        .expect("Failed to execute cue command");

    if !output.status.success() {
        panic!(
            "Cue generation failed:\n{}",
            String::from_utf8_lossy(&output.stderr)
        );
    }
    fs::write(&dest_path, &output.stdout).expect("Failed to write generated file");
}

注入生成的代码

生成的文件 (generated_docs.rs) 是一个 Rust 源代码片段,会在 src/main.rs 中被包含:

include!(concat!(env!("OUT_DIR"), "/generated_docs.rs"));

因为 generated_docs.rs 是构建产物,它 不会 被提交到版本控制。
README.mdCHANGELOG.md 则会被提交;它们直接在项目根目录(CARGO_MANIFEST_DIR)生成。

项目结构

cue
├── changelog.cue
├── documentation.cue   ← root package file
├── help.cue
├── LICENSE.cue
└── readme.cue
  • documentation.cue 是根包,重新导出其他文件。
  • help.cuechangelog.cuereadme.cue 包含实际的数据和转换。

CUE 中的 README 模板

readme.cue 文件定义了一个 full 字符串,用于组装最终的 README.md
下面是一个简化的摘录:

full: """
# tmplr
\(sections.tmplr)

Source:

为什么 tmplr?

(sections.why_tmplr)

快速开始

(sections.quick_start)

CLI 帮助

\(help.text)

安装

(sections.installation)

使用(扩展)

.tmplr 文件

(sections.tmplr_files)

章节类型

(sections.section_types)

魔法变量

(sections.magic_variables)

CLI

(sections.cli)

模板目录

(sections.templates_directory)

待办

(sections.todo) """ CLI Help 部分直接从 help.text 拉取,确保 README 始终反映当前的命令行输出,无需手动更新。

Result

在运行 cargo build(或任何触发构建脚本的命令)后,生成的 README.mdCHANGELOG.md 已经是最新的,准备提交。最终的仓库包含渲染后的文件以及完整的 .cue 源文件供检查。

整个扩展的开发耗时约 1.5 小时,其中大部分时间用于将现有的 Markdown 内容迁移到 CUE 文件中。

Back to Blog

相关文章

阅读更多 »