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)
}
}
|