File: BuildRuleTaskProducer.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 (244 lines) | stat: -rw-r--r-- 12,816 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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
//===----------------------------------------------------------------------===//
//
// 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 SWBCore
import SWBMacro
import SWBProtocol

final class BuildRuleTaskProducer: StandardTaskProducer, TaskProducer, ShellBasedTaskProducer {
    private unowned let action: BuildRuleScriptAction
    private let cbc: CommandBuildContext
    private unowned let delegate: any TaskGenerationDelegate
    private unowned let buildPhase: SWBCore.BuildPhase

    init(_ context: TaskProducerContext, action: BuildRuleScriptAction, cbc: CommandBuildContext, delegate: any TaskGenerationDelegate, buildPhase: SWBCore.BuildPhase) {
        self.action = action
        self.cbc = cbc
        self.delegate = delegate
        self.buildPhase = buildPhase
        super.init(context)
    }

    func serializeFileList(_ tasks: inout [any PlannedTask], to path: Path, paths: [any PlannedNode], context: TaskProducerContext) async -> any PlannedNode {
        let node = context.createNode(path)

        let contents = OutputByteStream()
        for path in paths {
            contents <<< (path.path.str + "\n")
        }
        context.writeFileSpec.constructFileTasks(CommandBuildContext(producer: context, scope: context.settings.globalScope, inputs: [], output: path), delegate, contents: contents.bytes, permissions: 0o755, preparesForIndexing: true, additionalTaskOrderingOptions: [.immediate, .ignorePhaseOrdering])

        return node
    }

    func pathForResolvedFileList(_ scope: MacroEvaluationScope, prefix: String, fileList: Path) -> Path {
        let checksumRawData = [
            scope.evaluate(BuiltinMacros.EFFECTIVE_PLATFORM_NAME),
            buildPhase.guid,
            action.identifier,
            scope.evaluate(BuiltinMacros.CURRENT_VARIANT),
            scope.evaluate(BuiltinMacros.CURRENT_ARCH),
            fileList.str,
        ] + cbc.inputs.map { $0.absolutePath.str }

        return scope.evaluate(BuiltinMacros.TEMP_DIR).join("\(prefix)-\(checksumRawData.joined(separator: "\n").md5())-\(fileList.basenameWithoutSuffix)-resolved.xcfilelist")
    }

    func createNodeForRule(_ delegate: any TaskGenerationDelegate, cbc: CommandBuildContext, context: TaskProducerContext, path: Path) -> any PlannedNode {
        // TODO: rdar://99446855 (Should we support USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES in build rules?)

        let ftb = FileToBuild(absolutePath: path, inferringTypeUsing: cbc.producer)
        if ftb.fileType.isWrapper {
            return delegate.createDirectoryTreeNode(path, excluding: [])
        } else {
            return delegate.createNode(path)
        }
    }

    // Resolve the rule input and output paths.
    func exportPathsRule(_ delegate: any TaskGenerationDelegate, _ environment: inout [String: String], _ cbc: CommandBuildContext, _ context: TaskProducerContext, _ paths: [MacroStringExpression], prefix: String, considerAllPathsDirectories: Bool = false) -> [any PlannedNode] {
        environment["\(prefix)_COUNT"] = String(paths.count)
        var exportedPaths: [any PlannedNode] = []
        for (i, expr) in paths.enumerated() {
            var path = Path(cbc.scope.evaluate(expr))
            if !path.isEmpty {
                // FIXME: We shouldn't need to 'normalize' here, the nodes should handle it: <rdar://problem/24568541> [Swift Build] Support node normalization at some point in the build system
                path = context.makeAbsolute(path).normalize()

                exportedPaths.append(createNodeForRule(delegate, cbc: cbc, context: context, path: path))
            }
            environment["\(prefix)_\(i)"] = path.str
        }

        return exportedPaths
    }

    /// Construct the tasks for an individual shell-script build rule.
    func generateTasks() async -> [any PlannedTask] {
        var tasks = [any PlannedTask]()
        // Shell script rules do not support grouping, currently.
        precondition(cbc.inputs.count == 1)
        let arch = cbc.scope.evaluate(BuiltinMacros.CURRENT_ARCH)
        let variant = cbc.scope.evaluate(BuiltinMacros.CURRENT_VARIANT)

        let input = cbc.inputs[0]
        let inputPath = input.absolutePath
        let inputDir = inputPath.dirname
        let inputName = inputPath.basename
        let (inputBase,inputSuffix) = Path(inputName).splitext()
        var inputVariables: [MacroDeclaration: String] = [
            BuiltinMacros.INPUT_FILE_PATH: inputPath.str,
            BuiltinMacros.INPUT_FILE_DIR: inputDir.str,
            BuiltinMacros.INPUT_FILE_NAME: inputName,
            BuiltinMacros.INPUT_FILE_BASE: inputBase,
            BuiltinMacros.INPUT_FILE_SUFFIX: inputSuffix,
            BuiltinMacros.INPUT_FILE_REGION_PATH_COMPONENT: cbc.input.regionVariantPathComponent,
        ]
        func lookup(_ macro: MacroDeclaration) -> MacroExpression? {
            if let value = inputVariables[macro] {
                return cbc.scope.namespace.parseLiteralString(value)
            }
            return nil
        }

        // Should the outputs of the script be declared as generated
        var declareOutputsGenerated: Bool = true

        // Compute the arguments to the shell.
        let commandLine = [action.interpreterPath, "-c", action.scriptSource]

        // Compute the environment to use for the shell script.
        var environment = await computeScriptEnvironment(.shellScriptPhase, scope: cbc.scope, settings: context.settings, workspaceContext: context.workspaceContext, allDeploymentTargetMacroNames: context.allDeploymentTargetMacroNames())

        // If we are in a headers build phase, expose visibility and output dir
        // information to the script and set the HEADER_OUTPUT_DIR macro value
        // for output path resolution.
        if buildPhase is SWBCore.HeadersBuildPhase {
            if let headerVisibility = input.headerVisibility, let outputDir = TargetHeaderInfo.outputPath(for: input.absolutePath, visibility: headerVisibility, scope: cbc.scope)?.dirname {
                environment["SCRIPT_HEADER_VISIBILITY"] = headerVisibility.rawValue
                inputVariables[BuiltinMacros.HEADER_OUTPUT_DIR] = outputDir.str

                // Although ostensibly the script may be generating these headers, we explicitly do not declare them as generated so that they do not end up in the auto-generated headermaps.
                declareOutputsGenerated = false
            } else {
                // Per the semantics of copy headers, we do not process files that have 'project' visibility.
                return []
            }
        }

        // Add the input file variables.
        for (macro,name) in inputVariables {
            environment[macro.name] = name
        }

        if let inputFlags = input.buildFile?.additionalArgs {
            environment["OTHER_INPUT_FILE_FLAGS"] = UNIXShellCommandCodec(encodingStrategy: .backslashes, encodingBehavior: .argumentsOnly).encode(cbc.scope.evaluate(inputFlags))
        }

        // Add the inputs and outputs.
        environment["SCRIPT_INPUT_FILE"] = inputPath.str
        // FIXME: We shouldn't need to 'normalize' here, the nodes should handle it: <rdar://problem/24568541> [Swift Build] Support node normalization at some point in the build system
        var inputs = [inputPath.normalize()]
        environment["SCRIPT_INPUT_FILE_COUNT"] = String(action.inputFiles.count)
        for (i, inputFile) in (action.inputFiles).enumerated() {
            // FIXME: We shouldn't need to 'normalize' here, the nodes should handle it: <rdar://problem/24568541> [Swift Build] Support node normalization at some point in the build system
            let inputPath = context.makeAbsolute(Path(cbc.scope.evaluate(inputFile, lookup: lookup))).normalize()
            inputs.append(inputPath)
            environment["SCRIPT_INPUT_FILE_\(i)"] = inputPath.str
        }

        var outputs = [Path]()
        environment["SCRIPT_OUTPUT_FILE_COUNT"] = String(action.outputFiles.count)
        for (i, outputFile) in action.outputFiles.enumerated() {
            // FIXME: We shouldn't need to 'normalize' here, the nodes should handle it: <rdar://problem/24568541> [Swift Build] Support node normalization at some point in the build system
            let outputPath = context.makeAbsolute(Path(cbc.scope.evaluate(outputFile.path, lookup: lookup))).normalize()
            outputs.append(outputPath)
            // Propagate the uniquing suffix from the input to the output, so a downstream tool which processes the output has access to it.
            delegate.declareOutput(FileToBuild(absolutePath: outputPath, inferringTypeUsing: cbc.producer, additionalArgs: outputFile.additionalCompilerFlags, uniquingSuffix: input.uniquingSuffix))
            if declareOutputsGenerated {
                delegate.declareGeneratedSourceFile(outputPath)
            }
            environment["SCRIPT_OUTPUT_FILE_\(i)"] = outputPath.str
        }

        let inputFileLists = exportPathsRule(delegate, &environment, cbc, context, action.inputFileLists, prefix: "SCRIPT_INPUT_FILE_LIST", considerAllPathsDirectories: true)
        let outputFileLists = exportPathsRule(delegate, &environment, cbc, context, action.outputFileLists, prefix: "SCRIPT_OUTPUT_FILE_LIST")

        func createNode(_ delegate: any TaskGenerationDelegate, path: Path) -> any PlannedNode {
            let ftb = FileToBuild(absolutePath: path, inferringTypeUsing: cbc.producer)
            if ftb.fileType.isWrapper {
                return delegate.createDirectoryTreeNode(path, excluding: [])
            } else {
                return delegate.createNode(path)
            }
        }

        let dependencyData: DependencyDataStyle?
        if let dependencyInfo = action.dependencyInfo {
            switch dependencyInfo {
            case .makefile(let path):
                let path = context.makeAbsolute(Path(cbc.scope.evaluate(path, lookup: lookup))).normalize()
                outputs.append(path)
                dependencyData = .makefile(path)
            case .dependencyInfo(let path):
                let path = context.makeAbsolute(Path(cbc.scope.evaluate(path, lookup: lookup))).normalize()
                outputs.append(path)
                dependencyData = .dependencyInfo(path)

            case .makefiles(let paths):
                let paths = paths.map{ context.makeAbsolute(Path(cbc.scope.evaluate($0, lookup: lookup))).normalize() }
                outputs.append(contentsOf: paths)
                dependencyData = .makefiles(paths)
            }
        } else {
            dependencyData = nil
        }

        var inputNodes = inputs.map{ createNodeForRule(delegate, cbc: cbc, context: context, path: $0) }
        var outputNodes = outputs.map{ delegate.createNode($0) as (any PlannedNode) }

        await handleFileLists(&tasks, &inputNodes, &outputNodes, &environment, cbc.scope, inputFileLists, outputFileLists, lookup: lookup)

        if outputNodes.isEmpty {
            delegate.error("shell script build rule for '\(inputPath.str)' must declare at least one output file")
            return []
        }

        let isSandboxingEnabled = BuildRuleTaskProducer.isSandboxingEnabled(context, buildPhase)
        // FIXME: We should move almost all of the logic from this method into the task spec, and simplify this.
        let execDescription = "Run shell script build rule on \(inputPath.str)"
        let ruleInfo = ["RuleScriptExecution"] + outputs.map { $0.str } + [inputPath.str, variant, arch]
        let enabledIndexBuildArena = cbc.scope.evaluate(BuiltinMacros.INDEX_ENABLE_BUILD_ARENA)
        let disabledScriptExecutionForIndexBuild = cbc.scope.evaluate(BuiltinMacros.INDEX_DISABLE_SCRIPT_EXECUTION)

        var taskCBC = cbc
        taskCBC.preparesForIndexing = enabledIndexBuildArena && !disabledScriptExecutionForIndexBuild
        context.shellScriptSpec.constructShellScriptTasks(
            taskCBC,
            delegate,
            ruleInfo: ruleInfo,
            commandLine: commandLine,
            environment: EnvironmentBindings(environment),
            inputs: inputNodes,
            outputs: outputNodes,
            dependencyData: dependencyData,
            execDescription: execDescription,
            showEnvironment: false,
            alwaysExecuteTask: false,
            enableSandboxing: isSandboxingEnabled
        )

        return []
    }

}