1 //! The cli entrypoint for the `generate` subcommand
2
3 use std::fs;
4 use std::path::{Path, PathBuf};
5
6 use anyhow::{bail, Context as AnyhowContext, Result};
7 use cargo_lock::Lockfile;
8 use clap::Parser;
9
10 use crate::config::Config;
11 use crate::context::Context;
12 use crate::lockfile::{lock_context, write_lockfile};
13 use crate::metadata::{load_metadata, Annotations, Cargo};
14 use crate::rendering::{write_outputs, Renderer};
15 use crate::splicing::SplicingManifest;
16 use crate::utils::normalize_cargo_file_paths;
17
18 /// Command line options for the `generate` subcommand
19 #[derive(Parser, Debug)]
20 #[clap(about = "Command line options for the `generate` subcommand", version)]
21 pub struct GenerateOptions {
22 /// The path to a Cargo binary to use for gathering metadata
23 #[clap(long, env = "CARGO")]
24 pub cargo: Option<PathBuf>,
25
26 /// The path to a rustc binary for use with Cargo
27 #[clap(long, env = "RUSTC")]
28 pub rustc: Option<PathBuf>,
29
30 /// The config file with information about the Bazel and Cargo workspace
31 #[clap(long)]
32 pub config: PathBuf,
33
34 /// A generated manifest of splicing inputs
35 #[clap(long)]
36 pub splicing_manifest: PathBuf,
37
38 /// The path to either a Cargo or Bazel lockfile
39 #[clap(long)]
40 pub lockfile: Option<PathBuf>,
41
42 /// The path to a [Cargo.lock](https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html) file.
43 #[clap(long)]
44 pub cargo_lockfile: PathBuf,
45
46 /// The directory of the current repository rule
47 #[clap(long)]
48 pub repository_dir: PathBuf,
49
50 /// A [Cargo config](https://doc.rust-lang.org/cargo/reference/config.html#configuration)
51 /// file to use when gathering metadata
52 #[clap(long)]
53 pub cargo_config: Option<PathBuf>,
54
55 /// Whether or not to ignore the provided lockfile and re-generate one
56 #[clap(long)]
57 pub repin: bool,
58
59 /// The path to a Cargo metadata `json` file. This file must be next to a `Cargo.toml` and `Cargo.lock` file.
60 #[clap(long)]
61 pub metadata: Option<PathBuf>,
62
63 /// If true, outputs will be printed instead of written to disk.
64 #[clap(long)]
65 pub dry_run: bool,
66 }
67
generate(opt: GenerateOptions) -> Result<()>68 pub fn generate(opt: GenerateOptions) -> Result<()> {
69 // Load the config
70 let config = Config::try_from_path(&opt.config)?;
71
72 // Go straight to rendering if there is no need to repin
73 if !opt.repin {
74 if let Some(lockfile) = &opt.lockfile {
75 let context = Context::try_from_path(lockfile)?;
76
77 // Render build files
78 let outputs = Renderer::new(config.rendering, config.supported_platform_triples)
79 .render(&context)?;
80
81 // make file paths compatible with bazel labels
82 let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
83
84 // Write the outputs to disk
85 write_outputs(normalized_outputs, opt.dry_run)?;
86
87 return Ok(());
88 }
89 }
90
91 // Ensure Cargo and Rustc are available for use during generation.
92 let rustc_bin = match &opt.rustc {
93 Some(bin) => bin,
94 None => bail!("The `--rustc` argument is required when generating unpinned content"),
95 };
96
97 let cargo_bin = Cargo::new(
98 match opt.cargo {
99 Some(bin) => bin,
100 None => bail!("The `--cargo` argument is required when generating unpinned content"),
101 },
102 rustc_bin.clone(),
103 );
104
105 // Ensure a path to a metadata file was provided
106 let metadata_path = match &opt.metadata {
107 Some(path) => path,
108 None => bail!("The `--metadata` argument is required when generating unpinned content"),
109 };
110
111 // Load Metadata and Lockfile
112 let (cargo_metadata, cargo_lockfile) = load_metadata(metadata_path)?;
113
114 // Annotate metadata
115 let annotations = Annotations::new(cargo_metadata, cargo_lockfile.clone(), config.clone())?;
116
117 // Generate renderable contexts for each package
118 let context = Context::new(annotations, config.rendering.are_sources_present())?;
119
120 // Render build files
121 let outputs = Renderer::new(
122 config.rendering.clone(),
123 config.supported_platform_triples.clone(),
124 )
125 .render(&context)?;
126
127 // make file paths compatible with bazel labels
128 let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
129
130 // Write the outputs to disk
131 write_outputs(normalized_outputs, opt.dry_run)?;
132
133 // Ensure Bazel lockfiles are written to disk so future generations can be short-circuited.
134 if let Some(lockfile) = opt.lockfile {
135 let splicing_manifest = SplicingManifest::try_from_path(&opt.splicing_manifest)?;
136
137 let lock_content =
138 lock_context(context, &config, &splicing_manifest, &cargo_bin, rustc_bin)?;
139
140 write_lockfile(lock_content, &lockfile, opt.dry_run)?;
141 }
142
143 update_cargo_lockfile(&opt.cargo_lockfile, cargo_lockfile)?;
144
145 Ok(())
146 }
147
update_cargo_lockfile(path: &Path, cargo_lockfile: Lockfile) -> Result<()>148 fn update_cargo_lockfile(path: &Path, cargo_lockfile: Lockfile) -> Result<()> {
149 let old_contents = fs::read_to_string(path).ok();
150 let new_contents = cargo_lockfile.to_string();
151
152 // Don't overwrite identical contents because timestamp changes may invalidate repo rules.
153 if old_contents.as_ref() == Some(&new_contents) {
154 return Ok(());
155 }
156
157 fs::write(path, new_contents)
158 .context("Failed to write Cargo.lock file back to the workspace.")?;
159
160 Ok(())
161 }
162