blob: a0570bae6d7545538fd5ab64f5f2bd0fe2c7e021 [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001//! Tools for rendering and writing BUILD and other Starlark files
2
3mod template_engine;
4
5use std::collections::BTreeMap;
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::str::FromStr;
9
10use anyhow::{bail, Context as AnyhowContext, Result};
11
12use crate::config::RenderConfig;
13use crate::context::Context;
14use crate::rendering::template_engine::TemplateEngine;
15use crate::splicing::default_splicing_package_crate_id;
16use crate::utils::starlark::Label;
17
18pub struct Renderer {
19 config: RenderConfig,
20 engine: TemplateEngine,
21}
22
23impl Renderer {
24 pub fn new(config: RenderConfig) -> Self {
25 let engine = TemplateEngine::new(&config);
26 Self { config, engine }
27 }
28
29 pub fn render(&self, context: &Context) -> Result<BTreeMap<PathBuf, String>> {
30 let mut output = BTreeMap::new();
31
32 output.extend(self.render_build_files(context)?);
33 output.extend(self.render_crates_module(context)?);
34
35 if let Some(vendor_mode) = &self.config.vendor_mode {
36 match vendor_mode {
37 crate::config::VendorMode::Local => {
38 // Nothing to do for local vendor crate
39 }
40 crate::config::VendorMode::Remote => {
41 output.extend(self.render_vendor_support_files(context)?);
42 }
43 }
44 }
45
46 Ok(output)
47 }
48
49 fn render_crates_module(&self, context: &Context) -> Result<BTreeMap<PathBuf, String>> {
50 let module_label = render_module_label(&self.config.crates_module_template, "defs.bzl")
51 .context("Failed to resolve string to module file label")?;
52 let module_build_label =
53 render_module_label(&self.config.crates_module_template, "BUILD.bazel")
54 .context("Failed to resolve string to module file label")?;
55
56 let mut map = BTreeMap::new();
57 map.insert(
58 Renderer::label_to_path(&module_label),
59 self.engine.render_module_bzl(context)?,
60 );
61 map.insert(
62 Renderer::label_to_path(&module_build_label),
63 self.engine.render_module_build_file(context)?,
64 );
65
66 Ok(map)
67 }
68
69 fn render_build_files(&self, context: &Context) -> Result<BTreeMap<PathBuf, String>> {
70 let default_splicing_package_id = default_splicing_package_crate_id();
71 self.engine
72 .render_crate_build_files(context)?
73 .into_iter()
74 // Do not render the default splicing package
75 .filter(|(id, _)| *id != &default_splicing_package_id)
76 // Do not render local packages
77 .filter(|(id, _)| !context.workspace_members.contains_key(id))
78 .map(|(id, content)| {
79 let ctx = &context.crates[id];
80 let label = match render_build_file_template(
81 &self.config.build_file_template,
82 &ctx.name,
83 &ctx.version,
84 ) {
85 Ok(label) => label,
86 Err(e) => bail!(e),
87 };
88
89 let filename = Renderer::label_to_path(&label);
90
91 Ok((filename, content))
92 })
93 .collect()
94 }
95
96 fn render_vendor_support_files(&self, context: &Context) -> Result<BTreeMap<PathBuf, String>> {
97 let module_label = render_module_label(&self.config.crates_module_template, "crates.bzl")
98 .context("Failed to resolve string to module file label")?;
99
100 let mut map = BTreeMap::new();
101 map.insert(
102 Renderer::label_to_path(&module_label),
103 self.engine.render_vendor_module_file(context)?,
104 );
105
106 Ok(map)
107 }
108
109 fn label_to_path(label: &Label) -> PathBuf {
110 match &label.package {
111 Some(package) => PathBuf::from(format!("{}/{}", package, label.target)),
112 None => PathBuf::from(&label.target),
113 }
114 }
115}
116
117/// Write a set of [CrateContext][crate::context::CrateContext] to disk.
118pub fn write_outputs(
119 outputs: BTreeMap<PathBuf, String>,
120 out_dir: &Path,
121 dry_run: bool,
122) -> Result<()> {
123 let outputs: BTreeMap<PathBuf, String> = outputs
124 .into_iter()
125 .map(|(path, content)| (out_dir.join(path), content))
126 .collect();
127
128 if dry_run {
129 for (path, content) in outputs {
130 println!(
131 "==============================================================================="
132 );
133 println!("{}", path.display());
134 println!(
135 "==============================================================================="
136 );
137 println!("{}\n", content);
138 }
139 } else {
140 for (path, content) in outputs {
141 // Ensure the output directory exists
142 fs::create_dir_all(
143 path.parent()
144 .expect("All file paths should have valid directories"),
145 )?;
146
147 fs::write(&path, content.as_bytes())
148 .context(format!("Failed to write file to disk: {}", path.display()))?;
149 }
150 }
151
152 Ok(())
153}
154
155/// Render the Bazel label of a crate
156pub fn render_crate_bazel_label(
157 template: &str,
158 repository_name: &str,
159 name: &str,
160 version: &str,
161 target: &str,
162) -> String {
163 template
164 .replace("{repository}", repository_name)
165 .replace("{name}", name)
166 .replace("{version}", version)
167 .replace("{target}", target)
168}
169
170/// Render the Bazel label of a crate
171pub fn render_crate_bazel_repository(
172 template: &str,
173 repository_name: &str,
174 name: &str,
175 version: &str,
176) -> String {
177 template
178 .replace("{repository}", repository_name)
179 .replace("{name}", name)
180 .replace("{version}", version)
181}
182
183/// Render the Bazel label of a crate
184pub fn render_crate_build_file(template: &str, name: &str, version: &str) -> String {
185 template
186 .replace("{name}", name)
187 .replace("{version}", version)
188}
189
190/// Render the Bazel label of a vendor module label
191pub fn render_module_label(template: &str, name: &str) -> Result<Label> {
192 Label::from_str(&template.replace("{file}", name))
193}
194
195/// Render the Bazel label of a platform triple
196pub fn render_platform_constraint_label(template: &str, triple: &str) -> String {
197 template.replace("{triple}", triple)
198}
199
200fn render_build_file_template(template: &str, name: &str, version: &str) -> Result<Label> {
201 Label::from_str(
202 &template
203 .replace("{name}", name)
204 .replace("{version}", version),
205 )
206}
207
208#[cfg(test)]
209mod test {
210 use super::*;
211
212 use crate::config::{Config, CrateId, VendorMode};
213 use crate::context::crate_context::{CrateContext, Rule};
214 use crate::context::{BuildScriptAttributes, Context, TargetAttributes};
215 use crate::metadata::Annotations;
216 use crate::test;
217
218 fn mock_render_config() -> RenderConfig {
219 serde_json::from_value(serde_json::json!({
220 "repository_name": "test_rendering"
221 }))
222 .unwrap()
223 }
224
225 fn mock_target_attributes() -> TargetAttributes {
226 TargetAttributes {
227 crate_name: "mock_crate".to_owned(),
228 crate_root: Some("src/root.rs".to_owned()),
229 ..TargetAttributes::default()
230 }
231 }
232
233 #[test]
234 fn render_rust_library() {
235 let mut context = Context::default();
236 let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned());
237 context.crates.insert(
238 crate_id.clone(),
239 CrateContext {
240 name: crate_id.name,
241 version: crate_id.version,
242 targets: vec![Rule::Library(mock_target_attributes())],
243 ..CrateContext::default()
244 },
245 );
246
247 let renderer = Renderer::new(mock_render_config());
248 let output = renderer.render(&context).unwrap();
249
250 let build_file_content = output
251 .get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel"))
252 .unwrap();
253
254 assert!(build_file_content.contains("rust_library("));
255 assert!(build_file_content.contains("name = \"mock_crate\""));
256 }
257
258 #[test]
259 fn render_cargo_build_script() {
260 let mut context = Context::default();
261 let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned());
262 context.crates.insert(
263 crate_id.clone(),
264 CrateContext {
265 name: crate_id.name,
266 version: crate_id.version,
267 targets: vec![Rule::BuildScript(TargetAttributes {
268 crate_name: "build_script_build".to_owned(),
269 crate_root: Some("build.rs".to_owned()),
270 ..TargetAttributes::default()
271 })],
272 // Build script attributes are required.
273 build_script_attrs: Some(BuildScriptAttributes::default()),
274 ..CrateContext::default()
275 },
276 );
277
278 let renderer = Renderer::new(mock_render_config());
279 let output = renderer.render(&context).unwrap();
280
281 let build_file_content = output
282 .get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel"))
283 .unwrap();
284
285 assert!(build_file_content.contains("cargo_build_script("));
286 assert!(build_file_content.contains("name = \"build_script_build\""));
287
288 // Ensure `cargo_build_script` requirements are met
289 assert!(build_file_content.contains("name = \"mock_crate_build_script\""));
290 }
291
292 #[test]
293 fn render_proc_macro() {
294 let mut context = Context::default();
295 let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned());
296 context.crates.insert(
297 crate_id.clone(),
298 CrateContext {
299 name: crate_id.name,
300 version: crate_id.version,
301 targets: vec![Rule::ProcMacro(mock_target_attributes())],
302 ..CrateContext::default()
303 },
304 );
305
306 let renderer = Renderer::new(mock_render_config());
307 let output = renderer.render(&context).unwrap();
308
309 let build_file_content = output
310 .get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel"))
311 .unwrap();
312
313 assert!(build_file_content.contains("rust_proc_macro("));
314 assert!(build_file_content.contains("name = \"mock_crate\""));
315 }
316
317 #[test]
318 fn render_binary() {
319 let mut context = Context::default();
320 let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned());
321 context.crates.insert(
322 crate_id.clone(),
323 CrateContext {
324 name: crate_id.name,
325 version: crate_id.version,
326 targets: vec![Rule::Binary(mock_target_attributes())],
327 ..CrateContext::default()
328 },
329 );
330
331 let renderer = Renderer::new(mock_render_config());
332 let output = renderer.render(&context).unwrap();
333
334 let build_file_content = output
335 .get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel"))
336 .unwrap();
337
338 assert!(build_file_content.contains("rust_binary("));
339 assert!(build_file_content.contains("name = \"mock_crate__bin\""));
340 }
341
342 #[test]
343 fn render_additive_build_contents() {
344 let mut context = Context::default();
345 let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned());
346 context.crates.insert(
347 crate_id.clone(),
348 CrateContext {
349 name: crate_id.name,
350 version: crate_id.version,
351 targets: vec![Rule::Binary(mock_target_attributes())],
352 additive_build_file_content: Some(
353 "# Hello World from additive section!".to_owned(),
354 ),
355 ..CrateContext::default()
356 },
357 );
358
359 let renderer = Renderer::new(mock_render_config());
360 let output = renderer.render(&context).unwrap();
361
362 let build_file_content = output
363 .get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel"))
364 .unwrap();
365
366 assert!(build_file_content.contains("# Hello World from additive section!"));
367 }
368
369 #[test]
370 fn render_aliases() {
371 let annotations = Annotations::new(
372 test::metadata::alias(),
373 test::lockfile::alias(),
374 Config::default(),
375 )
376 .unwrap();
377 let context = Context::new(annotations).unwrap();
378
379 let renderer = Renderer::new(mock_render_config());
380 let output = renderer.render(&context).unwrap();
381
382 let build_file_content = output.get(&PathBuf::from("BUILD.bazel")).unwrap();
383
384 assert!(build_file_content.contains(r#"name = "names-0.12.1-dev__names","#));
385 assert!(build_file_content.contains(r#"name = "names-0.13.0__names","#));
386 }
387
388 #[test]
389 fn render_crate_repositories() {
390 let mut context = Context::default();
391 let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned());
392 context.crates.insert(
393 crate_id.clone(),
394 CrateContext {
395 name: crate_id.name,
396 version: crate_id.version,
397 targets: vec![Rule::Library(mock_target_attributes())],
398 ..CrateContext::default()
399 },
400 );
401
402 let renderer = Renderer::new(mock_render_config());
403 let output = renderer.render(&context).unwrap();
404
405 let defs_module = output.get(&PathBuf::from("defs.bzl")).unwrap();
406
407 assert!(defs_module.contains("def crate_repositories():"));
408 }
409
410 #[test]
411 fn remote_remote_vendor_mode() {
412 let mut context = Context::default();
413 let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned());
414 context.crates.insert(
415 crate_id.clone(),
416 CrateContext {
417 name: crate_id.name,
418 version: crate_id.version,
419 targets: vec![Rule::Library(mock_target_attributes())],
420 ..CrateContext::default()
421 },
422 );
423
424 // Enable remote vendor mode
425 let config = RenderConfig {
426 vendor_mode: Some(VendorMode::Remote),
427 ..mock_render_config()
428 };
429
430 let renderer = Renderer::new(config);
431 let output = renderer.render(&context).unwrap();
432
433 let defs_module = output.get(&PathBuf::from("defs.bzl")).unwrap();
434 assert!(defs_module.contains("def crate_repositories():"));
435
436 let crates_module = output.get(&PathBuf::from("crates.bzl")).unwrap();
437 assert!(crates_module.contains("def crate_repositories():"));
438 }
439
440 #[test]
441 fn remote_local_vendor_mode() {
442 let mut context = Context::default();
443 let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned());
444 context.crates.insert(
445 crate_id.clone(),
446 CrateContext {
447 name: crate_id.name,
448 version: crate_id.version,
449 targets: vec![Rule::Library(mock_target_attributes())],
450 ..CrateContext::default()
451 },
452 );
453
454 // Enable local vendor mode
455 let config = RenderConfig {
456 vendor_mode: Some(VendorMode::Local),
457 ..mock_render_config()
458 };
459
460 let renderer = Renderer::new(config);
461 let output = renderer.render(&context).unwrap();
462
463 // Local vendoring does not produce a `crate_repositories` macro
464 let defs_module = output.get(&PathBuf::from("defs.bzl")).unwrap();
465 assert!(!defs_module.contains("def crate_repositories():"));
466
467 // Local vendoring does not produce a `crates.bzl` file.
468 assert!(output.get(&PathBuf::from("crates.bzl")).is_none());
469 }
470}