cargo_bazel/cli/
generate.rs1use std::fs;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use anyhow::{bail, Context as AnyhowContext, Result};
8use camino::Utf8PathBuf;
9use cargo_lock::Lockfile;
10use clap::Parser;
11
12use crate::config::Config;
13use crate::context::Context;
14use crate::lockfile::{lock_context, write_lockfile};
15use crate::metadata::{load_metadata, Annotations, Cargo, SourceAnnotation};
16use crate::rendering::{write_outputs, Renderer};
17use crate::splicing::SplicingManifest;
18use crate::utils::normalize_cargo_file_paths;
19use crate::utils::starlark::Label;
20
21#[derive(Parser, Debug)]
23#[clap(about = "Command line options for the `generate` subcommand", version)]
24pub struct GenerateOptions {
25 #[clap(long, env = "CARGO")]
27 pub cargo: Option<PathBuf>,
28
29 #[clap(long, env = "RUSTC")]
31 pub rustc: Option<PathBuf>,
32
33 #[clap(long)]
35 pub config: PathBuf,
36
37 #[clap(long)]
39 pub splicing_manifest: PathBuf,
40
41 #[clap(long)]
43 pub lockfile: Option<PathBuf>,
44
45 #[clap(long)]
47 pub cargo_lockfile: PathBuf,
48
49 #[clap(long)]
51 pub repository_dir: PathBuf,
52
53 #[clap(long)]
56 pub cargo_config: Option<PathBuf>,
57
58 #[clap(long)]
60 pub repin: bool,
61
62 #[clap(long)]
64 pub metadata: Option<PathBuf>,
65
66 #[clap(long)]
68 pub dry_run: bool,
69
70 #[clap(long)]
75 pub nonhermetic_root_bazel_workspace_dir: Utf8PathBuf,
76
77 #[clap(long)]
82 pub paths_to_track: PathBuf,
83
84 #[clap(long)]
89 pub(crate) generator: Option<Label>,
90
91 #[clap(long)]
97 pub warnings_output_path: PathBuf,
98}
99
100pub fn generate(opt: GenerateOptions) -> Result<()> {
101 let config = Config::try_from_path(&opt.config)?;
103
104 if !opt.repin {
106 if let Some(lockfile) = &opt.lockfile {
107 let context = Context::try_from_path(lockfile)?;
108
109 let outputs = Renderer::new(
111 Arc::new(config.rendering),
112 Arc::new(config.supported_platform_triples),
113 )
114 .render(&context, opt.generator)?;
115
116 let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
118
119 write_outputs(normalized_outputs, opt.dry_run)?;
121
122 write_paths_to_track(
123 &opt.paths_to_track,
124 &opt.warnings_output_path,
125 context
126 .crates
127 .values()
128 .filter_map(|crate_context| crate_context.repository.as_ref()),
129 context.unused_patches.iter(),
130 )?;
131
132 return Ok(());
133 }
134 }
135
136 let rustc_bin = match &opt.rustc {
138 Some(bin) => bin,
139 None => bail!("The `--rustc` argument is required when generating unpinned content"),
140 };
141
142 let cargo_bin = Cargo::new(
143 match opt.cargo {
144 Some(bin) => bin,
145 None => bail!("The `--cargo` argument is required when generating unpinned content"),
146 },
147 rustc_bin.clone(),
148 );
149
150 let metadata_path = match &opt.metadata {
152 Some(path) => path,
153 None => bail!("The `--metadata` argument is required when generating unpinned content"),
154 };
155
156 let (cargo_metadata, cargo_lockfile) = load_metadata(metadata_path)?;
158
159 let annotations = Annotations::new(
161 cargo_metadata,
162 cargo_lockfile.clone(),
163 config.clone(),
164 &opt.nonhermetic_root_bazel_workspace_dir,
165 )?;
166
167 write_paths_to_track(
168 &opt.paths_to_track,
169 &opt.warnings_output_path,
170 annotations.lockfile.crates.values(),
171 cargo_lockfile.patch.unused.iter(),
172 )?;
173
174 let context = Context::new(annotations, config.rendering.are_sources_present())?;
176
177 let outputs = Renderer::new(
179 Arc::new(config.rendering.clone()),
180 Arc::new(config.supported_platform_triples.clone()),
181 )
182 .render(&context, opt.generator)?;
183
184 let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
186
187 write_outputs(normalized_outputs, opt.dry_run)?;
189
190 if let Some(lockfile) = opt.lockfile {
192 let splicing_manifest = SplicingManifest::try_from_path(&opt.splicing_manifest)?;
193
194 let lock_content =
195 lock_context(context, &config, &splicing_manifest, &cargo_bin, rustc_bin)?;
196
197 write_lockfile(lock_content, &lockfile, opt.dry_run)?;
198 }
199
200 update_cargo_lockfile(&opt.cargo_lockfile, cargo_lockfile)?;
201
202 Ok(())
203}
204
205fn update_cargo_lockfile(path: &Path, cargo_lockfile: Lockfile) -> Result<()> {
206 let old_contents = fs::read_to_string(path).ok();
207 let new_contents = cargo_lockfile.to_string();
208
209 if old_contents.as_ref() == Some(&new_contents) {
211 return Ok(());
212 }
213
214 fs::write(path, new_contents)
215 .context("Failed to write Cargo.lock file back to the workspace.")?;
216
217 Ok(())
218}
219
220fn write_paths_to_track<
221 'a,
222 SourceAnnotations: Iterator<Item = &'a SourceAnnotation>,
223 UnusedPatches: Iterator<Item = &'a cargo_lock::Dependency>,
224>(
225 output_file: &Path,
226 warnings_output_path: &Path,
227 source_annotations: SourceAnnotations,
228 unused_patches: UnusedPatches,
229) -> Result<()> {
230 let paths_to_track: std::collections::BTreeSet<_> = source_annotations
231 .filter_map(|v| {
232 if let SourceAnnotation::Path { path } = v {
233 Some(path.join("Cargo.toml"))
234 } else {
235 None
236 }
237 })
238 .collect();
239 std::fs::write(
240 output_file,
241 serde_json::to_string(&paths_to_track).context("Failed to serialize paths to track")?,
242 )
243 .context("Failed to write paths to track")?;
244
245 let mut warnings = Vec::new();
246 for path_to_track in &paths_to_track {
247 warnings.push(format!("Build is not hermetic - path dependency pulling in crate at {path_to_track} is being used."));
248 }
249 for unused_patch in unused_patches {
250 warnings.push(format!("You have a [patch] Cargo.toml entry that is being ignored by cargo. Unused patch: {} {}{}", unused_patch.name, unused_patch.version, if let Some(source) = unused_patch.source.as_ref() { format!(" ({})", source) } else { String::new() }));
251 }
252
253 std::fs::write(
254 warnings_output_path,
255 serde_json::to_string(&warnings).context("Failed to serialize warnings to track")?,
256 )
257 .context("Failed to write warnings file")?;
258 Ok(())
259}