Periphery Bazel Rules
August 13, 2026 · View on GitHub
Bazel integration for Periphery.
Module Setup
Add rules_periphery to your MODULE.bazel and configure the Periphery
binary to scan with. For a released Periphery archive:
PERIPHERY_VERSION = "1.0.0"
bazel_dep(name = "rules_periphery", version = PERIPHERY_VERSION)
periphery = use_extension("@rules_periphery//:extensions.bzl", "periphery")
periphery.binary_archive(
# This example uses the macOS Apple Silicon archive.
url = "https://github.com/periphery-pro/cli-releases/releases/download/{0}/periphery-cli_{0}_macos_arm64.zip".format(PERIPHERY_VERSION),
sha256 = "...",
)
use_repo(periphery, "periphery_generated")
Or, to use a Periphery binary from your local machine:
periphery = use_extension("@rules_periphery//:extensions.bzl", "periphery")
periphery.local_binary(
# Absolute, or relative to the workspace root.
path = "path/to/periphery",
)
local_binary refers to a path on disk, which Bazel neither builds nor tracks.
The configured binary is exposed to the scan rules as a toolchain; you don't
need to reference it directly. use_repo(periphery, "periphery_generated") is
only required when using the scan target generation
entry point.
Declaring scan targets
Three rules and one convenience macro are provided:
scan— an executable target that prints results when run withbazel run.scan_test— a test target that fails when unused code is found. Use this to run Periphery in CI viabazel test.scan_report— a build target that runs Periphery at build time and writes the formatted report to a file output, which other rules can consume viadatadeps orsrcs.scan_auto— an executable target that discovers the workspace's top-level targets automatically, so you don't need to maintain adepslist by hand.
Apply the explicit scan rules to your top-level targets (applications, tests, command-line tools, etc.); their transitive dependencies are scanned too:
load("@rules_periphery//:rules.bzl", "scan", "scan_report", "scan_test")
scan(
name = "scan",
config = ".periphery.yml",
deps = [
"//App:MyApp",
"//Tests:MyAppTests",
],
)
scan_test(
name = "scan_test",
config = ".periphery.yml",
deps = [
"//App:MyApp",
"//Tests:MyAppTests",
],
)
scan_report(
name = "scan_report",
config = ".periphery.yml",
format = "json",
deps = [
"//App:MyApp",
"//Tests:MyAppTests",
],
)
# Print results.
bazel run //:scan
# Fail if unused code is found.
bazel test //:scan_test
# Write a report file to bazel-bin/scan_report.report.
bazel build //:scan_report
All rules accept periphery_args for forwarding additional arguments to
periphery scan. Files referenced by those arguments — such as a baseline —
must be declared via the data attribute:
scan_test(
name = "scan_test",
config = ".periphery.yml",
data = ["baseline.json"],
periphery_args = [
"--baseline",
"baseline.json",
],
deps = ["//App:MyApp"],
)
scan_report's format accepts any of Periphery's output formats: xcode,
csv, json, checkstyle, codeclimate, github-actions,
github-markdown, gitlab-codequality.
Generating the scan target
If you'd rather not maintain the deps list by hand, the scan_auto macro
discovers your workspace's top-level targets automatically:
load("@rules_periphery//:defs.bzl", "scan_auto")
scan_auto(
name = "scan_auto",
)
bazel run //:scan_auto
Running the target queries your workspace for top-level targets, generates a
hidden scan target, and runs it through a nested bazel run @periphery_generated//:scan.
The macro accepts optional configuration:
scan_auto(
name = "scan_auto",
# Override the default top-level target query.
query = "filter('^//App', kind('(ios_application) rule', deps(//...)))",
# Or just filter the default query.
filter = "^//App",
# Use a global index store instead of per-module stores.
global_indexstore = "/path/to/indexstore",
# Run the scan with Bazel visibility checking enabled.
check_visibility = True,
# Extra arguments forwarded to the nested `bazel run`.
bazel_args = [],
# Arguments forwarded to `periphery scan`.
periphery_args = ["--config", ".periphery.yml"],
)
Additional arguments can also be forwarded to periphery scan at runtime:
bazel run //:scan_auto -- --strict --quiet
Visibility checking
By default the generated scan target is built with --check_visibility=false,
since it references targets across your workspace that may not be visible to
it. Disabling visibility checking can invalidate Bazel's analysis cache,
resulting in slower subsequent builds.
Set check_visibility = True to avoid that, and grant the generated scan
target visibility to the targets it scans using the @rules_periphery//:generated
package group:
swift_library(
name = "MyLib",
visibility = ["@rules_periphery//:generated"],
)
Development
# Plumbing tests using a stub binary.
./tests/smoke.sh
# End-to-end tests using a real Periphery release binary (requires macOS).
./tests/e2e.sh
# Lint.
mise run lint