File: CopyFilesTaskProducer.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 (515 lines) | stat: -rw-r--r-- 35,295 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
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
//===----------------------------------------------------------------------===//
//
// 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 SWBCore
import SWBUtil
import SWBMacro
import SWBProtocol
import Foundation

/// Produces tasks for files in a Copy Files build phase in an Xcode target.
///
/// Also subclassed by ``SwiftPackageCopyFilesTaskProducer``.
class CopyFilesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBasedBuildPhaseTaskProducer {
    typealias ManagedBuildPhase = SWBCore.CopyFilesBuildPhase

    func prepare() {
        let scope = context.settings.globalScope

        // This is to provide a fallback to prevent any unexpected side-effects this close to shipping...
        guard scope.evaluate(BuiltinMacros.ENABLE_ADDITIONAL_CODESIGN_INPUT_TRACKING) else { return }

        let dstFolder = computeOutputDirectory(scope)

        let wrapperName = scope.evaluate(BuiltinMacros.WRAPPER_NAME)
        if !wrapperName.isEmpty {
            let wrapperPath = scope.evaluate(BuiltinMacros.TARGET_BUILD_DIR).join(wrapperName, preserveRoot: true)
            if wrapperPath.isAncestorOrEqual(of: dstFolder) {
                // Each of the files in the build phase need to be added as codesign inputs to ensure that the product is re-signed if any of these change and there is no matching code change that would otherwise trigger a re-sign. Ideally this could be replaced by watching the directory tree signature of the app bundle, but there are a host of issues that prevent that from happening.
                context.addAdditionalCodeSignInputs(buildPhase.buildFiles, context)
            }
        }
    }

    // We want to scan any app with a copy files phase for relevant privacy content.
    func declarePrivacyContentForApplicationIfNeeded(_ scope: MacroEvaluationScope, _ delegate: any TaskGenerationDelegate, _ ftb: FileToBuild) {
        guard ftb.absolutePath.basename == "PrivacyInfo.xcprivacy" else {
            return
        }

        func lookupProductType(_ ident: String) -> ProductTypeSpec? {
            do {
                return try context.getSpec(ident)
            } catch {
                context.error("Couldn't look up product type '\(ident)' in domain '\(context.domain)': \(error)")
                return nil
            }
        }

        let productTypeIdentifier = scope.evaluate(BuiltinMacros.PRODUCT_TYPE)
        guard let contextProductType = lookupProductType(productTypeIdentifier) else {
            return
        }

        guard let appProductType = lookupProductType("com.apple.product-type.application") else {
            return
        }
        guard contextProductType.conformsTo(appProductType) else {
            return
        }

        let builtWrapper = scope.evaluate(BuiltinMacros.BUILT_PRODUCTS_DIR).join(scope.evaluate(BuiltinMacros.WRAPPER_NAME))
        delegate.declareGeneratedPrivacyPlistContent(builtWrapper)
    }

    func generateTasks() async -> [any PlannedTask] {
        let scope = context.settings.globalScope

        // If this phase is only for deployment processing, don't run when we are not installing.
        guard !managedBuildPhase.runOnlyForDeploymentPostprocessing || scope.evaluate(BuiltinMacros.DEPLOYMENT_POSTPROCESSING) else {
            return []
        }

        // Files are copied for the "build" component, or the "api" or "headers" components if $(INSTALLAPI_COPY_PHASE) or $(INSTALLHDRS_COPY_PHASE) are enabled, respectively.
        //
        // FIXME: The latter feature here is rarely used, and not very flexible as any target which needs other copy phases won't be able to disable them selectively.
        let buildComponents = scope.evaluate(BuiltinMacros.BUILD_COMPONENTS)
        guard buildComponents.contains("build")
                || buildComponents.contains("installLoc")
                || (buildComponents.contains("api") && scope.evaluate(BuiltinMacros.INSTALLAPI_COPY_PHASE))
                || (buildComponents.contains("headers") && scope.evaluate(BuiltinMacros.INSTALLHDRS_COPY_PHASE)) else {
            return []
        }

        // Determine if we are honoring build rules (APPLY_RULES_IN_COPY_FILES) then lookup and apply them.
        var resolveBuildRules = scope.evaluate(BuiltinMacros.APPLY_RULES_IN_COPY_FILES)

        // Delegate to base implementation.
        // When generating a localization root, we do not want to apply APPLY_RULES_IN_COPY_FILES
        if buildComponents.contains("installLoc") {
            resolveBuildRules = false
        }
        let buildFilesContext = BuildFilesProcessingContext(scope, resolveBuildRules: resolveBuildRules)
        return await generateTasks(self, scope, buildFilesContext)
    }

    /// Compute the destination path for the copy.
    private func computeOutputDirectory(_ scope: MacroEvaluationScope) -> Path {
        // FIXME: Clean up the typing of these properties. <rdar://problem/23702533> Copy files phase properties should be stronger typed
        let subfolder: Path
        if self.managedBuildPhase.destinationSubfolder.stringRep == "<absolute>" || self.managedBuildPhase.destinationSubfolder.stringRep == "" {
            if scope.evaluate(BuiltinMacros.DEPLOYMENT_LOCATION) {
                subfolder = scope.evaluate(BuiltinMacros.INSTALL_ROOT)
            } else {
                subfolder = Path("/")
            }
        } else if self.managedBuildPhase.destinationSubfolder.stringRep == "<builtProductsDir>" {
            subfolder = scope.evaluate(BuiltinMacros.BUILT_PRODUCTS_DIR)
        } else {
            let destinationSubfolder = scope.evaluate(self.managedBuildPhase.destinationSubfolder)
            if destinationSubfolder.isEmpty {
                subfolder = scope.evaluate(BuiltinMacros.TARGET_BUILD_DIR).join(scope.evaluate(BuiltinMacros.WRAPPER_NAME), preserveRoot: true)
            } else {
                subfolder = scope.evaluate(BuiltinMacros.TARGET_BUILD_DIR).join(destinationSubfolder, preserveRoot: true)
            }
        }
        let subpath = Path(scope.evaluate(self.managedBuildPhase.destinationSubpath))
        let result = subfolder.join(subpath, preserveRoot: true).normalize()

        // Trim any trailing slashes, as the result is directly combined in spec files.
        return result.withoutTrailingSlash()
    }

    private func bundleFormat(_ ftb: FileToBuild) -> BundleFormat? {
        // Compute the format of the file being copied, if it's a bundle (shallow vs deep), and the version, if it's a framework.
        if case .targetProduct(let guid) = ftb.buildFile?.buildableItem, let referenceTarget = self.context.workspaceContext.workspace.dynamicTarget(for: guid, dynamicallyBuildingTargets: context.globalProductPlan.dynamicallyBuildingTargets) {
            // If the source file being copied is the product of a target, get the values from the project model.
            let parameters = self.context.configuredTarget?.parameters ?? BuildParameters(configuration: nil)
            let settings = self.context.settingsForProductReferenceTarget(referenceTarget, parameters: parameters)
            let isShallow = settings.globalScope.evaluate(BuiltinMacros.SHALLOW_BUNDLE)
            if ftb.fileType.isFramework {
                return .framework(version: isShallow ? nil : settings.globalScope.evaluate(BuiltinMacros.FRAMEWORK_VERSION))
            } else if ftb.fileType.isBundle {
                return .bundle(shallow: isShallow)
            } else {
                return nil
            }
        } else if ftb.fileType.isFramework {
            let current = ftb.absolutePath.join("Versions/Current")
            access(path: current)
            if context.fs.exists(current) {
                do {
                    return try .framework(version: context.fs.readlink(current).str)
                } catch {
                    context.warning("Couldn't resolve framework symlink for '\(current.str)': \(error)")
                    return .framework(version: "A")
                }
            } else {
                return .framework(version: nil)
            }
        } else if ftb.fileType.isBundle {
            let contents = ftb.absolutePath.join("Contents")
            access(path: contents)
            return .bundle(shallow: !context.fs.exists(contents))
        } else {
            return nil
        }
    }

    /// Custom override to support supplying the resources directory when constructing tasks.
    override func constructTasksForRule(_ rule: any BuildRuleAction, _ group: FileToBuildGroup, _ buildFilesContext: BuildFilesProcessingContext, _ scope: MacroEvaluationScope, _ delegate: any TaskGenerationDelegate) async {
        let dstFolder = computeOutputDirectory(scope)

        // FIXME: Merge the region variant.

        let cbc = CommandBuildContext(producer: context, scope: scope, inputs: group.files, isPreferredArch: buildFilesContext.belongsToPreferredArch, buildPhaseInfo: buildFilesContext.buildPhaseInfo(for: rule), resourcesDir: dstFolder, unlocalizedResourcesDir: dstFolder)
        await constructTasksForRule(rule, cbc, delegate)
    }

    override func addOutputFile(_ ftb: FileToBuild, _ producedFromGroup: FileToBuildGroup, _ buildFilesContext: BuildFilesProcessingContext, _ productDirectories: [Path], _ scope: MacroEvaluationScope, _ generatedByBuildRuleAction: any BuildRuleAction, addIfNoBuildRuleFound: Bool) {
        // Have the build files context group the file appropriately.  If the context doesn't think it should be added, we instruct it to add it anyway, as an ungrouped file.  This will cause it to be copied to the phase's destination folder if it is not already there (and to not emit any warnings if it is already there).
        super.addOutputFile(ftb, producedFromGroup, buildFilesContext, productDirectories, scope, generatedByBuildRuleAction, addIfNoBuildRuleFound: true)
    }

    func addTasksForUngroupedFile(_ ftb: FileToBuild, _ buildFilesContext: BuildFilesProcessingContext, _ scope: MacroEvaluationScope, _ tasks: inout [any PlannedTask]) async {
        let dstFolder = computeOutputDirectory(scope)

        // If the file is already in the destination path (e.g., a build rule placed it there), we are done and should not continue processing.
        if dstFolder.isAncestorOrEqual(of: ftb.absolutePath.dirname) {
            return
        }

        // Files with the `.embedInCode` rule should be skipped here.
        if ftb.buildFile?.resourceRule == .embedInCode {
            return
        }

        // Skip DriverKit drivers when building for the simulator, as we aren't able to to sign drivers differently for device versus simulator builds.
        if context.platform?.isSimulator == true && ftb.fileType.conformsTo(identifier: "wrapper.driver-extension") {
            // Similar diagnostics for platform filters and {EX,IN}CLUDED_SOURCE_FILE_NAMES are behind EnableDebugActivityLogs, but those are skipped due to explicit user action. In contrast, skipping drivers on the simulator is not likely to be expected and so users should see the diagnostic regardless of whether that dwrite is enabled.
            let platformFamilyName = context.platform?.familyDisplayName ?? "No Platform"
            context.note("Skipping '\(ftb.absolutePath.str)' because DriverKit drivers are not supported in the \(platformFamilyName) Simulator.", location: ftb.buildFile.map { buildFile in .buildFile(buildFileGUID: buildFile.guid, buildPhaseGUID: buildPhase.guid, targetGUID: targetContext.configuredTarget?.guid.stringValue ?? "") } ?? .unknown)
            return
        }

        // Compute the region path component.
        let regionPathComponent = Path(ftb.regionVariantPathComponent).normalize()

        let dst = dstFolder.join(regionPathComponent).join(ftb.absolutePath.basename)

        if scope.evaluate(BuiltinMacros.BUILD_COMPONENTS).contains("installLoc") {
            // For installLoc, skip any paths that aren't copied into an lproj.
            // For bundles that are the product of a target, we copy the product generated by the installLoc action for that target.
            // For other bundles (e.g., that are part of the project sources), we selectively copy the localized content in that bundle.
            var targetBundleProduct = false
            if ftb.fileType.isBundle, let buildableItem = ftb.buildFile?.buildableItem {
                switch buildableItem {
                case .targetProduct:
                    targetBundleProduct = true
                default:
                    let builtProductsDirectory = scope.evaluate(BuiltinMacros.BUILT_PRODUCTS_DIR)
                    // Another way we can tell if this is the product of another target (even in another project)
                    // is if it's path is in the built products directory
                    if ftb.absolutePath.relativeSubpath(from: builtProductsDirectory) != nil {
                        targetBundleProduct = true
                    } else {
                        await addTasksForEmbeddedLocalizedBundle(ftb, buildFilesContext, scope, &tasks)
                        return
                    }
                }
            }
            guard ftb.isValidLocalizedContent(scope) || targetBundleProduct else { return }
        }

        // Determine whether we should remove header directories on copy.
        let removeHeaderDirectories = ftb.removeHeadersOnCopy && scope.evaluate(BuiltinMacros.REMOVE_HEADERS_FROM_EMBEDDED_BUNDLES)

        // Determine whether we should remove static executables on copy, for example static frameworks whose executable is a static library.
        let removeStaticExecutables = scope.evaluate(BuiltinMacros.REMOVE_STATIC_EXECUTABLES_FROM_EMBEDDED_BUNDLES)

        // Determine whether we should re-sign on copy.  This is only done if the file-to-build wants it, *and* the file type supports it.
        let codeSignAfterCopying = ftb.codeSignOnCopy && ftb.fileType.codeSignOnCopy

        // Determine whether we should strip bitcode during copying.
        // STRIP_BITCODE_FROM_COPIED_FILES is set to YES for non-simulator embedded platforms, so we don't need to spend time stripping it for other platforms.
        let stripBitcode = scope.evaluate(BuiltinMacros.STRIP_BITCODE_FROM_COPIED_FILES) && codeSignAfterCopying

        // SUPPORT FOR MERGEABLE LIBRARIES
        // If this file was built as mergeable, and either we are a merged binary which is merging or re-exporting that file (and embedding it, which is the step that we're setting up here), or we are embedding that merged/reexported binary product as well as this product, then we need to skip copying the product's binary.
        // Note that we remove the binary even for the debug workflow, because the SourcesTaskProducer will copy only the binary part of the product into the merged product to mimic the actual merge workflow.
        // FIXME: At present only products of other targets in this build, and XCFrameworks, are supported here.
        var subpathsToExclude = [String]()
        var subpathsToStrip = [String]()

        /// Utility method to add a subpath to `subpathsToExclude`. If the subpath's first path component is `removeFirstPart` then it will be removed before adding the rest of the subpath.
        func addSubpath(_ string: String, removingFirstPartIfEqualTo firstPartToRemove: String) {
            guard !string.isEmpty else {
                return
            }
            let strParts = string.split(separator: Path.pathSeparator)
            if let firstPart = strParts.first {
                // (If there aren't any parts then we have nothing to add.)
                if strParts.count > 1, firstPart == firstPartToRemove {
                    let subpath = strParts[1...].joined(separator: Path.pathSeparatorString)
                    subpathsToExclude.append(subpath)
                }
                else if firstPart != firstPartToRemove {
                    // If string is *only* firstPartToRemove then we don't add it.
                    subpathsToExclude.append(string)
                }
            }
        }

        // The case where this file is being produced by another target.
        if let producingTarget = context.globalProductPlan.productPathsToProducingTargets[ftb.absolutePath], let mergingTargets = context.globalProductPlan.mergeableTargetsToMergingTargets[producingTarget], let configuredTarget = context.configuredTarget {
            // If either we are in the list of mergingTargets, or one of our dependencies is, then we want to skip.
            // FIXME: We should probably refine this to check that we're also embedding the dependency which is merging the item we're copying, though it's likely that we are.
            // FIXME: We should probably check whether the mergeable target is actually being linked, though it's unclear what edge case would cause that not to happen.
            var shouldSkipCopyingBinary = false
            if mergingTargets.contains(configuredTarget) {
                shouldSkipCopyingBinary = true
            }
            else {
                for dependency in context.globalProductPlan.dependencies(of: configuredTarget) {
                    if mergingTargets.contains(dependency) {
                        shouldSkipCopyingBinary = true
                        break
                    }
                }
            }
            let settings = context.globalProductPlan.planRequest.buildRequestContext.getCachedSettings(producingTarget.parameters, target: producingTarget.target)
            // We want to not exclude the binary if DONT_EMBED_REEXPORTED_MERGEABLE_LIBRARIES is enabled in debug builds.  If we get here then we know this is a mergeable library, but if MAKE_MERGEABLE is enabled on the producing target then we expect this to be a debug build.  (We want to check MAKE_MERGEABLE because it's possible that setting was enabled manually, and in that case DONT_EMBED_REEXPORTED_MERGEABLE_LIBRARIES doesn't apply.)
            if !settings.globalScope.evaluate(BuiltinMacros.MAKE_MERGEABLE) && scope.evaluate(BuiltinMacros.DONT_EMBED_REEXPORTED_MERGEABLE_LIBRARIES) {
                shouldSkipCopyingBinary = false
            }

            if shouldSkipCopyingBinary, let productType = settings.productType {
                // If this is a standalone binary product then we skip copying it altogether.  Otherwise PBXCp will exclude the subpaths we add to the list here.
                if productType.isWrapper {
                    addSubpath(settings.globalScope.evaluate(BuiltinMacros.EXECUTABLE_PATH).str, removingFirstPartIfEqualTo: settings.globalScope.evaluate(BuiltinMacros.FULL_PRODUCT_NAME).str)
                }
                else {
                    // Skip standalone binary altogether by returning out of this method.
                    return
                }
            }
        }
        // Other kinds of build files (right now, XCFrameworks).
        else if let buildFile = ftb.buildFile {
            // We're copying an XCFramework.  We need to resolve the build file to work with it further.
            // FIXME: It's unfortunate that we have to resolve the build file here to see if it's an XCFramework, but the FileToBuild's file type is that of the library inside the XCFramework.
            if let resolvedBuildFile = try? context.resolveBuildFileReference(buildFile), resolvedBuildFile.fileType.identifier == "wrapper.xcframework" {
                var xcFramework: XCFramework? = nil
                do {
                    // We need to get the XCFramework from the library being copied.  This logic is the current way to do this.
                    xcFramework = try XCFramework(path: resolvedBuildFile.absolutePath, fs: context.fs)
                } catch {
                    context.error(error.localizedDescription)
                }
                if let xcFramework = xcFramework, let library = xcFramework.findLibrary(sdk: context.sdk, sdkVariant: context.sdkVariant) {
                    // At this point we know this is an XCFramework which we're copying, and we've successfully resolved information about the relevant library in it that we're using.
                    // Now, if this library contains mergeable metadata then we *either* want to skip copying its binary (if we're merging it), *or* we want to strip mergeable metadata from it (if we're not merging it).
                    var shouldSkipCopyingBinary = false
                    var shouldStripBinary = false
                    if library.mergeableMetadata {
                        // We know we're copying a library which was built mergeable. Now what we want to know whether we're merging it, or one of our dependencies is.
                        if let configuredTarget = context.configuredTarget {
                            for cTarget in [configuredTarget] + context.globalProductPlan.dependencies(of: configuredTarget) {
                                // FIXME: Perhaps knowing "does this target link this XCFramework" is something that the GlobalProductPlan or XCFrameworkContext should know.
                                var didFindBuildFile = false
                                if let frameworksBuildPhase = (cTarget.target as? SWBCore.BuildPhaseTarget)?.frameworksBuildPhase {
                                    for linkedBuildFile in frameworksBuildPhase.buildFiles {
                                        // FIXME: This is sketchy: It's using the current context to evaluate a build file in potentially a different target. This might rarely matter since this code only applies to XCFrameworks, but it feels wrong.
                                        if let resolvedLinkedBuildFile = try? context.resolveBuildFileReference(linkedBuildFile), resolvedLinkedBuildFile.fileType.identifier == "wrapper.xcframework", resolvedBuildFile.absolutePath == resolvedLinkedBuildFile.absolutePath {
                                            // Here we know that this is an XCFramework which is being linked by us or one of our dependencies, and that this XCFramework is marked as mergeable.  So we decide what we want to do with it.
                                            didFindBuildFile = true
                                            let cTargetSettings = context.globalProductPlan.planRequest.buildRequestContext.getCachedSettings(cTarget.parameters, target: cTarget.target)
                                            let mergeLinkedLibraries = cTargetSettings.globalScope.evaluate(BuiltinMacros.MERGE_LINKED_LIBRARIES)
                                            if mergeLinkedLibraries {
                                                shouldSkipCopyingBinary = true
                                            } else {
                                                shouldStripBinary = true
                                            }
                                            break
                                        }
                                    }
                                }
                                if didFindBuildFile {
                                    break
                                }
                            }
                        }
                    }


                    // If we should skip copying it, then we set up the subpaths to exclude.
                    if shouldSkipCopyingBinary {
                        // If this is a standalone binary product then we skip copying it altogether.  Otherwise PBXCp will exclude the subpaths we add to the list here.
                        switch library.libraryType {
                        case .dynamicLibrary:
                            // Skip standalone binary altogether by returning out of this method.
                            return
                        case .framework:
                            if let binaryPath = library.binaryPath {
                                addSubpath(binaryPath.str, removingFirstPartIfEqualTo: library.libraryPath.str)
                            } else {
                                context.warning("XCFramework contains mergeable metadata but does not define a binary path: \(resolvedBuildFile.absolutePath)")
                            }
                        default:
                            // These types do not support mergeable metadata.  This should have been caught at XCFramework creation time, but perhaps someone edited the XCFramework after creation.
                            // In this case, we emit a warning and copy these items normally.
                            context.warning("XCFramework claims \(library.libraryType.libraryTypeName) contains mergeable metadata, which is not supported: \(resolvedBuildFile.absolutePath)")
                        }
                    }
                    // If we should strip the binary, then add the path to its binary to the subpaths to strip.
                    if shouldStripBinary {
                        switch library.libraryType {
                        case .dynamicLibrary, .framework:
                            if let binaryPath = library.binaryPath {
                                subpathsToStrip.append(binaryPath.str)
                            } else {
                                context.warning("XCFramework contains mergeable metadata but does not define a binary path: \(resolvedBuildFile.absolutePath)")
                            }
                        default:
                            // These types do not support mergeable metadata.  This should have been caught at XCFramework creation time, but perhaps someone edited the XCFramework after creation.
                            // In this case, we emit a warning and copy these items normally, without stripping any mergeable metadata they may have.  We may refine this in the future.
                            context.warning("XCFramework claims \(library.libraryType.libraryTypeName) contains mergeable metadata, which is not supported: \(resolvedBuildFile.absolutePath)")
                        }
                    }
                }
            }
        }

        // Block downstream compilation if copy files phases contain any header files or modulemaps.
        // Note that header visibility is not relevant here since the files are always copied.
        let registry = context.workspaceContext.core.specRegistry
        let fileTypes = registry.headerFileTypes + [registry.modulemapFileType]
        let (taskOrderingOptions, preparesForIndexing) = ftb.fileType.conformsToAny(fileTypes) ? (TaskOrderingOptions.compilationRequirement, true) : (nil, false)

        // Generate the tasks.
        let (_, _, (copyFileOrderingNode, resignFileOrderingNode)) = await appendGeneratedTasks(&tasks, usePhasedOrdering: true, options: taskOrderingOptions) { delegate -> (PlannedVirtualNode, (PlannedVirtualNode)?) in
            // Virtual output nodes for individual tasks for mutation tasks to depend on.
            // I'm not sure whether it's worth the logic to only create virtual nodes when we need them.  For logical simplicity, we create the nodes up-front whether or not we need them, unless they need information we compute later.
            let copyFileOrderingNode: PlannedVirtualNode = delegate.createVirtualNode("Copy \(dst.str)")
            let resignFileOrderingNode: (PlannedVirtualNode)?

            let ftbBundleFormat = bundleFormat(ftb)

            // If the target is aggregating tracked domains, then all copied binaries need to be scanned.
            if scope.evaluate(BuiltinMacros.AGGREGATE_TRACKED_DOMAINS) {
                declarePrivacyContentForApplicationIfNeeded(scope, delegate, ftb)

                switch ftbBundleFormat {
                case .bundle(_), .framework(_):
                    delegate.declareGeneratedPrivacyPlistContent(dst)
                default:
                    break
                }
            }

            // Add additional declared outputs if re-signing.
            //
            // These declarations are needed because the re-signing task we set up below defines dependencies on more than just the top-level wrapper, so if we don't declare those items then we'll get errors when creating the build description.  Note that this assumes the bundle we're copying *has* a binary.
            var additionalPresumedOutputs = [any PlannedNode]()
            if codeSignAfterCopying {
                var fileToSign = dst

                switch ftbBundleFormat {
                case let .framework(frameworkVersion):
                    let frameworkName = dst.basenameWithoutSuffix
                    if let frameworkVersion = frameworkVersion {
                        let versionRelativePath = "Versions/\(frameworkVersion)"

                        // If this is not a shallow-bundle framework, then we sign the "Versions/A" folder inside it.
                        fileToSign = fileToSign.join(versionRelativePath)

                        // With deep bundles, we also report the Versions/A folder as an output, because that's what codesign signs.
                        additionalPresumedOutputs.append(delegate.createNode(dst.join(versionRelativePath)))
                        additionalPresumedOutputs.append(delegate.createNode(dst.join(versionRelativePath).join(frameworkName)))
                    }
                    else {
                        additionalPresumedOutputs.append(delegate.createNode(dst.join(frameworkName)))
                    }
                case let .bundle(shallow):
                    let bundleName = dst.basenameWithoutSuffix
                    if !shallow {
                        additionalPresumedOutputs.append(delegate.createNode(dst.join("Contents/MacOS").join(bundleName)))
                    }
                    else {
                        additionalPresumedOutputs.append(delegate.createNode(dst.join(bundleName)))
                    }
                case nil:
                    break
                }

                // Construct the signing task.
                let resignNode = delegate.createVirtualNode("CodeSign \(fileToSign.str)")
                resignFileOrderingNode = resignNode
                let cbc = CommandBuildContext(producer: context, scope: scope, inputs: [FileToBuild(absolutePath: fileToSign, fileType: ftb.fileType)], commandOrderingInputs: [copyFileOrderingNode], commandOrderingOutputs: [resignNode])
                context.codesignSpec.constructCodesignTasks(cbc, delegate, productToSign: dst, bundleFormat: ftbBundleFormat, isReSignTask: true)
            } else {
                resignFileOrderingNode = nil
            }

            // Construct the copy task.
            let cbc = CommandBuildContext(producer: context, scope: scope, inputs: [ftb], output: dst, commandOrderingOutputs: [copyFileOrderingNode], resourcesDir: dstFolder, unlocalizedResourcesDir: dstFolder, preparesForIndexing: preparesForIndexing)

            let ignoreMissingInputs = scope.evaluate(BuiltinMacros.BUILD_COMPONENTS).contains("installLoc") && ftb.fileType.isBundle
            await context.copySpec.constructCopyTasks(cbc, delegate, removeHeaderDirectories: removeHeaderDirectories, removeStaticExecutables: removeStaticExecutables, excludeSubpaths: subpathsToExclude, stripSubpaths: subpathsToStrip, stripBitcode: stripBitcode, additionalPresumedOutputs: additionalPresumedOutputs, ignoreMissingInputs: ignoreMissingInputs, repairViaOwnershipAnalysis: true)

            return (copyFileOrderingNode, resignFileOrderingNode)
        }

        // Disable phase ordering for the ValidateEmbeddedBinary task so that the dependency on CodeSign (see below) does not cause a cycle due to an indirect dependency in the opposite direction because of phase ordering.
        await appendGeneratedTasks(&tasks, usePhasedOrdering: false, options: taskOrderingOptions) { delegate in
            // Validate the binary after copying and signing it, if appropriate.
            if ftb.fileType.validateOnCopy && ftb.fileType.isEmbeddableInProduct && (context.productType?.validateEmbeddedBinaries ?? false) {
                let signingIdentity = scope.evaluate(BuiltinMacros.EXPANDED_CODE_SIGN_IDENTITY)
                let infoPlistPath = scope.evaluate(BuiltinMacros.TARGET_BUILD_DIR).join(scope.evaluate(BuiltinMacros.INFOPLIST_PATH))

                // Ensure that ValidateEmbeddedBinary runs *after* CodeSign (of this product, not the product being copied).
                // This is important because the validation of the embedded product might depend on the parent product's signature.
                let codeSignOrderingNodes = ProductPostprocessingTaskProducer.pathsToSign(scope, context.settings).map { context.createVirtualNode("CodeSign \($0.path.str)") }
                let commandOrderingInputs: [any PlannedNode] = [delegate.createNode(infoPlistPath), copyFileOrderingNode] + (resignFileOrderingNode.map { [$0] } ?? []) + codeSignOrderingNodes
                let cbc = CommandBuildContext(producer: context, scope: scope, inputs: [FileToBuild(absolutePath: dst, fileType: ftb.fileType)], commandOrderingInputs: commandOrderingInputs)

                func lookup(_ macro: MacroDeclaration) -> MacroExpression? {
                    switch macro {
                    case BuiltinMacros.InfoPlistPath:
                        return cbc.scope.namespace.parseLiteralString(infoPlistPath.str)
                    case BuiltinMacros.SigningCert:
                        return cbc.scope.namespace.parseLiteralString(signingIdentity)
                    default:
                        return nil
                    }
                }

                if !scope.evaluate(BuiltinMacros.BUILD_COMPONENTS).contains("installLoc") {
                    await context.validateEmbeddedBinarySpec.constructValidateEmbeddedBinaryTask(cbc, delegate, lookup: lookup)
                }
            }
        }
    }

    private func addTasksForEmbeddedLocalizedBundle(_ ftb: FileToBuild, _ buildFilesContext: BuildFilesProcessingContext, _ scope: MacroEvaluationScope, _ tasks: inout [any PlannedTask]) async {
        await appendGeneratedTasks(&tasks) { delegate in
            let dstFolder = computeOutputDirectory(scope)
            let regionPathComponent = Path(ftb.regionVariantPathComponent).normalize()
            for file in localizedFilesToBuildForEmbeddedBundle(ftb.absolutePath, scope) {
                let localizableFtb = FileToBuild(context: context, absolutePath: ftb.absolutePath.join(file))
                let output = dstFolder.join(regionPathComponent).join(ftb.absolutePath.basename).join(file)

                await context.copySpec.constructCopyTasks(CommandBuildContext(producer: context, scope: scope, inputs: [localizableFtb], output: output, resourcesDir: dstFolder, unlocalizedResourcesDir: dstFolder), delegate)
            }
        }
    }
}