Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

##### Enhancements

- None.
- Added a `--bazel-query` option to override the default Bazel top-level target query.

##### Bug Fixes

- None.
- Follow embedded bundle and plugin edges transitively in the generated Bazel scan rule so custom Bazel queries can avoid building incorrectly transitioned targets while still analyzing extension- and plugin-reachable code.

## 3.7.4 (2026-04-26)

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,9 @@ By default, Periphery looks for the index store at `.build/debug/index/store`. T
bazel run @periphery -- scan --bazel
```

The `--bazel` option enables Bazel mode, which provides seamless integration with your project. It works by querying your project to identify all top-level targets, generating a hidden implementation of the [scan](https://github.com/peripheryapp/periphery/blob/master/bazel/rules.bzl) rule, and then invoking `bazel run`. You can filter the top-level targets with the `--bazel-filter <value>` option, where `<value>` will be passed as the first argument to Bazel's [filter](https://bazel.build/query/language#filter) operator. The generated query can be seen in the console with the `--verbose` option.
The `--bazel` option enables Bazel mode, which provides seamless integration with your project. It works by querying your project to identify all top-level targets, generating a hidden implementation of the [scan](https://github.com/peripheryapp/periphery/blob/master/bazel/rules.bzl) rule, and then invoking `bazel run`. You can filter the default top-level target query with the `--bazel-filter <value>` option, where `<value>` will be passed as the first argument to Bazel's [filter](https://bazel.build/query/language#filter) operator. You can also override the generated query entirely with `--bazel-query <value>`, which is useful when you need to exclude targets such as ones tagged `manual`, or avoid building targets that use an incorrect transition when built directly. The generated query can be seen in the console with the `--verbose` option.

Periphery's generated scan rule follows embedded bundle and plugin edges transitively, so you can root the scan in application targets while still analyzing code that is only reachable through extensions, app clips, watch applications, or Swift compiler plugins.

> [!TIP]
> By default, Periphery passes `--check_visibility=false` to `bazel run` to simplify integration, since the generated scan target references your project's targets which may not otherwise be visible. However, disabling visibility checking can invalidate Bazel's analysis cache, resulting in slower subsequent builds.
Expand Down
5 changes: 4 additions & 1 deletion Sources/Configuration/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ public final class Configuration {
@Setting(key: "bazel_filter", defaultValue: nil)
public var bazelFilter: String?

@Setting(key: "bazel_query", defaultValue: nil)
public var bazelQuery: String?

@Setting(key: "bazel_index_store", defaultValue: nil)
public var bazelIndexStore: FilePath?

Expand Down Expand Up @@ -224,7 +227,7 @@ public final class Configuration {
$disableUpdateCheck, $strict, $indexStorePath,
$skipBuild, $skipSchemesValidation, $cleanBuild, $buildArguments, $xcodeListArguments, $relativeResults,
$jsonPackageManifestPath, $retainCodableProperties, $retainEncodableProperties, $baseline, $writeBaseline,
$writeResults, $genericProjectConfig, $bazel, $bazelFilter, $bazelIndexStore, $bazelCheckVisibility,
$writeResults, $genericProjectConfig, $bazel, $bazelFilter, $bazelQuery, $bazelIndexStore, $bazelCheckVisibility,
]

private func buildFilenameMatchers(with patterns: [String]) -> [FilenameMatcher] {
Expand Down
4 changes: 4 additions & 0 deletions Sources/Frontend/Commands/ScanCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ struct ScanCommand: ParsableCommand {
@Option(help: "Filter pattern applied to the Bazel top-level targets query")
var bazelFilter: String?

@Option(help: "Query expression used for the Bazel top-level targets query. Overrides the default query and bypasses bazelFilter.")
var bazelQuery: String?

@Option(help: "Path to a global index store populated by Bazel. If provided, will be used instead of individual module stores.")
var bazelIndexStore: FilePath?

Expand Down Expand Up @@ -221,6 +224,7 @@ struct ScanCommand: ParsableCommand {
configuration.apply(\.$genericProjectConfig, genericProjectConfig)
configuration.apply(\.$bazel, bazel)
configuration.apply(\.$bazelFilter, bazelFilter)
configuration.apply(\.$bazelQuery, bazelQuery)
configuration.apply(\.$bazelIndexStore, bazelIndexStore)
configuration.apply(\.$bazelCheckVisibility, bazelCheckVisibility)

Expand Down
4 changes: 4 additions & 0 deletions Sources/ProjectDrivers/BazelProjectDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ public final class BazelProjectDriver: ProjectDriver {
}

private var query: String {
if let bazelQuery = configuration.bazelQuery {
return bazelQuery
}

let kinds = Self.topLevelKinds.joined(separator: "|")
let query = "filter('^//.*', kind('(\(kinds)) rule', deps(//...)))"

Expand Down
29 changes: 22 additions & 7 deletions bazel/internal/scan/scan.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ PeripheryInfo = provider(
},
)

_TRANSITIVE_ATTRS = [
"app_clips",
"deps",
"extension",
"extensions",
"plugins",
"swift_target",
"watch_application",
]

def _periphery_info_providers(rule_attr):
providers = []
for attr in _TRANSITIVE_ATTRS:
value = getattr(rule_attr, attr, None)
values = value if type(value) == "list" else ([value] if value else [])
for target in values:
if PeripheryInfo in target:
providers.append(target[PeripheryInfo])
return providers

def _force_indexstore_impl(settings, _attr):
return {
"//command_line_option:features": settings["//command_line_option:features"] + [
Expand Down Expand Up @@ -90,12 +110,7 @@ def _scan_inputs_aspect_impl(target, ctx):
elif ".xcmappingmodel" in resource.path:
xcmappingmodels.append(resource)

deps = getattr(ctx.rule.attr, "deps", [])
providers = [dep[PeripheryInfo] for dep in deps]
swift_target = getattr(ctx.rule.attr, "swift_target", None)

if swift_target:
providers.append(swift_target[PeripheryInfo])
providers = _periphery_info_providers(ctx.rule.attr)

swift_srcs_depset = depset(
swift_srcs,
Expand Down Expand Up @@ -206,5 +221,5 @@ def scan_impl(ctx):

scan_inputs_aspect = aspect(
_scan_inputs_aspect_impl,
attr_aspects = ["deps", "swift_target"],
attr_aspects = _TRANSITIVE_ATTRS,
)
Loading