File: LocalizationInfo.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 (132 lines) | stat: -rw-r--r-- 6,670 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
//===----------------------------------------------------------------------===//
//
// 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 SWBTaskConstruction
import SWBTaskExecution
import SWBBuildSystem

/// Errors that might be thrown while generating localization info from a build description.
enum LocalizationInfoErrors: Error {
    case noBuildDescriptionID // The client didn't set buildDescriptionID
    case noBuildDescription
}

/// A delegate object for generating localization info.
protocol LocalizationInfoDelegate: BuildDescriptionConstructionDelegate {
    var clientDelegate: any ClientDelegate { get }
}

/// The localization info for a particular target.
///
/// Encapsulates the target GUID and any stringsdata files produced by the latest build of that target.
struct LocalizationInfoOutput {
    /// The target GUID (not the ConfiguredTarget guid).
    let targetIdentifier: String

    /// Paths to source .xcstrings files used as inputs in this target.
    ///
    /// This collection specifically contains compilable files, AKA files in a Resources phase (not a Copy Files phase).
    fileprivate(set) var compilableXCStringsPaths: Set<Path> = []

    /// Paths to .stringsdata files produced by this target, grouped by build attributes such as platform and architecture.
    fileprivate(set) var producedStringsdataPaths: [LocalizationBuildPortion: Set<Path>] = [:]

    /// The name of the primary platform we were building for.
    ///
    /// Mac Catalyst is treated as its own platform.
    fileprivate(set) var effectivePlatformName: String?

    /// Paths to generated source code files holding string symbols, keyed by xcstrings file path.
    fileprivate(set) var generatedSymbolFilesByXCStringsPath = [Path: Set<Path>]()

}

extension BuildDescriptionManager {
    /// Generates and returns any applicable localization information from the build represented by `buildRequest`.
    ///
    /// Each returned Output object represents data for a single `Target` (not `ConfiguredTarget`).
    func generateLocalizationInfo(workspaceContext: WorkspaceContext, buildRequest: BuildRequest, buildRequestContext: BuildRequestContext, delegate: any LocalizationInfoDelegate, input: TaskGenerateLocalizationInfoInput) async throws -> [LocalizationInfoOutput] {
        // We require the client to set buildDescriptionID on the build request so that we can just lookup an existing build description.
        // This guarantees good performance and ensures that we won't need to re-plan if the files changed on disk.
        // Even if the files did change, we still want the plan from this specific build (which in practice will be a build that just completed).

        guard let descriptionID = buildRequest.buildDescriptionID else {
            assertionFailure("The client of generateLocalizationInfo should set buildDescriptionID on the build operation prior to calling the API.")
            throw LocalizationInfoErrors.noBuildDescriptionID
        }

        let buildDescription: BuildDescription
        do {
            if let retrievedBuildDescription = try await getNewOrCachedBuildDescription(.cachedOnly(descriptionID, request: buildRequest, buildRequestContext: buildRequestContext, workspaceContext: workspaceContext), clientDelegate: delegate.clientDelegate, constructionDelegate: delegate)?.buildDescription {
                buildDescription = retrievedBuildDescription
            } else {
                // If we don't receive a build description it means we were cancelled.
                return []
            }
        } catch {
            throw LocalizationInfoErrors.noBuildDescription
        }

        return buildDescription.generateLocalizationInfo(input: input)
    }
}

extension BuildDescription {
    /// Generates and returns information about the localized strings that were / will be extracted during this build.
    func generateLocalizationInfo(input: TaskGenerateLocalizationInfoInput) -> [LocalizationInfoOutput] {
        var outputsByTarget = [String: LocalizationInfoOutput]()

        // Produce only one LocalizationInfoOutput per target.
        taskStore.forEachTask { task in
            guard let targetGUID = task.forTarget?.target.guid else {
                // This task is not associated with a target at all.
                // Ignore for now.
                return // equivalent to `continue` since we're in a closure-based loop.
            }

            let taskLocalizationOutputs = task.generateLocalizationInfo(input: input)

            guard !taskLocalizationOutputs.isEmpty else {
                return // continue
            }

            let taskXCStringsPaths = Set(taskLocalizationOutputs.flatMap(\.compilableXCStringsPaths))
            let taskStringsdataPaths: [LocalizationBuildPortion: Set<Path>] = taskLocalizationOutputs
                .map(\.producedStringsdataPaths)
                .reduce([:], { aggregate, partial in aggregate.merging(partial, uniquingKeysWith: +) })
                .mapValues { Set($0) }

            // Only really expecting to have one platform for a given build.
            // So just use the first seen one as primary.
            let effectivePlatformName = taskLocalizationOutputs.compactMap(\.effectivePlatformName).first

            outputsByTarget[targetGUID, default: LocalizationInfoOutput(targetIdentifier: targetGUID)]
                .compilableXCStringsPaths.formUnion(taskXCStringsPaths)
            outputsByTarget[targetGUID]?.producedStringsdataPaths.merge(taskStringsdataPaths, uniquingKeysWith: { $0.union($1) })

            if outputsByTarget[targetGUID]?.effectivePlatformName == nil && effectivePlatformName != nil {
                outputsByTarget[targetGUID]?.effectivePlatformName = effectivePlatformName
            }

            let taskGeneratedSymbolFiles = taskLocalizationOutputs
                .map(\.generatedSymbolFilesByXCStringsPath)
                .reduce([:], { aggregate, partial in aggregate.merging(partial, uniquingKeysWith: +) })
                .mapValues { Set($0) }

            outputsByTarget[targetGUID]?.generatedSymbolFilesByXCStringsPath.merge(taskGeneratedSymbolFiles, uniquingKeysWith: { $0.union($1) })
        }

        return Array(outputsByTarget.values)
    }
}