Skip to content

Commit c4d33bf

Browse files
authored
Merge pull request #778 from sina-mahdavi/sina-mahdavi/clang-modules-dependency-info
Dependencies: support clang modules when parsing dependency validation info
2 parents 6e70b89 + 6a44f6a commit c4d33bf

File tree

4 files changed

+128
-43
lines changed

4 files changed

+128
-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: 114 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,51 @@ 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 .empty:
496+
payload = .clangDependencies(imports: [], includes: [])
497+
case let .v1(traceDataV1):
498+
// mapping each header path to the set of locations that include it
499+
var allFiles = [Path: Set<SWBUtil.Diagnostic.Location>]();
500+
501+
for entry in traceDataV1 {
502+
entry.includes.forEach { allFiles[$0, default: []].insert(.path(entry.source, fileLocation: nil)) }
503+
}
504+
505+
if isModular {
506+
let (imports, includes) = separateImportsFromIncludes(allFiles)
507+
outputDelegate.incrementTaskCounter(.moduleDependenciesScanned, by: imports.count)
508+
outputDelegate.incrementTaskCounter(.headerDependenciesScanned, by: includes.count)
509+
payload = .clangDependencies(imports: imports, includes: includes)
510+
} else {
511+
let includes = allFiles.map { file, locations in DependencyValidationInfo.Include(path: file, includeLocations: Array(locations)) }
512+
outputDelegate.incrementTaskCounter(.headerDependenciesScanned, by: includes.count)
513+
payload = .clangDependencies(imports: [], includes: includes)
514+
}
515+
case let .v2(traceDataV2):
516+
var allIncludes = [Path: Set<SWBUtil.Diagnostic.Location>]();
517+
var allImports = [String: Set<SWBUtil.Diagnostic.Location>]();
518+
519+
for entry in traceDataV2.dependencies {
520+
entry.includes.forEach { allIncludes[$0.file, default: []].insert(parseTraceSourceLocation($0.location)) }
521+
if isModular {
522+
entry.imports.forEach { allImports[$0.module, default: []].insert(parseTraceSourceLocation($0.location)) }
523+
}
524+
}
525+
526+
let imports = allImports.map { name, locations in DependencyValidationInfo.Import(dependency: ModuleDependency(name: name, accessLevel: .Private, optional: false), importLocations: Array(locations)) }
527+
let includes = allIncludes.map { file, locations in DependencyValidationInfo.Include(path: file, includeLocations: Array(locations)) }
512528
outputDelegate.incrementTaskCounter(.headerDependenciesScanned, by: includes.count)
529+
outputDelegate.incrementTaskCounter(.moduleDependenciesScanned, by: imports.count)
513530
payload = .clangDependencies(imports: imports, includes: includes)
514-
} else {
515-
outputDelegate.incrementTaskCounter(.headerDependenciesScanned, by: allFiles.count)
516-
payload = .clangDependencies(imports: [], includes: Array(allFiles))
517531
}
518532
} else {
519533
outputDelegate.incrementTaskCounter(.headerDependenciesNotValidatedTasks)
@@ -534,29 +548,28 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA
534548
}
535549
}
536550

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
551+
// Clang's dependency tracing V1 did not clearly distinguish modular imports from non-modular includes.
552+
// To keep supporting those trace files, just guess that if the file is contained in a framework, it comes from a module with
539553
// the same name. That is obviously not going to be reliable but it unblocks us from continuing experiments with
540554
// dependency specifications.
541-
private static func separateImportsFromIncludes(_ files: Set<Path>) -> ([DependencyValidationInfo.Import], [Path]) {
555+
private static func separateImportsFromIncludes(_ files: [Path: Set<SWBUtil.Diagnostic.Location>]) -> ([DependencyValidationInfo.Import], [DependencyValidationInfo.Include]) {
542556
func findFrameworkName(_ file: Path) -> String? {
543557
if file.fileExtension == "framework" {
544558
return file.basenameWithoutSuffix
545559
}
546560
return file.dirname.isEmpty || file.dirname.isRoot ? nil : findFrameworkName(file.dirname)
547561
}
548-
var moduleNames: [String] = []
549-
var includeFiles: [Path] = []
550-
for file in files {
562+
var moduleImportsByName = [String: Set<SWBUtil.Diagnostic.Location>]()
563+
var headerIncludes: [DependencyValidationInfo.Include] = []
564+
for (file, includeLocations) in files {
551565
if let frameworkName = findFrameworkName(file) {
552-
moduleNames.append(frameworkName)
566+
moduleImportsByName[frameworkName, default: []].formUnion(includeLocations)
553567
} else {
554-
includeFiles.append(file)
568+
headerIncludes.append(DependencyValidationInfo.Include(path: file, includeLocations: Array(includeLocations)))
555569
}
556570
}
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)
571+
let moduleImports = moduleImportsByName.map { name, locations in DependencyValidationInfo.Import(dependency: ModuleDependency(name: name, accessLevel: .Private, optional: false), importLocations: Array(locations)) }
572+
return (moduleImports, headerIncludes)
560573
}
561574
}
562575

@@ -623,9 +636,71 @@ public final class ClangNonModularCompileTaskAction: TaskAction {
623636
}
624637
}
625638

639+
fileprivate func parseTraceData(_ data: Data) throws -> TraceData? {
640+
// clang will emit an empty file instead of an empty array when there's nothing to trace
641+
if data.isEmpty {
642+
return .empty
643+
}
644+
645+
let jsonObject = try PropertyList.fromJSONData(data)
646+
if let version = jsonObject.dictValue?["version"]?.stringValue {
647+
if version == "2.0.0" {
648+
return .v2(try JSONDecoder().decode(TraceData.TraceFileV2.self, from: data))
649+
}
650+
return nil
651+
} else {
652+
// The initial unversioned format (v1) of the trace file generated from clang
653+
// is a JSON array so deserializing it as a dictionary will fail.
654+
return .v1(try JSONDecoder().decode(TraceData.TraceFileV1.self, from: data))
655+
}
656+
}
657+
658+
fileprivate func parseTraceSourceLocation(_ locationStr: String) -> SWBUtil.Diagnostic.Location {
659+
guard let match = locationStr.wholeMatch(of: #/(?<filename>.+):(?<line>\d+):(?<column>\d+)/#) else {
660+
return .unknown
661+
}
662+
let filename = Path(match.filename)
663+
let line = Int(match.line)
664+
let column = Int(match.column)
665+
if let line {
666+
return .path(filename, fileLocation: .textual(line: line, column: column))
667+
}
668+
return .unknown
669+
}
670+
626671
// Results from tracing header includes with "direct-per-file" filtering.
627672
// This is used to validate dependencies.
628-
fileprivate struct TraceData: Decodable {
629-
let source: Path
630-
let includes: [Path]
673+
fileprivate enum TraceData: Decodable {
674+
fileprivate struct Include: Decodable {
675+
let location: String
676+
let file: Path
677+
}
678+
679+
fileprivate struct Import: Decodable {
680+
let location: String
681+
let module: String
682+
let file: Path
683+
}
684+
685+
fileprivate struct TraceDataObjectV1: Decodable {
686+
let source: Path
687+
let includes: [Path]
688+
}
689+
690+
fileprivate struct TraceDataObjectV2: Decodable {
691+
let source: Path
692+
let includes: [Include]
693+
let imports: [Import]
694+
}
695+
696+
fileprivate typealias TraceFileV1 = [TraceDataObjectV1]
697+
698+
fileprivate struct TraceFileV2: Decodable {
699+
let version: String
700+
let dependencies: [TraceDataObjectV2]
701+
}
702+
703+
case empty
704+
case v1(TraceFileV1)
705+
case v2(TraceFileV2)
631706
}

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)