File: CleanOperation.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 (346 lines) | stat: -rw-r--r-- 16,584 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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

package import SWBCore
import SWBTaskExecution
package import SWBProtocol
package import SWBUtil

package import class Foundation.FileManager
package import var Foundation.NSCocoaErrorDomain
package import var Foundation.NSFileNoSuchFileError
package import var Foundation.NSLocalizedDescriptionKey
package import class Foundation.NSError
package import struct Foundation.URL
package import struct Foundation.UUID
import SWBMacro

package final class CleanOperation: BuildSystemOperation, TargetDependencyResolverDelegate {
    package var diagnosticContext: DiagnosticContextData {
        return DiagnosticContextData(target: nil)
    }

    package func diagnosticsEngine(for target: ConfiguredTarget?) -> DiagnosticProducingDelegateProtocolPrivate<DiagnosticsEngine> {
        return dependencyResolverDelegate?.diagnosticsEngine(for: target) ?? .init(_diagnosticsEngine)
    }

    package let cachedBuildSystems: any BuildSystemCache

    private let buildRequest: BuildRequest
    private let buildRequestContext: BuildRequestContext
    private let delegate: any BuildOperationDelegate
    private let dependencyResolverDelegate: (any TargetDependencyResolverDelegate)?
    private let _diagnosticsEngine = DiagnosticsEngine()
    private let style: BuildLocationStyle
    private let workspaceContext: WorkspaceContext
    private let ignoreCreatedByBuildSystemAttribute: Bool

    private var wasCancellationRequested = false
    package let uuid: UUID

    package init(buildRequest: BuildRequest, buildRequestContext: BuildRequestContext, workspaceContext: WorkspaceContext, style: BuildLocationStyle, delegate: any BuildOperationDelegate, cachedBuildSystems: any BuildSystemCache, dependencyResolverDelegate: (any TargetDependencyResolverDelegate)? = nil) {
        self.buildRequest = buildRequest
        self.buildRequestContext = buildRequestContext
        self.delegate = delegate
        self.dependencyResolverDelegate = dependencyResolverDelegate
        self.style = style
        self.uuid = UUID()
        self.workspaceContext = workspaceContext
        self.ignoreCreatedByBuildSystemAttribute = buildRequestContext.getCachedSettings(buildRequest.parameters).globalScope.evaluate(BuiltinMacros.IGNORE_CREATED_BY_BUILD_SYSTEM_ATTRIBUTE_DURING_CLEAN)
        self.cachedBuildSystems = cachedBuildSystems
    }

    package func buildDataDirectory() throws -> Path {
        return try BuildDescriptionManager.cacheDirectory(buildRequest, buildRequestContext: buildRequestContext, workspaceContext: workspaceContext).join("XCBuildData")
    }

    package func build() async -> BuildOperationEnded.Status {
        let buildOutputDelegate = delegate.buildStarted(self)

        if workspaceContext.userPreferences.enableDebugActivityLogs {
            showConnectionModeMessage(workspaceContext.core.connectionMode, buildOutputDelegate)
        }

        let buildDataDirectory: Path
        do {
            buildDataDirectory = try self.buildDataDirectory()
        } catch {
            emit(.init(behavior: .error, location: .unknown, data: DiagnosticData("\(error)")))
            return delegate.buildComplete(self, status: .failed, delegate: buildOutputDelegate, metrics: nil)
        }

        // Make sure potentially cached build systems are cleared, so that their database is closed.
        cachedBuildSystems.clearCachedBuildSystem(for: buildDataDirectory)

        let buildGraph = await TargetBuildGraph(workspaceContext: workspaceContext, buildRequest: buildRequest, buildRequestContext: buildRequestContext, delegate: self)

        // If there were any errors while constructing the build graph, we will stop cleaning.
        if hadErrors {
            return delegate.buildComplete(self, status: .failed, delegate: buildOutputDelegate, metrics: nil)
        }

        var buildFolders = await buildGraph.allTargets.asyncFlatMap { configuredTarget -> [Path] in
            let settings = buildRequestContext.getCachedSettings(configuredTarget.parameters, target: configuredTarget.target)
            if style == .legacy && configuredTarget.target.type == .external {
                await cleanExternalTarget(configuredTarget, settings: settings)
                return []
            } else {
                return workspaceContext.buildDirectories(settings: settings)
            }
        }

        if buildFolders.isEmpty {
            let settings = buildRequestContext.getCachedSettings(buildRequest.parameters)
            buildFolders = workspaceContext.buildDirectories(settings: settings)
        }

        cleanBuildFolders(buildFolders: Set(buildFolders), buildOutputDelegate: buildOutputDelegate)

        return delegate.buildComplete(self, status: nil, delegate: buildOutputDelegate, metrics: nil)
    }

    package func cancel() {
        wasCancellationRequested = true
    }

    package func abort() {
        // Not needed for clean operations because there is no concept of continuing to build after errors
    }

    private var hadErrors = false

    package func emit(_ diagnostic: Diagnostic) {
        if diagnostic.behavior == .error {
            hadErrors = true
        }
        dependencyResolverDelegate?.emit(diagnostic)
    }

    package func updateProgress(statusMessage: String, showInLog: Bool) {
        dependencyResolverDelegate?.updateProgress(statusMessage: statusMessage, showInLog: showInLog)
    }

    package var request: BuildRequest {
        return buildRequest
    }

    package var requestContext: BuildRequestContext {
        return buildRequestContext
    }

    package let subtaskProgressReporter: (any SubtaskProgressReporter)? = nil

    private func isBuildFolder(_ buildFolderPath: Path) -> Bool {
        do {
            if try workspaceContext.fs.hasCreatedByBuildSystemAttribute(buildFolderPath) {
                return true
            }
        } catch {
        }

        // If the attribute isn't set, consider the arena as an indicator for build-folderness.
        if let arena = buildRequest.parameters.arena {
            for path in [arena.derivedDataPath, arena.buildIntermediatesPath, arena.buildProductsPath] {
                if buildFolderPath == path || path.isAncestor(of: buildFolderPath) {
                    return true
                }
            }
        }

        return false
    }

    private final class CleanExecutableTask: ExecutableTask {
        init(commandLine: [String], workingDirectory: Path, environment: [String:String], configuredTarget: ConfiguredTarget, type: any TaskTypeDescription) {
            self.commandLine = commandLine.map { .literal(ByteString(encodingAsUTF8: $0)) }
            self.workingDirectory = workingDirectory
            self.environment = EnvironmentBindings(environment)
            self.forTarget = configuredTarget
            self.type = type
        }

        let commandLine: [CommandLineArgument]
        let environment: EnvironmentBindings
        let forTarget: ConfiguredTarget?
        let workingDirectory: Path
        let type: any TaskTypeDescription

        let additionalOutput: [String] = []
        let execDescription: String? = nil
        let dependencyData: DependencyDataStyle? = nil
        let payload: (any TaskPayload)? = nil
        let additionalSignatureData: String = ""

        var ruleInfo: [String] {
            return commandLine.map { $0.asString }
        }

        let inputPaths = [Path]()
        let outputPaths = [Path]()
        let targetDependencies = [ResolvedTargetDependency]()
        let executionInputs: [ExecutionNode]? = nil
        var priority: TaskPriority { .unspecified }
        var showInLog: Bool { !isGate }
        var showCommandLineInLog: Bool { true }
        let isDynamic = false
        let alwaysExecuteTask = false
        let isGate = false
        let showEnvironment = true
        let preparesForIndexing = false
        let llbuildControlDisabled = false
    }

    private func cleanExternalTarget(_ configuredTarget: ConfiguredTarget, settings: Settings) async {
        if self.wasCancellationRequested || _Concurrency.Task.isCancelled {
            return
        }

        delegate.targetPreparationStarted(self, configuredTarget: configuredTarget)
        delegate.targetStarted(self, configuredTarget: configuredTarget)

        let (executable, arguments, workingDirectory, environment) = constructCommandLine(for: configuredTarget.target as! SWBCore.ExternalTarget, action: "clean", settings: settings, workspaceContext: workspaceContext, scope: settings.globalScope, allDeploymentTargetMacroNames: [])
        let commandLine = [executable] + arguments

        let specLookupContext = SpecLookupCtxt(specRegistry: workspaceContext.core.specRegistry, platform: settings.platform)
        let taskType = specLookupContext.getSpec("com.apple.commands.shell-script") as! ShellScriptToolSpec
        let task = CleanExecutableTask(commandLine: commandLine, workingDirectory: workingDirectory, environment: environment, configuredTarget: configuredTarget, type: taskType)
        let taskIdentifier = task.identifier
        let taskOutputDelegate = delegate.taskStarted(self, taskIdentifier: taskIdentifier, task: task, dependencyInfo: nil)

        let resolvedExecutable = StackedSearchPath(environment: .init(environment), fs: workspaceContext.fs).lookup(Path(executable)) ?? Path(executable)

        do {
            let result = try await Process.getMergedOutput(url: URL(fileURLWithPath: resolvedExecutable.str), arguments: arguments, currentDirectoryURL: URL(fileURLWithPath: workingDirectory.str), environment: .init(environment))

            if !result.exitStatus.isSuccess {
                taskOutputDelegate.emitError("Failed to clean target '\(configuredTarget.target.name)': \(String(decoding: result.output, as: UTF8.self))")
            } else {
                taskOutputDelegate.emitOutput(ByteString(result.output))
            }

            taskOutputDelegate.updateResult(.exit(exitStatus: result.exitStatus, metrics: nil))
        } catch {
            taskOutputDelegate.emitError("Failed to clean target '\(configuredTarget.target.name)': \(error.localizedDescription)")
            taskOutputDelegate.updateResult(.exit(exitStatus: .exit(1), metrics: nil))
        }

        delegate.taskComplete(self, taskIdentifier: taskIdentifier, task: task, delegate: taskOutputDelegate)
        delegate.targetComplete(self, configuredTarget: configuredTarget)
    }

    private func formatError(_ error: NSError, message: String = "Failed to clean build folder") -> NSError {
        var description = error.localizedDescription
        if let reason = error.localizedFailureReason {
            description += " (\(reason))"
        }
        return NSError(domain: "org.swift.swift-build", code: 0, userInfo: [ NSLocalizedDescriptionKey: "\(message): \(description)" ])
    }

    private func cleanBuildFolders(buildFolders: Set<Path>, buildOutputDelegate: any BuildOutputDelegate) {
        let fs = workspaceContext.fs
        for buildFolderPath in buildFolders {
            if self.wasCancellationRequested || _Concurrency.Task.isCancelled || !fs.exists(buildFolderPath) {
                continue
            }

            var foundProjectAncestorPaths = false
            for project in workspaceContext.workspace.projects {
                if buildFolderPath.isAncestor(of: project.xcodeprojPath) {
                    let message = "Refusing to delete `\(buildFolderPath.str)` because it contains one of the projects in this workspace: `\(project.xcodeprojPath.str)`."
                    buildOutputDelegate.warning(message)
                    foundProjectAncestorPaths = true
                }
            }

            if foundProjectAncestorPaths {
                continue
            }

            if isBuildFolder(buildFolderPath) || ignoreCreatedByBuildSystemAttribute {
                do {
                    try deleteBuildFolder(buildFolderPath)
                } catch let error as NSError {
                    if error.domain == "org.swift.swift-build" {
                        buildOutputDelegate.error(error.localizedDescription)
                    } else {
                        buildOutputDelegate.error("Failed to clean build folder: \(error.localizedDescription)")
                    }
                }
            } else {
                var message = "Could not delete `\(buildFolderPath.str)` because it was not created by the build system"
                // The derived data remark doesn't apply for legacy locations, so let's not mention it here.
                if style == .regular {
                    message += " and it is not a subfolder of derived data."
                } else {
                    message += "."
                }
                buildOutputDelegate.emit(Diagnostic(behavior: .error, location: .unknown, data: DiagnosticData(message), childDiagnostics: [
                    Diagnostic(behavior: .note, location: .unknown, data: DiagnosticData("To mark this directory as deletable by the build system, run `\(UNIXShellCommandCodec(encodingStrategy: .singleQuotes, encodingBehavior: .fullCommandLine).encode(fs.commandLineArgumentsToApplyCreatedByBuildSystemAttribute(to: buildFolderPath)))` when it is created."))
                ]))
            }
        }
    }

    private func deleteBuildFolder(_ buildFolderPath: Path) throws {
        let buildFolderUrl = URL(fileURLWithPath: buildFolderPath.str)
        let tmpdir: URL
        do {
            tmpdir = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: buildFolderUrl, create: true)
        } catch let error as NSError {
            throw formatError(error, message: "Error while cleaning build folder, could not create item replacement directory for '\(buildFolderUrl.path)'")
        }
        let pathToDelete = tmpdir.appendingPathComponent("CleanBuildFolderInProgress")

        // 1. Delete destination if it already exists
        do {
            try FileManager.default.removeItem(at: pathToDelete)
        } catch let error as NSError {
            // If we couldn't delete it because it didn’t exist, ignore & continue, otherwise rethrow.
            if error.domain != NSCocoaErrorDomain || error.code != NSFileNoSuchFileError {
                throw formatError(error, message: "Error while cleaning build folder, could not remove '\(pathToDelete.path)'")
            }
        }

        // 2. Move build folder to a temporary directory on the same volume
        //
        // <rdar://problem/9725975>
        // We move the folder aside because we cannot pause the Indexer and
        // the Indexer intermittently writes files to Build/Intermediates.noindex
        // and if this occurs *during* removeItemAtURL: the operation fails
        // with a (misleading) permissions error.
        do {
            try FileManager.default.moveItem(at: buildFolderUrl, to: pathToDelete)
        } catch let error as NSError {
            throw formatError(error, message: "Error while cleaning build folder, could not move '\(buildFolderUrl.path)' to '\(pathToDelete.path)'")
        }

        // 3. Delete temporary destination folder
        do {
            try FileManager.default.removeItem(at: tmpdir)
        } catch let error as NSError {
            // If we couldn't move it because it doesn't exist, that's not really an error, otherwise rethrow.
            if error.domain != NSCocoaErrorDomain || error.code != NSFileNoSuchFileError {
                throw formatError(error, message: "Error while cleaning build folder, could not remove '\(pathToDelete.path)'")
            }
        }

        if workspaceContext.userPreferences.enableDebugActivityLogs {
            delegate.updateBuildProgress(statusMessage: "Deleted build folder: \(buildFolderPath.str)", showInLog: true)
        }
    }
}

extension WorkspaceContext {
    func buildDirectories(settings: Settings) -> [Path] {
        buildDirectoryMacros.map { settings.globalScope.evaluate($0) }
    }
}