1111//===----------------------------------------------------------------------===//
1212
1313import SWBUtil
14+ import SWBMacro
1415public import SWBCore
1516import Foundation
1617
@@ -47,6 +48,147 @@ public final class XCStringsCompilerSpec: GenericCompilerSpec, SpecIdentifierTyp
4748 return
4849 }
4950
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 {
50192 let commandLine = await commandLineFromTemplate ( cbc, delegate, optionContext: discoveredCommandLineToolSpecInfo ( cbc. producer, cbc. scope, delegate) ) . map ( \. asString)
51193
52194 // 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
75217 }
76218
77219 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+ )
79231 } else {
80232 // If there won't be any outputs, there's no reason to run the compiler.
81233 // 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
131283 }
132284
133285 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.
136287
137288 // These asserts just check to make sure the broader implementation hasn't changed since we wrote this method,
138289 // in case something here would need to change.
@@ -142,7 +293,18 @@ public final class XCStringsCompilerSpec: GenericCompilerSpec, SpecIdentifierTyp
142293
143294 // Our input paths are .xcstrings (only expecting 1).
144295 // 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
146308 }
147309
148310}
0 commit comments