Skip to content

Commit f5ec279

Browse files
committed
Interpret the working directories in compile_commands.json relative to the directory that contains compile_commands.json
Fixes #1908
1 parent 79964c8 commit f5ec279

File tree

7 files changed

+147
-43
lines changed

7 files changed

+147
-43
lines changed

Sources/BuildServerIntegration/CompilationDatabase.swift

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,35 @@ package struct CompilationDatabaseCompileCommand: Equatable, Codable {
7878
try container.encodeIfPresent(output, forKey: .output)
7979
}
8080

81-
/// The `DocumentURI` for this file. If `filename` is relative and `directory` is
82-
/// absolute, returns the concatenation. However, if both paths are relative,
83-
/// it falls back to `filename`, which is more likely to be the identifier
84-
/// that a caller will be looking for.
85-
package var uri: DocumentURI {
86-
if filename.isAbsolutePath || !directory.isAbsolutePath {
87-
return DocumentURI(filePath: filename, isDirectory: false)
88-
} else {
89-
return DocumentURI(URL(fileURLWithPath: directory).appending(component: filename, directoryHint: .notDirectory))
81+
/// The `DocumentURI` for this file. If this a relative path, it will be interpreted relative to the compile command's
82+
/// working directory, which in turn is relative to `compileCommandsDirectory`, the directory that contains the
83+
/// `compile_commands.json` file.
84+
package func uri(compileCommandsDirectory: URL) -> DocumentURI {
85+
if filename.isAbsolutePath {
86+
return DocumentURI(URL(fileURLWithPath: self.filename))
9087
}
88+
return DocumentURI(
89+
URL(
90+
fileURLWithPath: self.filename,
91+
relativeTo: self.directoryURL(compileCommandsDirectory: compileCommandsDirectory)
92+
)
93+
)
94+
}
95+
96+
/// A file URL representing `directory`. If `directory` is relative, it's interpreted relative to
97+
/// `compileCommandsDirectory`, the directory that contains the
98+
func directoryURL(compileCommandsDirectory: URL) -> URL {
99+
return URL(fileURLWithPath: directory, isDirectory: true, relativeTo: compileCommandsDirectory)
91100
}
92101
}
93102

103+
extension CodingUserInfoKey {
104+
/// When decoding `JSONCompilationDatabase` a `URL` representing the directory that contains the
105+
/// `compile_commands.json`.
106+
package static let compileCommandsDirectoryKey: CodingUserInfoKey =
107+
CodingUserInfoKey(rawValue: "lsp.compile-commands-dir")!
108+
}
109+
94110
/// The JSON clang-compatible compilation database.
95111
///
96112
/// Example:
@@ -110,13 +126,26 @@ package struct JSONCompilationDatabase: Equatable, Codable {
110126
private var pathToCommands: [DocumentURI: [Int]] = [:]
111127
var commands: [CompilationDatabaseCompileCommand] = []
112128

113-
package init(_ commands: [CompilationDatabaseCompileCommand] = []) {
129+
/// The directory that contains the `compile_commands.json` file.
130+
private let compileCommandsDirectory: URL
131+
132+
package init(_ commands: [CompilationDatabaseCompileCommand] = [], compileCommandsDirectory: URL) {
133+
self.compileCommandsDirectory = compileCommandsDirectory
114134
for command in commands {
115135
add(command)
116136
}
117137
}
118138

139+
/// Decode the `JSONCompilationDatabase` from a decoder.
140+
///
141+
/// A `URL` representing the directory that contains the `compile_commands.json` must be passed in the decoder's
142+
/// `userInfo` via the `compileCommandsDirectoryKey`.
119143
package init(from decoder: Decoder) throws {
144+
guard let compileCommandsDirectory = decoder.userInfo[.compileCommandsDirectoryKey] as? URL else {
145+
struct MissingCompileCommandsDirectoryKeyError: Error {}
146+
throw MissingCompileCommandsDirectoryKeyError()
147+
}
148+
self.compileCommandsDirectory = compileCommandsDirectory
120149
var container = try decoder.unkeyedContainer()
121150
while !container.isAtEnd {
122151
self.add(try container.decode(CompilationDatabaseCompileCommand.self))
@@ -135,7 +164,9 @@ package struct JSONCompilationDatabase: Equatable, Codable {
135164
/// - Returns: `nil` if the file does not exist
136165
package init(file: URL) throws {
137166
let data = try Data(contentsOf: file)
138-
self = try JSONDecoder().decode(JSONCompilationDatabase.self, from: data)
167+
let decoder = JSONDecoder()
168+
decoder.userInfo[.compileCommandsDirectoryKey] = file.deletingLastPathComponent()
169+
self = try decoder.decode(JSONCompilationDatabase.self, from: data)
139170
}
140171

141172
package func encode(to encoder: Encoder) throws {
@@ -156,7 +187,7 @@ package struct JSONCompilationDatabase: Equatable, Codable {
156187
}
157188

158189
private mutating func add(_ command: CompilationDatabaseCompileCommand) {
159-
let uri = command.uri
190+
let uri = command.uri(compileCommandsDirectory: compileCommandsDirectory)
160191
pathToCommands[uri, default: []].append(commands.count)
161192

162193
if let symlinkTarget = uri.symlinkTarget {

Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ fileprivate extension CompilationDatabaseCompileCommand {
2828
///
2929
/// If the compiler is a symlink to `swiftly`, it uses `swiftlyResolver` to find the corresponding executable in a
3030
/// real toolchain and returns that executable.
31-
func compiler(swiftlyResolver: SwiftlyResolver) async -> String? {
31+
func compiler(swiftlyResolver: SwiftlyResolver, compileCommandsDirectory: URL) async -> String? {
3232
guard let compiler = commandLine.first else {
3333
return nil
3434
}
3535
let swiftlyResolved = await orLog("Resolving swiftly") {
3636
try await swiftlyResolver.resolve(
3737
compiler: URL(fileURLWithPath: compiler),
38-
workingDirectory: URL(fileURLWithPath: directory)
38+
workingDirectory: directoryURL(compileCommandsDirectory: compileCommandsDirectory)
3939
)?.filePath
4040
}
4141
if let swiftlyResolved {
@@ -62,7 +62,17 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
6262

6363
private let connectionToSourceKitLSP: any Connection
6464

65-
package let configPath: URL
65+
/// The path of the `compile_commands.json` file
66+
private let configPath: URL
67+
68+
/// The directory containing the `compile_commands.json` file and relative to which the working directories in the
69+
/// compilation database will be interpreted.
70+
///
71+
/// Note that while `configPath` might be a symlink, this is always the directory of the compilation database's
72+
/// `realpath` since the user most likely wants to reference relative working directories relative to that path
73+
/// instead of relative to eg. a symlink in the project's root directory, which was just placed there so SourceKit-LSP
74+
/// finds the compilation database in a build directory.
75+
private var configDirectory: URL
6676

6777
private let swiftlyResolver = SwiftlyResolver()
6878

@@ -107,12 +117,14 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
107117
self.toolchainRegistry = toolchainRegistry
108118
self.connectionToSourceKitLSP = connectionToSourceKitLSP
109119
self.configPath = configPath
120+
// See comment on configDirectory why we do `realpath` here
121+
self.configDirectory = try configPath.realpath.deletingLastPathComponent()
110122
}
111123

112124
package func buildTargets(request: WorkspaceBuildTargetsRequest) async throws -> WorkspaceBuildTargetsResponse {
113125
let compilers = Set(
114126
await compdb.commands.asyncCompactMap { (command) -> String? in
115-
await command.compiler(swiftlyResolver: swiftlyResolver)
127+
await command.compiler(swiftlyResolver: swiftlyResolver, compileCommandsDirectory: configDirectory)
116128
}
117129
).sorted { $0 < $1 }
118130
let targets = try await compilers.asyncMap { compiler in
@@ -142,10 +154,11 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
142154
return nil
143155
}
144156
let commandsWithRequestedCompilers = await compdb.commands.lazy.asyncFilter { command in
145-
return await targetCompiler == command.compiler(swiftlyResolver: swiftlyResolver)
157+
return await targetCompiler
158+
== command.compiler(swiftlyResolver: swiftlyResolver, compileCommandsDirectory: configDirectory)
146159
}
147160
let sources = commandsWithRequestedCompilers.map {
148-
SourceItem(uri: $0.uri, kind: .file, generated: false)
161+
SourceItem(uri: $0.uri(compileCommandsDirectory: configDirectory), kind: .file, generated: false)
149162
}
150163
return SourcesItem(target: target, sources: Array(sources))
151164
}
@@ -172,14 +185,15 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
172185
) async throws -> TextDocumentSourceKitOptionsResponse? {
173186
let targetCompiler = try request.target.compileCommandsCompiler
174187
let command = await compdb[request.textDocument.uri].asyncFilter {
175-
return await $0.compiler(swiftlyResolver: swiftlyResolver) == targetCompiler
188+
return await $0.compiler(swiftlyResolver: swiftlyResolver, compileCommandsDirectory: configDirectory)
189+
== targetCompiler
176190
}.first
177191
guard let command else {
178192
return nil
179193
}
180194
return TextDocumentSourceKitOptionsResponse(
181195
compilerArguments: Array(command.commandLine.dropFirst()),
182-
workingDirectory: command.directory
196+
workingDirectory: try command.directoryURL(compileCommandsDirectory: configDirectory).filePath
183197
)
184198
}
185199

Sources/SKTestSupport/CheckCoding.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ import XCTest
2020
package func checkCoding<T: Codable & Equatable>(
2121
_ value: T,
2222
json: String,
23+
userInfo: [CodingUserInfoKey: any Sendable] = [:],
2324
file: StaticString = #filePath,
2425
line: UInt = #line
2526
) {
2627
let encoder = JSONEncoder()
28+
encoder.userInfo = userInfo
2729
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
2830

2931
let data = try! encoder.encode(WrapFragment(value: value))
@@ -42,6 +44,7 @@ package func checkCoding<T: Codable & Equatable>(
4244
XCTAssertEqual(json, str, file: file, line: line)
4345

4446
let decoder = JSONDecoder()
47+
decoder.userInfo = userInfo
4548
let decodedValue = try! decoder.decode(WrapFragment<T>.self, from: data).value
4649

4750
XCTAssertEqual(value, decodedValue, file: file, line: line)

Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ package struct IndexedSingleSwiftFileTestProject {
153153
filename: try testFileURL.filePath,
154154
commandLine: [try swiftc.filePath] + compilerArguments
155155
)
156-
]
156+
],
157+
compileCommandsDirectory: testWorkspaceDirectory
157158
)
158159
let encoder = JSONEncoder()
159160
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]

Tests/BuildServerIntegrationTests/CompilationDatabaseTests.swift

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -121,18 +121,28 @@ final class CompilationDatabaseTests: XCTestCase {
121121
}
122122

123123
func testJSONCompilationDatabaseCoding() {
124+
#if os(Windows)
125+
let fileSystemRoot = URL(filePath: #"C:\"#)
126+
#else
127+
let fileSystemRoot = URL(filePath: "/")
128+
#endif
129+
let userInfo = [CodingUserInfoKey.compileCommandsDirectoryKey: fileSystemRoot]
124130
checkCoding(
125-
JSONCompilationDatabase([]),
131+
JSONCompilationDatabase([], compileCommandsDirectory: fileSystemRoot),
126132
json: """
127133
[
128134
129135
]
130-
"""
136+
""",
137+
userInfo: userInfo
138+
)
139+
let db = JSONCompilationDatabase(
140+
[
141+
.init(directory: "a", filename: "b", commandLine: [], output: nil),
142+
.init(directory: "c", filename: "b", commandLine: [], output: nil),
143+
],
144+
compileCommandsDirectory: fileSystemRoot
131145
)
132-
let db = JSONCompilationDatabase([
133-
.init(directory: "a", filename: "b", commandLine: [], output: nil),
134-
.init(directory: "c", filename: "b", commandLine: [], output: nil),
135-
])
136146
checkCoding(
137147
db,
138148
json: """
@@ -152,7 +162,8 @@ final class CompilationDatabaseTests: XCTestCase {
152162
"file" : "b"
153163
}
154164
]
155-
"""
165+
""",
166+
userInfo: userInfo
156167
)
157168
}
158169

@@ -177,9 +188,9 @@ final class CompilationDatabaseTests: XCTestCase {
177188
output: nil
178189
)
179190

180-
let db = JSONCompilationDatabase([cmd1, cmd2, cmd3])
191+
let db = JSONCompilationDatabase([cmd1, cmd2, cmd3], compileCommandsDirectory: URL(filePath: fileSystemRoot))
181192

182-
XCTAssertEqual(db[DocumentURI(filePath: "b", isDirectory: false)], [cmd1])
193+
XCTAssertEqual(db[DocumentURI(filePath: "\(fileSystemRoot)a/b", isDirectory: false)], [cmd1])
183194
XCTAssertEqual(db[DocumentURI(filePath: "\(fileSystemRoot)c/b", isDirectory: false)], [cmd2])
184195
XCTAssertEqual(db[DocumentURI(filePath: "\(fileSystemRoot)b", isDirectory: false)], [cmd3])
185196
}
@@ -256,28 +267,35 @@ final class CompilationDatabaseTests: XCTestCase {
256267
}
257268

258269
func testCompilationDatabaseBuildServer() async throws {
270+
#if os(Windows)
271+
let fileSystemRoot = URL(filePath: #"C:\"#)
272+
#else
273+
let fileSystemRoot = URL(filePath: "/")
274+
#endif
275+
let workingDirectory = try fileSystemRoot.appending(components: "a").filePath
276+
let filePath = try fileSystemRoot.appending(components: "a", "a.swift").filePath
259277
try await checkCompilationDatabaseBuildServer(
260278
"""
261279
[
262280
{
263-
"file": "/a/a.swift",
264-
"directory": "/a",
265-
"arguments": ["swiftc", "-swift-version", "4", "/a/a.swift"]
281+
"file": "\(filePath)",
282+
"directory": "\(workingDirectory)",
283+
"arguments": ["swiftc", "-swift-version", "4", "\(filePath)"]
266284
}
267285
]
268286
"""
269287
) { buildServer in
270288
let settings = try await buildServer.sourceKitOptions(
271289
request: TextDocumentSourceKitOptionsRequest(
272-
textDocument: TextDocumentIdentifier(DocumentURI(URL(fileURLWithPath: "/a/a.swift"))),
290+
textDocument: TextDocumentIdentifier(DocumentURI(URL(fileURLWithPath: "\(filePath)"))),
273291
target: BuildTargetIdentifier.createCompileCommands(compiler: "swiftc"),
274292
language: .swift
275293
)
276294
)
277295

278296
XCTAssertNotNil(settings)
279-
XCTAssertEqual(settings?.workingDirectory, "/a")
280-
XCTAssertEqual(settings?.compilerArguments, ["-swift-version", "4", "/a/a.swift"])
297+
XCTAssertEqual(settings?.workingDirectory, workingDirectory)
298+
XCTAssertEqual(settings?.compilerArguments, ["-swift-version", "4", filePath])
281299
assertNil(await buildServer.indexStorePath)
282300
assertNil(await buildServer.indexDatabasePath)
283301
}

Tests/SourceKitLSPTests/CompilationDatabaseTests.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,40 @@ final class CompilationDatabaseTests: XCTestCase {
354354
}
355355
}
356356
}
357+
358+
func testCompilationDatabaseWithRelativeDirectory() async throws {
359+
let project = try await MultiFileTestProject(files: [
360+
"projectA/headers/header.h": """
361+
int 1️⃣foo2️⃣() {}
362+
""",
363+
"projectA/main.cpp": """
364+
#include "header.h"
365+
366+
int main() {
367+
3️⃣foo();
368+
}
369+
""",
370+
"compile_commands.json": """
371+
[
372+
{
373+
"directory": "projectA",
374+
"arguments": [
375+
"clang",
376+
"-I", "headers"
377+
],
378+
"file": "main.cpp"
379+
}
380+
]
381+
""",
382+
])
383+
384+
let (mainUri, positions) = try project.openDocument("main.cpp")
385+
386+
let definition = try await project.testClient.send(
387+
DefinitionRequest(textDocument: TextDocumentIdentifier(mainUri), position: positions["3️⃣"])
388+
)
389+
XCTAssertEqual(definition?.locations, [try project.location(from: "1️⃣", to: "2️⃣", in: "header.h")])
390+
}
357391
}
358392

359393
private let defaultSDKArgs: String = {

Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -787,13 +787,16 @@ final class WorkspaceTestDiscoveryTests: XCTestCase {
787787
let swiftc = try await unwrap(ToolchainRegistry.forTesting.default?.swiftc)
788788
let uri = try project.uri(for: "MyTests.swift")
789789

790-
let compilationDatabase = JSONCompilationDatabase([
791-
CompilationDatabaseCompileCommand(
792-
directory: try project.scratchDirectory.filePath,
793-
filename: uri.pseudoPath,
794-
commandLine: [try swiftc.filePath, uri.pseudoPath]
795-
)
796-
])
790+
let compilationDatabase = JSONCompilationDatabase(
791+
[
792+
CompilationDatabaseCompileCommand(
793+
directory: try project.scratchDirectory.filePath,
794+
filename: uri.pseudoPath,
795+
commandLine: [try swiftc.filePath, uri.pseudoPath]
796+
)
797+
],
798+
compileCommandsDirectory: project.scratchDirectory
799+
)
797800

798801
try await project.changeFileOnDisk(
799802
JSONCompilationDatabaseBuildServer.dbName,

0 commit comments

Comments
 (0)