Skip to content

Commit 864677b

Browse files
committed
Validate MODULE_DEPENDENCIES using tracing from Clang
This currently only checks Clang compilations with modules because there were some issues with introducing a new task action for non-modular Clang compilations. rdar://150313957
1 parent 572505d commit 864677b

File tree

7 files changed

+318
-6
lines changed

7 files changed

+318
-6
lines changed

Sources/SWBCore/Dependencies.swift

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,60 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
8080
self.init(validate: validate, moduleDependencies: settings.moduleDependencies, fixItContext: fixItContext)
8181
}
8282

83-
/// Nil `imports` means the current toolchain doesn't have the features to gather imports. This is temporarily required to support running against older toolchains.
83+
/// Make diagnostics for missing module dependencies from Clang imports.
84+
///
85+
/// The compiler tracing information does not provide the import locations or whether they are public imports
86+
/// (which depends on whether the import is in an installed header file).
87+
/// If `files` is nil, the current toolchain does support the feature to trace imports.
88+
public func makeDiagnostics(files: [Path]?) -> [Diagnostic] {
89+
guard validate != .no else { return [] }
90+
guard let files else {
91+
return [Diagnostic(
92+
behavior: .warning,
93+
location: .unknown,
94+
data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES.name)"))]
95+
}
96+
97+
// The following is a provisional/incomplete mechanism for resolving a module dependency from a file path.
98+
// For now, just grab the framework name and assume there is a module with the same name.
99+
func findFrameworkName(_ file: Path) -> String? {
100+
if file.fileExtension == "framework" {
101+
return file.basenameWithoutSuffix
102+
}
103+
return file.dirname.isEmpty || file.dirname.isRoot ? nil : findFrameworkName(file.dirname)
104+
}
105+
106+
let moduleDependencyNames = moduleDependencies.map { $0.name }
107+
let fileNames = files.compactMap { findFrameworkName($0) }
108+
let missingDeps = fileNames.filter {
109+
return !moduleDependencyNames.contains($0)
110+
}.map {
111+
ModuleDependency(name: $0, accessLevel: .Private)
112+
}
113+
114+
guard !missingDeps.isEmpty else { return [] }
115+
116+
let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning
117+
118+
let fixIt = fixItContext?.makeFixIt(newModules: missingDeps)
119+
let fixIts = fixIt.map { [$0] } ?? []
120+
121+
let message = "Missing entries in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(missingDeps.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))"
122+
123+
let location: Diagnostic.Location = fixIt.map {
124+
Diagnostic.Location.path($0.sourceRange.path, line: $0.sourceRange.endLine, column: $0.sourceRange.endColumn)
125+
} ?? Diagnostic.Location.buildSetting(BuiltinMacros.MODULE_DEPENDENCIES)
126+
127+
return [Diagnostic(
128+
behavior: behavior,
129+
location: location,
130+
data: DiagnosticData(message),
131+
fixIts: fixIts)]
132+
}
133+
134+
/// Make diagnostics for missing module dependencies from Swift imports.
135+
///
136+
/// If `imports` is nil, the current toolchain does not support the features to gather imports.
84137
public func makeDiagnostics(imports: [(ModuleDependency, importLocations: [Diagnostic.Location])]?) -> [Diagnostic] {
85138
guard validate != .no else { return [] }
86139
guard let imports else {

Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,10 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
432432

433433
public let fileNameMapPath: Path?
434434

435-
fileprivate init(serializedDiagnosticsPath: Path?, indexingPayload: ClangIndexingPayload?, explicitModulesPayload: ClangExplicitModulesPayload? = nil, outputObjectFilePath: Path? = nil, fileNameMapPath: Path? = nil, developerPathString: String? = nil) {
435+
public let moduleDependenciesContext: ModuleDependenciesContext?
436+
public let traceFilePath: Path?
437+
438+
fileprivate init(serializedDiagnosticsPath: Path?, indexingPayload: ClangIndexingPayload?, explicitModulesPayload: ClangExplicitModulesPayload? = nil, outputObjectFilePath: Path? = nil, fileNameMapPath: Path? = nil, developerPathString: String? = nil, moduleDependenciesContext: ModuleDependenciesContext? = nil, traceFilePath: Path? = nil) {
436439
if let developerPathString, explicitModulesPayload == nil {
437440
self.dependencyInfoEditPayload = .init(removablePaths: [], removableBasenames: [], developerPath: Path(developerPathString))
438441
} else {
@@ -443,27 +446,33 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
443446
self.explicitModulesPayload = explicitModulesPayload
444447
self.outputObjectFilePath = outputObjectFilePath
445448
self.fileNameMapPath = fileNameMapPath
449+
self.moduleDependenciesContext = moduleDependenciesContext
450+
self.traceFilePath = traceFilePath
446451
}
447452

448453
public func serialize<T: Serializer>(to serializer: T) {
449-
serializer.serializeAggregate(6) {
454+
serializer.serializeAggregate(8) {
450455
serializer.serialize(serializedDiagnosticsPath)
451456
serializer.serialize(indexingPayload)
452457
serializer.serialize(explicitModulesPayload)
453458
serializer.serialize(outputObjectFilePath)
454459
serializer.serialize(fileNameMapPath)
455460
serializer.serialize(dependencyInfoEditPayload)
461+
serializer.serialize(moduleDependenciesContext)
462+
serializer.serialize(traceFilePath)
456463
}
457464
}
458465

459466
public init(from deserializer: any Deserializer) throws {
460-
try deserializer.beginAggregate(6)
467+
try deserializer.beginAggregate(8)
461468
self.serializedDiagnosticsPath = try deserializer.deserialize()
462469
self.indexingPayload = try deserializer.deserialize()
463470
self.explicitModulesPayload = try deserializer.deserialize()
464471
self.outputObjectFilePath = try deserializer.deserialize()
465472
self.fileNameMapPath = try deserializer.deserialize()
466473
self.dependencyInfoEditPayload = try deserializer.deserialize()
474+
self.moduleDependenciesContext = try deserializer.deserialize()
475+
self.traceFilePath = try deserializer.deserialize()
467476
}
468477
}
469478

@@ -1156,6 +1165,22 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
11561165
dependencyData = nil
11571166
}
11581167

1168+
let moduleDependenciesContext = cbc.producer.moduleDependenciesContext
1169+
let traceFilePath: Path?
1170+
if clangInfo?.hasFeature("print-headers-direct-per-file") ?? false,
1171+
(moduleDependenciesContext?.validate ?? .defaultValue) != .no {
1172+
let file = Path(outputNode.path.str + ".trace.json")
1173+
commandLine += [
1174+
"-Xclang", "-header-include-file",
1175+
"-Xclang", file.str,
1176+
"-Xclang", "-header-include-filtering=direct-per-file",
1177+
"-Xclang", "-header-include-format=json"
1178+
]
1179+
traceFilePath = file
1180+
} else {
1181+
traceFilePath = nil
1182+
}
1183+
11591184
// Add the diagnostics serialization flag. We currently place the diagnostics file right next to the output object file.
11601185
let diagFilePath: Path?
11611186
if let serializedDiagnosticsOptions = self.serializedDiagnosticsOptions(scope: cbc.scope, outputPath: outputNode.path) {
@@ -1266,7 +1291,9 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
12661291
explicitModulesPayload: explicitModulesPayload,
12671292
outputObjectFilePath: shouldGenerateRemarks ? outputNode.path : nil,
12681293
fileNameMapPath: verifierPayload?.fileNameMapPath,
1269-
developerPathString: recordSystemHeaderDepsOutsideSysroot ? cbc.scope.evaluate(BuiltinMacros.DEVELOPER_DIR).str : nil
1294+
developerPathString: recordSystemHeaderDepsOutsideSysroot ? cbc.scope.evaluate(BuiltinMacros.DEVELOPER_DIR).str : nil,
1295+
moduleDependenciesContext: moduleDependenciesContext,
1296+
traceFilePath: traceFilePath
12701297
)
12711298

12721299
var inputNodes: [any PlannedNode] = inputDeps.map { delegate.createNode($0) }
@@ -1316,6 +1343,15 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
13161343
extraInputs = []
13171344
}
13181345

1346+
if let moduleDependenciesContext {
1347+
guard let jsonData = try? JSONEncoder(outputFormatting: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]).encode(moduleDependenciesContext),
1348+
let signature = String(data: jsonData, encoding: .utf8) else {
1349+
delegate.error("failed to serialize 'MODULE_DEPENDENCIES' context information")
1350+
return
1351+
}
1352+
additionalSignatureData += "|\(signature)"
1353+
}
1354+
13191355
// Finally, create the task.
13201356
delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo, additionalSignatureData: additionalSignatureData, commandLine: commandLine, additionalOutput: additionalOutput, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: inputNodes + extraInputs, outputs: [outputNode], action: action ?? delegate.taskActionCreationDelegate.createDeferredExecutionTaskActionIfRequested(userPreferences: cbc.producer.userPreferences), execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilationForIndexableSourceFile], usesExecutionInputs: usesExecutionInputs, showEnvironment: true, priority: .preferred)
13211357

Sources/SWBCore/ToolInfo/ClangToolInfo.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public struct DiscoveredClangToolSpecInfo: DiscoveredCommandLineToolSpecInfo {
3838
case wSystemHeadersInModule = "Wsystem-headers-in-module"
3939
case extractAPISupportsCPlusPlus = "extract-api-supports-cpp"
4040
case deploymentTargetEnvironmentVariables = "deployment-target-environment-variables"
41+
case printHeadersDirectPerFile = "print-headers-direct-per-file"
4142
}
4243
public var toolFeatures: ToolFeatures<FeatureFlag>
4344
public func hasFeature(_ feature: String) -> Bool {
@@ -46,6 +47,10 @@ public struct DiscoveredClangToolSpecInfo: DiscoveredCommandLineToolSpecInfo {
4647
if feature == FeatureFlag.extractAPISupportsCPlusPlus.rawValue {
4748
return clangVersion > Version(17)
4849
}
50+
// FIXME: Remove once the feature flag is added to clang.
51+
if feature == FeatureFlag.printHeadersDirectPerFile.rawValue, let clangVersion {
52+
return clangVersion >= Version(1700, 3, 10, 2)
53+
}
4954
return toolFeatures.has(feature)
5055
}
5156

Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,23 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA
248248
casDBs = nil
249249
}
250250

251+
// Check if verifying dependencies from trace data is enabled.
252+
let traceFilePath: Path?
253+
let moduleDependenciesContext: ModuleDependenciesContext?
254+
if let payload = task.payload as? ClangTaskPayload {
255+
traceFilePath = payload.traceFilePath
256+
moduleDependenciesContext = payload.moduleDependenciesContext
257+
} else {
258+
traceFilePath = nil
259+
moduleDependenciesContext = nil
260+
}
261+
if let traceFilePath {
262+
// Remove the trace output file if it already exists.
263+
if executionDelegate.fs.exists(traceFilePath) {
264+
try executionDelegate.fs.remove(traceFilePath)
265+
}
266+
}
267+
251268
var lastResult: CommandResult? = nil
252269
for command in dependencyInfo.commands {
253270
if let casDBs {
@@ -304,6 +321,30 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA
304321
return lastResult ?? .failed
305322
}
306323
}
324+
325+
if let moduleDependenciesContext, lastResult == .succeeded {
326+
// Verify the dependencies from the trace data.
327+
let files: [Path]?
328+
if let traceFilePath {
329+
let fs = executionDelegate.fs
330+
let traceData = try JSONDecoder().decode(Array<TraceData>.self, from: Data(fs.read(traceFilePath)))
331+
332+
var allFiles = Set<Path>()
333+
traceData.forEach { allFiles.formUnion(Set($0.includes)) }
334+
files = Array(allFiles)
335+
} else {
336+
files = nil
337+
}
338+
let diagnostics = moduleDependenciesContext.makeDiagnostics(files: files)
339+
for diagnostic in diagnostics {
340+
outputDelegate.emit(diagnostic)
341+
}
342+
343+
if diagnostics.contains(where: { $0.behavior == .error }) {
344+
return .failed
345+
}
346+
}
347+
307348
return lastResult ?? .failed
308349
} catch {
309350
outputDelegate.emitError("\(error)")
@@ -431,3 +472,10 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA
431472
)
432473
}
433474
}
475+
476+
// Results from tracing header includes with "direct-per-file" filtering.
477+
// This is used to validate dependencies.
478+
fileprivate struct TraceData: Decodable {
479+
let source: Path
480+
let includes: [Path]
481+
}

Sources/SWBTestSupport/CoreBasedTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ extension CoreBasedTests {
147147
if clangInfo.clangVersion > Version(17) {
148148
realToolFeatures.insert(.extractAPISupportsCPlusPlus)
149149
}
150+
if let clangVersion = clangInfo.clangVersion, clangVersion >= Version(1700, 3, 10, 2) {
151+
realToolFeatures.insert(.printHeadersDirectPerFile)
152+
}
153+
150154
return ToolFeatures(realToolFeatures)
151155
}
152156
}

Tests/SWBBuildSystemTests/DependencyValidationTests.swift

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
328328
}
329329

330330
@Test(.requireSDKs(.host))
331-
func validateModuleDependencies() async throws {
331+
func validateModuleDependenciesSwift() async throws {
332332
try await withTemporaryDirectory { tmpDir in
333333
let testWorkspace = try await TestWorkspace(
334334
"Test",
@@ -458,4 +458,71 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
458458
}
459459
}
460460
}
461+
462+
@Test(.requireSDKs(.host), .requireClangFeatures(.printHeadersDirectPerFile))
463+
func validateModuleDependenciesClang() async throws {
464+
try await withTemporaryDirectory { tmpDir async throws -> Void in
465+
let testWorkspace = TestWorkspace(
466+
"Test",
467+
sourceRoot: tmpDir.join("Test"),
468+
projects: [
469+
TestProject(
470+
"aProject",
471+
groupTree: TestGroup(
472+
"Sources", path: "Sources",
473+
children: [
474+
TestFile("CoreFoo.m")
475+
]),
476+
buildConfigurations: [
477+
TestBuildConfiguration(
478+
"Debug",
479+
buildSettings: [
480+
"PRODUCT_NAME": "$(TARGET_NAME)",
481+
"CLANG_ENABLE_MODULES": "YES",
482+
"CLANG_ENABLE_EXPLICIT_MODULES": "YES",
483+
"GENERATE_INFOPLIST_FILE": "YES",
484+
"MODULE_DEPENDENCIES": "Foundation",
485+
"VALIDATE_MODULE_DEPENDENCIES": "YES_ERROR",
486+
"SDKROOT": "$(HOST_PLATFORM)",
487+
"SUPPORTED_PLATFORMS": "$(HOST_PLATFORM)",
488+
"DSTROOT": tmpDir.join("dstroot").str,
489+
]
490+
)
491+
],
492+
targets: [
493+
TestStandardTarget(
494+
"CoreFoo", type: .framework,
495+
buildPhases: [
496+
TestSourcesBuildPhase(["CoreFoo.m"]),
497+
TestFrameworksBuildPhase()
498+
])
499+
])
500+
]
501+
)
502+
503+
let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false)
504+
let SRCROOT = testWorkspace.sourceRoot.join("aProject")
505+
506+
// Write the source files.
507+
try await tester.fs.writeFileContents(SRCROOT.join("Sources/CoreFoo.m")) { contents in
508+
contents <<< """
509+
#include <Foundation/Foundation.h>
510+
#include <Accelerate/Accelerate.h>
511+
512+
void f0(void) { };
513+
"""
514+
}
515+
516+
// Expect complaint about undeclared dependency
517+
try await tester.checkBuild(parameters: BuildParameters(configuration: "Debug"), runDestination: .host, persistent: true) { results in
518+
results.checkError(.contains("Missing entries in MODULE_DEPENDENCIES: Accelerate"))
519+
}
520+
521+
// Declaring dependencies resolves the problem
522+
try await tester.checkBuild(parameters: BuildParameters(configuration: "Debug", overrides: ["MODULE_DEPENDENCIES": "Foundation Accelerate"]), runDestination: .host, persistent: true) { results in
523+
results.checkNoErrors()
524+
}
525+
}
526+
}
527+
461528
}

0 commit comments

Comments
 (0)