Squashed 'third_party/rules_rust/' content from commit bf59038ca
git-subtree-dir: third_party/rules_rust
git-subtree-split: bf59038cac11798cbaef9f3bf965bad8182b97fa
Signed-off-by: Brian Silverman <bsilver16384@gmail.com>
Change-Id: I5a20e403203d670df467ea97dde9a4ac40339a8d
diff --git a/tools/rustfmt/srcs/main.rs b/tools/rustfmt/srcs/main.rs
new file mode 100644
index 0000000..4e4419f
--- /dev/null
+++ b/tools/rustfmt/srcs/main.rs
@@ -0,0 +1,185 @@
+use std::env;
+use std::path::PathBuf;
+use std::process::{Command, Stdio};
+use std::str;
+
+fn main() {
+ // Gather all command line and environment settings
+ let options = parse_args();
+
+ // Gather a list of all formattable targets
+ let targets = query_rustfmt_targets(&options);
+
+ // Run rustfmt on these targets
+ apply_rustfmt(&options, &targets);
+}
+
+/// Perform a `bazel` query to determine a list of Bazel targets which are to be formatted.
+fn query_rustfmt_targets(options: &Config) -> Vec<String> {
+ // Determine what packages to query
+ let scope = match options.packages.is_empty() {
+ true => "//...:all".to_owned(),
+ false => {
+ // Check to see if all the provided packages are actually targets
+ let is_all_targets = options
+ .packages
+ .iter()
+ .all(|pkg| match label::analyze(pkg) {
+ Ok(tgt) => tgt.name != "all",
+ Err(_) => false,
+ });
+
+ // Early return if a list of targets and not packages were provided
+ if is_all_targets {
+ return options.packages.clone();
+ }
+
+ options.packages.join(" + ")
+ }
+ };
+
+ let query_args = vec![
+ "query".to_owned(),
+ format!(
+ r#"kind('{types}', {scope}) except attr(tags, 'norustfmt', kind('{types}', {scope}))"#,
+ types = "^rust_",
+ scope = scope
+ ),
+ ];
+
+ let child = Command::new(&options.bazel)
+ .current_dir(&options.workspace)
+ .args(query_args)
+ .stdout(Stdio::piped())
+ .stderr(Stdio::inherit())
+ .spawn()
+ .expect("Failed to spawn bazel query command");
+
+ let output = child
+ .wait_with_output()
+ .expect("Failed to wait on spawned command");
+
+ if !output.status.success() {
+ std::process::exit(output.status.code().unwrap_or(1));
+ }
+
+ str::from_utf8(&output.stdout)
+ .expect("Invalid stream from command")
+ .split('\n')
+ .filter(|line| !line.is_empty())
+ .map(|line| line.to_string())
+ .collect()
+}
+
+/// Build a list of Bazel targets using the `rustfmt_aspect` to produce the
+/// arguments to use when formatting the sources of those targets.
+fn generate_rustfmt_target_manifests(options: &Config, targets: &[String]) {
+ let build_args = vec![
+ "build".to_owned(),
+ format!(
+ "--aspects={}//rust:defs.bzl%rustfmt_aspect",
+ env!("ASPECT_REPOSITORY")
+ ),
+ "--output_groups=rustfmt_manifest".to_owned(),
+ ];
+
+ let child = Command::new(&options.bazel)
+ .current_dir(&options.workspace)
+ .args(build_args)
+ .args(targets)
+ .stdout(Stdio::piped())
+ .stderr(Stdio::inherit())
+ .spawn()
+ .expect("Failed to spawn command");
+
+ let output = child
+ .wait_with_output()
+ .expect("Failed to wait on spawned command");
+
+ if !output.status.success() {
+ std::process::exit(output.status.code().unwrap_or(1));
+ }
+}
+
+/// Run rustfmt on a set of Bazel targets
+fn apply_rustfmt(options: &Config, targets: &[String]) {
+ // Ensure the targets are first built and a manifest containing `rustfmt`
+ // arguments are generated before formatting source files.
+ generate_rustfmt_target_manifests(options, targets);
+
+ for target in targets.iter() {
+ // Replace any `:` characters and strip leading slashes
+ let target_path = target.replace(':', "/").trim_start_matches('/').to_owned();
+
+ // Find a manifest for the current target. Not all targets will have one
+ let manifest = options.workspace.join("bazel-bin").join(format!(
+ "{}.{}",
+ &target_path,
+ rustfmt_lib::RUSTFMT_MANIFEST_EXTENSION,
+ ));
+
+ if !manifest.exists() {
+ continue;
+ }
+
+ // Load the manifest containing rustfmt arguments
+ let rustfmt_config = rustfmt_lib::parse_rustfmt_manifest(&manifest);
+
+ // Ignore any targets which do not have source files. This can
+ // occur in cases where all source files are generated.
+ if rustfmt_config.sources.is_empty() {
+ continue;
+ }
+
+ // Run rustfmt
+ let status = Command::new(&options.rustfmt_config.rustfmt)
+ .current_dir(&options.workspace)
+ .arg("--edition")
+ .arg(rustfmt_config.edition)
+ .arg("--config-path")
+ .arg(&options.rustfmt_config.config)
+ .args(rustfmt_config.sources)
+ .status()
+ .expect("Failed to run rustfmt");
+
+ if !status.success() {
+ std::process::exit(status.code().unwrap_or(1));
+ }
+ }
+}
+
+/// A struct containing details used for executing rustfmt.
+#[derive(Debug)]
+struct Config {
+ /// The path of the Bazel workspace root.
+ pub workspace: PathBuf,
+
+ /// The Bazel executable to use for builds and queries.
+ pub bazel: PathBuf,
+
+ /// Information about the current rustfmt binary to run.
+ pub rustfmt_config: rustfmt_lib::RustfmtConfig,
+
+ /// Optionally, users can pass a list of targets/packages/scopes
+ /// (eg `//my:target` or `//my/pkg/...`) to control the targets
+ /// to be formatted. If empty, all targets in the workspace will
+ /// be formatted.
+ pub packages: Vec<String>,
+}
+
+/// Parse command line arguments and environment variables to
+/// produce config data for running rustfmt.
+fn parse_args() -> Config {
+ Config{
+ workspace: PathBuf::from(
+ env::var("BUILD_WORKSPACE_DIRECTORY")
+ .expect("The environment variable BUILD_WORKSPACE_DIRECTORY is required for finding the workspace root")
+ ),
+ bazel: PathBuf::from(
+ env::var("BAZEL_REAL")
+ .unwrap_or_else(|_| "bazel".to_owned())
+ ),
+ rustfmt_config: rustfmt_lib::parse_rustfmt_config(),
+ packages: env::args().skip(1).collect(),
+ }
+}