Cargo 구축 스크립트를 사용하여 Rust 모듈 자동 생성

나는 방금 Cargo build scripts을 어떻게 사용하는지 배웠다.그들은 매우 쿨하다.

배경.


만약 당신이 상하문에 관심이 없다면, 여기는 build script part입니다.
나는 처음부터 나의 개인 사이트를 재건하고 있으며, 그곳에서 나의 개발 블로그 게시물을 다시 발표할 계획이다.웹 페이지에 HTML을 생성하기 위해 askama 라이브러리를 선택했습니다.이 도구는 약간 Jinja (또는 녹슨 세계의 tera ) 과 비슷하지만, 템플릿 형식 검사를 하고, 이를 응용 프로그램의 실행 가능한 파일로 직접 컴파일하는 현저한 차이가 있다.
예를 들어, 다음은 상위 skel.html 템플릿입니다.
<!DOCTYPE html>
<html dir="ltr" lang="en">

<head>
  <meta charset="utf-8" />
  <title>{% block title %}{% endblock %} - deciduously.com</title>
  <meta name="Description" content="Ben Lovy's personal website" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0" />
  <link rel="icon" type="image/x-icon" href="/favicon.svg" />
  <link rel="stylesheet" href="/main.css" />
  <link rel="manifest" href="/manifest.json" />
</head>

<body>
  <header>
    <nav>
      {% for link in links %}
      <a class="{% if link.target == "/" %}font-extrabold text-lg{% else %}italic{% endif %} px-10"
        href={{ link.target }}>{{ link.name }}</a>
      {% endfor %}
    </nav>
  </header>
  <main>
    {% block content %}{% endblock %}
  </main>
  <footer class="text-xs italic">
    © 2020 Ben Lovy - <a href="https://github.com/deciduously/deciduously-com" target="_blank"
      rel="noreferrer">source</a>
  </footer>
</body>

</html>
extends을 사용하여 하위 페이지를 만들고 사용자가 정의한 block을 채울 수 있습니다.
{% extends "skel.html" %}
{% block title %}404{% endblock %}
{% block content %}<h1>NOT FOUND!</h1>{% endblock %}
Rust의 경우 이 태그를 표시하려면 구조를 만들고 파일을 태그로 직접 전달해야 합니다.
#[derive(Template)]
#[template(path = "skel.html")]
pub struct SkelTemplate {
    links: &'static [Hyperlink],
}

impl Default for SkelTemplate {
    fn default() -> Self {
        Self { links: &NAV }
    }
}
템플릿에 {% for link in links %}이 표시되면 구조 필드에 Rust가 저장한 내용이 구체적으로 표시됩니다.표시된 태그를 추출하려면 구조를 인스턴스화하고 render()askama을 호출하면 자동으로 생성됩니다.
pub async fn four_oh_four() -> HandlerResult {
    let template = FourOhFourTemplate::default();
    let html = template.render().expect("Should render markup");
    string_handler(&html, "text/html", Some(StatusCode::NOT_FOUND)).await
}
데이터를 주입할 필요가 있으면 struct에 저장하고 구조 함수 (또는 다른 방법) 를 정의해서 데이터를 추가해야 합니다.그것의 작업 원리는 네가 기대한 다른 녹 자국과 같다.이 템플릿에 유입된 모든 데이터는 이 구조에서 정의되고 표시에 도달하기 전에 컴파일러가 검증합니다.
이것은 위대하다. 왜냐하면 모든 원인 때문에 녹 방지 유형 검사는 보통 위대하기 때문이다.템플릿이 바이너리 파일로 직접 변환되어 미리 컴파일되기 때문에, 실행할 때 파일 IO가 발생하지 않고, 순환과 조건 같은 모든 템플릿 작업은 실제 Rust 순환과 조건으로 전환됩니다.정말 잘 됐다.
라벨에서 신기한 일:
#[derive(Template)]
#[template(path = "skel.html")]
procedural macro입니다.당신의 코드가 컴파일되었을 때, 그것들은 다른 어떤 일이 발생하기 전에 전개될 것이다.이 경우 템플릿을 분석하고 컴파일을 시작하기 전에 생성된 Rust 코드를 모듈에 삽입합니다. 컴파일을 시작할 때 impl MyTemplate {} 블록이고, 그 중에는 render(&self) 방법이 포함되어 있으며, 이 방법을 호출할 수 있습니다.바로 이 매크로 확장 단계입니다. 컴파일 단계가 아니라 skel.html과 같은 실제 템플릿 파일이 파일 시스템에서 열립니다. <crate root>/templates에 있다고 가정하면 코드가 이 파일들을 다시 읽지 않을 것입니다.

문제.


나는 HTML 대신 가격을 내려 나의 글을 쓰고 싶다.이것은 내 태그를 HTML로 변환한 다음 제공해야 한다는 것을 의미한다.좋아, 괜찮아, 프로그래밍 언어를 켜고 있어. pulldown-cmark 의 세 줄 질문입니다.
let parser = pulldown_cmark::Parser::new("# THE BEST HEADING");
let mut html = String::new();
html::push_html(&mut html, parser);
println!("{}", html); // <h1>THE BEST HEADING</h1>
그러나 이 생성된 표지는 skel.html 샘플 파일을 계승하여 같은 사이트의 일부처럼 보여야 한다.간단합니다. 파일마다 새 템플릿을 만들 수 있습니다.
만약 이것이 나의 가격 인하라면juust를 약간 확대합니다.
---
title: "COOL POST"
---
# THE BEST HEADING

But _nothing_ compared to this intro!
이것은 나의 표시이다.
{% extends "skel.html" %}
{% block title %}COOL POST{% endblock %}
{% block content %}<h1>THE BEST HEADING</h1>
<p>But <em>nothing</em> compared to this intro!</p>{% endblock %}
이것은 문자열 조작 문제입니다. 다시 한 번 말씀드리지만, 우리는 프로그래밍 언어를 구동하고 있기 때문에 동의합니다.
fn write_template(title: &str, html: &str, file: &mut std::fs::File) -> Result<(), std::io::Error> {
    writeln!(file, "{{% extends \"skel.html\" %}}")?;
    writeln!(file, "{{% block title %}}{}{{% endblock %}}", title)?;
    writeln!(file, "{{% block content %}}{}{{% endblock %}}", html)?;
    Ok(())
}
너는 아마 이미 이곳의 장애를 알아맞혔을 것이다.이러한 Askama 템플릿을 가격 인하에서 벗어나 디스크에 쓰기 위해 코드를 실행해야 합니다.그러나, 우리가 이 과정을 실행할 기회가 생겼을 때, 우리의 모든 템플릿 매크로는 확장되었다.
작업을 하기 위해서는 宏 확장 단계 전에 어떤 방식으로 이 템플릿 파일과 상응하는 구조를 자동으로 생성해야 한다. 우리가 이미 설명한 바와 같이 이것은 다른 어떤 일 이전에 발생한 것이다.우르르

복구


내가 처음으로 이 문제를 해결했을 때, 나는...응, 나는 그것을 전혀 해결하지 못했어.반대로 나는 나의 실행 가능한 파일을 위해 별도의 내장 CLI 명령을 만들어서 이 문제를 처리했다. 그래서 나는 publish 모드와 serve 모드를 가지고 있다.생산 바이너리 파일을 구축하기 전에 publish을 호출해야 한다.그것은 효과가 있었지만, 나는 그것이 싫다.
또 다른 선택은 Askama를 버리고 앞에서 언급한 tera 만 사용해서 실행할 때 작업을 완성하는 것이다.이렇게 하면 빠르고 간단할 뿐만 아니라, 일을 충분히 완성할 수 있으니, 너는 아마 이렇게 해야 할 것이다.그러나 형식 검사와 바이너리 파일을 포함하는 데 실패했습니다.나도 고집이 세다.
다행히도 구축 스크립트가 있습니다!

스크립트 구성 섹션


build.rs 파일은 판자 상자의 루트 디렉터리에 src 이외에 놓을 수 있다.이것은 너의 상자의 일부분이 아니다.만약 존재한다면, cargo은 박스에 도착하기 전에 그것을 컴파일하고 실행할 것입니다.
문서 링크의 예는 외국 금융 기관에 적용됩니다.
// Example custom build script.
fn main() {
    // Tell Cargo that if the given file changes, to rerun this build script.
    println!("cargo:rerun-if-changed=src/hello.c");
    // Use the `cc` crate to build a C file and statically link it.
    cc::Build::new()
        .file("src/hello.c")
        .compile("hello");
}
이 스크립트는 hello.c이 변경되었는지 확인하고 컴파일러 박스를 컴파일하기 전에 필요에 따라 다시 생성합니다.
한 가지 골치 아픈 일은 당신이 cargo:stdout에 편지를 써서 스크립트 내부에서 println!("cargo:rerun-if-changed=src/hello.c");과 통신하는 것입니다.이 경로는 디렉터리에 귀속되지 않기 때문에, templates/의 모든 템플릿의 변화를 관찰하려면, 각각의 파일을 stdout으로 한 줄씩 써야 한다.
일반적인 이전 Rust 프로그램으로서 이것은 진정한 문제가 아니다. 우리는 디렉터리를 읽을 수 있고, 찾은 줄마다 println!() 문장을 생성할 수 있다.
#[derive(Debug, Default)]
pub struct Blog {
    pub posts: Vec<BlogPost>,
}

impl Blog {
    fn new() -> Self {
        let mut ret = Blog::default();
        // scrape posts
        let paths = std::fs::read_dir("blog").expect("Should locate blog directory");
        for path in paths {
            let path = path.expect("Could not open blog post").path();
            let post = BlogPost::new(ret.total(), path);
            ret.posts.push(post);
        }
        ret
    }
    fn total(&self) -> usize {
        self.posts.len()
    }
}

fn main() {
    let blog = Blog::new();
    println!("cargo:rerun-if-changed=blog");
    for p in &blog.posts {
        println!("cargo:rerun-if-changed=blog/{}.md", p.url_name);
    }
}
그럼 됐어.따라서 만약에 우리가 Rust를 사용할 수 있다면 우리는 std::fs::Filewriteln!()을 사용할 수 있다. 우리가 위에서 Askama 템플릿을 만들 때와 같다.왜 녹슬지 않았어요?
fn write_link_info_type(file: &mut std::fs::File) -> Result<(), std::io::Error> {
    writeln!(file, "#[derive(Debug, Clone, Copy)]")?;
    writeln!(file, "pub struct LinkInfo {{")?;
    writeln!(file, "    pub id: usize,")?;
    writeln!(file, "    pub url_name: &'static str,")?;
    writeln!(file, "    pub title: &'static str,")?;
    writeln!(file, "}}\n")?;
    Ok(())
}

fn generate_module() -> Result<(), std::io::Error> {
    let mut module = std::fs::File::create(&format!("src/{}.rs", "blog"))?;
    write_link_info_type(&mut module)?;
    Ok(())
}

fn main() {
    if let Err(e) = generate_module() {
        eprintln!("Error: {}", e);
    }
}
이 빌드 스크립트는 다음과 같이 src/blog.rs 상자에 파일을 팝업합니다.
#[derive(Debug, Clone, Copy)]
pub struct LinkInfo {
    pub id: usize,
    pub url_name: &'static str,
    pub title: &'static str,
}
녹슨 것 같아!main.rs 또는 lib.rs에 추가하기만 하면 됩니다.
mod blog;
펑, 새로운 모듈.하지만 상황은 호전될 겁니다.너는 Rust 표준 라이브러리를 사용할 수 있을 뿐만 아니라, cargo에서 찾을 수 있는 모든 것을 사용할 수 있다.다음과 같이 구축 단계에 Cargo.toml에 의존 항목을 추가할 수 있습니다.
[build-dependencies]
pest = "2.1"
pest_derive = "2.1"

[build-dependencies.pulldown-cmark]
default-features = false
version = "0.6"
여기에 정의된 내용은 build.rs에만 적용됩니다.이 두 부분에서 어떤 내용을 사용하려면 이 파일의 두 부분에 추가해야 합니다.여기에서 당신이 유일하게 사용할 수 없는 것은 바로 당신의 판자 상자입니다. 왜냐하면 정의에 따라 그것은 아직 건설되지 않았기 때문입니다.이외에 너는 또 갈 수 있다.
Markdown 헤드에 대해 더 세밀한 입도 제어를 해서 처리 프로그램과 템플릿 파이프를 녹슬게 하기로 결정했습니다. 그래서 저는 pest 을 사용하여 제 블로그 글 해석기를 통합시켜 헤드에서 기어오르도록 했습니다.
header = { header_guard ~ attribute{3,6} ~ header_guard }
    header_guard = _{ "-"{3} ~ NEWLINE }
    attribute = { key ~ ": " ~ value ~ NEWLINE }
        key = { (ASCII_ALPHANUMERIC | "_")+ }
        value = { (ASCII_ALPHANUMERIC | PUNCTUATION | " " | ":" | "/" | "+")* }

body = { ANY* }

draft = { SOI ~ header ~ body? ~ EOI }
즉, 빌드 스크립트에서 블로그 게시물의 구조를 분석하고 생성할 수 있습니다.
// Compiles drafts to templates and generates struct
#[derive(Parser)]
#[grammar = "draft.pest"]
struct Draft;

#[derive(Debug, Default, Clone)]
pub struct BlogPost {
    pub cover_image: Option<String>,
    pub description: Option<String>,
    pub edited: Option<String>, // only if published
    pub id: usize,
    pub published: bool,
    pub markdown: String,
    pub url_name: String,
    pub title: String,
}
여기서 Pest 파서를 사용하여 태그 파일을 처리할 수 있습니다.
impl BlogPost {
    fn new(id: usize, path: PathBuf) -> Self {
        // Init empty post
        let mut ret = Self::default();
        ret.id = id;
        ret.url_name = path.file_stem().unwrap().to_str().unwrap().to_string();

        // fill in struct from draft
        let md_file = fs::read_to_string(path.to_str().unwrap()).expect("Could not read draft");
        let parse_tree = Draft::parse(Rule::draft, &md_file)
            .expect("Could not parse draft")
            .next()
            .unwrap();
        // cycle through each attribute
        // unwrap is safe - if it parsed, there are between 3 and 6
        let mut parse_tree_inner = parse_tree.into_inner();

        // set header
        let header = parse_tree_inner.next().unwrap();
        let attributes = header.into_inner();
        for attr in attributes {
            let mut name: &str = "";
            let mut value: &str = "";
            for attr_part in attr.into_inner() {
                match attr_part.as_rule() {
                    Rule::key => name = attr_part.as_str(),
                    Rule::value => value = attr_part.as_str(),
                    _ => unreachable!(),
                }
            }
            match name {
                "cover_image" => ret.cover_image = Some(value.to_string()),
                "description" => ret.description = Some(value.to_string()),
                "edited" => ret.edited = Some(value.to_string()),
                "published" => {
                    ret.published = match value {
                        "true" => true,
                        _ => false,
                    }
                }
                "title" => ret.title = value.to_string(),
                _ => {}
            }
        }

        // set body
        let body = parse_tree_inner.next().unwrap();
        ret.markdown = body.as_str().to_string();

        // done
        ret
    }
}
구축 스크립트가 메모리에 올바른 조직의 메타데이터를 가진 모든 블로그 게시물을 가지고 있는 이상 필요한 템플릿을 작성하는 방법을 알려줄 수 있습니다.
    fn write_template(&self) -> Result<(), std::io::Error> {
        let mut file = std::fs::File::create(&format!("templates/post_{}.html", self.url_name))?;
        let parser = pulldown_cmark::Parser::new(&self.markdown);
        let mut html = String::new();
        html::push_html(&mut html, parser);
        writeln!(file, "{{#  This file was auto-generated by build.rs #}}")?;
        writeln!(file, "{{% extends \"skel.html\" %}}")?;
        writeln!(file, "{{% block title %}}{}{{% endblock %}}", self.title)?;
        writeln!(file, "{{% block content %}}{}{{% endblock %}}", html)?;
        Ok(())
    }
드라이버 코드는 삭제된 모든 게시물을 순환해서 이 방법을 호출하기만 하면 됩니다.그러나 Askama에서도 Rust 모듈을 생성할 수만 있다면 렌더링할 struct가 필요합니다.
    fn struct_name(&self) -> String {
        format!("Blog{}Template", self.id)
    }
    fn write_template_struct(&self, file: &mut std::fs::File) -> Result<(), std::io::Error> {
        writeln!(file, "#[derive(Template)]")?;
        writeln!(file, "#[template(path = \"post_{}.html\")]", self.url_name)?;
        writeln!(file, "pub struct {} {{", &self.struct_name())?;
        writeln!(file, "    links: &'static [Hyperlink],")?;
        writeln!(file, "}}")?;
        writeln!(file, "impl Default for {} {{", &self.struct_name())?;
        writeln!(file, "    fn default() -> Self {{")?;
        writeln!(file, "        Self {{ links: &NAV }}")?;
        writeln!(file, "    }}")?;
        writeln!(file, "}}\n")?;
        Ok(())
    }
그러면 src/blog.rs과 비슷한 사운드가 발생합니다.
#[derive(Template)]
#[template(path = "post_cool-post.html")]
pub struct Blog0Template {
    links: &'static [Hyperlink],
}
impl Default for Blog0Template {
    fn default() -> Self {
        Self { links: &NAV }
    }
}
동일한 writeln!() 정책을 사용하여 각 패브릭에 대해 하나의 매칭 암 세트를 포함하는 프로세서를 자동으로 생성합니다.
pub async fn blog_handler(path_str: &str) -> HandlerResult {
    match path_str {
        "/cool-post" => {
            string_handler(
                &Blog0Template::default()
                    .render()
                    .expect("Should render markup"),
                "text/html",
                None,
            )
            .await
        }
        // etc ...
        _ => four_oh_four().await,
    }
}
또한 일부 메타데이터를 스크래치하여 정보를 저장할 정적 값을 만들어 게시물 목록 페이지를 만듭니다.
lazy_static! {
    pub static ref LINKINFO: BlogLinkInfo = {
        let mut ret = BlogLinkInfo::default();
        ret.posts.push(LinkInfo {
            id: 0,
            title: "Cool Post",
            url_name: "cool-post",
        });
        // etc...
}
그것들을 함께 놓으면 녹이 슬어 보일 것이다. 이것이 전부라는 것을 너는 알고 있다. 여기에 일부 부분이 있다.
fn generate_handler(blog: &Blog, file: &mut std::fs::File) -> Result<(), std::io::Error> {
    writeln!(file, "pub async fn blog_handler(path_str: &str) -> HandlerResult {{")?;
    writeln!(file, "    match path_str {{")?;
    for p in &blog.posts {
        p.write_handler_match_arm(file)?;
    }
    writeln!(file, "        _ => four_oh_four().await,")?;
    writeln!(file, "    }}")?;
    writeln!(file, "}}")?;
    Ok(())
}

fn generate_module(blog: &Blog) -> Result<(), std::io::Error> {
    let mut module = fs::File::create(&format!("src/{}.rs", MODULE_NAME))?;

    write_imports(&mut module)?;

    write_link_info_type(&mut module)?;
    write_blog_link_info_type(&mut module)?;

    generate_blog_link_info(blog, &mut module)?;
    generate_template_structs(blog, &mut module)?;
    generate_posts(blog)?;
    generate_handler(blog, &mut module)?;

    Ok(())
}
현재 askama의 프로그램 매크로가 실제 박스를 컴파일하기 시작할 때 깨어났을 때 templates/의 모든 템플릿 파일과 프로젝트에서 *.md 파일을 사용하는 데 필요한 녹 코드가 생성되어 박스의 나머지 부분에서 호출할 수 있습니다:
// src/blog.rs
// this module was auto-generated by build.rs
use crate::{
    config::NAV,
    handlers::{four_oh_four, string_handler, HandlerResult},
    types::Hyperlink,
};
use askama::Template;
use lazy_static::lazy_static;

#[derive(Debug, Clone, Copy)]
pub struct LinkInfo {
    pub id: usize,
    pub url_name: &'static str,
    pub title: &'static str,
}

#[derive(Debug, Default)]
pub struct BlogLinkInfo {
    pub posts: Vec<LinkInfo>,
}

lazy_static! {
    pub static ref LINKINFO: BlogLinkInfo = {
        let mut ret = BlogLinkInfo::default();
        ret.posts.push(LinkInfo {
            id: 0,
            title: "Cool Post",
            url_name: "cool-post",
        });
        ret.posts.push(LinkInfo {
            id: 1,
            title: "Kind Of Alright Post",
            url_name: "honestly-meh",
        });
        ret
    };
}

#[derive(Template)]
#[template(path = "post_cool-post.html")]
pub struct Blog0Template {
    links: &'static [Hyperlink],
}
impl Default for Blog0Template {
    fn default() -> Self {
        Self { links: &NAV }
    }
}

#[derive(Template)]
#[template(path = "post_honestly-meh.html")]
pub struct Blog1Template {
    links: &'static [Hyperlink],
}
impl Default for Blog1Template {
    fn default() -> Self {
        Self { links: &NAV }
    }
}

pub async fn blog_handler(path_str: &str) -> HandlerResult {
    match path_str {
        "/cool-post" => {
            string_handler(
                &Blog0Template::default()
                    .render()
                    .expect("Should render markup"),
                "text/html",
                None,
            )
            .await
        }
        "/honestly-meh" => {
            string_handler(
                &Blog1Template::default()
                    .render()
                    .expect("Should render markup"),
                "text/html",
                None,
            )
            .await
        }
        _ => four_oh_four().await,
    }
}
이 디렉터리의 파일을 변경할 때마다 구축 스크립트는 이 파일을 일치하도록 다시 설정하기 때문에 태그 파일만 걱정하면 블로그를 관리할 수 있습니다.
...너도 알다시피 정적 사이트 같은 거.미친 놈.
스크립트를 구축하는 것은 매우 강력합니다. 스크립트를 사용해서 무엇을 합니까?
스콧 블레이크의 Unsplash 사진

좋은 웹페이지 즐겨찾기