Build.rs-ing Documentation with Cuelang
Source: Dev.to
Motivation
I have a small utility called tmplr whose purpose is to generate files or file sets from human‑readable templates (check it out, it’s neat!).
At version 0.0.9 I kept bumping version information in several places:
HELPshown with-h/--helpREADME.md(which duplicated the help text)Cargo.toml- Local git tag (
git tag v0.0.9)
Although this works, it adds unnecessary overhead. Since I’m a huge fan of CUE and already generate most of my configuration/data with it, I decided to let CUE handle the version injection and other documentation generation tasks.
Using a build.rs script
Rust allows a pre‑build script (a full Rust program) that runs before compilation. I leveraged this to:
- Inject the help text into
src/main.rs - Generate a full
README.md - Generate a
CHANGELOG.mdbased on smarter entries - Re‑use parts of the documentation in other places
Below is the complete build.rs used for the project.
use std::env;
use std::fs;
use std::path::Path;
use std::process::Command;
fn main() {
// Injects "HELP" into src/main.rs
prepare_documentation_code();
generate_doc("README.md", "readme.full");
generate_doc("CHANGELOG.md", "changelog.text");
// Re‑run the script when any file under `cue/` changes
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");
}
Injecting the generated code
The generated file (generated_docs.rs) is a Rust source fragment that gets included in src/main.rs:
include!(concat!(env!("OUT_DIR"), "/generated_docs.rs"));
Because generated_docs.rs is a build artifact, it is not checked into version control.
README.md and CHANGELOG.md, however, are checked in; they are generated directly in the project root (CARGO_MANIFEST_DIR).
Project layout
cue
├── changelog.cue
├── documentation.cue ← root package file
├── help.cue
├── LICENSE.cue
└── readme.cue
documentation.cueis the root package that re‑exports the other files.help.cue,changelog.cue, andreadme.cuecontain the actual data and transformations.
README template in CUE
The readme.cue file defines a full string that assembles the final README.md.
A simplified excerpt looks like this:
full: """
# tmplr
\(sections.tmplr)
## Why tmplr?
\(sections.why_tmplr)
# Quick Start
\(sections.quick_start)
# CLI Help
````markdown
\(help.text)
Installation
(sections.installation)
Usage (extended)
.tmplr files
(sections.tmplr_files)
Section Types
(sections.section_types)
Magic Variables
(sections.magic_variables)
CLI
(sections.cli)
Templates directory
(sections.templates_directory)
TODO
(sections.todo) """
The `CLI Help` section pulls directly from `help.text`, ensuring the README always reflects the current command‑line output without manual updates.
## Result
After running `cargo build` (or any command that triggers the build script), the generated `README.md` and `CHANGELOG.md` are up‑to‑date and ready to be committed. The final repository contains the rendered files and the full `.cue` sources for inspection.
The entire extension took about **1.5 hours** to develop, most of which was spent porting existing Markdown content into CUE files.