Skip to main content

redskull_lib/
renderer.rs

1//! Rendering recipes to output formats.
2
3use crate::recipe::*;
4use std::fmt::Write;
5
6/// Trait for rendering a Recipe to a string format.
7pub trait Renderer {
8    fn render(&self, recipe: &Recipe) -> String;
9}
10
11/// Renders a Recipe as conda-build meta.yaml (v0 format).
12pub struct MetaYamlRenderer;
13
14impl Renderer for MetaYamlRenderer {
15    fn render(&self, recipe: &Recipe) -> String {
16        let mut out = String::new();
17        self.render_preamble(&mut out, &recipe.preamble);
18        self.render_package(&mut out);
19        self.render_source(&mut out, &recipe.source);
20        self.render_build(&mut out, &recipe.build);
21        self.render_requirements(&mut out, &recipe.requirements);
22        self.render_test(&mut out, &recipe.test);
23        self.render_about(&mut out, &recipe.about);
24        self.render_extra(&mut out, &recipe.extra);
25        // Ensure the output ends with exactly one trailing newline.
26        let trimmed = out.trim_end_matches('\n');
27        format!("{trimmed}\n")
28    }
29}
30
31impl MetaYamlRenderer {
32    fn render_preamble(&self, out: &mut String, preamble: &Preamble) {
33        writeln!(out, "{{% set name = \"{}\" %}}", preamble.name).unwrap();
34        writeln!(out, "{{% set version = \"{}\" %}}", preamble.version).unwrap();
35        writeln!(out).unwrap();
36    }
37
38    fn render_package(&self, out: &mut String) {
39        writeln!(out, "package:").unwrap();
40        writeln!(out, "  name: {{{{ name }}}}").unwrap();
41        writeln!(out, "  version: {{{{ version }}}}").unwrap();
42        writeln!(out).unwrap();
43    }
44
45    fn render_source(&self, out: &mut String, source: &Source) {
46        writeln!(out, "source:").unwrap();
47        writeln!(out, "  url: {}", source.url).unwrap();
48        // Emit `fn:` so conda-build can recognize the format and auto-strip
49        // the single top-level directory inside the tarball. Without it,
50        // crates.io URLs (which end in `/download`, no extension) produce a
51        // build where `cargo` can't find Cargo.toml in the work dir.
52        if !source.filename.is_empty() {
53            writeln!(out, "  fn: {}", source.filename).unwrap();
54        }
55        writeln!(out, "  sha256: {}", source.sha256).unwrap();
56        writeln!(out).unwrap();
57    }
58
59    fn render_build(&self, out: &mut String, build: &Build) {
60        writeln!(out, "build:").unwrap();
61        writeln!(out, "  number: 0").unwrap();
62        if let Some(script) = &build.script {
63            if script.contains('\n') {
64                writeln!(out, "  script: |").unwrap();
65                for line in script.lines() {
66                    writeln!(out, "    {line}").unwrap();
67                }
68            } else {
69                writeln!(out, "  script: {script}").unwrap();
70            }
71        }
72        if build.with_run_exports {
73            writeln!(out, "  run_exports:").unwrap();
74            writeln!(
75                out,
76                "    - {{{{ pin_subpackage(\"{}\", max_pin=\"{}\") }}}}",
77                build.name, build.max_pin
78            )
79            .unwrap();
80        }
81        writeln!(out).unwrap();
82    }
83
84    fn render_requirement(&self, out: &mut String, req: &Requirement) {
85        match (&req.version, &req.selector) {
86            (None, None) => writeln!(out, "    - {}", req.name).unwrap(),
87            (Some(ver), None) => writeln!(out, "    - {} {}", req.name, ver).unwrap(),
88            (None, Some(sel)) => writeln!(out, "    - {}  # [{sel}]", req.name).unwrap(),
89            (Some(ver), Some(sel)) => {
90                writeln!(out, "    - {} {}  # [{sel}]", req.name, ver).unwrap()
91            }
92        }
93    }
94
95    fn render_requirements(&self, out: &mut String, requirements: &Requirements) {
96        writeln!(out, "requirements:").unwrap();
97        if !requirements.build.is_empty() {
98            writeln!(out, "  build:").unwrap();
99            for req in &requirements.build {
100                self.render_requirement(out, req);
101            }
102        }
103        if !requirements.host.is_empty() {
104            writeln!(out, "  host:").unwrap();
105            for req in &requirements.host {
106                self.render_requirement(out, req);
107            }
108        }
109        if !requirements.run.is_empty() {
110            writeln!(out, "  run:").unwrap();
111            for req in &requirements.run {
112                self.render_requirement(out, req);
113            }
114        }
115        writeln!(out).unwrap();
116    }
117
118    fn render_test(&self, out: &mut String, test: &Test) {
119        if test.commands.is_empty() {
120            return;
121        }
122        writeln!(out, "test:").unwrap();
123        writeln!(out, "  commands:").unwrap();
124        for command in &test.commands {
125            writeln!(out, "    - {command}").unwrap();
126        }
127        writeln!(out).unwrap();
128    }
129
130    fn render_about(&self, out: &mut String, about: &About) {
131        writeln!(out, "about:").unwrap();
132        if let Some(home) = &about.home {
133            writeln!(out, "  home: {home}").unwrap();
134        }
135        if let Some(license) = &about.license {
136            writeln!(out, "  license: {license}").unwrap();
137        }
138        if let Some(license_family) = &about.license_family {
139            writeln!(out, "  license_family: {license_family}").unwrap();
140        }
141        match about.license_file.len() {
142            0 => {}
143            1 => writeln!(out, "  license_file: {}", about.license_file[0]).unwrap(),
144            _ => {
145                writeln!(out, "  license_file:").unwrap();
146                for f in &about.license_file {
147                    writeln!(out, "    - {f}").unwrap();
148                }
149            }
150        }
151        if let Some(summary) = &about.summary {
152            writeln!(out, "  summary: {summary}").unwrap();
153        }
154        if let Some(dev_url) = &about.dev_url {
155            writeln!(out, "  dev_url: {dev_url}").unwrap();
156        }
157        if let Some(doc_url) = &about.doc_url {
158            writeln!(out, "  doc_url: {doc_url}").unwrap();
159        }
160        writeln!(out).unwrap();
161    }
162
163    fn render_extra(&self, out: &mut String, extra: &Extra) {
164        if extra.additional_platforms.is_empty()
165            && extra.recipe_maintainers.is_empty()
166            && extra.identifiers.is_empty()
167            && extra.skip_platforms.is_empty()
168        {
169            return;
170        }
171        writeln!(out, "extra:").unwrap();
172        if !extra.additional_platforms.is_empty() {
173            writeln!(out, "  additional-platforms:").unwrap();
174            for plat in &extra.additional_platforms {
175                writeln!(out, "    - {plat}").unwrap();
176            }
177        }
178        if !extra.recipe_maintainers.is_empty() {
179            writeln!(out, "  recipe-maintainers:").unwrap();
180            for m in &extra.recipe_maintainers {
181                writeln!(out, "    - {m}").unwrap();
182            }
183        }
184        if !extra.identifiers.is_empty() {
185            writeln!(out, "  identifiers:").unwrap();
186            for id in &extra.identifiers {
187                writeln!(out, "    - {id}").unwrap();
188            }
189        }
190        if !extra.skip_platforms.is_empty() {
191            writeln!(out, "  skip-platforms:").unwrap();
192            for plat in &extra.skip_platforms {
193                writeln!(out, "    - {plat}").unwrap();
194            }
195        }
196        writeln!(out).unwrap();
197    }
198}