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::FeatureGenerator;
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
20 /// Command line options for the `vendor` subcommand
21 #[derive(Parser, Debug)]
22 #[clap(about = "Command line options for the `vendor` subcommand", version)]
23 pub struct VendorOptions {
24 /// The path to a Cargo binary to use for gathering metadata
25 #[clap(long, env = "CARGO")]
26 pub cargo: PathBuf,
27
28 /// The path to a rustc binary for use with Cargo
29 #[clap(long, env = "RUSTC")]
30 pub rustc: PathBuf,
31
32 /// The path to a buildifier binary for formatting generated BUILD files
33 #[clap(long)]
34 pub buildifier: Option<PathBuf>,
35
36 /// The config file with information about the Bazel and Cargo workspace
37 #[clap(long)]
38 pub config: PathBuf,
39
40 /// A generated manifest of splicing inputs
41 #[clap(long)]
42 pub splicing_manifest: PathBuf,
43
44 /// The path to a [Cargo.lock](https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html) file.
45 #[clap(long)]
46 pub cargo_lockfile: Option<PathBuf>,
47
48 /// A [Cargo config](https://doc.rust-lang.org/cargo/reference/config.html#configuration)
49 /// file to use when gathering metadata
50 #[clap(long)]
51 pub cargo_config: Option<PathBuf>,
52
53 /// The desired update/repin behavior. The arguments passed here are forward to
54 /// [cargo update](https://doc.rust-lang.org/cargo/commands/cargo-update.html). See
55 /// [crate::metadata::CargoUpdateRequest] for details on the values to pass here.
56 #[clap(long, env = "CARGO_BAZEL_REPIN", num_args=0..=1, default_missing_value = "true")]
57 pub repin: Option<CargoUpdateRequest>,
58
59 /// The path to a Cargo metadata `json` file.
60 #[clap(long)]
61 pub metadata: Option<PathBuf>,
62
63 /// The path to a bazel binary
64 #[clap(long, env = "BAZEL_REAL", default_value = "bazel")]
65 pub bazel: PathBuf,
66
67 /// The directory in which to build the workspace. A `Cargo.toml` file
68 /// should always be produced within this directory.
69 #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
70 pub workspace_dir: PathBuf,
71
72 /// If true, outputs will be printed instead of written to disk.
73 #[clap(long)]
74 pub dry_run: bool,
75 }
76
77 /// Run buildifier on a given file.
buildifier_format(bin: &Path, file: &Path) -> Result<ExitStatus>78 fn buildifier_format(bin: &Path, file: &Path) -> Result<ExitStatus> {
79 let status = process::Command::new(bin)
80 .args(["-lint=fix", "-mode=fix", "-warnings=all"])
81 .arg(file)
82 .status()
83 .context("Failed to apply buildifier fixes")?;
84
85 if !status.success() {
86 bail!(status)
87 }
88
89 Ok(status)
90 }
91
92 /// Query the Bazel output_base to determine the location of external repositories.
locate_bazel_output_base(bazel: &Path, workspace_dir: &Path) -> Result<PathBuf>93 fn locate_bazel_output_base(bazel: &Path, workspace_dir: &Path) -> Result<PathBuf> {
94 // Allow a predefined environment variable to take precedent. This
95 // solves for the specific needs of Bazel CI on Github.
96 if let Ok(output_base) = env::var("OUTPUT_BASE") {
97 return Ok(PathBuf::from(output_base));
98 }
99
100 let output = process::Command::new(bazel)
101 .current_dir(workspace_dir)
102 .args(["info", "output_base"])
103 .output()
104 .context("Failed to query the Bazel workspace's `output_base`")?;
105
106 if !output.status.success() {
107 bail!(output.status)
108 }
109
110 Ok(PathBuf::from(
111 String::from_utf8_lossy(&output.stdout).trim(),
112 ))
113 }
114
vendor(opt: VendorOptions) -> Result<()>115 pub fn vendor(opt: VendorOptions) -> Result<()> {
116 let output_base = locate_bazel_output_base(&opt.bazel, &opt.workspace_dir)?;
117
118 // Load the all config files required for splicing a workspace
119 let splicing_manifest = SplicingManifest::try_from_path(&opt.splicing_manifest)?
120 .resolve(&opt.workspace_dir, &output_base);
121
122 let temp_dir = tempfile::tempdir().context("Failed to create temporary directory")?;
123
124 // Generate a splicer for creating a Cargo workspace manifest
125 let splicer = Splicer::new(PathBuf::from(temp_dir.as_ref()), splicing_manifest)
126 .context("Failed to create splicer")?;
127
128 // Splice together the manifest
129 let manifest_path = splicer
130 .splice_workspace(&opt.cargo)
131 .context("Failed to splice workspace")?;
132
133 let cargo = Cargo::new(opt.cargo);
134
135 // Gather a cargo lockfile
136 let cargo_lockfile = generate_lockfile(
137 &manifest_path,
138 &opt.cargo_lockfile,
139 cargo.clone(),
140 &opt.rustc,
141 &opt.repin,
142 )?;
143
144 // Load the config from disk
145 let config = Config::try_from_path(&opt.config)?;
146
147 let feature_map = FeatureGenerator::new(cargo.clone(), opt.rustc.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 feature_map,
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)?;
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 // Cache the file names for potential use with buildifier
181 let file_names: BTreeSet<PathBuf> = outputs.keys().cloned().collect();
182
183 // First ensure vendoring and rendering happen in a clean directory
184 let vendor_dir_label = render_module_label(&config.rendering.crates_module_template, "BUILD")?;
185 let vendor_dir = opt.workspace_dir.join(vendor_dir_label.package().unwrap());
186 if vendor_dir.exists() {
187 fs::remove_dir_all(&vendor_dir)
188 .with_context(|| format!("Failed to delete {}", vendor_dir.display()))?;
189 }
190
191 // Store the updated Cargo.lock
192 if let Some(path) = &opt.cargo_lockfile {
193 fs::write(path, cargo_lockfile.to_string())
194 .context("Failed to write Cargo.lock file back to the workspace.")?;
195 }
196
197 // Vendor the crates from the spliced workspace
198 if matches!(config.rendering.vendor_mode, Some(VendorMode::Local)) {
199 VendorGenerator::new(cargo, opt.rustc.clone())
200 .generate(manifest_path.as_path_buf(), &vendor_dir)
201 .context("Failed to vendor dependencies")?;
202 }
203
204 // Write outputs
205 write_outputs(outputs, &opt.workspace_dir, opt.dry_run)
206 .context("Failed writing output files")?;
207
208 // Optionally apply buildifier fixes
209 if let Some(buildifier_bin) = opt.buildifier {
210 for file in file_names {
211 let file_path = opt.workspace_dir.join(file);
212 buildifier_format(&buildifier_bin, &file_path)
213 .with_context(|| format!("Failed to run buildifier on {}", file_path.display()))?;
214 }
215 }
216
217 Ok(())
218 }
219