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
101pub fn generate(opt: GenerateOptions) -> Result<()> {
102 let config = Config::try_from_path(&opt.config)?;
104
105 if !opt.repin {
107 if let Some(lockfile) = &opt.lockfile {
108 let context = Context::try_from_path(lockfile)?;
109
110 let outputs = Renderer::new(
112 Arc::new(config.rendering),
113 Arc::new(config.supported_platform_triples),
114 )
115 .render(&context, opt.generator)?;
116
117 let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
119
120 write_outputs(normalized_outputs, opt.dry_run)?;
122
123 let splicing_manifest = SplicingManifest::try_from_path(&opt.splicing_manifest)?;
124
125 write_paths_to_track(
126 &opt.paths_to_track,
127 &opt.warnings_output_path,
128 splicing_manifest.manifests.keys().cloned(),
129 context
130 .crates
131 .values()
132 .filter_map(|crate_context| crate_context.repository.as_ref()),
133 context.unused_patches.iter(),
134 )?;
135
136 return Ok(());
137 }
138 }
139
140 let rustc_bin = match &opt.rustc {
142 Some(bin) => bin,
143 None => bail!("The `--rustc` argument is required when generating unpinned content"),
144 };
145
146 let cargo_bin = Cargo::new(
147 match opt.cargo {
148 Some(bin) => bin,
149 None => bail!("The `--cargo` argument is required when generating unpinned content"),
150 },
151 rustc_bin.clone(),
152 );
153
154 let metadata_path = match &opt.metadata {
156 Some(path) => path,
157 None => bail!("The `--metadata` argument is required when generating unpinned content"),
158 };
159
160 let lockfile_path = metadata_path
162 .parent()
163 .expect("metadata files should always have parents")
164 .join("Cargo.lock");
165 if !lockfile_path.exists() {
166 bail!(
167 "The metadata file at {} is not next to a `Cargo.lock` file.",
168 metadata_path.display()
169 )
170 }
171 let (cargo_metadata, cargo_lockfile) = load_metadata(metadata_path, &lockfile_path)?;
172
173 let annotations = Annotations::new(
175 cargo_metadata,
176 &Some(lockfile_path),
177 cargo_lockfile.clone(),
178 config.clone(),
179 &opt.nonhermetic_root_bazel_workspace_dir,
180 )?;
181
182 let splicing_manifest = SplicingManifest::try_from_path(&opt.splicing_manifest)?;
183
184 write_paths_to_track(
185 &opt.paths_to_track,
186 &opt.warnings_output_path,
187 splicing_manifest.manifests.keys().cloned(),
188 annotations.lockfile.crates.values(),
189 cargo_lockfile.patch.unused.iter(),
190 )?;
191
192 let context = Context::new(annotations, config.rendering.are_sources_present())?;
194
195 let outputs = Renderer::new(
197 Arc::new(config.rendering.clone()),
198 Arc::new(config.supported_platform_triples.clone()),
199 )
200 .render(&context, opt.generator)?;
201
202 let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
204
205 write_outputs(normalized_outputs, opt.dry_run)?;
207
208 if let Some(lockfile) = opt.lockfile {
210 let lock_content =
211 lock_context(context, &config, &splicing_manifest, &cargo_bin, rustc_bin)?;
212
213 write_lockfile(lock_content, &lockfile, opt.dry_run)?;
214 }
215
216 update_cargo_lockfile(&opt.cargo_lockfile, cargo_lockfile)?;
217
218 Ok(())
219}
220
221fn update_cargo_lockfile(path: &Path, cargo_lockfile: Lockfile) -> Result<()> {
222 let old_contents = fs::read_to_string(path).ok();
223 let new_contents = cargo_lockfile.to_string();
224
225 if old_contents.as_ref() == Some(&new_contents) {
227 return Ok(());
228 }
229
230 fs::write(path, new_contents)
231 .context("Failed to write Cargo.lock file back to the workspace.")?;
232
233 Ok(())
234}
235
236fn write_paths_to_track<
237 'a,
238 SourceAnnotations: Iterator<Item = &'a SourceAnnotation>,
239 Paths: Iterator<Item = Utf8PathBuf>,
240 UnusedPatches: Iterator<Item = &'a cargo_lock::Dependency>,
241>(
242 output_file: &Path,
243 warnings_output_path: &Path,
244 manifests: Paths,
245 source_annotations: SourceAnnotations,
246 unused_patches: UnusedPatches,
247) -> Result<()> {
248 let source_annotation_manifests: BTreeSet<_> = source_annotations
249 .filter_map(|v| {
250 if let SourceAnnotation::Path { path } = v {
251 Some(path.join("Cargo.toml"))
252 } else {
253 None
254 }
255 })
256 .collect();
257 let paths_to_track: BTreeSet<_> = source_annotation_manifests
258 .iter()
259 .cloned()
260 .chain(manifests)
261 .collect();
262 std::fs::write(
263 output_file,
264 serde_json::to_string(&paths_to_track).context("Failed to serialize paths to track")?,
265 )
266 .context("Failed to write paths to track")?;
267
268 let mut warnings = Vec::new();
269 for source_annotation_manifest in &source_annotation_manifests {
270 warnings.push(format!("Build is not hermetic - path dependency pulling in crate at {source_annotation_manifest} is being used."));
271 }
272 for unused_patch in unused_patches {
273 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() }));
274 }
275
276 std::fs::write(
277 warnings_output_path,
278 serde_json::to_string(&warnings).context("Failed to serialize warnings to track")?,
279 )
280 .context("Failed to write warnings file")?;
281 Ok(())
282}