cargo_bazel/cli/
generate.rs

1//! The cli entrypoint for the `generate` subcommand
2
3use 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/// Command line options for the `generate` subcommand
22#[derive(Parser, Debug)]
23#[clap(about = "Command line options for the `generate` subcommand", version)]
24pub struct GenerateOptions {
25    /// The path to a Cargo binary to use for gathering metadata
26    #[clap(long, env = "CARGO")]
27    pub cargo: Option<PathBuf>,
28
29    /// The path to a rustc binary for use with Cargo
30    #[clap(long, env = "RUSTC")]
31    pub rustc: Option<PathBuf>,
32
33    /// The config file with information about the Bazel and Cargo workspace
34    #[clap(long)]
35    pub config: PathBuf,
36
37    /// A generated manifest of splicing inputs
38    #[clap(long)]
39    pub splicing_manifest: PathBuf,
40
41    /// The path to either a Cargo or Bazel lockfile
42    #[clap(long)]
43    pub lockfile: Option<PathBuf>,
44
45    /// The path to a [Cargo.lock](https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html) file.
46    #[clap(long)]
47    pub cargo_lockfile: PathBuf,
48
49    /// The directory of the current repository rule
50    #[clap(long)]
51    pub repository_dir: PathBuf,
52
53    /// A [Cargo config](https://doc.rust-lang.org/cargo/reference/config.html#configuration)
54    /// file to use when gathering metadata
55    #[clap(long)]
56    pub cargo_config: Option<PathBuf>,
57
58    /// Whether or not to ignore the provided lockfile and re-generate one
59    #[clap(long)]
60    pub repin: bool,
61
62    /// The path to a Cargo metadata `json` file. This file must be next to a `Cargo.toml` and `Cargo.lock` file.
63    #[clap(long)]
64    pub metadata: Option<PathBuf>,
65
66    /// If true, outputs will be printed instead of written to disk.
67    #[clap(long)]
68    pub dry_run: bool,
69
70    /// The path to the Bazel root workspace (i.e. the directory containing the WORKSPACE.bazel file or similar).
71    /// BE CAREFUL with this value. We never want to include it in a lockfile hash (to keep lockfiles portable),
72    /// which means you also should not use it anywhere that _should_ be guarded by a lockfile hash.
73    /// You basically never want to use this value.
74    #[clap(long)]
75    pub nonhermetic_root_bazel_workspace_dir: Utf8PathBuf,
76
77    /// Path to write a list of files which the repository_rule should watch.
78    /// If any of these paths change, the repository rule should be rerun.
79    /// These files may be outside of the Bazel-managed workspace.
80    /// A (possibly empty) JSON sorted array of strings will be unconditionally written to this file.
81    #[clap(long)]
82    pub paths_to_track: PathBuf,
83
84    /// The label of this binary, if it was built in bootstrap mode.
85    /// BE CAREFUL with this value. We never want to include it in a lockfile hash (to keep lockfiles portable),
86    /// which means you also should not use it anywhere that _should_ be guarded by a lockfile hash.
87    /// You basically never want to use this value.
88    #[clap(long)]
89    pub(crate) generator: Option<Label>,
90
91    /// Path to write a list of warnings which the repository rule should emit.
92    /// A (possibly empty) JSON array of strings will be unconditionally written to this file.
93    /// Each warning should be printed.
94    /// This mechanism exists because this process's output is often hidden by default,
95    /// so this provides a way for the repository rule to force printing.
96    #[clap(long)]
97    pub warnings_output_path: PathBuf,
98}
99
100pub fn generate(opt: GenerateOptions) -> Result<()> {
101    // Load the config
102    let config = Config::try_from_path(&opt.config)?;
103
104    // Go straight to rendering if there is no need to repin
105    if !opt.repin {
106        if let Some(lockfile) = &opt.lockfile {
107            let context = Context::try_from_path(lockfile)?;
108
109            // Render build files
110            let outputs = Renderer::new(
111                Arc::new(config.rendering),
112                Arc::new(config.supported_platform_triples),
113            )
114            .render(&context, opt.generator)?;
115
116            // make file paths compatible with bazel labels
117            let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
118
119            // Write the outputs to disk
120            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    // Ensure Cargo and Rustc are available for use during generation.
137    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    // Ensure a path to a metadata file was provided
151    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    // Load Metadata and Lockfile
157    let (cargo_metadata, cargo_lockfile) = load_metadata(metadata_path)?;
158
159    // Annotate metadata
160    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    // Generate renderable contexts for each package
175    let context = Context::new(annotations, config.rendering.are_sources_present())?;
176
177    // Render build files
178    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    // make file paths compatible with bazel labels
185    let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
186
187    // Write the outputs to disk
188    write_outputs(normalized_outputs, opt.dry_run)?;
189
190    // Ensure Bazel lockfiles are written to disk so future generations can be short-circuited.
191    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    // Don't overwrite identical contents because timestamp changes may invalidate repo rules.
210    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}