Skip to main content

cargo_bazel/cli/
generate.rs

1//! The cli entrypoint for the `generate` subcommand
2
3use 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/// Command line options for the `generate` subcommand
23#[derive(Parser, Debug)]
24#[clap(about = "Command line options for the `generate` subcommand", version)]
25pub struct GenerateOptions {
26    /// The path to a Cargo binary to use for gathering metadata
27    #[clap(long, env = "CARGO")]
28    pub cargo: Option<PathBuf>,
29
30    /// The path to a rustc binary for use with Cargo
31    #[clap(long, env = "RUSTC")]
32    pub rustc: Option<PathBuf>,
33
34    /// The config file with information about the Bazel and Cargo workspace
35    #[clap(long)]
36    pub config: PathBuf,
37
38    /// A generated manifest of splicing inputs
39    #[clap(long)]
40    pub splicing_manifest: PathBuf,
41
42    /// The path to either a Cargo or Bazel lockfile
43    #[clap(long)]
44    pub lockfile: Option<PathBuf>,
45
46    /// The path to a [Cargo.lock](https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html) file.
47    #[clap(long)]
48    pub cargo_lockfile: PathBuf,
49
50    /// The directory of the current repository rule
51    #[clap(long)]
52    pub repository_dir: PathBuf,
53
54    /// A [Cargo config](https://doc.rust-lang.org/cargo/reference/config.html#configuration)
55    /// file to use when gathering metadata
56    #[clap(long)]
57    pub cargo_config: Option<PathBuf>,
58
59    /// Whether or not to ignore the provided lockfile and re-generate one
60    #[clap(long)]
61    pub repin: bool,
62
63    /// The path to a Cargo metadata `json` file. This file must be next to a `Cargo.toml` and `Cargo.lock` file.
64    #[clap(long)]
65    pub metadata: Option<PathBuf>,
66
67    /// If true, outputs will be printed instead of written to disk.
68    #[clap(long)]
69    pub dry_run: bool,
70
71    /// The path to the Bazel root workspace (i.e. the directory containing the WORKSPACE.bazel file or similar).
72    /// BE CAREFUL with this value. We never want to include it in a lockfile hash (to keep lockfiles portable),
73    /// which means you also should not use it anywhere that _should_ be guarded by a lockfile hash.
74    /// You basically never want to use this value.
75    #[clap(long)]
76    pub nonhermetic_root_bazel_workspace_dir: Utf8PathBuf,
77
78    /// Path to write a list of files which the repository_rule should watch.
79    /// If any of these paths change, the repository rule should be rerun.
80    /// These files may be outside of the Bazel-managed workspace.
81    /// A (possibly empty) JSON sorted array of strings will be unconditionally written to this file.
82    #[clap(long)]
83    pub paths_to_track: PathBuf,
84
85    /// The label of this binary, if it was built in bootstrap mode.
86    /// BE CAREFUL with this value. We never want to include it in a lockfile hash (to keep lockfiles portable),
87    /// which means you also should not use it anywhere that _should_ be guarded by a lockfile hash.
88    /// You basically never want to use this value.
89    #[clap(long)]
90    pub(crate) generator: Option<Label>,
91
92    /// Path to write a list of warnings which the repository rule should emit.
93    /// A (possibly empty) JSON array of strings will be unconditionally written to this file.
94    /// Each warning should be printed.
95    /// This mechanism exists because this process's output is often hidden by default,
96    /// so this provides a way for the repository rule to force printing.
97    #[clap(long)]
98    pub warnings_output_path: PathBuf,
99
100    /// Whether to skip writing the cargo lockfile back after resolving.
101    /// You may want to set this if your dependency versions are maintained externally through a non-trivial set-up.
102    /// But you probably don't want to set this.
103    #[clap(long)]
104    pub skip_cargo_lockfile_overwrite: bool,
105
106    /// Whether to strip internal dependencies from the cargo lockfile.
107    /// You may want to use this if you want to maintain a cargo lockfile for bazel only.
108    /// Bazel only requires external dependencies to be present in the lockfile.
109    /// By removing internal dependencies, the lockfile changes less frequently which reduces merge conflicts
110    /// in other lockfiles where the cargo lockfile's sha is stored.
111    #[clap(long)]
112    pub strip_internal_dependencies_from_cargo_lockfile: bool,
113}
114
115pub fn generate(opt: GenerateOptions) -> Result<()> {
116    // Load the config
117    let config = Config::try_from_path(&opt.config)?;
118
119    // Go straight to rendering if there is no need to repin
120    if !opt.repin {
121        if let Some(lockfile) = &opt.lockfile {
122            let context = Context::try_from_path(lockfile)?;
123
124            // Render build files
125            let outputs = Renderer::new(
126                Arc::new(config.rendering),
127                Arc::new(config.supported_platform_triples),
128            )
129            .render(&context, opt.generator)?;
130
131            // make file paths compatible with bazel labels
132            let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
133
134            // Write the outputs to disk
135            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    // Ensure Cargo and Rustc are available for use during generation.
156    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    // Ensure a path to a metadata file was provided
170    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    // Load Metadata and Lockfile
176    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    // Annotate metadata
189    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    // Generate renderable contexts for each package
209    let context = Context::new(annotations, config.rendering.are_sources_present())?;
210
211    // Render build files
212    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    // make file paths compatible with bazel labels
219    let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
220
221    // Write the outputs to disk
222    write_outputs(normalized_outputs, opt.dry_run)?;
223
224    // Ensure Bazel lockfiles are written to disk so future generations can be short-circuited.
225    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 packages to only keep external dependencies (those with a source)
249        .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    // Don't overwrite identical contents because timestamp changes may invalidate repo rules.
263    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        // Paths outside the bazel workspace cannot be `.watch`-ed.
300        .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}