diff --git a/build-support/bin/generate_docs.py b/build-support/bin/generate_docs.py
index 23d21c894b3e..9d93e5ebcda2 100644
--- a/build-support/bin/generate_docs.py
+++ b/build-support/bin/generate_docs.py
@@ -232,6 +232,7 @@ def create_parser() -> argparse.ArgumentParser:
def run_pants_help_all() -> dict[str, Any]:
# List all (stable enough) backends here.
backends = [
+ "pants.backend.build_files.fmt.buildifier",
"pants.backend.awslambda.python",
"pants.backend.codegen.protobuf.lint.buf",
"pants.backend.codegen.protobuf.python",
diff --git a/docs/markdown/Using Pants/concepts/enabling-backends.md b/docs/markdown/Using Pants/concepts/enabling-backends.md
index 3b124cf166fa..0bdc8e16ecae 100644
--- a/docs/markdown/Using Pants/concepts/enabling-backends.md
+++ b/docs/markdown/Using Pants/concepts/enabling-backends.md
@@ -22,6 +22,7 @@ Available backends
| Backend | What it does | Docs |
| :-------------------------------------------------------- | :------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------- |
+| `pants.backend.build_files.fmt.buildifier` | Enables autoformatting `BUILD` files using `buildifier`. | |
| `pants.backend.awslambda.python` | Enables generating an AWS Lambda zip file from Python code. | [AWS Lambda](doc:awslambda-python) |
| `pants.backend.codegen.protobuf.lint.buf` | Activate the Buf formatter and linter for Protocol Buffers. | [Protobuf](doc:protobuf-python) |
| `pants.backend.codegen.protobuf.python` | Enables generating Python from Protocol Buffers. Includes gRPC support. | [Protobuf and gRPC](doc:protobuf-python) |
@@ -38,7 +39,7 @@ Available backends
| `pants.backend.experimental.python.lint.pyupgrade` | Enables Pyupgrade, which upgrades to new Python syntax: | [Linters and formatters](doc:python-linters-and-formatters) |
| `pants.backend.experimental.python.packaging.pyoxidizer` | Enables `pyoxidizer_binary` target. | [PyOxidizer](doc:pyoxidizer) |
| `pants.backend.google_cloud_function.python` | Enables generating a Google Cloud Function from Python code. | [Google Cloud Function](doc:google-cloud-function-python) |
-| `pants.backend.plugin_development` | Enables `pants_requirements` target. | [Plugins overview](doc:plugins-overview) |
+| `pants.backend.plugin_development` | Enables `pants_requirements` target. | [Plugins overview](doc:plugins-overview) |
| `pants.backend.python` | Core Python support. | [Enabling Python support](doc:python-backend) |
| `pants.backend.python.mixed_interpreter_constraints` | Adds the `py-constraints` goal for insights on Python interpreter constraints. | [Interpreter compatibility](doc:python-interpreter-compatibility) |
| `pants.backend.python.lint.bandit` | Enables Bandit, the Python security linter: . | [Linters and formatters](doc:python-linters-and-formatters) |
diff --git a/src/python/pants/backend/build_files/fmt/buildifier/BUILD b/src/python/pants/backend/build_files/fmt/buildifier/BUILD
new file mode 100644
index 000000000000..27e5628650aa
--- /dev/null
+++ b/src/python/pants/backend/build_files/fmt/buildifier/BUILD
@@ -0,0 +1,6 @@
+# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+python_sources()
+
+python_tests(name="tests")
diff --git a/src/python/pants/backend/build_files/fmt/buildifier/__init__.py b/src/python/pants/backend/build_files/fmt/buildifier/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/src/python/pants/backend/build_files/fmt/buildifier/register.py b/src/python/pants/backend/build_files/fmt/buildifier/register.py
new file mode 100644
index 000000000000..c0faab78d470
--- /dev/null
+++ b/src/python/pants/backend/build_files/fmt/buildifier/register.py
@@ -0,0 +1,10 @@
+# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+from pants.backend.build_files.fmt.buildifier import rules as buildifier_rules
+
+
+def rules():
+ return [
+ *buildifier_rules.rules(),
+ ]
diff --git a/src/python/pants/backend/build_files/fmt/buildifier/rules.py b/src/python/pants/backend/build_files/fmt/buildifier/rules.py
new file mode 100644
index 000000000000..9c4f57105205
--- /dev/null
+++ b/src/python/pants/backend/build_files/fmt/buildifier/rules.py
@@ -0,0 +1,48 @@
+# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+from pants.backend.build_files.fmt.buildifier.subsystem import Buildifier
+from pants.core.goals.fmt import FmtResult, _FmtBuildFilesRequest
+from pants.core.util_rules.external_tool import DownloadedExternalTool, ExternalToolRequest
+from pants.engine.internals.native_engine import Digest, MergeDigests, Snapshot
+from pants.engine.internals.selectors import Get
+from pants.engine.platform import Platform
+from pants.engine.process import Process, ProcessResult
+from pants.engine.rules import collect_rules, rule
+from pants.engine.unions import UnionRule
+from pants.util.logging import LogLevel
+from pants.util.strutil import pluralize
+
+
+class BuildifierRequest(_FmtBuildFilesRequest):
+ name = "buildifier"
+
+
+@rule(desc="Format with Buildifier", level=LogLevel.DEBUG)
+async def buildfier_fmt(request: BuildifierRequest, buildifier: Buildifier) -> FmtResult:
+ buildifier_tool = await Get(
+ DownloadedExternalTool, ExternalToolRequest, buildifier.get_request(Platform.current)
+ )
+ input_digest = await Get(
+ Digest,
+ MergeDigests((request.snapshot.digest, buildifier_tool.digest)),
+ )
+ result = await Get(
+ ProcessResult,
+ Process(
+ argv=[buildifier_tool.exe, "-type=build", *request.snapshot.files],
+ input_digest=input_digest,
+ output_files=request.snapshot.files,
+ description=f"Run buildifier on {pluralize(len(request.snapshot.files), 'file')}.",
+ level=LogLevel.DEBUG,
+ ),
+ )
+ output_snapshot = await Get(Snapshot, Digest, result.output_digest)
+ return FmtResult.create(request, result, output_snapshot)
+
+
+def rules():
+ return [
+ *collect_rules(),
+ UnionRule(_FmtBuildFilesRequest, BuildifierRequest),
+ ]
diff --git a/src/python/pants/backend/build_files/fmt/buildifier/rules_integration_test.py b/src/python/pants/backend/build_files/fmt/buildifier/rules_integration_test.py
new file mode 100644
index 000000000000..9fc320288411
--- /dev/null
+++ b/src/python/pants/backend/build_files/fmt/buildifier/rules_integration_test.py
@@ -0,0 +1,86 @@
+# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+from __future__ import annotations
+
+from textwrap import dedent
+
+import pytest
+
+from pants.backend.build_files.fmt.buildifier.rules import BuildifierRequest
+from pants.backend.build_files.fmt.buildifier.rules import rules as buildifier_rules
+from pants.backend.codegen.protobuf.target_types import rules as target_types_rules
+from pants.core.goals.fmt import FmtResult
+from pants.core.util_rules import external_tool
+from pants.engine.fs import PathGlobs
+from pants.engine.internals.native_engine import Snapshot
+from pants.testutil.rule_runner import QueryRule, RuleRunner
+
+
+class Materials:
+ def __init__(self, **kwargs):
+ pass
+
+
+@pytest.fixture
+def rule_runner() -> RuleRunner:
+ return RuleRunner(
+ rules=[
+ *buildifier_rules(),
+ *external_tool.rules(),
+ *target_types_rules(),
+ QueryRule(FmtResult, [BuildifierRequest]),
+ ],
+ # NB: Objects are easier to test with
+ objects={"materials": Materials},
+ )
+
+
+GOOD_FILE = dedent(
+ """\
+ materials(
+ drywall = 40,
+ status = "paid",
+ studs = 200,
+ )
+ """
+)
+
+BAD_FILE = dedent(
+ """\
+ materials(status='paid', studs=200, drywall=40)
+ """
+)
+
+
+def run_buildifier(rule_runner: RuleRunner) -> FmtResult:
+ rule_runner.set_options(
+ ["--backend-packages=pants.backend.build_files.fmt.buildifier"],
+ env_inherit={"PATH", "PYENV_ROOT"},
+ )
+ snapshot = rule_runner.request(Snapshot, [PathGlobs(["**/BUILD"])])
+ fmt_result = rule_runner.request(FmtResult, [BuildifierRequest(snapshot)])
+ return fmt_result
+
+
+def test_passing(rule_runner: RuleRunner) -> None:
+ rule_runner.write_files({"BUILD": GOOD_FILE})
+ fmt_result = run_buildifier(rule_runner)
+ assert fmt_result.output == rule_runner.make_snapshot({"BUILD": GOOD_FILE})
+ assert fmt_result.did_change is False
+
+
+def test_failing(rule_runner: RuleRunner) -> None:
+ rule_runner.write_files({"BUILD": BAD_FILE})
+ fmt_result = run_buildifier(rule_runner)
+ assert fmt_result.output == rule_runner.make_snapshot({"BUILD": GOOD_FILE})
+ assert fmt_result.did_change is True
+
+
+def test_multiple_files(rule_runner: RuleRunner) -> None:
+ rule_runner.write_files({"good/BUILD": GOOD_FILE, "bad/BUILD": BAD_FILE})
+ fmt_result = run_buildifier(rule_runner)
+ assert fmt_result.output == rule_runner.make_snapshot(
+ {"good/BUILD": GOOD_FILE, "bad/BUILD": GOOD_FILE}
+ )
+ assert fmt_result.did_change is True
diff --git a/src/python/pants/backend/build_files/fmt/buildifier/subsystem.py b/src/python/pants/backend/build_files/fmt/buildifier/subsystem.py
new file mode 100644
index 000000000000..dec4bf3fe4fb
--- /dev/null
+++ b/src/python/pants/backend/build_files/fmt/buildifier/subsystem.py
@@ -0,0 +1,46 @@
+# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+from pants.core.util_rules.external_tool import TemplatedExternalTool
+from pants.option.option_types import ArgsListOption, SkipOption
+from pants.util.strutil import softwrap
+
+
+class Buildifier(TemplatedExternalTool):
+ options_scope = "buildifier"
+ name = "Buildifier"
+ help = softwrap(
+ """
+ Buildifier is a tool for formatting BUILD files with a standard convention.
+
+ Pants supports running Buildifier on your Pants BUILD files for several reasons:
+ - You might like the style that buildifier uses.
+ - You might be incrementally adopting Pants from Bazel, and are already using buildifier.
+
+ Please note that there are differences from Bazel's BUILD files (which are Starlark) and
+ Pants' BUILD files (which are Python), so buildifier may issue a syntax error.
+ In practice, these errors should be rare. See https://bazel.build/rules/language#differences_with_python.
+ """
+ )
+
+ default_version = "5.1.0"
+ default_known_versions = [
+ "5.1.0|macos_x86_64|c9378d9f4293fc38ec54a08fbc74e7a9d28914dae6891334401e59f38f6e65dc|7125968",
+ "5.1.0|macos_arm64 |745feb5ea96cb6ff39a76b2821c57591fd70b528325562486d47b5d08900e2e4|7334498",
+ "5.1.0|linux_x86_64|52bf6b102cb4f88464e197caac06d69793fa2b05f5ad50a7e7bf6fbd656648a3|7226100",
+ "5.1.0|linux_arm64 |917d599dbb040e63ae7a7e1adb710d2057811902fdc9e35cce925ebfd966eeb8|7171938",
+ ]
+ default_url_template = (
+ "https://github.com/bazelbuild/buildtools/releases/download/{version}/buildifier-{platform}"
+ )
+ default_url_platform_mapping = {
+ "macos_arm64": "darwin-arm64",
+ "macos_x86_64": "darwin-amd64",
+ "linux_arm64": "linux-arm64",
+ "linux_x86_64": "linux-amd64",
+ }
+
+ skip = SkipOption("fmt")
+ args = ArgsListOption(example="-lint=fix")
+
+ # NB: buildifier doesn't (yet) support config files https://github.com/bazelbuild/buildtools/issues/479
diff --git a/src/python/pants/bin/BUILD b/src/python/pants/bin/BUILD
index 74ecd6227701..c53ce93f96a8 100644
--- a/src/python/pants/bin/BUILD
+++ b/src/python/pants/bin/BUILD
@@ -13,6 +13,7 @@ python_sources(
target(
name="plugins",
dependencies=[
+ "src/python/pants/backend/build_files/fmt/buildifier",
"src/python/pants/backend/awslambda/python",
"src/python/pants/backend/codegen/protobuf/lint/buf",
"src/python/pants/backend/codegen/protobuf/python",