Added `rustfmt` rules and aspects. (#722)
* Added `rustfmt` rules and aspects
* Regenerate documentation
* Update rust/private/rustfmt.bzl
Co-authored-by: Daniel Wagner-Hall <dawagner@gmail.com>
* Update test/rustfmt/rustfmt_failure_test.sh
Co-authored-by: Daniel Wagner-Hall <dawagner@gmail.com>
* Update tools/rustfmt/srcs/main.rs
Co-authored-by: Daniel Wagner-Hall <dawagner@gmail.com>
* Created a helper function for parsing formattable srcs
* Regenerate documentation
* Remove `canonicalize`
* Combined `rustfmt_check_aspect` into `rustfmt_aspect`.
* Regenerate documentation
* Label fields should be public so they're usable outside of the crate.
* The `rustfmt` rule binaries can now take a list of targets
* Fixed missing configs
* Added a test rule and used a build setting to control the rustfmt config
* Regenerate documentation
* Updated function name
* Updated comment
* Updated rustmt query to be more broad
* Updated rustfmt manifest format
* Regenerate documentation
Co-authored-by: Daniel Wagner-Hall <dawagner@gmail.com>
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index 0e386bb..06af059 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -100,6 +100,20 @@
platform: ubuntu1804
shell_commands:
- ./test/clippy/clippy_failure_test.sh
+ rustfmt_examples:
+ name: Rustfmt on Examples
+ platform: ubuntu2004
+ working_directory: examples
+ build_flags:
+ - "--aspects=@rules_rust//rust:defs.bzl%rustfmt_aspect"
+ - "--output_groups=rustfmt_checks"
+ build_targets:
+ - //...
+ rustfmt_failure:
+ name: Negative Rustfmt Tests
+ platform: ubuntu2004
+ run_targets:
+ - "//test/rustfmt:test_runner"
ubuntu2004_clang:
name: Ubuntu 20.04 with Clang
platform: ubuntu2004
diff --git a/BUILD.bazel b/BUILD.bazel
index feb93a2..72110d5 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -17,3 +17,10 @@
build_setting_default = "human",
visibility = ["//visibility:public"],
)
+
+# This setting is used by the rustfmt rules. See https://bazelbuild.github.io/rules_rust/rust_fmt.html
+label_flag(
+ name = "rustfmt.toml",
+ build_setting_default = "//tools/rustfmt:rustfmt.toml",
+ visibility = ["//visibility:public"],
+)
diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel
index 64e2dd0..acb3ade 100644
--- a/docs/BUILD.bazel
+++ b/docs/BUILD.bazel
@@ -89,6 +89,14 @@
],
),
page(
+ name = "rust_fmt",
+ header_template = ":rust_fmt.vm",
+ symbols = [
+ "rustfmt_aspect",
+ "rustfmt_test",
+ ],
+ ),
+ page(
name = "rust_proto",
symbols = [
"rust_grpc_library",
diff --git a/docs/flatten.md b/docs/flatten.md
index 6a0bca5..9007579 100644
--- a/docs/flatten.md
+++ b/docs/flatten.md
@@ -33,6 +33,8 @@
* [rust_wasm_bindgen](#rust_wasm_bindgen)
* [rust_wasm_bindgen_repositories](#rust_wasm_bindgen_repositories)
* [rust_wasm_bindgen_toolchain](#rust_wasm_bindgen_toolchain)
+* [rustfmt_aspect](#rustfmt_aspect)
+* [rustfmt_test](#rustfmt_test)
<a id="#crate_universe"></a>
@@ -1281,6 +1283,25 @@
| <a id="rust_wasm_bindgen_toolchain-bindgen"></a>bindgen | The label of a <code>wasm-bindgen-cli</code> executable. | <a href="https://bazel.build/docs/build-ref.html#labels">Label</a> | optional | None |
+<a id="#rustfmt_test"></a>
+
+## rustfmt_test
+
+<pre>
+rustfmt_test(<a href="#rustfmt_test-name">name</a>, <a href="#rustfmt_test-targets">targets</a>)
+</pre>
+
+A test rule for performing `rustfmt --check` on a set of targets
+
+**ATTRIBUTES**
+
+
+| Name | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="rustfmt_test-name"></a>name | A unique name for this target. | <a href="https://bazel.build/docs/build-ref.html#name">Name</a> | required | |
+| <a id="rustfmt_test-targets"></a>targets | Rust targets to run <code>rustfmt --check</code> on. | <a href="https://bazel.build/docs/build-ref.html#labels">List of labels</a> | optional | [] |
+
+
<a id="#cargo_build_script"></a>
## cargo_build_script
@@ -1757,3 +1778,40 @@
| <a id="rust_clippy_aspect-name"></a>name | A unique name for this target. | <a href="https://bazel.build/docs/build-ref.html#name">Name</a> | required | |
+<a id="#rustfmt_aspect"></a>
+
+## rustfmt_aspect
+
+<pre>
+rustfmt_aspect(<a href="#rustfmt_aspect-name">name</a>)
+</pre>
+
+This aspect is used to gather information about a crate for use in rustfmt and perform rustfmt checks
+
+Output Groups:
+
+- `rustfmt_manifest`: A manifest used by rustfmt binaries to provide crate specific settings.
+- `rustfmt_checks`: Executes `rustfmt --check` on the specified target.
+
+The build setting `@rules_rust//:rustfmt.toml` is used to control the Rustfmt [configuration settings][cs]
+used at runtime.
+
+[cs]: https://rust-lang.github.io/rustfmt/
+
+This aspect is executed on any target which provides the `CrateInfo` provider. However
+users may tag a target with `norustfmt` to have it skipped. Additionally, generated
+source files are also ignored by this aspect.
+
+
+**ASPECT ATTRIBUTES**
+
+
+
+**ATTRIBUTES**
+
+
+| Name | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="rustfmt_aspect-name"></a>name | A unique name for this target. | <a href="https://bazel.build/docs/build-ref.html#name">Name</a> | required | |
+
+
diff --git a/docs/index.md b/docs/index.md
index 6658de5..ad388c0 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -42,6 +42,7 @@
- [defs](defs.md): standard rust rules for building and testing libraries and binaries.
- [rust_doc](rust_doc.md): rules for generating and testing rust documentation.
- [rust_clippy](rust_clippy.md): rules for running [clippy](https://github.com/rust-lang/rust-clippy#readme).
+- [rust_fmt](rust_fmt.md): rules for running [rustfmt](https://github.com/rust-lang/rustfmt#readme).
- [rust_proto](rust_proto.md): rules for generating [protobuf](https://developers.google.com/protocol-buffers).
and [gRPC](https://grpc.io) stubs.
- [rust_bindgen](rust_bindgen.md): rules for generating C++ bindings.
diff --git a/docs/rust_fmt.md b/docs/rust_fmt.md
new file mode 100644
index 0000000..fed766c
--- /dev/null
+++ b/docs/rust_fmt.md
@@ -0,0 +1,104 @@
+<!-- Generated with Stardoc: http://skydoc.bazel.build -->
+# Rust Fmt
+
+* [rustfmt_aspect](#rustfmt_aspect)
+* [rustfmt_test](#rustfmt_test)
+
+
+## Overview
+
+
+[Rustfmt][rustfmt] is a tool for formatting Rust code according to style guidelines.
+By default, Rustfmt uses a style which conforms to the [Rust style guide][rsg] that
+has been formalized through the [style RFC process][rfcp]. A complete list of all
+configuration options can be found in the [Rustfmt GitHub Pages][rgp].
+
+
+
+### Setup
+
+
+Formatting your Rust targets' source code requires no setup outside of loading `rules_rust`
+in your workspace. Simply run `bazel run @rules_rust//tools/rustfmt` to format source code.
+
+In addition to this formatter, a check can be added to your build phase using the [rustfmt_aspect](#rustfmt-aspect)
+aspect. Simply add the following to a `.bazelrc` file to enable this check.
+
+```text
+build --aspects=@rules_rust//rust:defs.bzl%rustfmt_aspect
+build --output_groups=+rustfmt_checks
+```
+
+It's recommended to only enable this aspect in your CI environment so formatting issues do not
+impact user's ability to rapidly iterate on changes.
+
+The `rustfmt_aspect` also uses a `--@rules_rust//:rustfmt.toml` setting which determines the
+[configuration file][rgp] used by the formatter (`@rules_rust//tools/rustfmt`) and the aspect
+(`rustfmt_aspect`). This flag can be added to your `.bazelrc` file to ensure a consistent config
+file is used whenever `rustfmt` is run:
+
+```text
+build --@rules_rust//:rustfmt.toml=//:rustfmt.toml
+```
+
+[rustfmt]: https://github.com/rust-lang/rustfmt#readme
+[rsg]: https://github.com/rust-lang-nursery/fmt-rfcs/blob/master/guide/guide.md
+[rfcp]: https://github.com/rust-lang-nursery/fmt-rfcs
+[rgp]: https://rust-lang.github.io/rustfmt/
+
+<a id="#rustfmt_test"></a>
+
+## rustfmt_test
+
+<pre>
+rustfmt_test(<a href="#rustfmt_test-name">name</a>, <a href="#rustfmt_test-targets">targets</a>)
+</pre>
+
+A test rule for performing `rustfmt --check` on a set of targets
+
+**ATTRIBUTES**
+
+
+| Name | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="rustfmt_test-name"></a>name | A unique name for this target. | <a href="https://bazel.build/docs/build-ref.html#name">Name</a> | required | |
+| <a id="rustfmt_test-targets"></a>targets | Rust targets to run <code>rustfmt --check</code> on. | <a href="https://bazel.build/docs/build-ref.html#labels">List of labels</a> | optional | [] |
+
+
+<a id="#rustfmt_aspect"></a>
+
+## rustfmt_aspect
+
+<pre>
+rustfmt_aspect(<a href="#rustfmt_aspect-name">name</a>)
+</pre>
+
+This aspect is used to gather information about a crate for use in rustfmt and perform rustfmt checks
+
+Output Groups:
+
+- `rustfmt_manifest`: A manifest used by rustfmt binaries to provide crate specific settings.
+- `rustfmt_checks`: Executes `rustfmt --check` on the specified target.
+
+The build setting `@rules_rust//:rustfmt.toml` is used to control the Rustfmt [configuration settings][cs]
+used at runtime.
+
+[cs]: https://rust-lang.github.io/rustfmt/
+
+This aspect is executed on any target which provides the `CrateInfo` provider. However
+users may tag a target with `norustfmt` to have it skipped. Additionally, generated
+source files are also ignored by this aspect.
+
+
+**ASPECT ATTRIBUTES**
+
+
+
+**ATTRIBUTES**
+
+
+| Name | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="rustfmt_aspect-name"></a>name | A unique name for this target. | <a href="https://bazel.build/docs/build-ref.html#name">Name</a> | required | |
+
+
diff --git a/docs/rust_fmt.vm b/docs/rust_fmt.vm
new file mode 100644
index 0000000..a21cdf3
--- /dev/null
+++ b/docs/rust_fmt.vm
@@ -0,0 +1,41 @@
+#[[
+## Overview
+]]#
+
+[Rustfmt][rustfmt] is a tool for formatting Rust code according to style guidelines.
+By default, Rustfmt uses a style which conforms to the [Rust style guide][rsg] that
+has been formalized through the [style RFC process][rfcp]. A complete list of all
+configuration options can be found in the [Rustfmt GitHub Pages][rgp].
+
+
+#[[
+### Setup
+]]#
+
+Formatting your Rust targets' source code requires no setup outside of loading `rules_rust`
+in your workspace. Simply run `bazel run @rules_rust//tools/rustfmt` to format source code.
+
+In addition to this formatter, a check can be added to your build phase using the [rustfmt_aspect](#rustfmt-aspect)
+aspect. Simply add the following to a `.bazelrc` file to enable this check.
+
+```text
+build --aspects=@rules_rust//rust:defs.bzl%rustfmt_aspect
+build --output_groups=+rustfmt_checks
+```
+
+It's recommended to only enable this aspect in your CI environment so formatting issues do not
+impact user's ability to rapidly iterate on changes.
+
+The `rustfmt_aspect` also uses a `--@rules_rust//:rustfmt.toml` setting which determines the
+[configuration file][rgp] used by the formatter (`@rules_rust//tools/rustfmt`) and the aspect
+(`rustfmt_aspect`). This flag can be added to your `.bazelrc` file to ensure a consistent config
+file is used whenever `rustfmt` is run:
+
+```text
+build --@rules_rust//:rustfmt.toml=//:rustfmt.toml
+```
+
+[rustfmt]: https://github.com/rust-lang/rustfmt#readme
+[rsg]: https://github.com/rust-lang-nursery/fmt-rfcs/blob/master/guide/guide.md
+[rfcp]: https://github.com/rust-lang-nursery/fmt-rfcs
+[rgp]: https://rust-lang.github.io/rustfmt/
diff --git a/docs/symbols.bzl b/docs/symbols.bzl
index 67351cc..fc9a7a2 100644
--- a/docs/symbols.bzl
+++ b/docs/symbols.bzl
@@ -50,6 +50,8 @@
_rust_static_library = "rust_static_library",
_rust_test = "rust_test",
_rust_test_suite = "rust_test_suite",
+ _rustfmt_aspect = "rustfmt_aspect",
+ _rustfmt_test = "rustfmt_test",
)
load(
"@rules_rust//rust:repositories.bzl",
@@ -113,3 +115,6 @@
crate_universe = _crate_universe
crate = _crate
+
+rustfmt_aspect = _rustfmt_aspect
+rustfmt_test = _rustfmt_test
diff --git a/rust/defs.bzl b/rust/defs.bzl
index e1c2458..112086d 100644
--- a/rust/defs.bzl
+++ b/rust/defs.bzl
@@ -49,6 +49,11 @@
"//rust/private:rustdoc_test.bzl",
_rust_doc_test = "rust_doc_test",
)
+load(
+ "//rust/private:rustfmt.bzl",
+ _rustfmt_aspect = "rustfmt_aspect",
+ _rustfmt_test = "rustfmt_test",
+)
rust_library = _rust_library
# See @rules_rust//rust/private:rust.bzl for a complete description.
@@ -96,7 +101,13 @@
# See @rules_rust//rust/private:common.bzl for a complete description.
rust_analyzer_aspect = _rust_analyzer_aspect
-# See @rules_rust//rust:private/rust_analyzer.bzl for a complete description.
+# See @rules_rust//rust/private:rust_analyzer.bzl for a complete description.
rust_analyzer = _rust_analyzer
-# See @rules_rust//rust:private/rust_analyzer.bzl for a complete description.
+# See @rules_rust//rust/private:rust_analyzer.bzl for a complete description.
+
+rustfmt_aspect = _rustfmt_aspect
+# See @rules_rust//rust/private:rustfmt.bzl for a complete description.
+
+rustfmt_test = _rustfmt_test
+# See @rules_rust//rust/private:rustfmt.bzl for a complete description.
diff --git a/rust/private/rustfmt.bzl b/rust/private/rustfmt.bzl
new file mode 100644
index 0000000..43051e5
--- /dev/null
+++ b/rust/private/rustfmt.bzl
@@ -0,0 +1,175 @@
+"""A module defining rustfmt rules"""
+
+load(":common.bzl", "rust_common")
+load(":utils.bzl", "find_toolchain")
+
+def _find_rustfmtable_srcs(target, aspect_ctx = None):
+ """Parse a target for rustfmt formattable sources.
+
+ Args:
+ target (Target): The target the aspect is running on.
+ aspect_ctx (ctx, optional): The aspect's context object.
+
+ Returns:
+ list: A list of formattable sources (`File`).
+ """
+ if rust_common.crate_info not in target:
+ return []
+
+ # Targets annotated with `norustfmt` will not be formatted
+ if aspect_ctx and "norustfmt" in aspect_ctx.rule.attr.tags:
+ return []
+
+ crate_info = target[rust_common.crate_info]
+
+ # Filter out any generated files
+ srcs = [src for src in crate_info.srcs.to_list() if src.is_source]
+
+ return srcs
+
+def _generate_manifest(edition, srcs, ctx):
+ # Gather the source paths to non-generated files
+ src_paths = [src.path for src in srcs]
+
+ # Write the rustfmt manifest
+ manifest = ctx.actions.declare_file(ctx.label.name + ".rustfmt")
+ ctx.actions.write(
+ output = manifest,
+ content = "\n".join(src_paths + [
+ edition,
+ ]),
+ )
+
+ return manifest
+
+def _perform_check(edition, srcs, ctx):
+ toolchain = find_toolchain(ctx)
+
+ marker = ctx.actions.declare_file(ctx.label.name + ".rustfmt.ok")
+
+ args = ctx.actions.args()
+ args.add("--touch-file")
+ args.add(marker)
+ args.add("--")
+ args.add(toolchain.rustfmt)
+ args.add("--edition")
+ args.add(edition)
+ args.add("--check")
+ args.add_all(srcs)
+
+ ctx.actions.run(
+ executable = ctx.executable._process_wrapper,
+ inputs = srcs,
+ outputs = [marker],
+ tools = [toolchain.rustfmt],
+ arguments = [args],
+ mnemonic = "Rustfmt",
+ )
+
+ return marker
+
+def _rustfmt_aspect_impl(target, ctx):
+ srcs = _find_rustfmtable_srcs(target, ctx)
+
+ # If there are no formattable sources, do nothing.
+ if not srcs:
+ return []
+
+ # Parse the edition to use for formatting from the target
+ edition = target[rust_common.crate_info].edition
+
+ manifest = _generate_manifest(edition, srcs, ctx)
+ marker = _perform_check(edition, srcs, ctx)
+
+ return [
+ OutputGroupInfo(
+ rustfmt_manifest = depset([manifest]),
+ rustfmt_checks = depset([marker]),
+ ),
+ ]
+
+rustfmt_aspect = aspect(
+ implementation = _rustfmt_aspect_impl,
+ doc = """\
+This aspect is used to gather information about a crate for use in rustfmt and perform rustfmt checks
+
+Output Groups:
+
+- `rustfmt_manifest`: A manifest used by rustfmt binaries to provide crate specific settings.
+- `rustfmt_checks`: Executes `rustfmt --check` on the specified target.
+
+The build setting `@rules_rust//:rustfmt.toml` is used to control the Rustfmt [configuration settings][cs]
+used at runtime.
+
+[cs]: https://rust-lang.github.io/rustfmt/
+
+This aspect is executed on any target which provides the `CrateInfo` provider. However
+users may tag a target with `norustfmt` to have it skipped. Additionally, generated
+source files are also ignored by this aspect.
+""",
+ attrs = {
+ "_process_wrapper": attr.label(
+ doc = "A process wrapper for running rustfmt on all platforms",
+ cfg = "exec",
+ executable = True,
+ default = Label("//util/process_wrapper"),
+ ),
+ },
+ incompatible_use_toolchain_transition = True,
+ fragments = ["cpp"],
+ host_fragments = ["cpp"],
+ toolchains = [
+ str(Label("//rust:toolchain")),
+ ],
+)
+
+def _rustfmt_test_impl(ctx):
+ # 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.
+ runner = ctx.actions.declare_file("{}{}".format(
+ ctx.label.name,
+ ctx.executable._runner.extension,
+ ))
+
+ ctx.actions.symlink(
+ output = runner,
+ target_file = ctx.executable._runner,
+ is_executable = True,
+ )
+
+ manifests = [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),
+ )
+
+ runfiles = runfiles.merge(
+ ctx.attr._runner[DefaultInfo].default_runfiles,
+ )
+
+ return [DefaultInfo(
+ files = depset([runner]),
+ runfiles = runfiles,
+ executable = runner,
+ )]
+
+rustfmt_test = rule(
+ implementation = _rustfmt_test_impl,
+ doc = "A test rule for performing `rustfmt --check` on a set of targets",
+ attrs = {
+ "targets": attr.label_list(
+ doc = "Rust targets to run `rustfmt --check` on.",
+ providers = [rust_common.crate_info],
+ aspects = [rustfmt_aspect],
+ ),
+ "_runner": attr.label(
+ doc = "The rustfmt test runner",
+ cfg = "exec",
+ executable = True,
+ default = Label("//tools/rustfmt:rustfmt_test"),
+ ),
+ },
+ test = True,
+)
diff --git a/test/rustfmt/BUILD.bazel b/test/rustfmt/BUILD.bazel
index 2e4b67b..5ab4876 100644
--- a/test/rustfmt/BUILD.bazel
+++ b/test/rustfmt/BUILD.bazel
@@ -1,17 +1,56 @@
-load("@rules_rust//test/rustfmt:rustfmt_generator.bzl", "rustfmt_generator")
+load("@rules_rust//rust:defs.bzl", "rust_binary", "rustfmt_test")
-rustfmt_generator(
- name = "formatted",
- src = ":unformatted.rs",
+exports_files([
+ "test_rustfmt.toml",
+])
+
+rust_binary(
+ name = "formatted_2018",
+ srcs = ["srcs/2018/formatted.rs"],
+ edition = "2018",
)
-sh_test(
- name = "rustfmt_test",
- size = "small",
- srcs = [":rustfmt_test.sh"],
- data = [
- ":formatted.rs",
- ":unformatted.rs",
- ],
- deps = ["@bazel_tools//tools/bash/runfiles"],
+rustfmt_test(
+ name = "test_formatted_2018",
+ targets = [":formatted_2018"],
+)
+
+rust_binary(
+ name = "unformatted_2018",
+ srcs = ["srcs/2018/unformatted.rs"],
+ edition = "2018",
+)
+
+rustfmt_test(
+ name = "test_unformatted_2018",
+ tags = ["manual"],
+ targets = [":unformatted_2018"],
+)
+
+rust_binary(
+ name = "formatted_2015",
+ srcs = ["srcs/2015/formatted.rs"],
+ edition = "2015",
+)
+
+rustfmt_test(
+ name = "test_formatted_2015",
+ targets = [":formatted_2015"],
+)
+
+rust_binary(
+ name = "unformatted_2015",
+ srcs = ["srcs/2015/unformatted.rs"],
+ edition = "2015",
+)
+
+rustfmt_test(
+ name = "test_unformatted_2015",
+ tags = ["manual"],
+ targets = [":unformatted_2015"],
+)
+
+sh_binary(
+ name = "test_runner",
+ srcs = ["rustfmt_failure_test.sh"],
)
diff --git a/test/rustfmt/rustfmt_failure_test.sh b/test/rustfmt/rustfmt_failure_test.sh
new file mode 100755
index 0000000..0a658e8
--- /dev/null
+++ b/test/rustfmt/rustfmt_failure_test.sh
@@ -0,0 +1,89 @@
+#!/bin/bash
+
+# Runs Bazel build commands over rustfmt rules, where some are expected
+# to fail.
+#
+# Can be run from anywhere within the rules_rust workspace.
+
+set -euo pipefail
+
+if [[ -z "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then
+ echo "This script should be run under Bazel"
+ exit 1
+fi
+
+cd "${BUILD_WORKSPACE_DIRECTORY}"
+
+# Executes a bazel build command and handles the return value, exiting
+# upon seeing an error.
+#
+# Takes two arguments:
+# ${1}: The expected return code.
+# ${2}: The target within "//test/rustfmt" to be tested.
+function check_build_result() {
+ local ret=0
+ echo -n "Testing ${2}... "
+ (bazel test //test/rustfmt:"${2}" &> /dev/null) || ret="$?" && true
+ if [[ "${ret}" -ne "${1}" ]]; then
+ echo "FAIL: Unexpected return code [saw: ${ret}, want: ${1}] building target //test/rustfmt:${2}"
+ echo " Run \"bazel test //test/rustfmt:${2}\" to see the output"
+ exit 1
+ else
+ echo "OK"
+ fi
+}
+
+function test_all() {
+ local -r TEST_OK=0
+ local -r TEST_FAILED=3
+
+ check_build_result $TEST_FAILED test_unformatted_2015
+ check_build_result $TEST_FAILED test_unformatted_2018
+ check_build_result $TEST_OK test_formatted_2015
+ check_build_result $TEST_OK test_formatted_2018
+}
+
+function test_apply() {
+ local -r TEST_OK=0
+ local -r TEST_FAILED=3
+
+ temp_dir="$(mktemp -d -t ci-XXXXXXXXXX)"
+ new_workspace="${temp_dir}/rules_rust_test_rustfmt"
+
+ mkdir -p "${new_workspace}/test/rustfmt" && \
+ cp -r test/rustfmt/* "${new_workspace}/test/rustfmt/" && \
+ cat << EOF > "${new_workspace}/WORKSPACE.bazel"
+workspace(name = "rules_rust_test_rustfmt")
+local_repository(
+ name = "rules_rust",
+ path = "${BUILD_WORKSPACE_DIRECTORY}",
+)
+load("@rules_rust//rust:repositories.bzl", "rust_repositories")
+rust_repositories()
+EOF
+
+ pushd "${new_workspace}"
+
+ # Format a specific target
+ bazel run @rules_rust//tools/rustfmt -- //test/rustfmt:unformatted_2018
+
+ check_build_result $TEST_FAILED test_unformatted_2015
+ check_build_result $TEST_OK test_unformatted_2018
+ check_build_result $TEST_OK test_formatted_2015
+ check_build_result $TEST_OK test_formatted_2018
+
+ # Format all targets
+ bazel run @rules_rust//tools/rustfmt --@rules_rust//:rustfmt.toml=//test/rustfmt:test_rustfmt.toml
+
+ check_build_result $TEST_OK test_unformatted_2015
+ check_build_result $TEST_OK test_unformatted_2018
+ check_build_result $TEST_OK test_formatted_2015
+ check_build_result $TEST_OK test_formatted_2018
+
+ popd
+
+ rm -rf "${temp_dir}"
+}
+
+test_all
+test_apply
diff --git a/test/rustfmt/rustfmt_generator.bzl b/test/rustfmt/rustfmt_generator.bzl
index 930906c..9586fba 100644
--- a/test/rustfmt/rustfmt_generator.bzl
+++ b/test/rustfmt/rustfmt_generator.bzl
@@ -2,6 +2,9 @@
# buildifier: disable=bzl-visibility
load("//rust/private:utils.bzl", "find_toolchain")
+# buildifier: disable=print
+print("WARNING: `rustfmt_generator` is deprecated. Instead, see https://bazelbuild.github.io/rules_rust/rustfmt.html")
+
def _rustfmt_generator_impl(ctx):
toolchain = find_toolchain(ctx)
rustfmt_bin = toolchain.rustfmt
diff --git a/test/rustfmt/rustfmt_test.sh b/test/rustfmt/rustfmt_test.sh
deleted file mode 100755
index a288b30..0000000
--- a/test/rustfmt/rustfmt_test.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/bash
-set -euxo pipefail
-
-formatted="$(rlocation rules_rust/test/rustfmt/formatted.rs)"
-unformatted="$(rlocation rules_rust/test/rustfmt/unformatted.rs)"
-
-# Ensure that the file was formatted
-! diff "$unformatted" "$formatted"
diff --git a/test/rustfmt/srcs/2015/formatted.rs b/test/rustfmt/srcs/2015/formatted.rs
new file mode 100644
index 0000000..a204013
--- /dev/null
+++ b/test/rustfmt/srcs/2015/formatted.rs
@@ -0,0 +1,3 @@
+fn main() {
+ println!("2015");
+}
diff --git a/test/rustfmt/srcs/2015/unformatted.rs b/test/rustfmt/srcs/2015/unformatted.rs
new file mode 100644
index 0000000..d6e473f
--- /dev/null
+++ b/test/rustfmt/srcs/2015/unformatted.rs
@@ -0,0 +1 @@
+fn main(){println!("2015");}
diff --git a/test/rustfmt/srcs/2018/formatted.rs b/test/rustfmt/srcs/2018/formatted.rs
new file mode 100644
index 0000000..dff90f2
--- /dev/null
+++ b/test/rustfmt/srcs/2018/formatted.rs
@@ -0,0 +1,40 @@
+use std::future::Future;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake};
+use std::thread::{self, Thread};
+
+/// A waker that wakes up the current thread when called.
+struct ThreadWaker(Thread);
+
+impl Wake for ThreadWaker {
+ fn wake(self: Arc<Self>) {
+ self.0.unpark();
+ }
+}
+
+/// Run a future to completion on the current thread.
+fn block_on<T>(fut: impl Future<Output = T>) -> T {
+ // Pin the future so it can be polled.
+ let mut fut = Box::pin(fut);
+
+ // Create a new context to be passed to the future.
+ let t = thread::current();
+ let waker = Arc::new(ThreadWaker(t)).into();
+ let mut cx = Context::from_waker(&waker);
+
+ // Run the future to completion.
+ loop {
+ match fut.as_mut().poll(&mut cx) {
+ Poll::Ready(res) => return res,
+ Poll::Pending => thread::park(),
+ }
+ }
+}
+
+async fn edition() -> i32 {
+ 2018
+}
+
+fn main() {
+ println!("{}", block_on(edition()));
+}
diff --git a/test/rustfmt/srcs/2018/unformatted.rs b/test/rustfmt/srcs/2018/unformatted.rs
new file mode 100644
index 0000000..454ac49
--- /dev/null
+++ b/test/rustfmt/srcs/2018/unformatted.rs
@@ -0,0 +1,19 @@
+use std::future::Future; use std::sync::Arc; use std::task::{Context, Poll, Wake}; use std::thread::{self, Thread};
+/// A waker that wakes up the current thread when called.
+struct ThreadWaker(Thread);
+impl Wake for ThreadWaker {fn wake(self: Arc<Self>) {self.0.unpark();}}
+/// Run a future to completion on the current thread.
+fn block_on<T>(fut: impl Future<Output = T>) -> T {
+// Pin the future so it can be polled.
+let mut fut = Box::pin(fut);
+// Create a new context to be passed to the future.
+let t = thread::current();let waker = Arc::new(ThreadWaker(t)).into();
+let mut cx = Context::from_waker(&waker);
+// Run the future to completion.
+loop {match fut.as_mut().poll(&mut cx) {
+Poll::Ready(res) => return res, Poll::Pending => thread::park(),
+}
+}
+}
+async fn edition() -> i32 {2018}
+fn main(){println!("{}", block_on(edition()));}
diff --git a/test/rustfmt/test_rustfmt.toml b/test/rustfmt/test_rustfmt.toml
new file mode 100644
index 0000000..ac5d99f
--- /dev/null
+++ b/test/rustfmt/test_rustfmt.toml
@@ -0,0 +1 @@
+control_brace_style = "AlwaysNextLine"
diff --git a/test/rustfmt/unformatted.rs b/test/rustfmt/unformatted.rs
deleted file mode 100644
index f36f291..0000000
--- a/test/rustfmt/unformatted.rs
+++ /dev/null
@@ -1 +0,0 @@
-fn example(){println!("test");}
diff --git a/test/unit/crate_name/crate_name_test.bzl b/test/unit/crate_name/crate_name_test.bzl
index 20708f8..d8efb94 100644
--- a/test/unit/crate_name/crate_name_test.bzl
+++ b/test/unit/crate_name/crate_name_test.bzl
@@ -126,14 +126,14 @@
rust_library(
name = "invalid/default-crate-name",
srcs = ["lib.rs"],
- tags = ["manual"],
+ tags = ["manual", "norustfmt"],
)
rust_library(
name = "invalid-custom-crate-name",
crate_name = "hyphens-not-allowed",
srcs = ["lib.rs"],
- tags = ["manual"],
+ tags = ["manual", "norustfmt"],
)
default_crate_name_library_test(
diff --git a/tools/runfiles/runfiles.rs b/tools/runfiles/runfiles.rs
index f30088f..8f3bf5d 100644
--- a/tools/runfiles/runfiles.rs
+++ b/tools/runfiles/runfiles.rs
@@ -66,7 +66,7 @@
}
/// Returns the .runfiles directory for the currently executing binary.
-fn find_runfiles_dir() -> io::Result<PathBuf> {
+pub fn find_runfiles_dir() -> io::Result<PathBuf> {
let exec_path = std::env::args().nth(0).expect("arg 0 was not set");
let mut binary_path = PathBuf::from(&exec_path);
diff --git a/tools/rustfmt/BUILD.bazel b/tools/rustfmt/BUILD.bazel
new file mode 100644
index 0000000..2394456
--- /dev/null
+++ b/tools/rustfmt/BUILD.bazel
@@ -0,0 +1,60 @@
+load("//rust:defs.bzl", "rust_binary", "rust_library")
+
+package(default_visibility = ["//visibility:public"])
+
+exports_files(["rustfmt.toml"])
+
+alias(
+ name = "rustfmt_bin",
+ actual = select({
+ "@rules_rust//rust/platform:aarch64-apple-darwin": "@rust_darwin_aarch64//:rustfmt_bin",
+ "@rules_rust//rust/platform:aarch64-unknown-linux-gnu": "@rust_linux_aarch64//:rustfmt_bin",
+ "@rules_rust//rust/platform:x86_64-apple-darwin": "@rust_darwin_x86_64//:rustfmt_bin",
+ "@rules_rust//rust/platform:x86_64-pc-windows-msvc": "@rust_windows_x86_64//:rustfmt_bin",
+ "@rules_rust//rust/platform:x86_64-unknown-linux-gnu": "@rust_linux_x86_64//:rustfmt_bin",
+ }),
+)
+
+rust_library(
+ name = "rustfmt_lib",
+ srcs = glob(
+ ["srcs/**/*.rs"],
+ exclude = ["srcs/**/*main.rs"],
+ ),
+ data = [
+ ":rustfmt_bin",
+ "//:rustfmt.toml",
+ ],
+ edition = "2018",
+ rustc_env = {
+ "RUSTFMT": "$(rootpath :rustfmt_bin)",
+ "RUSTFMT_CONFIG": "$(rootpath //:rustfmt.toml)",
+ },
+)
+
+rust_binary(
+ name = "rustfmt",
+ srcs = [
+ "srcs/main.rs",
+ ],
+ data = [
+ "//:rustfmt.toml",
+ ],
+ edition = "2018",
+ deps = [
+ ":rustfmt_lib",
+ "//util/label",
+ ],
+)
+
+rust_binary(
+ name = "rustfmt_test",
+ srcs = [
+ "srcs/test_main.rs",
+ ],
+ edition = "2018",
+ deps = [
+ ":rustfmt_lib",
+ "//tools/runfiles",
+ ],
+)
diff --git a/tools/rustfmt/rustfmt.toml b/tools/rustfmt/rustfmt.toml
new file mode 100644
index 0000000..44bdbf2
--- /dev/null
+++ b/tools/rustfmt/rustfmt.toml
@@ -0,0 +1 @@
+# rustfmt options: https://rust-lang.github.io/rustfmt/
diff --git a/tools/rustfmt/srcs/lib.rs b/tools/rustfmt/srcs/lib.rs
new file mode 100644
index 0000000..35ccf78
--- /dev/null
+++ b/tools/rustfmt/srcs/lib.rs
@@ -0,0 +1,76 @@
+use std::env;
+use std::fs;
+use std::path::{Path, PathBuf};
+
+/// The expected extension of rustfmt manifest files generated by `rustfmt_aspect`.
+pub const RUSTFMT_MANIFEST_EXTENSION: &'static 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 {
+ /// The rustfmt binary from the currently active toolchain
+ pub rustfmt: PathBuf,
+
+ /// The rustfmt config file containing rustfmt settings.
+ /// https://rust-lang.github.io/rustfmt/
+ pub config: PathBuf,
+}
+
+/// 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"),
+ }
+}
+
+/// A struct of target specific information for use in running `rustfmt`.
+#[derive(Debug)]
+pub struct RustfmtManifest {
+ /// The Rust edition of the Bazel target
+ pub edition: String,
+
+ /// A list of all (non-generated) source files for formatting.
+ pub sources: Vec<String>,
+}
+
+/// Parse rustfmt flags from a manifest generated by builds using `rustfmt_aspect`.
+pub fn parse_rustfmt_manifest(manifest: &Path) -> RustfmtManifest {
+ let content = fs::read_to_string(manifest).expect(&format!(
+ "Failed to read rustfmt manifest: {}",
+ manifest.display()
+ ));
+
+ let mut lines: Vec<String> = content
+ .split("\n")
+ .into_iter()
+ .filter(|s| !s.is_empty())
+ .map(|s| s.to_owned())
+ .collect();
+
+ let edition = lines
+ .pop()
+ .expect("There should always be at least 1 line in the manifest");
+ edition
+ .parse::<i32>()
+ .expect("The edition should be a numeric value. eg `2018`.");
+
+ RustfmtManifest {
+ edition: edition,
+ sources: lines,
+ }
+}
diff --git a/tools/rustfmt/srcs/main.rs b/tools/rustfmt/srcs/main.rs
new file mode 100644
index 0000000..74fea5d
--- /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;
+
+use label;
+use rustfmt_lib;
+
+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: &Vec<String>) {
+ let build_args = vec![
+ "build",
+ "--aspects=@rules_rust//rust:defs.bzl%rustfmt_aspect",
+ "--output_groups=rustfmt_manifest",
+ ];
+
+ 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: &Vec<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(),
+ }
+}
diff --git a/tools/rustfmt/srcs/test_main.rs b/tools/rustfmt/srcs/test_main.rs
new file mode 100644
index 0000000..d1520d7
--- /dev/null
+++ b/tools/rustfmt/srcs/test_main.rs
@@ -0,0 +1,98 @@
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+use runfiles;
+use rustfmt_lib;
+
+fn main() {
+ // Gather all and environment settings
+ let options = parse_args();
+
+ // Perform rustfmt for each manifest available
+ run_rustfmt(&options);
+}
+
+/// Run rustfmt on a set of Bazel targets
+fn run_rustfmt(options: &Config) {
+ // In order to ensure the test parses all sources, we separately
+ // track whether or not a failure has occured when checking formatting.
+ let mut is_failure: bool = false;
+
+ for manifest in options.manifests.iter() {
+ // Ignore any targets which do not have source files. This can
+ // occur in cases where all source files are generated.
+ if manifest.sources.is_empty() {
+ continue;
+ }
+
+ // Run rustfmt
+ let status = Command::new(&options.rustfmt_config.rustfmt)
+ .arg("--check")
+ .arg("--edition")
+ .arg(&manifest.edition)
+ .arg("--config-path")
+ .arg(&options.rustfmt_config.config)
+ .args(&manifest.sources)
+ .status()
+ .expect("Failed to run rustfmt");
+
+ if !status.success() {
+ is_failure = true;
+ }
+ }
+
+ if is_failure {
+ std::process::exit(1);
+ }
+}
+
+/// A struct containing details used for executing rustfmt.
+#[derive(Debug)]
+struct Config {
+ /// Information about the current rustfmt binary to run.
+ pub rustfmt_config: rustfmt_lib::RustfmtConfig,
+
+ /// A list of manifests containing information about sources
+ /// to check using rustfmt.
+ 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,
+ );
+
+ if manifests.is_empty() {
+ panic!("No manifests were found");
+ }
+
+ Config {
+ rustfmt_config: rustfmt_lib::parse_rustfmt_config(),
+ manifests: manifests
+ .iter()
+ .map(|manifest| rustfmt_lib::parse_rustfmt_manifest(&manifest))
+ .collect(),
+ }
+}
diff --git a/util/label/label.rs b/util/label/label.rs
index 158a570..bd11a6e 100644
--- a/util/label/label.rs
+++ b/util/label/label.rs
@@ -18,9 +18,9 @@
#[derive(Debug, PartialEq)]
pub struct Label<'s> {
- repository_name: Option<&'s str>,
- package_name: Option<&'s str>,
- name: &'s str,
+ pub repository_name: Option<&'s str>,
+ pub package_name: Option<&'s str>,
+ pub name: &'s str,
}
type Result<T, E = LabelError> = core::result::Result<T, E>;