Skip to content

Commit 02d7b50

Browse files
authored
Refactor / clean-up SKOptions handling (spotify#20)
* Refactor / clean-up SKOptions handling * CommandRunnerFake: Also validate the cwd * CommandRunnerFake: Also throw on unmocked commands * Remove unnecessary thread-safety handling
1 parent 9572da3 commit 02d7b50

File tree

11 files changed

+979
-438
lines changed

11 files changed

+979
-438
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) 2025 Spotify AB.
2+
//
3+
// Licensed to the Apache Software Foundation (ASF) under one
4+
// or more contributor license agreements. See the NOTICE file
5+
// distributed with this work for additional information
6+
// regarding copyright ownership. The ASF licenses this file
7+
// to you under the Apache License, Version 2.0 (the
8+
// "License"); you may not use this file except in compliance
9+
// with the License. You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing,
14+
// software distributed under the License is distributed on an
15+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
// KIND, either express or implied. See the License for the
17+
// specific language governing permissions and limitations
18+
// under the License.
19+
20+
import Foundation
21+
22+
private let logger = makeFileLevelBSPLogger()
23+
24+
enum BazelTargetAquerierError: Error, LocalizedError {
25+
case noMnemonics
26+
case noTargets
27+
28+
var errorDescription: String? {
29+
switch self {
30+
case .noMnemonics:
31+
return "A list of mnemonics is necessary to aquery targets"
32+
case .noTargets:
33+
return "A list of targets is necessary to run aqueries"
34+
}
35+
}
36+
}
37+
38+
/// Small abstraction to handle and cache the results of bazel _action queries_.
39+
/// FIXME: This is separate from BazelTargetQuerier because of the different output types, but we can unify these.
40+
///
41+
/// FIXME: Currently uses text outputs, should use proto instead so that we can organize and test this properly.
42+
final class BazelTargetAquerier {
43+
44+
private let commandRunner: CommandRunner
45+
private var queryCache = [String: String]()
46+
47+
init(commandRunner: CommandRunner = ShellCommandRunner()) {
48+
self.commandRunner = commandRunner
49+
}
50+
51+
func aquery(
52+
forConfig config: InitializedServerConfig,
53+
mnemonics: Set<String>,
54+
additionalFlags: [String]
55+
) throws
56+
-> String
57+
{
58+
guard !mnemonics.isEmpty else {
59+
throw BazelTargetAquerierError.noMnemonics
60+
}
61+
62+
let targets = config.baseConfig.targets
63+
guard !targets.isEmpty else {
64+
throw BazelTargetAquerierError.noTargets
65+
}
66+
67+
let mnemonicsFilter = mnemonics.sorted().joined(separator: "|")
68+
let depsQuery = BazelTargetQuerier.queryDepsString(forTargets: targets)
69+
70+
let otherFlags = additionalFlags.joined(separator: " ")
71+
let cmd =
72+
"aquery \"mnemonic('\(mnemonicsFilter)', \(depsQuery))\" \(otherFlags)"
73+
logger.info("Processing root aquery request")
74+
75+
if let cached = queryCache[cmd] {
76+
logger.debug("Returning cached results")
77+
return cached
78+
}
79+
80+
// Run the aquery on the special index output base since that's where we will build at.
81+
let output = try commandRunner.bazelIndexAction(
82+
initializedConfig: config,
83+
cmd: cmd
84+
)
85+
86+
queryCache[cmd] = output
87+
88+
return output
89+
}
90+
91+
func clearCache() {
92+
queryCache = [:]
93+
}
94+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright (c) 2025 Spotify AB.
2+
//
3+
// Licensed to the Apache Software Foundation (ASF) under one
4+
// or more contributor license agreements. See the NOTICE file
5+
// distributed with this work for additional information
6+
// regarding copyright ownership. The ASF licenses this file
7+
// to you under the Apache License, Version 2.0 (the
8+
// "License"); you may not use this file except in compliance
9+
// with the License. You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing,
14+
// software distributed under the License is distributed on an
15+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
// KIND, either express or implied. See the License for the
17+
// specific language governing permissions and limitations
18+
// under the License.
19+
20+
import BuildServerProtocol
21+
import Foundation
22+
import LanguageServerProtocol
23+
24+
private let logger = makeFileLevelBSPLogger()
25+
26+
enum BazelTargetCompilerArgsExtractorError: Error, LocalizedError {
27+
case invalidObjCUri(String)
28+
29+
var errorDescription: String? {
30+
switch self {
31+
case .invalidObjCUri(let uri):
32+
return "Unexpected non-Swift URI missing root URI prefix: \(uri)"
33+
}
34+
}
35+
}
36+
37+
/// Abstraction that handles running action queries and extracting the compiler args for a given target file.
38+
final class BazelTargetCompilerArgsExtractor {
39+
40+
private let aquerier: BazelTargetAquerier
41+
private let config: InitializedServerConfig
42+
private var argsCache = [String: [String]?]()
43+
44+
init(aquerier: BazelTargetAquerier = BazelTargetAquerier(), config: InitializedServerConfig) {
45+
self.aquerier = aquerier
46+
self.config = config
47+
}
48+
49+
func compilerArgs(
50+
forDoc textDocument: URI,
51+
inTarget bazelTarget: String,
52+
language: Language
53+
) throws -> [String]? {
54+
// Ignore Obj-C header requests, since these don't compile
55+
if textDocument.stringValue.hasSuffix(".h") {
56+
return nil
57+
}
58+
59+
// For Swift, compilation is done at the target-level. But for ObjC, it's file-based instead.
60+
let cacheKey: String
61+
let contentToQuery: String
62+
if language == .swift {
63+
cacheKey = bazelTarget
64+
contentToQuery = bazelTarget
65+
} else {
66+
// Make the path relative, as this is what aquery will return
67+
let fullUri = textDocument.stringValue
68+
let prefixToCut = "file://" + config.rootUri + "/"
69+
guard fullUri.hasPrefix(prefixToCut) else {
70+
throw BazelTargetCompilerArgsExtractorError.invalidObjCUri(fullUri)
71+
}
72+
let parsedFile = String(fullUri.dropFirst(prefixToCut.count))
73+
cacheKey = bazelTarget + "|" + parsedFile
74+
contentToQuery = parsedFile
75+
}
76+
77+
logger.info("Fetching compiler args for \(cacheKey)")
78+
79+
if let cached = argsCache[cacheKey] {
80+
logger.debug("Returning cached results")
81+
return cached
82+
}
83+
84+
// First, get the root aquery, which contains all the compilation steps for the targets the BSP was configured for.
85+
let rootAquery = try aquerier.aquery(
86+
forConfig: config,
87+
mnemonics: ["SwiftCompile", "ObjcCompile"],
88+
additionalFlags: ["--noinclude_artifacts"]
89+
)
90+
91+
// Then, extract the compiler arguments for the target file from the root aquery.
92+
let processedArgs = CompilerArgumentsProcessor.extractAndProcessCompilerArgs(
93+
fromAquery: rootAquery,
94+
bazelTarget: bazelTarget,
95+
contentToQuery: contentToQuery,
96+
language: language,
97+
initializedConfig: config
98+
)
99+
argsCache[cacheKey] = processedArgs
100+
return processedArgs
101+
}
102+
103+
func clearCache() {
104+
argsCache = [:]
105+
aquerier.clearCache()
106+
}
107+
}

0 commit comments

Comments
 (0)