xref: /aosp_15_r20/external/bazelbuild-rules_rust/crate_universe/src/cli/vendor.rs (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
1 //! The cli entrypoint for the `vendor` subcommand
2 
3 use std::collections::BTreeSet;
4 use std::env;
5 use std::fs;
6 use std::path::{Path, PathBuf};
7 use std::process::{self, ExitStatus};
8 
9 use anyhow::{bail, Context as AnyhowContext, Result};
10 use clap::Parser;
11 
12 use crate::config::{Config, VendorMode};
13 use crate::context::Context;
14 use crate::metadata::CargoUpdateRequest;
15 use crate::metadata::TreeResolver;
16 use crate::metadata::{Annotations, Cargo, Generator, MetadataGenerator, VendorGenerator};
17 use crate::rendering::{render_module_label, write_outputs, Renderer};
18 use crate::splicing::{generate_lockfile, Splicer, SplicingManifest, WorkspaceMetadata};
19 use crate::utils::normalize_cargo_file_paths;
20 
21 /// Command line options for the `vendor` subcommand
22 #[derive(Parser, Debug)]
23 #[clap(about = "Command line options for the `vendor` subcommand", version)]
24 pub struct VendorOptions {
25     /// The path to a Cargo binary to use for gathering metadata
26     #[clap(long, env = "CARGO")]
27     pub cargo: PathBuf,
28 
29     /// The path to a rustc binary for use with Cargo
30     #[clap(long, env = "RUSTC")]
31     pub rustc: PathBuf,
32 
33     /// The path to a buildifier binary for formatting generated BUILD files
34     #[clap(long)]
35     pub buildifier: Option<PathBuf>,
36 
37     /// The config file with information about the Bazel and Cargo workspace
38     #[clap(long)]
39     pub config: PathBuf,
40 
41     /// A generated manifest of splicing inputs
42     #[clap(long)]
43     pub splicing_manifest: 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: Option<PathBuf>,
48 
49     /// A [Cargo config](https://doc.rust-lang.org/cargo/reference/config.html#configuration)
50     /// file to use when gathering metadata
51     #[clap(long)]
52     pub cargo_config: Option<PathBuf>,
53 
54     /// The desired update/repin behavior. The arguments passed here are forward to
55     /// [cargo update](https://doc.rust-lang.org/cargo/commands/cargo-update.html). See
56     /// [crate::metadata::CargoUpdateRequest] for details on the values to pass here.
57     #[clap(long, env = "CARGO_BAZEL_REPIN", num_args=0..=1, default_missing_value = "true")]
58     pub repin: Option<CargoUpdateRequest>,
59 
60     /// The path to a Cargo metadata `json` file.
61     #[clap(long)]
62     pub metadata: Option<PathBuf>,
63 
64     /// The path to a bazel binary
65     #[clap(long, env = "BAZEL_REAL", default_value = "bazel")]
66     pub bazel: PathBuf,
67 
68     /// The directory in which to build the workspace. A `Cargo.toml` file
69     /// should always be produced within this directory.
70     #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
71     pub workspace_dir: PathBuf,
72 
73     /// If true, outputs will be printed instead of written to disk.
74     #[clap(long)]
75     pub dry_run: bool,
76 }
77 
78 /// Run buildifier on a given file.
buildifier_format(bin: &Path, file: &Path) -> Result<ExitStatus>79 fn buildifier_format(bin: &Path, file: &Path) -> Result<ExitStatus> {
80     let status = process::Command::new(bin)
81         .args(["-lint=fix", "-mode=fix", "-warnings=all"])
82         .arg(file)
83         .status()
84         .context("Failed to apply buildifier fixes")?;
85 
86     if !status.success() {
87         bail!(status)
88     }
89 
90     Ok(status)
91 }
92 
93 /// Query the Bazel output_base to determine the location of external repositories.
locate_bazel_output_base(bazel: &Path, workspace_dir: &Path) -> Result<PathBuf>94 fn locate_bazel_output_base(bazel: &Path, workspace_dir: &Path) -> Result<PathBuf> {
95     // Allow a predefined environment variable to take precedent. This
96     // solves for the specific needs of Bazel CI on Github.
97     if let Ok(output_base) = env::var("OUTPUT_BASE") {
98         return Ok(PathBuf::from(output_base));
99     }
100 
101     let output = process::Command::new(bazel)
102         .current_dir(workspace_dir)
103         .args(["info", "output_base"])
104         .output()
105         .context("Failed to query the Bazel workspace's `output_base`")?;
106 
107     if !output.status.success() {
108         bail!(output.status)
109     }
110 
111     Ok(PathBuf::from(
112         String::from_utf8_lossy(&output.stdout).trim(),
113     ))
114 }
115 
vendor(opt: VendorOptions) -> Result<()>116 pub fn vendor(opt: VendorOptions) -> Result<()> {
117     let output_base = locate_bazel_output_base(&opt.bazel, &opt.workspace_dir)?;
118 
119     // Load the all config files required for splicing a workspace
120     let splicing_manifest = SplicingManifest::try_from_path(&opt.splicing_manifest)?
121         .resolve(&opt.workspace_dir, &output_base);
122 
123     let temp_dir = tempfile::tempdir().context("Failed to create temporary directory")?;
124 
125     // Generate a splicer for creating a Cargo workspace manifest
126     let splicer = Splicer::new(PathBuf::from(temp_dir.as_ref()), splicing_manifest)
127         .context("Failed to create splicer")?;
128 
129     let cargo = Cargo::new(opt.cargo, opt.rustc.clone());
130 
131     // Splice together the manifest
132     let manifest_path = splicer
133         .splice_workspace(&cargo)
134         .context("Failed to splice workspace")?;
135 
136     // Gather a cargo lockfile
137     let cargo_lockfile = generate_lockfile(
138         &manifest_path,
139         &opt.cargo_lockfile,
140         cargo.clone(),
141         &opt.repin,
142     )?;
143 
144     // Load the config from disk
145     let config = Config::try_from_path(&opt.config)?;
146 
147     let resolver_data = TreeResolver::new(cargo.clone()).generate(
148         manifest_path.as_path_buf(),
149         &config.supported_platform_triples,
150     )?;
151 
152     // Write the registry url info to the manifest now that a lockfile has been generated
153     WorkspaceMetadata::write_registry_urls_and_feature_map(
154         &cargo,
155         &cargo_lockfile,
156         resolver_data,
157         manifest_path.as_path_buf(),
158         manifest_path.as_path_buf(),
159     )?;
160 
161     // Write metadata to the workspace for future reuse
162     let (cargo_metadata, cargo_lockfile) = Generator::new()
163         .with_cargo(cargo.clone())
164         .with_rustc(opt.rustc.clone())
165         .generate(manifest_path.as_path_buf())?;
166 
167     // Annotate metadata
168     let annotations = Annotations::new(cargo_metadata, cargo_lockfile.clone(), config.clone())?;
169 
170     // Generate renderable contexts for earch package
171     let context = Context::new(annotations, config.rendering.are_sources_present())?;
172 
173     // Render build files
174     let outputs = Renderer::new(
175         config.rendering.clone(),
176         config.supported_platform_triples.clone(),
177     )
178     .render(&context)?;
179 
180     // First ensure vendoring and rendering happen in a clean directory
181     let vendor_dir_label = render_module_label(&config.rendering.crates_module_template, "BUILD")?;
182     let vendor_dir = opt.workspace_dir.join(vendor_dir_label.package().unwrap());
183     if vendor_dir.exists() {
184         fs::remove_dir_all(&vendor_dir)
185             .with_context(|| format!("Failed to delete {}", vendor_dir.display()))?;
186     }
187 
188     // Store the updated Cargo.lock
189     if let Some(path) = &opt.cargo_lockfile {
190         fs::write(path, cargo_lockfile.to_string())
191             .context("Failed to write Cargo.lock file back to the workspace.")?;
192     }
193 
194     if matches!(config.rendering.vendor_mode, Some(VendorMode::Local)) {
195         VendorGenerator::new(cargo, opt.rustc.clone())
196             .generate(manifest_path.as_path_buf(), &vendor_dir)
197             .context("Failed to vendor dependencies")?;
198     }
199 
200     // make cargo versioned crates compatible with bazel labels
201     let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.workspace_dir);
202 
203     // buildifier files to check
204     let file_names: BTreeSet<PathBuf> = normalized_outputs.keys().cloned().collect();
205 
206     // Write outputs
207     write_outputs(normalized_outputs, opt.dry_run).context("Failed writing output files")?;
208 
209     // Optionally apply buildifier fixes
210     if let Some(buildifier_bin) = opt.buildifier {
211         for file in file_names {
212             let file_path = opt.workspace_dir.join(file);
213             buildifier_format(&buildifier_bin, &file_path)
214                 .with_context(|| format!("Failed to run buildifier on {}", file_path.display()))?;
215         }
216     }
217 
218     Ok(())
219 }
220