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
101pub fn generate(opt: GenerateOptions) -> Result<()> {
102    // Load the config
103    let config = Config::try_from_path(&opt.config)?;
104
105    // Go straight to rendering if there is no need to repin
106    if !opt.repin {
107        if let Some(lockfile) = &opt.lockfile {
108            let context = Context::try_from_path(lockfile)?;
109
110            // Render build files
111            let outputs = Renderer::new(
112                Arc::new(config.rendering),
113                Arc::new(config.supported_platform_triples),
114            )
115            .render(&context, opt.generator)?;
116
117            // make file paths compatible with bazel labels
118            let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
119
120            // Write the outputs to disk
121            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    // Ensure Cargo and Rustc are available for use during generation.
141    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    // Ensure a path to a metadata file was provided
155    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    // Load Metadata and Lockfile
161    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    // Annotate metadata
174    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    // Generate renderable contexts for each package
193    let context = Context::new(annotations, config.rendering.are_sources_present())?;
194
195    // Render build files
196    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    // make file paths compatible with bazel labels
203    let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
204
205    // Write the outputs to disk
206    write_outputs(normalized_outputs, opt.dry_run)?;
207
208    // Ensure Bazel lockfiles are written to disk so future generations can be short-circuited.
209    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    // Don't overwrite identical contents because timestamp changes may invalidate repo rules.
226    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}