|
1 | 1 | //! The cli entrypoint for the `vendor` subcommand |
2 | 2 |
|
3 | | -use std::collections::{BTreeSet, HashMap}; |
| 3 | +use std::collections::{BTreeMap, HashMap}; |
4 | 4 | use std::env; |
5 | 5 | use std::fs; |
| 6 | +use std::io::Write; |
6 | 7 | use std::path::{Path, PathBuf}; |
7 | 8 | use std::process::{self, ExitStatus}; |
8 | 9 | use std::sync::Arc; |
@@ -89,19 +90,41 @@ pub struct VendorOptions { |
89 | 90 | pub nonhermetic_root_bazel_workspace_dir: Utf8PathBuf, |
90 | 91 | } |
91 | 92 |
|
92 | | -/// Run buildifier on a given file. |
93 | | -fn buildifier_format(bin: &Path, file: &Path) -> anyhow::Result<ExitStatus> { |
94 | | - let status = process::Command::new(bin) |
| 93 | +/// Format content via buildifier's stdin/stdout, avoiding the need to write |
| 94 | +/// the file to disk before formatting. The `path` argument is passed as |
| 95 | +/// `--path` so buildifier can infer the file type (BUILD vs .bzl). |
| 96 | +/// |
| 97 | +/// See <https://github.com/bazelbuild/rules_rust/issues/2972>. |
| 98 | +fn buildifier_format(bin: &Path, content: &str, path: &Path) -> anyhow::Result<String> { |
| 99 | + let mut child = process::Command::new(bin) |
95 | 100 | .args(["-lint=fix", "-mode=fix", "-warnings=all"]) |
96 | | - .arg(file) |
97 | | - .status() |
98 | | - .context("Failed to apply buildifier fixes")?; |
99 | | - |
100 | | - if !status.success() { |
101 | | - bail!(status) |
| 101 | + .arg(format!("--path={}", path.display())) |
| 102 | + .stdin(process::Stdio::piped()) |
| 103 | + .stdout(process::Stdio::piped()) |
| 104 | + .stderr(process::Stdio::piped()) |
| 105 | + .spawn() |
| 106 | + .context("Failed to spawn buildifier")?; |
| 107 | + |
| 108 | + child |
| 109 | + .stdin |
| 110 | + .take() |
| 111 | + .unwrap() |
| 112 | + .write_all(content.as_bytes()) |
| 113 | + .context("Failed to write to buildifier stdin")?; |
| 114 | + |
| 115 | + let output = child |
| 116 | + .wait_with_output() |
| 117 | + .context("Failed to wait for buildifier")?; |
| 118 | + |
| 119 | + if !output.status.success() { |
| 120 | + bail!( |
| 121 | + "buildifier failed on {}: {}", |
| 122 | + path.display(), |
| 123 | + String::from_utf8_lossy(&output.stderr) |
| 124 | + ); |
102 | 125 | } |
103 | 126 |
|
104 | | - Ok(status) |
| 127 | + String::from_utf8(output.stdout).context("buildifier produced invalid UTF-8") |
105 | 128 | } |
106 | 129 |
|
107 | 130 | /// Run `bazel mod tidy` in a workspace. |
@@ -295,21 +318,25 @@ pub fn vendor(opt: VendorOptions) -> anyhow::Result<()> { |
295 | 318 | // make cargo versioned crates compatible with bazel labels |
296 | 319 | let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.workspace_dir); |
297 | 320 |
|
298 | | - // buildifier files to check |
299 | | - let file_names: BTreeSet<PathBuf> = normalized_outputs.keys().cloned().collect(); |
| 321 | + // Optionally format outputs through buildifier before writing to disk. |
| 322 | + // Piping via stdin avoids a race where a freshly-written file may not yet |
| 323 | + // be visible to the buildifier subprocess. |
| 324 | + let normalized_outputs = if let Some(ref buildifier_bin) = opt.buildifier { |
| 325 | + normalized_outputs |
| 326 | + .into_iter() |
| 327 | + .map(|(path, content)| { |
| 328 | + let formatted = buildifier_format(buildifier_bin, &content, &path) |
| 329 | + .with_context(|| format!("Failed to run buildifier on {}", path.display()))?; |
| 330 | + Ok((path, formatted)) |
| 331 | + }) |
| 332 | + .collect::<anyhow::Result<BTreeMap<_, _>>>()? |
| 333 | + } else { |
| 334 | + normalized_outputs |
| 335 | + }; |
300 | 336 |
|
301 | 337 | // Write outputs |
302 | 338 | write_outputs(normalized_outputs, opt.dry_run).context("Failed writing output files")?; |
303 | 339 |
|
304 | | - // Optionally apply buildifier fixes |
305 | | - if let Some(buildifier_bin) = opt.buildifier { |
306 | | - for file in file_names { |
307 | | - let file_path = opt.workspace_dir.join(file); |
308 | | - buildifier_format(&buildifier_bin, &file_path) |
309 | | - .with_context(|| format!("Failed to run buildifier on {}", file_path.display()))?; |
310 | | - } |
311 | | - } |
312 | | - |
313 | 340 | // Optionally perform bazel mod tidy to update the MODULE.bazel file |
314 | 341 | if bazel_info.release >= semver::Version::new(7, 0, 0) { |
315 | 342 | let module_bazel = opt.workspace_dir.join("MODULE.bazel"); |
|
0 commit comments