1use std::collections::BTreeSet;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use anyhow::{bail, Context as AnyhowContext, Result};
9use camino::Utf8PathBuf;
10use cargo_lock::Lockfile;
11use clap::Parser;
12
13use crate::config::Config;
14use crate::context::Context;
15use crate::lockfile::{lock_context, write_lockfile};
16use crate::metadata::{load_metadata, Annotations, Cargo, SourceAnnotation};
17use crate::rendering::{write_outputs, Renderer};
18use crate::splicing::SplicingManifest;
19use crate::utils::normalize_cargo_file_paths;
20use crate::utils::starlark::Label;
21
22#[derive(Parser, Debug)]
24#[clap(about = "Command line options for the `generate` subcommand", version)]
25pub struct GenerateOptions {
26 #[clap(long, env = "CARGO")]
28 pub cargo: Option<PathBuf>,
29
30 #[clap(long, env = "RUSTC")]
32 pub rustc: Option<PathBuf>,
33
34 #[clap(long)]
36 pub config: PathBuf,
37
38 #[clap(long)]
40 pub splicing_manifest: PathBuf,
41
42 #[clap(long)]
44 pub lockfile: Option<PathBuf>,
45
46 #[clap(long)]
48 pub cargo_lockfile: PathBuf,
49
50 #[clap(long)]
52 pub repository_dir: PathBuf,
53
54 #[clap(long)]
57 pub cargo_config: Option<PathBuf>,
58
59 #[clap(long)]
61 pub repin: bool,
62
63 #[clap(long)]
65 pub metadata: Option<PathBuf>,
66
67 #[clap(long)]
69 pub dry_run: bool,
70
71 #[clap(long)]
76 pub nonhermetic_root_bazel_workspace_dir: Utf8PathBuf,
77
78 #[clap(long)]
83 pub paths_to_track: PathBuf,
84
85 #[clap(long)]
90 pub(crate) generator: Option<Label>,
91
92 #[clap(long)]
98 pub warnings_output_path: PathBuf,
99
100 #[clap(long)]
104 pub skip_cargo_lockfile_overwrite: bool,
105
106 #[clap(long)]
112 pub strip_internal_dependencies_from_cargo_lockfile: bool,
113}
114
115pub fn generate(opt: GenerateOptions) -> Result<()> {
116 let config = Config::try_from_path(&opt.config)?;
118
119 if !opt.repin {
121 if let Some(lockfile) = &opt.lockfile {
122 let context = Context::try_from_path(lockfile)?;
123
124 let outputs = Renderer::new(
126 Arc::new(config.rendering),
127 Arc::new(config.supported_platform_triples),
128 )
129 .render(&context, opt.generator)?;
130
131 let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
133
134 write_outputs(normalized_outputs, opt.dry_run)?;
136
137 let splicing_manifest = SplicingManifest::try_from_path(&opt.splicing_manifest)?;
138
139 write_paths_to_track(
140 &opt.paths_to_track,
141 &opt.warnings_output_path,
142 splicing_manifest.manifests.keys().cloned(),
143 context
144 .crates
145 .values()
146 .filter_map(|crate_context| crate_context.repository.as_ref()),
147 context.unused_patches.iter(),
148 &opt.nonhermetic_root_bazel_workspace_dir,
149 )?;
150
151 return Ok(());
152 }
153 }
154
155 let rustc_bin = match &opt.rustc {
157 Some(bin) => bin,
158 None => bail!("The `--rustc` argument is required when generating unpinned content"),
159 };
160
161 let cargo_bin = Cargo::new(
162 match opt.cargo {
163 Some(bin) => bin,
164 None => bail!("The `--cargo` argument is required when generating unpinned content"),
165 },
166 rustc_bin.clone(),
167 );
168
169 let metadata_path = match &opt.metadata {
171 Some(path) => path,
172 None => bail!("The `--metadata` argument is required when generating unpinned content"),
173 };
174
175 let lockfile_path = metadata_path
177 .parent()
178 .expect("metadata files should always have parents")
179 .join("Cargo.lock");
180 if !lockfile_path.exists() {
181 bail!(
182 "The metadata file at {} is not next to a `Cargo.lock` file.",
183 metadata_path.display()
184 )
185 }
186 let (cargo_metadata, cargo_lockfile) = load_metadata(metadata_path, &lockfile_path)?;
187
188 let annotations = Annotations::new(
190 cargo_metadata,
191 &Some(lockfile_path),
192 cargo_lockfile.clone(),
193 config.clone(),
194 &opt.nonhermetic_root_bazel_workspace_dir,
195 )?;
196
197 let splicing_manifest = SplicingManifest::try_from_path(&opt.splicing_manifest)?;
198
199 write_paths_to_track(
200 &opt.paths_to_track,
201 &opt.warnings_output_path,
202 splicing_manifest.manifests.keys().cloned(),
203 annotations.lockfile.crates.values(),
204 cargo_lockfile.patch.unused.iter(),
205 &opt.nonhermetic_root_bazel_workspace_dir,
206 )?;
207
208 let context = Context::new(annotations, config.rendering.are_sources_present())?;
210
211 let outputs = Renderer::new(
213 Arc::new(config.rendering.clone()),
214 Arc::new(config.supported_platform_triples.clone()),
215 )
216 .render(&context, opt.generator)?;
217
218 let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
220
221 write_outputs(normalized_outputs, opt.dry_run)?;
223
224 if let Some(lockfile) = opt.lockfile {
226 let lock_content =
227 lock_context(context, &config, &splicing_manifest, &cargo_bin, rustc_bin)?;
228
229 write_lockfile(lock_content, &lockfile, opt.dry_run)?;
230 }
231
232 if !opt.skip_cargo_lockfile_overwrite {
233 let cargo_lockfile_to_write = if opt.strip_internal_dependencies_from_cargo_lockfile {
234 remove_internal_dependencies_from_cargo_lockfile(cargo_lockfile)
235 } else {
236 cargo_lockfile
237 };
238 update_cargo_lockfile(&opt.cargo_lockfile, cargo_lockfile_to_write)?;
239 }
240
241 Ok(())
242}
243
244fn remove_internal_dependencies_from_cargo_lockfile(cargo_lockfile: Lockfile) -> Lockfile {
245 let filtered_packages: Vec<_> = cargo_lockfile
246 .packages
247 .into_iter()
248 .filter(|pkg| pkg.source.is_some())
250 .collect();
251
252 Lockfile {
253 packages: filtered_packages,
254 ..cargo_lockfile
255 }
256}
257
258fn update_cargo_lockfile(path: &Path, cargo_lockfile: Lockfile) -> Result<()> {
259 let old_contents = fs::read_to_string(path).ok();
260 let new_contents = cargo_lockfile.to_string();
261
262 if old_contents.as_ref() == Some(&new_contents) {
264 return Ok(());
265 }
266
267 fs::write(path, new_contents)
268 .context("Failed to write Cargo.lock file back to the workspace.")?;
269
270 Ok(())
271}
272
273fn write_paths_to_track<
274 'a,
275 SourceAnnotations: Iterator<Item = &'a SourceAnnotation>,
276 Paths: Iterator<Item = Utf8PathBuf>,
277 UnusedPatches: Iterator<Item = &'a cargo_lock::Dependency>,
278>(
279 output_file: &Path,
280 warnings_output_path: &Path,
281 manifests: Paths,
282 source_annotations: SourceAnnotations,
283 unused_patches: UnusedPatches,
284 nonhermetic_root_bazel_workspace_dir: &Utf8PathBuf,
285) -> Result<()> {
286 let source_annotation_manifests: BTreeSet<_> = source_annotations
287 .filter_map(|v| {
288 if let SourceAnnotation::Path { path } = v {
289 Some(path.join("Cargo.toml"))
290 } else {
291 None
292 }
293 })
294 .collect();
295 let paths_to_track: BTreeSet<_> = source_annotation_manifests
296 .iter()
297 .cloned()
298 .chain(manifests)
299 .filter(|p| p.starts_with(nonhermetic_root_bazel_workspace_dir))
301 .collect();
302 std::fs::write(
303 output_file,
304 serde_json::to_string(&paths_to_track).context("Failed to serialize paths to track")?,
305 )
306 .context("Failed to write paths to track")?;
307
308 let mut warnings = Vec::new();
309 for source_annotation_manifest in &source_annotation_manifests {
310 warnings.push(format!("Build is not hermetic - path dependency pulling in crate at {source_annotation_manifest} is being used."));
311 }
312 for unused_patch in unused_patches {
313 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() }));
314 }
315
316 std::fs::write(
317 warnings_output_path,
318 serde_json::to_string(&warnings).context("Failed to serialize warnings to track")?,
319 )
320 .context("Failed to write warnings file")?;
321 Ok(())
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use crate::test;
328
329 #[test]
330 fn test_remove_internal_dependencies_from_cargo_lockfile_workspace_build_scripts_deps_should_remove_internal_dependencies(
331 ) {
332 let original_lockfile = test::lockfile::workspace_build_scripts_deps();
333
334 let filtered_lockfile =
335 remove_internal_dependencies_from_cargo_lockfile(original_lockfile.clone());
336
337 assert!(filtered_lockfile.packages.len() < original_lockfile.packages.len());
338
339 assert!(original_lockfile
340 .packages
341 .iter()
342 .any(|pkg| pkg.name.as_str() == "child"));
343 assert!(!filtered_lockfile
344 .packages
345 .iter()
346 .any(|pkg| pkg.name.as_str() == "child"));
347
348 assert!(filtered_lockfile
349 .packages
350 .iter()
351 .any(|pkg| pkg.name.as_str() == "anyhow"));
352 }
353}