Skip to content

Commit 5349aac

Browse files
sina-mahdavibob-wilson
authored andcommitted
Dependencies: support clang modules when parsing dependency validation info
1 parent ad1c3cf commit 5349aac

File tree

4 files changed

+119
-43
lines changed

4 files changed

+119
-43
lines changed

Sources/SWBCore/Dependencies.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,8 +438,18 @@ public struct DependencyValidationInfo: Hashable, Sendable, Codable {
438438
}
439439
}
440440

441+
public struct Include: Hashable, Sendable, Codable {
442+
public let path: Path
443+
public let includeLocations: [Diagnostic.Location]
444+
445+
public init(path: Path, includeLocations: [Diagnostic.Location]) {
446+
self.path = path
447+
self.includeLocations = includeLocations
448+
}
449+
}
450+
441451
public enum Payload: Hashable, Sendable, Codable {
442-
case clangDependencies(imports: [Import], includes: [Path])
452+
case clangDependencies(imports: [Import], includes: [Include])
443453
case swiftDependencies(imports: [Import])
444454
case unsupported
445455
}

Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift

Lines changed: 105 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -353,8 +353,6 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA
353353
}
354354
}
355355

356-
357-
358356
/// Intended to be called during task dependency setup.
359357
/// If remote caching is enabled along with integrated cache queries, it will request
360358
/// a `ClangCachingMaterializeKeyTaskAction` as task dependency.
@@ -485,35 +483,49 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA
485483
outputDelegate: any TaskOutputDelegate
486484
) throws {
487485
let payload: DependencyValidationInfo.Payload
488-
if let traceFilePath {
489-
let traceFileContent = try fileSystem.read(traceFilePath)
490-
491-
let traceData: Array<TraceData>
492-
// clang will emit an empty file instead of an empty array when there's nothing to trace
493-
if traceFileContent.isEmpty {
494-
traceData = []
495-
} else {
496-
do {
497-
traceData = try JSONDecoder().decode(Array<TraceData>.self, from: Data(traceFileContent))
498-
} catch {
499-
throw StubError.error("Failed to decode json trace at \(traceFilePath.str): \(error)")
500-
}
501-
}
502-
503-
var allFiles = Set<Path>()
504-
traceData.forEach { allFiles.formUnion(Set($0.includes)) }
486+
if let traceFilePath,
487+
let traceData = try parseTraceData(Data(fileSystem.read(traceFilePath))) {
505488

506489
outputDelegate.incrementTaskCounter(.headerDependenciesValidatedTasks)
507-
508490
if isModular {
509-
let (imports, includes) = separateImportsFromIncludes(allFiles)
510491
outputDelegate.incrementTaskCounter(.moduleDependenciesValidatedTasks)
511-
outputDelegate.incrementTaskCounter(.moduleDependenciesScanned, by: imports.count)
492+
}
493+
494+
switch traceData {
495+
case let .V1(traceDataV1):
496+
// mapping each header path to the set of locations that include it
497+
var allFiles = [Path: Set<SWBUtil.Diagnostic.Location>]();
498+
499+
for entry in traceDataV1 {
500+
entry.includes.forEach { allFiles[$0, default: []].insert(.path(entry.source, fileLocation: nil)) }
501+
}
502+
503+
if isModular {
504+
let (imports, includes) = separateImportsFromIncludes(allFiles)
505+
outputDelegate.incrementTaskCounter(.moduleDependenciesScanned, by: imports.count)
506+
outputDelegate.incrementTaskCounter(.headerDependenciesScanned, by: includes.count)
507+
payload = .clangDependencies(imports: imports, includes: includes)
508+
} else {
509+
let includes = allFiles.map { file, locations in DependencyValidationInfo.Include(path: file, includeLocations: Array(locations)) }
510+
outputDelegate.incrementTaskCounter(.headerDependenciesScanned, by: includes.count)
511+
payload = .clangDependencies(imports: [], includes: includes)
512+
}
513+
case let .V2(traceDataV2):
514+
var allIncludes = [Path: Set<SWBUtil.Diagnostic.Location>]();
515+
var allImports = [String: Set<SWBUtil.Diagnostic.Location>]();
516+
517+
for entry in traceDataV2.dependencies {
518+
entry.includes.forEach { allIncludes[$0.file, default: []].insert(parseTraceSourceLocation($0.location)) }
519+
if isModular {
520+
entry.imports.forEach { allImports[$0.module, default: []].insert(parseTraceSourceLocation($0.location)) }
521+
}
522+
}
523+
524+
let imports = allImports.map { name, locations in DependencyValidationInfo.Import(dependency: ModuleDependency(name: name, accessLevel: .Private, optional: false), importLocations: Array(locations)) }
525+
let includes = allIncludes.map { file, locations in DependencyValidationInfo.Include(path: file, includeLocations: Array(locations)) }
512526
outputDelegate.incrementTaskCounter(.headerDependenciesScanned, by: includes.count)
527+
outputDelegate.incrementTaskCounter(.moduleDependenciesScanned, by: imports.count)
513528
payload = .clangDependencies(imports: imports, includes: includes)
514-
} else {
515-
outputDelegate.incrementTaskCounter(.headerDependenciesScanned, by: allFiles.count)
516-
payload = .clangDependencies(imports: [], includes: Array(allFiles))
517529
}
518530
} else {
519531
outputDelegate.incrementTaskCounter(.headerDependenciesNotValidatedTasks)
@@ -534,29 +546,27 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA
534546
}
535547
}
536548

537-
// Clang's dependency tracing does not currently clearly distinguish modular imports from non-modular includes.
538-
// Until that gets fixed, just guess that if the file is contained in a framework, it comes from a module with
549+
// Clang's dependency tracing V1 did not clearly distinguish modular imports from non-modular includes.
550+
// To keep supporting those trace files, just guess that if the file is contained in a framework, it comes from a module with
539551
// the same name. That is obviously not going to be reliable but it unblocks us from continuing experiments with
540552
// dependency specifications.
541-
private static func separateImportsFromIncludes(_ files: Set<Path>) -> ([DependencyValidationInfo.Import], [Path]) {
553+
private static func separateImportsFromIncludes(_ files: [Path: Set<SWBUtil.Diagnostic.Location>]) -> ([DependencyValidationInfo.Import], [DependencyValidationInfo.Include]) {
542554
func findFrameworkName(_ file: Path) -> String? {
543555
if file.fileExtension == "framework" {
544556
return file.basenameWithoutSuffix
545557
}
546558
return file.dirname.isEmpty || file.dirname.isRoot ? nil : findFrameworkName(file.dirname)
547559
}
548-
var moduleNames: [String] = []
549-
var includeFiles: [Path] = []
550-
for file in files {
560+
var moduleImports: [DependencyValidationInfo.Import] = []
561+
var headerIncludes: [DependencyValidationInfo.Include] = []
562+
for (file, includeLocations) in files {
551563
if let frameworkName = findFrameworkName(file) {
552-
moduleNames.append(frameworkName)
564+
moduleImports.append(DependencyValidationInfo.Import(dependency: ModuleDependency(name: frameworkName, accessLevel: .Private, optional: false), importLocations: Array(includeLocations)))
553565
} else {
554-
includeFiles.append(file)
566+
headerIncludes.append(DependencyValidationInfo.Include(path: file, includeLocations: Array(includeLocations)))
555567
}
556568
}
557-
let moduleDependencies = moduleNames.map { ModuleDependency(name: $0, accessLevel: .Private, optional: false) }
558-
let moduleImports = moduleDependencies.map { DependencyValidationInfo.Import(dependency: $0, importLocations: []) }
559-
return (moduleImports, includeFiles)
569+
return (moduleImports, headerIncludes)
560570
}
561571
}
562572

@@ -623,9 +633,65 @@ public final class ClangNonModularCompileTaskAction: TaskAction {
623633
}
624634
}
625635

636+
fileprivate func parseTraceData(_ data: Data) throws -> TraceData? {
637+
if let jsonObject = try JSONSerialization.jsonObject(with: data) as? [String: Any],
638+
let version = jsonObject["version"] as? String {
639+
if version == "2.0.0" {
640+
return .V2(try JSONDecoder().decode(TraceData.TraceFileV2.self, from: data))
641+
}
642+
return nil
643+
} else {
644+
// The initial unversioned format (v1) of the trace file generated from clang
645+
// is a JSON array so deserializing it as a dictionary will fail.
646+
return .V1(try JSONDecoder().decode(TraceData.TraceFileV1.self, from: data))
647+
}
648+
}
649+
650+
fileprivate func parseTraceSourceLocation(_ locationStr: String) -> SWBUtil.Diagnostic.Location {
651+
guard let match = locationStr.wholeMatch(of: #/(.+):(\d+):(\d+)/#) else {
652+
return .unknown
653+
}
654+
let filename = Path(match.1)
655+
let line = Int(match.2)
656+
let column = Int(match.3)
657+
if let line {
658+
return .path(filename, fileLocation: .textual(line: line, column: column))
659+
}
660+
return .unknown
661+
}
662+
626663
// Results from tracing header includes with "direct-per-file" filtering.
627664
// This is used to validate dependencies.
628-
fileprivate struct TraceData: Decodable {
629-
let source: Path
630-
let includes: [Path]
665+
fileprivate enum TraceData: Decodable {
666+
fileprivate struct Include: Decodable {
667+
let location: String
668+
let file: Path
669+
}
670+
671+
fileprivate struct Import: Decodable {
672+
let location: String
673+
let module: String
674+
let file: Path
675+
}
676+
677+
fileprivate struct TraceDataObjectV1: Decodable {
678+
let source: Path
679+
let includes: [Path]
680+
}
681+
682+
fileprivate struct TraceDataObjectV2: Decodable {
683+
let source: Path
684+
let includes: [Include]
685+
let imports: [Import]
686+
}
687+
688+
fileprivate typealias TraceFileV1 = [TraceDataObjectV1]
689+
690+
fileprivate struct TraceFileV2: Decodable {
691+
let version: String
692+
let dependencies: [TraceDataObjectV2]
693+
}
694+
695+
case V1(TraceFileV1)
696+
case V2(TraceFileV2)
631697
}

Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public final class ValidateDependenciesTaskAction: TaskAction {
4545
}
4646

4747
do {
48-
var allClangIncludes = Set<Path>()
48+
var allClangIncludes = Set<DependencyValidationInfo.Include>()
4949
var allClangImports = Set<DependencyValidationInfo.Import>()
5050
var allSwiftImports = Set<DependencyValidationInfo.Import>()
5151
var unsupported = false
@@ -116,7 +116,7 @@ public final class ValidateDependenciesTaskAction: TaskAction {
116116
if unsupported {
117117
diagnostics.append(contentsOf: [headerContext.makeUnsupportedToolchainDiagnostic()])
118118
} else {
119-
let (missingDeps, unusedDeps) = headerContext.computeMissingAndUnusedDependencies(includes: Array(allClangIncludes))
119+
let (missingDeps, unusedDeps) = headerContext.computeMissingAndUnusedDependencies(includes: allClangIncludes.map { $0.path })
120120
outputDelegate.incrementCounter(.headerDependenciesMissing, by: missingDeps.count)
121121
outputDelegate.incrementCounter(.headerDependenciesUnused, by: unusedDeps.count)
122122

Tests/SWBBuildSystemTests/DependencyValidationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
542542

543543
// Expect complaint about undeclared dependency
544544
try await tester.checkBuild(parameters: BuildParameters(configuration: "Debug"), runDestination: .host, persistent: true) { results in
545-
results.checkError(.contains("Missing entries in MODULE_DEPENDENCIES: Foundation (for task"))
545+
results.checkError(.contains("Missing entry in MODULE_DEPENDENCIES: Foundation (for task"))
546546
}
547547

548548
// Declaring dependencies resolves the problem

0 commit comments

Comments
 (0)