Build.rs-ing 문서화 Cuelang 사용

발행: (2026년 1월 14일 오후 06:06 GMT+9)
5 min read
원문: Dev.to

Source: Dev.to

위에 제공된 텍스트 외에 번역할 내용이 없습니다. 번역하고 싶은 전체 문서를 알려주시면 한국어로 번역해 드리겠습니다.

동기

저는 tmplr이라는 작은 유틸리티를 가지고 있는데, 이 유틸리티는 사람이 읽을 수 있는 템플릿으로부터 파일이나 파일 집합을 생성하는 것이 목적입니다 (한번 확인해 보세요, 멋집니다!).
버전 0.0.9에서는 여러 곳에서 버전 정보를 계속 올렸습니다:

  • HELP-h / --help 로 표시됨
  • README.md (도움말 텍스트를 복제함)
  • Cargo.toml
  • 로컬 git 태그 (git tag v0.0.9)

이렇게 하면 동작은 하지만 불필요한 오버헤드가 발생합니다. 저는 CUE의 열렬한 팬이며 이미 대부분의 설정/데이터를 CUE로 생성하고 있기 때문에, 버전 주입 및 기타 문서 생성 작업을 CUE가 담당하도록 결정했습니다.

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() {
    // 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");
}

생성된 코드 삽입

생성된 파일(generated_docs.rs)은 src/main.rs에 포함되는 Rust 소스 조각입니다:

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.cue, changelog.cue, 그리고 readme.cue는 실제 데이터와 변환을 포함합니다.

CUE의 README 템플릿

readme.cue 파일은 최종 README.md를 조합하는 full 문자열을 정의합니다.
간단히 추출한 예시는 다음과 같습니다:

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

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)

## Templates directory
\(sections.templates_directory)

# TODO
\(sections.todo)
"""

CLI Help 섹션은 help.text에서 직접 가져와서, README가 수동 업데이트 없이 현재 명령줄 출력과 항상 일치하도록 보장합니다.

결과

cargo build(또는 빌드 스크립트를 트리거하는 모든 명령)를 실행하면 생성된 README.mdCHANGELOG.md가 최신 상태가 되어 커밋 준비가 됩니다. 최종 리포지토리에는 렌더링된 파일과 검토를 위한 전체 .cue 소스가 포함됩니다.

전체 확장 기능을 개발하는 데 약 1.5 시간이 걸렸으며, 대부분은 기존 Markdown 내용을 CUE 파일로 포팅하는 데 사용되었습니다.

Back to Blog

관련 글

더 보기 »

6년 이상 C# 사용 후 Rust로 전환 계획

저는 C로 6~7년 동안 작업해 왔지만, 시간이 지나면서 점점 부피가 크고 제한적인 느낌이 듭니다. 사물의 동작 방식을 정확하고 low‑level로 제어하던 것이 그리워집니다.