Refactored the Rustfmt tool to work cross-platform (#1375)
* Refactored rustfmt tool to work cross-platform
* Update tools/rustfmt/srcs/lib.rs
Co-authored-by: Daniel Wagner-Hall <dawagner@gmail.com>
* Assume `RUST_DEFAULT_EDITION` is always set.
* Unified edition querys
* Silenced rustfmt error in generated sources
Co-authored-by: Daniel Wagner-Hall <dawagner@gmail.com>
diff --git a/rust/private/BUILD.bazel b/rust/private/BUILD.bazel
index 9debaa3..40f4a69 100644
--- a/rust/private/BUILD.bazel
+++ b/rust/private/BUILD.bazel
@@ -1,5 +1,6 @@
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
load("//rust/private:rust_analyzer.bzl", "rust_analyzer_detect_sysroot")
+load("//rust/private:rustfmt.bzl", "rustfmt_workspace_name")
load("//rust/private:stamp.bzl", "stamp_build_setting")
bzl_library(
@@ -24,3 +25,8 @@
name = "rust_analyzer_detect_sysroot",
visibility = ["//visibility:public"],
)
+
+rustfmt_workspace_name(
+ name = "rustfmt_workspace_name",
+ visibility = ["//visibility:public"],
+)
diff --git a/rust/private/rustfmt.bzl b/rust/private/rustfmt.bzl
index 9c59f48..ca7b7ae 100644
--- a/rust/private/rustfmt.bzl
+++ b/rust/private/rustfmt.bzl
@@ -138,9 +138,10 @@
# The executable of a test target must be the output of an action in
# the rule implementation. This file is simply a symlink to the real
# rustfmt test runner.
+ is_windows = ctx.executable._runner.extension == ".exe"
runner = ctx.actions.declare_file("{}{}".format(
ctx.label.name,
- ctx.executable._runner.extension,
+ ".exe" if is_windows else "",
))
ctx.actions.symlink(
@@ -149,22 +150,33 @@
is_executable = True,
)
- manifests = [target[OutputGroupInfo].rustfmt_manifest for target in ctx.attr.targets]
+ manifests = depset(transitive = [target[OutputGroupInfo].rustfmt_manifest for target in ctx.attr.targets])
srcs = [depset(_find_rustfmtable_srcs(target)) for target in ctx.attr.targets]
runfiles = ctx.runfiles(
- transitive_files = depset(transitive = manifests + srcs),
+ transitive_files = depset(transitive = srcs + [manifests]),
)
runfiles = runfiles.merge(
ctx.attr._runner[DefaultInfo].default_runfiles,
)
- return [DefaultInfo(
- files = depset([runner]),
- runfiles = runfiles,
- executable = runner,
- )]
+ path_env_sep = ";" if is_windows else ":"
+
+ return [
+ DefaultInfo(
+ files = depset([runner]),
+ runfiles = runfiles,
+ executable = runner,
+ ),
+ testing.TestEnvironment({
+ "RUSTFMT_MANIFESTS": path_env_sep.join([
+ manifest.short_path
+ for manifest in sorted(manifests.to_list())
+ ]),
+ "RUST_BACKTRACE": "1",
+ }),
+ ]
rustfmt_test = rule(
implementation = _rustfmt_test_impl,
@@ -184,3 +196,22 @@
},
test = True,
)
+
+def _rustfmt_workspace_name_impl(ctx):
+ output = ctx.actions.declare_file(ctx.label.name)
+
+ ctx.actions.write(
+ output = output,
+ content = "RUSTFMT_WORKSPACE={}".format(
+ ctx.workspace_name,
+ ),
+ )
+
+ return [DefaultInfo(
+ files = depset([output]),
+ )]
+
+rustfmt_workspace_name = rule(
+ implementation = _rustfmt_workspace_name_impl,
+ doc = "A rule for detecting the workspace name for Rustfmt runfiles.",
+)
diff --git a/test/generated_inputs/BUILD.bazel b/test/generated_inputs/BUILD.bazel
index 3ed32d9..2820282 100644
--- a/test/generated_inputs/BUILD.bazel
+++ b/test/generated_inputs/BUILD.bazel
@@ -28,6 +28,7 @@
":src.rs",
],
edition = "2018",
+ tags = ["norustfmt"],
)
rust_library(
@@ -38,6 +39,7 @@
],
crate_root = "lib.rs",
edition = "2018",
+ tags = ["norustfmt"],
)
rust_library(
@@ -50,6 +52,7 @@
crate_root = ":src.rs",
edition = "2018",
rustc_flags = ["--cfg=generated_file_as_root"],
+ tags = ["norustfmt"],
)
# When no lib.rs, main.rs file exists, we try to use the file that carries
@@ -62,12 +65,14 @@
],
edition = "2018",
rustc_flags = ["--cfg=generated_file_as_root"],
+ tags = ["norustfmt"],
)
rust_test(
name = "generated_src_test",
crate = ":use_generated_src_with_crate_root_defined",
edition = "2018",
+ tags = ["norustfmt"],
)
rust_binary(
@@ -77,6 +82,7 @@
":src.rs",
],
edition = "2018",
+ tags = ["norustfmt"],
)
input_from_different_cfg(
@@ -93,12 +99,14 @@
name = "generated_src_in_different_cfg_as_root",
srcs = [":input_from_different_cfg"],
edition = "2018",
+ tags = ["norustfmt"],
)
rust_test(
name = "generated_src_in_different_cfg_as_root_test",
crate = "generated_src_in_different_cfg_as_root",
edition = "2018",
+ tags = ["norustfmt"],
)
rust_library(
@@ -109,12 +117,14 @@
],
crate_root = "root.rs",
edition = "2018",
+ tags = ["norustfmt"],
)
rust_test(
name = "generated_src_in_different_cfg_test",
crate = "generated_src_in_different_cfg",
edition = "2018",
+ tags = ["norustfmt"],
)
bool_flag(
diff --git a/test/generated_inputs/lib_for_src.rs b/test/generated_inputs/lib_for_src.rs
index 697bc9d..6d2a037 100644
--- a/test/generated_inputs/lib_for_src.rs
+++ b/test/generated_inputs/lib_for_src.rs
@@ -1 +1,3 @@
-pub fn forty_two() -> i32 { 42 }
+pub fn forty_two() -> i32 {
+ 42
+}
diff --git a/tools/rustfmt/BUILD.bazel b/tools/rustfmt/BUILD.bazel
index db9faa6..b1e2e38 100644
--- a/tools/rustfmt/BUILD.bazel
+++ b/tools/rustfmt/BUILD.bazel
@@ -23,6 +23,12 @@
"RUSTFMT": "$(rootpath //rust/toolchain:current_exec_rustfmt_files)",
"RUSTFMT_CONFIG": "$(rootpath //:rustfmt.toml)",
},
+ rustc_env_files = [
+ "@rules_rust//rust/private:rustfmt_workspace_name",
+ ],
+ deps = [
+ "//tools/runfiles",
+ ],
)
rust_binary(
@@ -36,7 +42,11 @@
edition = "2018",
rustc_env = {
"ASPECT_REPOSITORY": aspect_repository(),
+ "RUST_DEFAULT_EDITION": "$(RUST_DEFAULT_EDITION)",
},
+ toolchains = [
+ "@rules_rust//rust/toolchain:current_rust_toolchain",
+ ],
deps = [
":rustfmt_lib",
"//util/label",
diff --git a/tools/rustfmt/srcs/lib.rs b/tools/rustfmt/srcs/lib.rs
index ad2e86a..2d8accc 100644
--- a/tools/rustfmt/srcs/lib.rs
+++ b/tools/rustfmt/srcs/lib.rs
@@ -5,18 +5,6 @@
/// The expected extension of rustfmt manifest files generated by `rustfmt_aspect`.
pub const RUSTFMT_MANIFEST_EXTENSION: &str = "rustfmt";
-/// Generate an absolute path to a file without resolving symlinks
-fn absolutify_existing<T: AsRef<Path>>(path: &T) -> std::io::Result<PathBuf> {
- let absolute_path = if path.as_ref().is_absolute() {
- path.as_ref().to_owned()
- } else {
- std::env::current_dir()
- .expect("Failed to get working directory")
- .join(path)
- };
- std::fs::metadata(&absolute_path).map(|_| absolute_path)
-}
-
/// A struct containing details used for executing rustfmt.
#[derive(Debug)]
pub struct RustfmtConfig {
@@ -31,11 +19,26 @@
/// Parse command line arguments and environment variables to
/// produce config data for running rustfmt.
pub fn parse_rustfmt_config() -> RustfmtConfig {
- RustfmtConfig {
- rustfmt: absolutify_existing(&env!("RUSTFMT")).expect("Unable to find rustfmt binary"),
- config: absolutify_existing(&env!("RUSTFMT_CONFIG"))
- .expect("Unable to find rustfmt config file"),
+ let runfiles = runfiles::Runfiles::create().unwrap();
+
+ let rustfmt = runfiles.rlocation(concat!(env!("RUSTFMT_WORKSPACE"), "/", env!("RUSTFMT")));
+ if !rustfmt.exists() {
+ panic!("rustfmt does not exist at: {}", rustfmt.display());
}
+
+ let config = runfiles.rlocation(concat!(
+ env!("RUSTFMT_WORKSPACE"),
+ "/",
+ env!("RUSTFMT_CONFIG")
+ ));
+ if !config.exists() {
+ panic!(
+ "rustfmt config file does not exist at: {}",
+ config.display()
+ );
+ }
+
+ RustfmtConfig { rustfmt, config }
}
/// A struct of target specific information for use in running `rustfmt`.
@@ -45,7 +48,7 @@
pub edition: String,
/// A list of all (non-generated) source files for formatting.
- pub sources: Vec<String>,
+ pub sources: Vec<PathBuf>,
}
/// Parse rustfmt flags from a manifest generated by builds using `rustfmt_aspect`.
@@ -67,8 +70,33 @@
.parse::<i32>()
.expect("The edition should be a numeric value. eg `2018`.");
+ let runfiles = runfiles::Runfiles::create().unwrap();
+
RustfmtManifest {
edition,
- sources: lines,
+ sources: lines
+ .into_iter()
+ .map(|src| runfiles.rlocation(format!("{}/{}", env!("RUSTFMT_WORKSPACE"), src)))
+ .collect(),
}
}
+
+#[cfg(target_family = "windows")]
+const PATH_ENV_SEP: &str = ";";
+
+#[cfg(target_family = "unix")]
+const PATH_ENV_SEP: &str = ":";
+
+/// Parse the runfiles of the current executable for manifests generated
+/// by the `rustfmt_aspect` aspect.
+pub fn find_manifests() -> Vec<PathBuf> {
+ let runfiles = runfiles::Runfiles::create().unwrap();
+
+ std::env::var("RUSTFMT_MANIFESTS")
+ .map(|var| {
+ var.split(PATH_ENV_SEP)
+ .map(|path| runfiles.rlocation(format!("{}/{}", env!("RUSTFMT_WORKSPACE"), path)))
+ .collect()
+ })
+ .unwrap_or_default()
+}
diff --git a/tools/rustfmt/srcs/main.rs b/tools/rustfmt/srcs/main.rs
index 4e4419f..dc29968 100644
--- a/tools/rustfmt/srcs/main.rs
+++ b/tools/rustfmt/srcs/main.rs
@@ -1,8 +1,12 @@
+//! A tool for querying Rust source files wired into Bazel and running Rustfmt on them.
+
+use std::collections::HashMap;
use std::env;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str;
+/// The Bazel Rustfmt tool entry point
fn main() {
// Gather all command line and environment settings
let options = parse_args();
@@ -14,52 +18,40 @@
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,
- });
+/// The edition to use in cases where the default edition is unspecified by Bazel
+const FALLBACK_EDITION: &str = "2018";
- // Early return if a list of targets and not packages were provided
- if is_all_targets {
- return options.packages.clone();
- }
+/// Determine the Rust edition to use in cases where a target has not explicitly
+/// specified the edition via an `edition` attribute.
+fn get_default_edition() -> &'static str {
+ if !env!("RUST_DEFAULT_EDITION").is_empty() {
+ env!("RUST_DEFAULT_EDITION")
+ } else {
+ FALLBACK_EDITION
+ }
+}
- options.packages.join(" + ")
- }
- };
+/// Get a list of all editions to run formatting for
+fn get_editions() -> Vec<String> {
+ vec!["2015".to_owned(), "2018".to_owned(), "2021".to_owned()]
+}
- 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)
+/// Run a bazel command, capturing stdout while streaming stderr to surface errors
+fn bazel_command(bazel_bin: &Path, args: &[String], current_dir: &Path) -> Vec<String> {
+ let child = Command::new(bazel_bin)
+ .current_dir(current_dir)
+ .args(args)
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
- .expect("Failed to spawn bazel query command");
+ .expect("Failed to spawn bazel command");
let output = child
.wait_with_output()
.expect("Failed to wait on spawned command");
if !output.status.success() {
+ eprintln!("Failed to perform `bazel query` command.");
std::process::exit(output.status.code().unwrap_or(1));
}
@@ -71,74 +63,92 @@
.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(),
+/// The regex representation of an empty `edition` attribute
+const EMPTY_EDITION: &str = "^$";
+
+/// Query for all `*.rs` files in a workspace that are dependencies of targets with the requested edition.
+fn edition_query(bazel_bin: &Path, edition: &str, scope: &str, current_dir: &Path) -> Vec<String> {
+ let query_args = vec![
+ "query".to_owned(),
+ // Query explanation:
+ // Filter all local targets ending in `*.rs`.
+ // Get all source files.
+ // Get direct dependencies.
+ // Get all targets with the specified `edition` attribute.
+ // Except for targets tagged with `norustfmt`.
+ // And except for targets with a populated `crate` attribute since `crate` defines edition for this target
format!(
- "--aspects={}//rust:defs.bzl%rustfmt_aspect",
- env!("ASPECT_REPOSITORY")
+ r#"let scope = set({scope}) in filter("^//.*\.rs$", kind("source file", deps(attr(edition, "{edition}", $scope) except attr(tags, "(^\[|, )norustfmt(, |\]$)", $scope) + attr(crate, ".*", $scope), 1)))"#,
+ edition = edition,
+ scope = scope,
),
- "--output_groups=rustfmt_manifest".to_owned(),
+ "--keep_going".to_owned(),
+ "--noimplicit_deps".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");
+ bazel_command(bazel_bin, &query_args, current_dir)
+}
- let output = child
- .wait_with_output()
- .expect("Failed to wait on spawned command");
+/// Perform a `bazel` query to determine all source files which are to be
+/// formatted for particular Rust editions.
+fn query_rustfmt_targets(options: &Config) -> HashMap<String, Vec<String>> {
+ let scope = options
+ .packages
+ .clone()
+ .into_iter()
+ .reduce(|acc, item| acc + " " + &item)
+ .unwrap_or_else(|| "//...:all".to_owned());
- if !output.status.success() {
- std::process::exit(output.status.code().unwrap_or(1));
- }
+ let editions = get_editions();
+ let default_edition = get_default_edition();
+
+ editions
+ .into_iter()
+ .map(|edition| {
+ let mut targets = edition_query(&options.bazel, &edition, &scope, &options.workspace);
+
+ // For all targets relying on the toolchain for it's edition,
+ // query anything with an unset edition
+ if edition == default_edition {
+ targets.extend(edition_query(
+ &options.bazel,
+ EMPTY_EDITION,
+ &scope,
+ &options.workspace,
+ ))
+ }
+
+ (edition, targets)
+ })
+ .collect()
}
/// 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);
+fn apply_rustfmt(options: &Config, editions_and_targets: &HashMap<String, Vec<String>>) {
+ // There is no work to do if the list of targets is empty
+ if editions_and_targets.is_empty() {
+ return;
+ }
- 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() {
+ for (edition, targets) in editions_and_targets.iter() {
+ if targets.is_empty() {
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;
- }
+ // Get paths to all formattable sources
+ let sources: Vec<String> = targets
+ .iter()
+ .map(|target| target.replace(':', "/").trim_start_matches('/').to_owned())
+ .collect();
// Run rustfmt
let status = Command::new(&options.rustfmt_config.rustfmt)
.current_dir(&options.workspace)
.arg("--edition")
- .arg(rustfmt_config.edition)
+ .arg(edition)
.arg("--config-path")
.arg(&options.rustfmt_config.config)
- .args(rustfmt_config.sources)
+ .args(sources)
.status()
.expect("Failed to run rustfmt");
diff --git a/tools/rustfmt/srcs/test_main.rs b/tools/rustfmt/srcs/test_main.rs
index ea2adba..57cd203 100644
--- a/tools/rustfmt/srcs/test_main.rs
+++ b/tools/rustfmt/srcs/test_main.rs
@@ -1,5 +1,4 @@
-use std::fs;
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
use std::process::Command;
fn main() {
@@ -55,31 +54,9 @@
pub manifests: Vec<rustfmt_lib::RustfmtManifest>,
}
-/// Parse the runfiles of the current executable for manifests generated
-/// but the `rustfmt_aspect` aspect.
-fn find_manifests(dir: &Path, manifests: &mut Vec<PathBuf>) {
- if dir.is_dir() {
- for entry in fs::read_dir(dir).expect("Failed to read directory contents") {
- let entry = entry.expect("Failed to read directory entry");
- let path = entry.path();
- if path.is_dir() {
- find_manifests(&path, manifests);
- } else if let Some(ext) = path.extension() {
- if ext == rustfmt_lib::RUSTFMT_MANIFEST_EXTENSION {
- manifests.extend(vec![path]);
- }
- }
- }
- }
-}
-
/// Parse settings from the environment into a config struct
fn parse_args() -> Config {
- let mut manifests: Vec<PathBuf> = Vec::new();
- find_manifests(
- &runfiles::find_runfiles_dir().expect("Failed to find runfiles directory"),
- &mut manifests,
- );
+ let manifests: Vec<PathBuf> = rustfmt_lib::find_manifests();
if manifests.is_empty() {
panic!("No manifests were found");