cargo_bazel/cli/
vendor.rs1use std::collections::BTreeSet;
4use std::env;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::process::{self, ExitStatus};
8use std::sync::Arc;
9
10use anyhow::{bail, Context as AnyhowContext, Result};
11use camino::Utf8PathBuf;
12use clap::Parser;
13
14use crate::config::{Config, VendorMode};
15use crate::context::Context;
16use crate::metadata::CargoUpdateRequest;
17use crate::metadata::TreeResolver;
18use crate::metadata::{Annotations, Cargo, Generator, MetadataGenerator, VendorGenerator};
19use crate::rendering::{render_module_label, write_outputs, Renderer};
20use crate::splicing::{generate_lockfile, Splicer, SplicingManifest, WorkspaceMetadata};
21use crate::utils::normalize_cargo_file_paths;
22
23#[derive(Parser, Debug)]
25#[clap(about = "Command line options for the `vendor` subcommand", version)]
26pub struct VendorOptions {
27 #[clap(long, env = "CARGO")]
29 pub cargo: PathBuf,
30
31 #[clap(long, env = "RUSTC")]
33 pub rustc: PathBuf,
34
35 #[clap(long)]
37 pub buildifier: Option<PathBuf>,
38
39 #[clap(long)]
41 pub config: PathBuf,
42
43 #[clap(long)]
45 pub splicing_manifest: PathBuf,
46
47 #[clap(long)]
49 pub cargo_lockfile: Option<PathBuf>,
50
51 #[clap(long)]
54 pub cargo_config: Option<PathBuf>,
55
56 #[clap(long, env = "CARGO_BAZEL_REPIN", num_args=0..=1, default_missing_value = "true")]
60 pub repin: Option<CargoUpdateRequest>,
61
62 #[clap(long)]
64 pub metadata: Option<PathBuf>,
65
66 #[clap(long, env = "BAZEL_REAL", default_value = "bazel")]
68 pub bazel: PathBuf,
69
70 #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
73 pub workspace_dir: PathBuf,
74
75 #[clap(long)]
77 pub dry_run: bool,
78
79 #[clap(long)]
84 pub nonhermetic_root_bazel_workspace_dir: Utf8PathBuf,
85}
86
87fn buildifier_format(bin: &Path, file: &Path) -> Result<ExitStatus> {
89 let status = process::Command::new(bin)
90 .args(["-lint=fix", "-mode=fix", "-warnings=all"])
91 .arg(file)
92 .status()
93 .context("Failed to apply buildifier fixes")?;
94
95 if !status.success() {
96 bail!(status)
97 }
98
99 Ok(status)
100}
101
102fn locate_bazel_output_base(bazel: &Path, workspace_dir: &Path) -> Result<PathBuf> {
104 if let Ok(output_base) = env::var("OUTPUT_BASE") {
107 return Ok(PathBuf::from(output_base));
108 }
109
110 let output = process::Command::new(bazel)
111 .current_dir(workspace_dir)
112 .args(["info", "output_base"])
113 .output()
114 .context("Failed to query the Bazel workspace's `output_base`")?;
115
116 if !output.status.success() {
117 bail!(output.status)
118 }
119
120 Ok(PathBuf::from(
121 String::from_utf8_lossy(&output.stdout).trim(),
122 ))
123}
124
125pub fn vendor(opt: VendorOptions) -> Result<()> {
126 let output_base = locate_bazel_output_base(&opt.bazel, &opt.workspace_dir)?;
127
128 let splicing_manifest = SplicingManifest::try_from_path(&opt.splicing_manifest)?
130 .resolve(&opt.workspace_dir, &output_base);
131
132 let temp_dir = tempfile::tempdir().context("Failed to create temporary directory")?;
133 let temp_dir_path = Utf8PathBuf::from_path_buf(temp_dir.as_ref().to_path_buf())
134 .unwrap_or_else(|path| panic!("Temporary directory wasn't valid UTF-8: {:?}", path));
135
136 let splicer =
138 Splicer::new(temp_dir_path, splicing_manifest).context("Failed to create splicer")?;
139
140 let cargo = Cargo::new(opt.cargo, opt.rustc.clone());
141
142 let manifest_path = splicer
144 .splice_workspace(opt.nonhermetic_root_bazel_workspace_dir.as_std_path())
145 .context("Failed to splice workspace")?;
146
147 let cargo_lockfile = generate_lockfile(
149 &manifest_path,
150 &opt.cargo_lockfile,
151 cargo.clone(),
152 &opt.repin,
153 )?;
154
155 let config = Config::try_from_path(&opt.config)?;
157
158 let resolver_data = TreeResolver::new(cargo.clone()).generate(
159 manifest_path.as_path_buf(),
160 &config.supported_platform_triples,
161 )?;
162
163 WorkspaceMetadata::write_registry_urls_and_feature_map(
165 &cargo,
166 &cargo_lockfile,
167 resolver_data,
168 manifest_path.as_path_buf(),
169 manifest_path.as_path_buf(),
170 )?;
171
172 let (cargo_metadata, cargo_lockfile) = Generator::new()
174 .with_cargo(cargo.clone())
175 .with_rustc(opt.rustc.clone())
176 .generate(manifest_path.as_path_buf())?;
177
178 let annotations = Annotations::new(
180 cargo_metadata,
181 cargo_lockfile.clone(),
182 config.clone(),
183 &opt.nonhermetic_root_bazel_workspace_dir,
184 )?;
185
186 let context = Context::new(annotations, config.rendering.are_sources_present())?;
188
189 let outputs = Renderer::new(
191 Arc::new(config.rendering.clone()),
192 Arc::new(config.supported_platform_triples),
193 )
194 .render(&context, None)?;
195
196 let vendor_dir_label = render_module_label(&config.rendering.crates_module_template, "BUILD")?;
198 let vendor_dir = opt.workspace_dir.join(vendor_dir_label.package().unwrap());
199 if vendor_dir.exists() {
200 fs::remove_dir_all(&vendor_dir)
201 .with_context(|| format!("Failed to delete {}", vendor_dir.display()))?;
202 }
203
204 if let Some(path) = &opt.cargo_lockfile {
206 fs::write(path, cargo_lockfile.to_string())
207 .context("Failed to write Cargo.lock file back to the workspace.")?;
208 }
209
210 if matches!(config.rendering.vendor_mode, Some(VendorMode::Local)) {
211 VendorGenerator::new(cargo, opt.rustc.clone())
212 .generate(manifest_path.as_path_buf(), &vendor_dir)
213 .context("Failed to vendor dependencies")?;
214 }
215
216 let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.workspace_dir);
218
219 let file_names: BTreeSet<PathBuf> = normalized_outputs.keys().cloned().collect();
221
222 write_outputs(normalized_outputs, opt.dry_run).context("Failed writing output files")?;
224
225 if let Some(buildifier_bin) = opt.buildifier {
227 for file in file_names {
228 let file_path = opt.workspace_dir.join(file);
229 buildifier_format(&buildifier_bin, &file_path)
230 .with_context(|| format!("Failed to run buildifier on {}", file_path.display()))?;
231 }
232 }
233
234 Ok(())
235}