11
11
//===----------------------------------------------------------------------===//
12
12
13
13
import SWBUtil
14
+ import SWBMacro
14
15
public import SWBCore
15
16
import Foundation
16
17
@@ -47,6 +48,147 @@ public final class XCStringsCompilerSpec: GenericCompilerSpec, SpecIdentifierTyp
47
48
return
48
49
}
49
50
51
+ if shouldGenerateSymbols ( cbc) {
52
+ constructSymbolGenerationTask ( cbc, delegate)
53
+ }
54
+
55
+ if shouldCompileCatalog ( cbc) {
56
+ await constructCatalogCompilationTask ( cbc, delegate)
57
+ }
58
+ }
59
+
60
+ public override var supportsInstallHeaders : Bool {
61
+ // Yes but we will only perform symbol generation in that case.
62
+ return true
63
+ }
64
+
65
+ public override var supportsInstallAPI : Bool {
66
+ // Yes but we will only perform symbol generation in that case.
67
+ // This matches Asset Catalog symbol generation in order to workaround an issue with header whitespace.
68
+ // rdar://106447203 (Symbols: Enabling symbols for IB causes installapi failure)
69
+ return true
70
+ }
71
+
72
+ /// Whether we should generate tasks to generate code symbols for strings.
73
+ private func shouldGenerateSymbols( _ cbc: CommandBuildContext ) -> Bool {
74
+ guard cbc. scope. evaluate ( BuiltinMacros . STRING_CATALOG_GENERATE_SYMBOLS) else {
75
+ return false
76
+ }
77
+
78
+ // Yes for standard builds/installs as well as headers/api and exportloc (which includes headers).
79
+ // No for installloc.
80
+ let buildComponents = cbc. scope. evaluate ( BuiltinMacros . BUILD_COMPONENTS)
81
+ guard buildComponents. contains ( " build " ) || buildComponents. contains ( " headers " ) || buildComponents. contains ( " api " ) else {
82
+ return false
83
+ }
84
+
85
+ // Avoid symbol generation for xcstrings inside variant groups because that implies association with a resource such as a xib.
86
+ guard cbc. input. regionVariantName == nil else {
87
+ return false
88
+ }
89
+
90
+ // We are only supporting Swift symbols at the moment so don't even generate the task if there are not Swift sources.
91
+ // If this is a synthesized Package resource target, we won't have Swift sources either.
92
+ // That's good since the symbol gen will happen for the code target instead.
93
+ let targetContainsSwiftSources = ( cbc. producer. configuredTarget? . target as? StandardTarget ) ? . sourcesBuildPhase? . containsSwiftSources ( cbc. producer, cbc. producer, cbc. scope, cbc. producer. filePathResolver) ?? false
94
+ guard targetContainsSwiftSources else {
95
+ return false
96
+ }
97
+
98
+ return true
99
+ }
100
+
101
+ /// Whether we should generate tasks to compile the .xcstrings file to .strings/dict files.
102
+ private func shouldCompileCatalog( _ cbc: CommandBuildContext ) -> Bool {
103
+ // Yes for standard builds/installs and installloc.
104
+ // No for exportloc and headers/api.
105
+ let buildComponents = cbc. scope. evaluate ( BuiltinMacros . BUILD_COMPONENTS)
106
+ guard buildComponents. contains ( " build " ) || buildComponents. contains ( " installLoc " ) else {
107
+ return false
108
+ }
109
+
110
+ // If this is a Package target with a synthesized resource target, compile the catalog with the resources instead of here.
111
+ let isMainPackageWithResourceBundle = !cbc. scope. evaluate ( BuiltinMacros . PACKAGE_RESOURCE_BUNDLE_NAME) . isEmpty
112
+ return !isMainPackageWithResourceBundle
113
+ }
114
+
115
+ private struct SymbolGenPayload : TaskPayload {
116
+
117
+ let effectivePlatformName : String
118
+
119
+ init ( effectivePlatformName: String ) {
120
+ self . effectivePlatformName = effectivePlatformName
121
+ }
122
+
123
+ func serialize< T> ( to serializer: T ) where T : SWBUtil . Serializer {
124
+ serializer. serializeAggregate ( 1 ) {
125
+ serializer. serialize ( effectivePlatformName)
126
+ }
127
+ }
128
+
129
+ init ( from deserializer: any SWBUtil . Deserializer ) throws {
130
+ try deserializer. beginAggregate ( 1 )
131
+ self . effectivePlatformName = try deserializer. deserialize ( )
132
+ }
133
+
134
+ }
135
+
136
+ public override var payloadType : ( any TaskPayload . Type ) ? {
137
+ return SymbolGenPayload . self
138
+ }
139
+
140
+ /// Generates a task for generating code symbols for strings.
141
+ private func constructSymbolGenerationTask( _ cbc: CommandBuildContext , _ delegate: any TaskGenerationDelegate ) {
142
+ // The template spec file contains fields suitable for the compilation step.
143
+ // But here we construct a custom command line for symbol generation.
144
+ let execPath = resolveExecutablePath ( cbc, Path ( " xcstringstool " ) )
145
+ var commandLine = [ execPath. str, " generate-symbols " ]
146
+
147
+ // For now shouldGenerateSymbols only returns true if there are Swift sources.
148
+ // So we only generate Swift symbols for now.
149
+ commandLine. append ( contentsOf: [ " --language " , " swift " ] )
150
+
151
+ let outputDir = cbc. scope. evaluate ( BuiltinMacros . DERIVED_SOURCES_DIR)
152
+ commandLine. append ( contentsOf: [ " --output-directory " , outputDir. str] )
153
+
154
+ // Input file
155
+ let inputPath = cbc. input. absolutePath
156
+ commandLine. append ( inputPath. str)
157
+
158
+ let outputPaths = [
159
+ " GeneratedStringSymbols_ \( inputPath. basenameWithoutSuffix) .swift "
160
+ ]
161
+ . map { fileName in
162
+ return outputDir. join ( fileName)
163
+ }
164
+
165
+ for output in outputPaths {
166
+ delegate. declareOutput ( FileToBuild ( absolutePath: output, inferringTypeUsing: cbc. producer) )
167
+ }
168
+
169
+ // Use just first path for now since we're not even sure if we'll support languages beyond Swift.
170
+ let ruleInfo = [ " GenerateStringSymbols " , outputPaths. first!. str, inputPath. str]
171
+ let execDescription = " Generate symbols for \( inputPath. basename) "
172
+
173
+ let payload = SymbolGenPayload ( effectivePlatformName: LocalizationBuildPortion . effectivePlatformName ( scope: cbc. scope, sdkVariant: cbc. producer. sdkVariant) )
174
+
175
+ delegate. createTask (
176
+ type: self ,
177
+ payload: payload,
178
+ ruleInfo: ruleInfo,
179
+ commandLine: commandLine,
180
+ environment: environmentFromSpec ( cbc, delegate) ,
181
+ workingDirectory: cbc. producer. defaultWorkingDirectory,
182
+ inputs: [ inputPath] ,
183
+ outputs: outputPaths,
184
+ execDescription: execDescription,
185
+ preparesForIndexing: true ,
186
+ enableSandboxing: enableSandboxing
187
+ )
188
+ }
189
+
190
+ /// Generates a task for compiling the .xcstrings to .strings/dict files.
191
+ private func constructCatalogCompilationTask( _ cbc: CommandBuildContext , _ delegate: any TaskGenerationDelegate ) async {
50
192
let commandLine = await commandLineFromTemplate ( cbc, delegate, optionContext: discoveredCommandLineToolSpecInfo ( cbc. producer, cbc. scope, delegate) ) . map ( \. asString)
51
193
52
194
// We can't know our precise outputs statically because we don't know what languages are in the xcstrings file,
@@ -75,7 +217,17 @@ public final class XCStringsCompilerSpec: GenericCompilerSpec, SpecIdentifierTyp
75
217
}
76
218
77
219
if !outputs. isEmpty {
78
- delegate. createTask ( type: self , ruleInfo: defaultRuleInfo ( cbc, delegate) , commandLine: commandLine, environment: environmentFromSpec ( cbc, delegate) , workingDirectory: cbc. producer. defaultWorkingDirectory, inputs: [ cbc. input. absolutePath] , outputs: outputs, execDescription: resolveExecutionDescription ( cbc, delegate) , enableSandboxing: enableSandboxing)
220
+ delegate. createTask (
221
+ type: self ,
222
+ ruleInfo: defaultRuleInfo ( cbc, delegate) ,
223
+ commandLine: commandLine,
224
+ environment: environmentFromSpec ( cbc, delegate) ,
225
+ workingDirectory: cbc. producer. defaultWorkingDirectory,
226
+ inputs: [ cbc. input. absolutePath] ,
227
+ outputs: outputs,
228
+ execDescription: resolveExecutionDescription ( cbc, delegate) ,
229
+ enableSandboxing: enableSandboxing
230
+ )
79
231
} else {
80
232
// If there won't be any outputs, there's no reason to run the compiler.
81
233
// However, we still need to leave some indication in the build graph that there was a compilable xcstrings file here so that generateLocalizationInfo can discover it.
@@ -131,8 +283,7 @@ public final class XCStringsCompilerSpec: GenericCompilerSpec, SpecIdentifierTyp
131
283
}
132
284
133
285
public override func generateLocalizationInfo( for task: any ExecutableTask , input: TaskGenerateLocalizationInfoInput ) -> [ TaskGenerateLocalizationInfoOutput ] {
134
- // Tell the build system about the xcstrings file we took as input.
135
- // No need to use a TaskPayload for this because the only data we need is input path, which is already stored on the Task.
286
+ // Tell the build system about the xcstrings file we took as input, as well as any generated symbol files.
136
287
137
288
// These asserts just check to make sure the broader implementation hasn't changed since we wrote this method,
138
289
// in case something here would need to change.
@@ -142,7 +293,18 @@ public final class XCStringsCompilerSpec: GenericCompilerSpec, SpecIdentifierTyp
142
293
143
294
// Our input paths are .xcstrings (only expecting 1).
144
295
// NOTE: We also take same-named .strings/dict files as input, but those are only used to diagnose errors and when they exist we fail before we ever generate the task.
145
- return [ TaskGenerateLocalizationInfoOutput ( compilableXCStringsPaths: task. inputPaths) ]
296
+ var infos = [ TaskGenerateLocalizationInfoOutput ( compilableXCStringsPaths: task. inputPaths) ]
297
+
298
+ if let payload = task. payload as? SymbolGenPayload ,
299
+ let xcstringsPath = task. inputPaths. only {
300
+ let generatedSourceFiles = task. outputPaths. filter { $0. fileExtension == " swift " }
301
+ var info = TaskGenerateLocalizationInfoOutput ( )
302
+ info. effectivePlatformName = payload. effectivePlatformName
303
+ info. generatedSymbolFilesByXCStringsPath = [ xcstringsPath: generatedSourceFiles]
304
+ infos. append ( info)
305
+ }
306
+
307
+ return infos
146
308
}
147
309
148
310
}
0 commit comments