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
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import Foundation

public enum BazelTargetDiscovererError: LocalizedError, Equatable {
case noRulesProvided
case noTargetsDiscovered
case noLocationsProvided

public var errorDescription: String? {
switch self {
case .noRulesProvided: return "No rules provided!"
case .noTargetsDiscovered: return "No targets discovered!"
case .noLocationsProvided: return "No locations provided!"
}
}
}
Expand All @@ -23,14 +27,22 @@ public enum BazelTargetDiscoverer {
public static func discoverTargets(
for rules: [TopLevelRuleType] = TopLevelRuleType.allCases,
bazelWrapper: String = "bazel",
locations: [String] = [],
commandRunner: CommandRunner? = nil
) throws -> [String] {
let commandRunner = commandRunner ?? ShellCommandRunner()
guard !rules.isEmpty else {
throw BazelTargetDiscovererError.noRulesProvided
}

let query = rules.map {
"kind(\($0.rawValue), ...)"
guard !locations.isEmpty else {
throw BazelTargetDiscovererError.noLocationsProvided
}
.joined(separator: " + ")

let commandRunner = commandRunner ?? ShellCommandRunner()

let kindsQuery = "\"" + rules.map { $0.rawValue }.joined(separator: "|") + "\""
let locationsQuery = locations.joined(separator: " union ")
let query = "kind(\(kindsQuery), \(locationsQuery))"

let cmd = "\(bazelWrapper) query '\(query)' --output label"

Expand Down
3 changes: 2 additions & 1 deletion Sources/SourceKitBazelBSP/Server/BaseServerConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ package struct BaseServerConfig: Equatable {
// We need to post-process the target list provided by the user
// because the queries will always return the "full" label.
// e.g: "//foo/bar" -> "//foo/bar:bar"
self.targets = targets.map { $0.toFullLabel() }
// We need to also de-dupe them if the user passed wildcards in Serve.swift.
self.targets = Set(targets.map { $0.toFullLabel() }).sorted()
}
}

Expand Down
77 changes: 56 additions & 21 deletions Sources/sourcekit-bazel-bsp/Commands/Serve.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct Serve: ParsableCommand {
@Option(
parsing: .singleValue,
help:
"The *top level* Bazel application or test target that this should serve a BSP for. Can be specified multiple times. It's best to keep this list small if possible for performance reasons. If not specified, the server will try to discover top-level targets automatically."
"The *top level* Bazel application or test target that this should serve a BSP for. Can be specified multiple times. Wildcards are supported (e.g. //foo/...). It's best to keep this list small if possible for performance reasons. If not specified, the server will try to discover top-level targets automatically."
)
var target: [String] = []

Expand Down Expand Up @@ -69,30 +69,33 @@ struct Serve: ParsableCommand {

let targets: [String]
if !target.isEmpty {
targets = target
} else {
// If the user provided no specific targets, try to discover them
// in the workspace.
logger.warning(
"No targets specified (--target)! Will now try to discover them. This can cause the BSP to perform poorly if we find too many targets. Prefer using --target explicitly if possible."
)
do {
let rulesToUse: [TopLevelRuleType]
if !topLevelRuleToDiscover.isEmpty {
rulesToUse = topLevelRuleToDiscover
var expandedTargets = [String]()
var targetsToExpand = [String]()
for _target in target {
if _target.hasSuffix("...") {
targetsToExpand.append(_target)
} else {
rulesToUse = TopLevelRuleType.allCases
expandedTargets.append(_target)
}
targets = try BazelTargetDiscoverer.discoverTargets(
for: rulesToUse,
bazelWrapper: bazelWrapper
)
} catch {
logger.error(
"Failed to initialize server: Could not discover targets. Please check your Bazel configuration or try specifying targets explicitly with `--target` instead. Failure: \(error)"
}
if !targetsToExpand.isEmpty {
expandedTargets.append(
contentsOf: try expandWildcardTargets(
bazelWrapper: bazelWrapper,
targets: targetsToExpand,
topLevelRuleToDiscover: topLevelRuleToDiscover
)
)
throw error
}
targets = expandedTargets
} else {
// If the user provided no specific targets, try to discover them
// in the workspace.
targets = try expandWildcardTargets(
bazelWrapper: bazelWrapper,
targets: ["..."],
topLevelRuleToDiscover: topLevelRuleToDiscover
)
}

let config = BaseServerConfig(
Expand All @@ -103,7 +106,39 @@ struct Serve: ParsableCommand {
compileTopLevel: compileTopLevel,
indexBuildBatchSize: indexBuildBatchSize
)

logger.debug("Initializing BSP with targets: \(targets)")

let server = SourceKitBazelBSPServer(baseConfig: config)
server.run()
}

private func expandWildcardTargets(
bazelWrapper: String,
targets: [String],
topLevelRuleToDiscover: [TopLevelRuleType]
) throws -> [String] {
let targetsString = targets.joined(separator: ", ")
logger.warning(
"Will expand wildcard targets (\(targetsString, privacy: .public)). This can cause the BSP to perform poorly if we find too many targets. Prefer passing explicit targets via --targets if possible."
)
let rulesToUse: [TopLevelRuleType]
if !topLevelRuleToDiscover.isEmpty {
rulesToUse = topLevelRuleToDiscover
} else {
rulesToUse = TopLevelRuleType.allCases
}
do {
return try BazelTargetDiscoverer.discoverTargets(
for: rulesToUse,
bazelWrapper: bazelWrapper,
locations: targets
)
} catch {
logger.error(
"Failed to initialize server: Could not expand wildcard targets (\(targetsString, privacy: .public)). Please check your Bazel configuration or try specifying targets explicitly with `--target` instead. Failure: \(error, privacy: .public)"
)
throw error
}
}
}
70 changes: 59 additions & 11 deletions Tests/SourceKitBazelBSPTests/BazelTargetDiscovererTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,60 +23,108 @@ import Testing
@testable import SourceKitBazelBSP

struct BazelTargetDiscovererTests {
@Test("Discovers single rule type with multiple targets")
@Test
func discoverSingleRuleTypeWithMultipleTargets() throws {
let commandRunner = CommandRunnerFake()
commandRunner.setResponse(
for: "fakeBazel query 'kind(ios_application, ...)' --output label",
for: "fakeBazel query 'kind(\"ios_application\", ...)' --output label",
response: "//Example/HelloWorld:HelloWorld\n//Example/AnotherApp:AnotherApp\n"
)

let targets = try BazelTargetDiscoverer.discoverTargets(
for: [.iosApplication],
bazelWrapper: "fakeBazel",
commandRunner: commandRunner
locations: ["..."],
commandRunner: commandRunner,
)

#expect(targets == ["//Example/HelloWorld:HelloWorld", "//Example/AnotherApp:AnotherApp"])
#expect(commandRunner.commands.count == 1)
#expect(commandRunner.commands[0].command == "fakeBazel query 'kind(ios_application, ...)' --output label")
#expect(commandRunner.commands[0].command == "fakeBazel query 'kind(\"ios_application\", ...)' --output label")
}

@Test("Discovers multiple rule types")
@Test
func discoverMultipleRuleTypes() throws {
let commandRunner = CommandRunnerFake()
commandRunner.setResponse(
for: "fakeBazel query 'kind(ios_application, ...) + kind(ios_unit_test, ...)' --output label",
for: "fakeBazel query 'kind(\"ios_application|ios_unit_test\", ...)' --output label",
response: "//Example/HelloWorld:HelloWorld\n//Example/HelloWorldTests:HelloWorldTests\n"
)

let targets = try BazelTargetDiscoverer.discoverTargets(
for: [.iosApplication, .iosUnitTest],
bazelWrapper: "fakeBazel",
commandRunner: commandRunner
locations: ["..."],
commandRunner: commandRunner,
)

#expect(targets == ["//Example/HelloWorld:HelloWorld", "//Example/HelloWorldTests:HelloWorldTests"])
#expect(commandRunner.commands.count == 1)
#expect(
commandRunner.commands[0].command
== "fakeBazel query 'kind(ios_application, ...) + kind(ios_unit_test, ...)' --output label"
== "fakeBazel query 'kind(\"ios_application|ios_unit_test\", ...)' --output label"
)
}

@Test("Throws error when no targets found")
@Test
func discoverAtMultipleLocations() throws {
let commandRunner = CommandRunnerFake()
commandRunner.setResponse(
for: "fakeBazel query 'kind(\"ios_application\", //A/... union //B/...)' --output label",
response: "//Example/HelloWorld:HelloWorld\n//Example/AnotherApp:AnotherApp\n"
)

let targets = try BazelTargetDiscoverer.discoverTargets(
for: [.iosApplication],
bazelWrapper: "fakeBazel",
locations: ["//A/...", "//B/..."],
commandRunner: commandRunner,
)

#expect(targets == ["//Example/HelloWorld:HelloWorld", "//Example/AnotherApp:AnotherApp"])
#expect(commandRunner.commands.count == 1)
#expect(
commandRunner.commands[0].command
== "fakeBazel query 'kind(\"ios_application\", //A/... union //B/...)' --output label"
)
}

@Test
func throwsErrorWhenNoTargetsFound() throws {
let commandRunner = CommandRunnerFake()
commandRunner.setResponse(
for: "fakeBazel query 'kind(ios_application, ...)' --output label",
for: "fakeBazel query 'kind(\"ios_application\", ...)' --output label",
response: "\n \n"
)

#expect(throws: BazelTargetDiscovererError.noTargetsDiscovered) {
try BazelTargetDiscoverer.discoverTargets(
for: [.iosApplication],
bazelWrapper: "fakeBazel",
commandRunner: commandRunner
locations: ["..."],
commandRunner: commandRunner,
)
}
}

@Test
func failsIfNoRulesProvided() throws {
#expect(throws: BazelTargetDiscovererError.noRulesProvided) {
try BazelTargetDiscoverer.discoverTargets(
for: [],
bazelWrapper: "fakeBazel",
locations: ["..."],
)
}
}

@Test
func failsIfNoLocationsProvided() throws {
#expect(throws: BazelTargetDiscovererError.noLocationsProvided) {
try BazelTargetDiscoverer.discoverTargets(
for: [.iosApplication],
bazelWrapper: "fakeBazel",
locations: [],
)
}
}
Expand Down
2 changes: 1 addition & 1 deletion rules/setup_sourcekit_bsp.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ setup_sourcekit_bsp = rule(
),
# We avoid using label_list here to not trigger unnecessary bazel dependency graph checks.
"targets": attr.string_list(
doc = "The *top level* Bazel applications or test targets that this should serve a BSP for. It's best to keep this list small if possible for performance reasons. If not specified, the server will try to discover top-level targets automatically",
doc = "The *top level* Bazel applications or test targets that this should serve a BSP for. Wildcards are supported (e.g. //foo/...). It's best to keep this list small if possible for performance reasons. If not specified, the server will try to discover top-level targets automatically",
mandatory = True,
),
"bazel_wrapper": attr.string(
Expand Down