File: ClangScanTaskAction.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (211 lines) | stat: -rw-r--r-- 10,715 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SWBUtil
import SWBLibc
public import SWBCore
public import enum SWBLLBuild.BuildValueKind
import Foundation

public final class ClangScanTaskAction: TaskAction, BuildValueValidatingTaskAction {
    public override class var toolIdentifier: String {
        return "clang-scan-modules"
    }

    private struct Options {
        static func emitUsage(_ name: String, _ outputDelegate: any TaskOutputDelegate) {
            outputDelegate.emitOutput { stream in
                stream <<< "usage: \(name) -o <scanning-output-path> -- <command_line_args>\n"
            }
        }

        let scanningOutput: Path
        let commandLine: [String]

        init?(_ commandLine: AnySequence<String>, workingDirectory: Path, executionDelegate: any TaskExecutionDelegate, outputDelegate: any TaskOutputDelegate) {
            var parsedOutput: Path?
            var cliArguments: [String]?
            var expandResponseFiles: Bool = false

            let generator = commandLine.makeIterator()
            // Skip the executable.
            let programName = generator.next() ?? "<<missing program name>>"

        argumentIteration:
            while let arg = generator.next() {
                switch arg {
                case "-o":
                    if let outputPath = generator.next() {
                        parsedOutput = Path(outputPath)
                    } else {
                        break argumentIteration
                    }
                case "--expand-response-files":
                    expandResponseFiles = true
                case "--":
                    cliArguments = Array(generator)
                    break argumentIteration
                default:
                    outputDelegate.error("unexpected argument: \(arg)")
                    continue
                }
            }

            guard let parsedOutput else {
                outputDelegate.error("Scanning output path missing in command line \(Array(commandLine))")
                Options.emitUsage(programName, outputDelegate)
                return nil
            }
            guard let cliArguments else {
                outputDelegate.error("Command line arguments missing in command line \(Array(commandLine))")
                Options.emitUsage(programName, outputDelegate)
                return nil
            }

            self.scanningOutput = parsedOutput
            if expandResponseFiles {
                do {
                    self.commandLine = try ResponseFiles.expandResponseFiles(cliArguments, fileSystem: executionDelegate.fs, relativeTo: workingDirectory)
                } catch {
                    outputDelegate.error(error.localizedDescription)
                    return nil
                }
            } else {
                self.commandLine = cliArguments
            }
        }
    }

    public func isResultValid(_ task: any ExecutableTask, _ operationContext: DynamicTaskOperationContext, buildValue: BuildValue) -> Bool {
        fatalError("Unexpectedly called the old version of isResultValid")
    }

    public func isResultValid(_ task: any ExecutableTask, _ operationContext: DynamicTaskOperationContext, buildValue: BuildValue, fallback: (BuildValue) -> Bool) -> Bool {
        // FIXME: Checking the CAS results here has a fundamental limitation.
        //        This scan task might belong to a compilation that is itself up-to-date and we might be forcing this task to run unnecessarily.
        //        It would be better to make this task dynamic and control it from within the compile task. This way we could avoid an unnecessary scan.
        fallback(buildValue) && areCASResultsValid(task, operationContext)
    }

    private func areCASResultsValid(_ task: any ExecutableTask, _ operationContext: DynamicTaskOperationContext) -> Bool {
        guard let clangPayload = task.payload as? ClangTaskPayload, let explicitModulesPayload = clangPayload.explicitModulesPayload else {
            return true
        }

        guard let casOptions = explicitModulesPayload.casOptions else {
            return true
        }

        guard let casDBs = try? operationContext.clangModuleDependencyGraph.getCASDatabases(libclangPath: explicitModulesPayload.libclangPath, casOptions: casOptions) else {
            return false
        }

        guard let dependencyInfo = try? operationContext.clangModuleDependencyGraph.queryDependencies(at: explicitModulesPayload.scanningOutputPath, fileSystem: localFS) else {
            return false
        }

        for includeTreeID in dependencyInfo.transitiveIncludeTreeIDs {
            // FIXME: Deduplicate the loop body amongst all ClangScanTaskAction.

            guard let isMaterialized = try? casDBs.isMaterialized(casID: includeTreeID), isMaterialized else {
                return false
            }
        }

        return true
    }

    public override func taskSetup(_ task: any ExecutableTask, executionDelegate: any TaskExecutionDelegate, dynamicExecutionDelegate: any DynamicTaskExecutionDelegate) {
        for (index, input) in (task.executionInputs ?? []).enumerated() {
            dynamicExecutionDelegate.requestInputNode(node: input, nodeID: UInt(index))
        }
    }

    public override func performTaskAction(_ task: any ExecutableTask, dynamicExecutionDelegate: any DynamicTaskExecutionDelegate, executionDelegate: any TaskExecutionDelegate, clientDelegate: any TaskExecutionClientDelegate, outputDelegate: any TaskOutputDelegate) async -> CommandResult {
        guard let clangPayload = task.payload as? ClangTaskPayload, let explicitModulesPayload = clangPayload.explicitModulesPayload else {
            outputDelegate.error("invalid payload for explicit module support")
            return .failed
        }

        guard let options = Options(task.commandLineAsStrings, workingDirectory: task.workingDirectory, executionDelegate: executionDelegate, outputDelegate: outputDelegate) else {
            outputDelegate.emitError("Unable to parse argument list.")
            return .failed
        }

        let clangModuleDependencyGraph = dynamicExecutionDelegate.operationContext.clangModuleDependencyGraph

        let result: ClangModuleDependencyGraph.ScanResult
        do {
            result = try clangModuleDependencyGraph.scanModuleDependencies(
                libclangPath: explicitModulesPayload.libclangPath,
                scanningOutputPath: explicitModulesPayload.scanningOutputPath,
                usesCompilerLauncher: explicitModulesPayload.usesCompilerLauncher,
                usesSerializedDiagnostics: clangPayload.serializedDiagnosticsPath?.fileExtension == "dia",
                fileCommandLine: options.commandLine,
                workingDirectory: task.workingDirectory,
                casOptions: explicitModulesPayload.casOptions,
                cacheFallbackIfNotAvailable: explicitModulesPayload.cacheFallbackIfNotAvailable,
                verifyingModule: explicitModulesPayload.verifyingModule,
                outputPath: explicitModulesPayload.outputPath.str,
                reportRequiredTargetDependencies: explicitModulesPayload.reportRequiredTargetDependencies,
                fileSystem: executionDelegate.fs
            )

        } catch DependencyScanner.Error.dependencyScanDiagnostics(let clangDiagnostics) {
            let diagnostics = clangDiagnostics.map { Diagnostic($0, workingDirectory: task.workingDirectory, appendToOutputStream: true) }
            for diag in diagnostics {
                outputDelegate.emit(diag)
            }
            if !diagnostics.contains(where: { $0.behavior == .error }) {
                outputDelegate.error("failed to scan dependencies for source '\(explicitModulesPayload.sourcePath.str)'")
            }
            return .failed
        } catch DependencyScanner.Error.dependencyScanErrorString(let errorString) {
            outputDelegate.error("There was an error scanning dependencies for source '\(explicitModulesPayload.sourcePath.str)':\n\(errorString)")
            return .failed
        } catch DependencyScanner.Error.dependencyScanUnknownError {
            outputDelegate.error("There was an unknown error scanning dependencies for source '\(explicitModulesPayload.sourcePath.str)'")
            return .failed
        } catch {
            outputDelegate.error("There was an error scanning dependencies for source '\(explicitModulesPayload.sourcePath.str)':\n\(error)")
            return .failed
        }

        var dependencyPaths = result.dependencyPaths
        if let filteringPath = explicitModulesPayload.dependencyFilteringRootPath {
            dependencyPaths = dependencyPaths.filter {
                // We intentionally do a prefix check instead of an ancestor check here, for performance reasons. The filtering path (SDK path) and paths returned by the compiler are guaranteed to be normalized, which makes this safe.
                // TODO: Replace with rdar://107496178 (libClang dependency scanner should not report system headers when -MMD is passed on the command line) when that is available.
                !$0.str.hasPrefix(filteringPath.str)
            }
        }

        if executionDelegate.userPreferences.enableDebugActivityLogs {
            outputDelegate.emitOutput(ByteString(encodingAsUTF8: "Discovered dependency nodes:\n" + dependencyPaths.map(\.str).joined(separator: "\n") + "\n"))
        }

        for dep in dependencyPaths {
            dynamicExecutionDelegate.discoveredDependencyNode(ExecutionNode(identifier: dep.str))
        }

        if let target = task.forTarget {
            for requiredDependency in result.requiredTargetDependencies {
                guard requiredDependency.target.guid != task.forTarget?.guid else {
                    continue
                }
                executionDelegate.taskDiscoveredRequiredTargetDependency(target: target, antecedent: requiredDependency.target, reason: .clangModuleDependency(translationUnit: explicitModulesPayload.sourcePath, dependencyModuleName: requiredDependency.moduleName), warningLevel: explicitModulesPayload.reportRequiredTargetDependencies)
            }
        }

        return .succeeded
    }
}