Skip to content

Commit 7bdc275

Browse files
Generalize relocating source-generating resource files to the sources phase (#705)
1 parent 69e5636 commit 7bdc275

File tree

3 files changed

+139
-96
lines changed

3 files changed

+139
-96
lines changed

Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift

Lines changed: 112 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,12 @@ package final class BuildFilesProcessingContext: BuildFileFilteringContext {
8484
let belongsToPreferredArch: Bool
8585
let currentArchSpec: ArchitectureSpec?
8686

87-
package init(_ scope: MacroEvaluationScope, belongsToPreferredArch: Bool = true, currentArchSpec: ArchitectureSpec? = nil, resolveBuildRules: Bool = true, resourcesDir: Path? = nil, tmpResourcesDir: Path? = nil) {
87+
/// If `true`, avoid emitting any diagnostics via the task producer context.
88+
///
89+
/// This might be set in cases where `BuildFilesProcessingContext` is being used for ephemeral grouping operations outside of the main grouping routine.
90+
private let repressDiagnostics: Bool
91+
92+
package init(_ scope: MacroEvaluationScope, belongsToPreferredArch: Bool = true, currentArchSpec: ArchitectureSpec? = nil, resolveBuildRules: Bool = true, resourcesDir: Path? = nil, tmpResourcesDir: Path? = nil, repressDiagnostics: Bool = false) {
8893
// Define the predicates for filtering source files.
8994
//
9095
// FIXME: Factor this out, and make this machinery efficient.
@@ -97,6 +102,7 @@ package final class BuildFilesProcessingContext: BuildFileFilteringContext {
97102
self.belongsToPreferredArch = belongsToPreferredArch
98103
self.currentArchSpec = currentArchSpec
99104
self.currentPlatformFilter = PlatformFilter(scope)
105+
self.repressDiagnostics = repressDiagnostics
100106
}
101107

102108
/// Adds the file to build to the appropriate group for the task producer being processed, including resolving a build rule action for that group if appropriate.
@@ -113,8 +119,10 @@ package final class BuildFilesProcessingContext: BuildFileFilteringContext {
113119
let buildRuleMatchResult = taskProducerContext.buildRuleSet.match(ftb, scope)
114120
let provisionalRuleAction = buildRuleMatchResult.action
115121

116-
for diagnostic in buildRuleMatchResult.diagnostics {
117-
taskProducerContext.emit(diagnostic.behavior, diagnostic.message)
122+
if !repressDiagnostics {
123+
for diagnostic in buildRuleMatchResult.diagnostics {
124+
taskProducerContext.emit(diagnostic.behavior, diagnostic.message)
125+
}
118126
}
119127

120128
// If this file is the output of some task, then we perform some checks to see whether we should process it.
@@ -125,8 +133,10 @@ package final class BuildFilesProcessingContext: BuildFileFilteringContext {
125133
if generatedByBuildRuleAction === provisionalRuleAction {
126134
// If we should not add if we didn't find an appropriate build rule, then emit a warning and return.
127135
guard addIfNoBuildRuleFound else {
128-
let currentArch = scope.evaluate(BuiltinMacros.CURRENT_ARCH)
129-
taskProducerContext.warning("no rule to process file '\(ftb.absolutePath.str)' of type '\(ftb.fileType.identifier)'" + (currentArch != "undefined_arch" ? " for architecture '\(scope.evaluate(BuiltinMacros.CURRENT_ARCH))'" : ""))
136+
if !repressDiagnostics {
137+
let currentArch = scope.evaluate(BuiltinMacros.CURRENT_ARCH)
138+
taskProducerContext.warning("no rule to process file '\(ftb.absolutePath.str)' of type '\(ftb.fileType.identifier)'" + (currentArch != "undefined_arch" ? " for architecture '\(scope.evaluate(BuiltinMacros.CURRENT_ARCH))'" : ""))
139+
}
130140
return
131141
}
132142
// If we should always add, then do so as an ungrouped file.
@@ -183,7 +193,10 @@ package final class BuildFilesProcessingContext: BuildFileFilteringContext {
183193

184194
// If we've already processed a group with this identifier, then emit an error to the user as this is likely a project configuration error. For example, if a project is generating code (e.g. from a build rule) and multiple versions of the same file are being generated, and thus being processed, this is potentially very bad, especially if those files don't contain the same output!
185195
guard !processedGroupIdents.contains(groupIdent) else {
186-
return taskProducerContext.error("the file group with identifier '\(groupIdent)' has already been processed.")
196+
if !repressDiagnostics {
197+
taskProducerContext.error("the file group with identifier '\(groupIdent)' has already been processed.")
198+
}
199+
return
187200
}
188201

189202
// Find or create the group for the identifier we got back.
@@ -214,30 +227,43 @@ package final class BuildFilesProcessingContext: BuildFileFilteringContext {
214227

215228
/// Allow `collectionGroups` to subsume `singletonGroups`. The initial pass in groupAndAddTasksForFiles looks at one file at a time to assign rules and groups. Certain grouping strategies need to inspect multiple files to group them (e.g. sticker packs need to group an asset catalog and loose strings files matching the sticker pack - without grouping every other strings file as well). This function allows each collectionGroup to inspect
216229
fileprivate func mergeGroups(_ context: TaskProducerContext) {
230+
let allGroupedSingletonGroups = subsumeAdditionalFilesIfDesired(from: self.singletonGroups, context)
231+
232+
if !allGroupedSingletonGroups.isEmpty {
233+
self.singletonGroups = Queue(self.singletonGroups.filter { !allGroupedSingletonGroups.contains($0) })
234+
}
235+
}
236+
237+
/// Allow `collectionGroups` to subsume `filesToSubsume` if desired.
238+
///
239+
/// There is no guarantee that all or even any of the files in `filesToSubsume` will actually be subsumed.
240+
///
241+
/// This method will never add additional groups. It can only add to existing ones.
242+
///
243+
/// - returns: The files that were subsumed.
244+
fileprivate func subsumeAdditionalFilesIfDesired(from filesToSubsume: some Sequence<FileToBuildGroup>, _ context: TaskProducerContext) -> Set<FileToBuildGroup> {
217245
// This assumes that groupAdditionalFiles() rarely chooses to group anything.
218246

219-
var allGroupedSingletonGroups = Set<FileToBuildGroup>()
247+
var allSubsumedGroups = Set<FileToBuildGroup>()
220248
for collectionGroup in collectionGroups {
221249
if let rule = collectionGroup.assignedBuildRuleAction {
222250
for grouper in rule.inputFileGroupingStrategies {
223-
let groupedSingletonGroups = grouper.groupAdditionalFiles(to: collectionGroup, from: self.singletonGroups, context: context)
251+
let subsumedGroups = grouper.groupAdditionalFiles(to: collectionGroup, from: filesToSubsume, context: context)
224252

225-
for group in groupedSingletonGroups {
253+
for group in subsumedGroups {
226254
collectionGroup.files.append(contentsOf: group.files)
227255

228-
if allGroupedSingletonGroups.contains(group) {
256+
if !repressDiagnostics && allSubsumedGroups.contains(group) {
229257
context.error("Multiple rules merged: \(group.files[0].absolutePath)")
230258
}
231259
}
232260

233-
allGroupedSingletonGroups.formUnion(groupedSingletonGroups)
261+
allSubsumedGroups.formUnion(subsumedGroups)
234262
}
235263
}
236264
}
237265

238-
if !allGroupedSingletonGroups.isEmpty {
239-
self.singletonGroups = Queue(self.singletonGroups.filter { !allGroupedSingletonGroups.contains($0) })
240-
}
266+
return allSubsumedGroups
241267
}
242268

243269
// Returns the next file group to process, or nil if all groups have been processed.
@@ -295,6 +321,19 @@ extension TaskProducerContext {
295321
}
296322
}
297323

324+
extension PluginManager {
325+
/// Returns identifiers of file types that can generate sources, and therefore need to be processed within the Sources build phase (at least if there are any existing source files).
326+
///
327+
/// Asset Catalogs would be one example of this, so that they can generate symbols.
328+
func fileTypesProducingGeneratedSources() -> [String] {
329+
var compileToSwiftFileTypes : [String] = []
330+
for groupingStragegyExtensions in extensions(of: InputFileGroupingStrategyExtensionPoint.self) {
331+
compileToSwiftFileTypes.append(contentsOf: groupingStragegyExtensions.fileTypesCompilingToSwiftSources())
332+
}
333+
return compileToSwiftFileTypes
334+
}
335+
}
336+
298337
// MARK:
299338

300339

@@ -352,7 +391,12 @@ package class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
352391
}
353392

354393
/// Allows subclasses to contribute additional build files.
355-
func additionalBuildFiles(_ scope: MacroEvaluationScope) -> [SWBCore.BuildFile] {
394+
func additionalBuildFiles(_ scope: MacroEvaluationScope) async -> [SWBCore.BuildFile] {
395+
return []
396+
}
397+
398+
/// Allows subclasses to specify build files that should be skipped by this task producer.
399+
func buildFilesToSkip(_ scope: MacroEvaluationScope) async -> Set<Ref<SWBCore.BuildFile>> {
356400
return []
357401
}
358402

@@ -376,14 +420,6 @@ package class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
376420

377421
let buildPhaseFileWarningContext = BuildPhaseFileWarningContext(context, scope)
378422

379-
// Sadly we need to make various decisions based on codegen of Asset and String Catalogs.
380-
// We can remove this when we get rid of build phases.
381-
let sourceFileCount = (self.targetContext.configuredTarget?.target as? SWBCore.StandardTarget)?.sourcesBuildPhase?.buildFiles.count ?? 0
382-
let stringsFileTypes = ["text.plist.strings", "text.plist.stringsdict"].map { context.lookupFileType(identifier: $0)! }
383-
var xcstringsBases = Set<String>()
384-
let shouldCodeGenAssets = scope.evaluate(BuiltinMacros.ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS) && sourceFileCount > 0
385-
let shouldCodeGenStrings = scope.evaluate(BuiltinMacros.STRING_CATALOG_GENERATE_SYMBOLS) && sourceFileCount > 0
386-
387423
// Helper function for adding a resolved item. The build file can be nil here if the client wants to add a file divorced from any build file (e.g., because the build file contains context which shouldn't be applied to this file).
388424
func addResolvedItem(buildFile: SWBCore.BuildFile?, path: Path, reference: SWBCore.Reference?, fileType: FileTypeSpec, shouldUsePrefixHeader: Bool = true) {
389425
let base = path.basenameWithoutSuffix.lowercased()
@@ -408,26 +444,16 @@ package class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
408444
addResolvedItem(buildFile: nil, path: path, reference: nil, fileType: fileType, shouldUsePrefixHeader: shouldUsePrefixHeader)
409445
}
410446

411-
for buildFile in buildPhase.buildFiles + additionalBuildFiles(scope) {
447+
let buildFilesToSkip = await self.buildFilesToSkip(scope)
448+
for buildFile in await buildPhase.buildFiles + additionalBuildFiles(scope) {
449+
guard !buildFilesToSkip.contains(Ref(buildFile)) else {
450+
continue
451+
}
452+
412453
// Resolve the reference.
413454
do {
414455
let (reference, path, fileType) = try context.resolveBuildFileReference(buildFile)
415456

416-
if shouldCodeGenAssets {
417-
// Ignore xcassets in Resource Copy Phase since they're now added to the Compile Sources phase for codegen.
418-
if producer.buildPhase is SWBCore.ResourcesBuildPhase && fileType.conformsTo(identifier: "folder.abstractassetcatalog") {
419-
continue
420-
}
421-
}
422-
if shouldCodeGenStrings {
423-
// Ignore xcstrings in Resource Copy Phase since they're now added to the Compile Sources phase for codegen.
424-
if producer.buildPhase is SWBCore.ResourcesBuildPhase && fileType.conformsTo(identifier: "text.json.xcstrings") {
425-
// Keep the basename because later we need to ignore same-named .strings/dict files as well.
426-
xcstringsBases.insert(path.basenameWithoutSuffix)
427-
continue
428-
}
429-
}
430-
431457
// Compilation of .rkassets depends on additional auxiliary inputs that are not
432458
// accessible from a spec class. Instead, they are handled entirely by their own
433459
// task producer (RealityAssetsTaskProducer), so skip processing them as part of
@@ -585,14 +611,10 @@ package class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
585611
}
586612
}
587613

588-
var compileToSwiftFileTypes : [String] = []
589-
for groupingStragegyExtensions in await context.workspaceContext.core.pluginManager.extensions(of: InputFileGroupingStrategyExtensionPoint.self) {
590-
compileToSwiftFileTypes.append(contentsOf: groupingStragegyExtensions.fileTypesCompilingToSwiftSources())
591-
}
592-
593614
// Reorder resolvedBuildFiles so that file types which compile to Swift appear first in the list and so are processed first.
594615
// This is needed because generated sources aren't added to the the main source code list.
595616
// rdar://102834701 (File grouping for 'collection groups' is sensitive to ordering of build phase members)
617+
let compileToSwiftFileTypes = await context.workspaceContext.core.pluginManager.fileTypesProducingGeneratedSources()
596618
var compileToSwiftFiles = [ResolvedBuildFile]()
597619
var otherBuildFiles = [ResolvedBuildFile]()
598620
for resolvedBuildFile in resolvedBuildFiles {
@@ -645,14 +667,6 @@ package class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
645667
continue
646668
}
647669

648-
// Ignore certain .strings/dict files in Resources phase when codegen for xcstrings is enabled.
649-
if shouldCodeGenStrings &&
650-
producer.buildPhase is SWBCore.ResourcesBuildPhase &&
651-
fileType.conformsToAny(stringsFileTypes) &&
652-
xcstringsBases.contains(path.basenameWithoutSuffix) {
653-
continue
654-
}
655-
656670
// Have the build files context add the file to the appropriate file group.
657671
buildFilesContext.addFile(fileToBuild, context, scope)
658672
}
@@ -770,6 +784,52 @@ package class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
770784
}
771785
}
772786

787+
/// Filters `buildFiles` down to only those files that are necessary inputs to source code generation.
788+
///
789+
/// For example, this could include Asset Catalogs (and any of the files they subsume in their grouping strategy).
790+
func sourceGenerationInputFiles(from buildFiles: [SWBCore.BuildFile], scope: MacroEvaluationScope) async -> [SWBCore.BuildFile] {
791+
guard !buildFiles.isEmpty else {
792+
return []
793+
}
794+
795+
let fileIdentifiersGeneratingSources = await context.workspaceContext.core.pluginManager.fileTypesProducingGeneratedSources()
796+
guard !fileIdentifiersGeneratingSources.isEmpty else {
797+
return []
798+
}
799+
800+
var grouper: BuildFilesProcessingContext?
801+
var ungroupedFiles = [FileToBuild]()
802+
for buildFile in buildFiles {
803+
guard let fileRef = try? targetContext.resolveBuildFileReference(buildFile) else {
804+
continue
805+
}
806+
807+
let ftb = FileToBuild(absolutePath: fileRef.absolutePath, fileType: fileRef.fileType, buildFile: buildFile, regionVariantName: fileRef.absolutePath.regionVariantName)
808+
809+
guard fileIdentifiersGeneratingSources.contains(where: { identifier in fileRef.fileType.conformsTo(identifier: identifier) }) else {
810+
ungroupedFiles.append(ftb)
811+
continue
812+
}
813+
814+
if grouper == nil {
815+
grouper = BuildFilesProcessingContext(scope, repressDiagnostics: true)
816+
}
817+
818+
grouper?.addFile(ftb, context, scope)
819+
}
820+
821+
guard let grouper else {
822+
return []
823+
}
824+
825+
let remainingFiles = ungroupedFiles.map { ftb in
826+
FileToBuildGroup(ftb.absolutePath.str, files: [ftb], action: nil)
827+
}
828+
_ = grouper.subsumeAdditionalFilesIfDesired(from: remainingFiles, context)
829+
830+
return grouper.collectionGroups.flatMap(\.files).compactMap(\.buildFile)
831+
}
832+
773833
/// This method is used by the `installLoc` build action to return the paths to localized content within a bundle.
774834
/// For example a bundle which is part of the project sources, where only the localized content in the bundle should be copied in the `installLoc` action.
775835
/// - returns: A list of path strings relative to the absolute path of `ftb`.

Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,28 @@ final class ResourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBa
195195
}
196196
}
197197

198+
override func buildFilesToSkip(_ scope: MacroEvaluationScope) async -> Set<Ref<SWBCore.BuildFile>> {
199+
// Some files might generate sources (e.g. generating symbols) and thus were moved to the Sources phase.
200+
// So they should be skipped in Resources.
201+
202+
let standardTarget = targetContext.configuredTarget?.target as? StandardTarget
203+
let sourceFiles = standardTarget?.sourcesBuildPhase?.buildFiles ?? []
204+
let resourceFiles = standardTarget?.resourcesBuildPhase?.buildFiles ?? []
205+
206+
guard !sourceFiles.isEmpty && !resourceFiles.isEmpty else {
207+
return []
208+
}
209+
210+
let files = await sourceGenerationInputFiles(from: resourceFiles, scope: scope)
211+
return Set(files.map({ Ref($0) }))
212+
}
213+
198214
override func additionalFilesToBuild(_ scope: MacroEvaluationScope) -> [FileToBuild] {
199215
var additionalFilesToBuild: [FileToBuild] = []
200216

201-
// Add the generated xcassets when we're not generating asset symbols since we'll be handling the other xcassets here as well.
217+
// Add the generated xcassets when there are no sources since we'll be handling the other xcassets here as well.
202218
let sourceFiles = (self.targetContext.configuredTarget?.target as? StandardTarget)?.sourcesBuildPhase?.buildFiles.count ?? 0
203-
if (!scope.evaluate(BuiltinMacros.ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS) || sourceFiles == 0) && scope.evaluate(BuiltinMacros.APP_PLAYGROUND_GENERATE_ASSET_CATALOG) {
219+
if sourceFiles == 0 && scope.evaluate(BuiltinMacros.APP_PLAYGROUND_GENERATE_ASSET_CATALOG) {
204220
let assetCatalogToBeGenerated = scope.evaluate(BuiltinMacros.APP_PLAYGROUND_GENERATED_ASSET_CATALOG_FILE)
205221
additionalFilesToBuild.append(
206222
FileToBuild(absolutePath: assetCatalogToBeGenerated, inferringTypeUsing: context)

0 commit comments

Comments
 (0)