@@ -25,7 +25,13 @@ fileprivate final class ProjectGenerator {
2525 private var project = Xcode . Project ( )
2626 private let allTarget : Xcode . Target
2727
28- private var groups : [ RelativePath : Xcode . Group ] = [ : ]
28+ enum CachedGroup {
29+ /// Covered by a parent folder reference.
30+ case covered
31+ /// Present in the project.
32+ case present( Xcode . Group )
33+ }
34+ private var groups : [ RelativePath : CachedGroup ] = [ : ]
2935 private var files : [ RelativePath : Xcode . FileReference ] = [ : ]
3036 private var targets : [ String : Xcode . Target ] = [ : ]
3137 private var unbuildableSources : [ ClangTarget . Source ] = [ ]
@@ -103,7 +109,7 @@ fileprivate final class ProjectGenerator {
103109 /// for a file path relative to the project root.
104110 private func parentGroup(
105111 for path: RelativePath
106- ) -> ( parentGroup: Xcode . Group , childPath: RelativePath ) {
112+ ) -> ( parentGroup: Xcode . Group , childPath: RelativePath ) ? {
107113 guard let parent = path. parentDir else {
108114 // We've already handled paths under the repo, so this must be for
109115 // paths outside the repo.
@@ -114,18 +120,31 @@ fileprivate final class ProjectGenerator {
114120 if parent == repoRelativePath || parent == mainRepoDirInProject {
115121 return ( project. mainGroup, path)
116122 }
117- return ( group ( for: parent) , RelativePath ( path. fileName) )
123+ guard let parentGroup = group ( for: parent) else { return nil }
124+ return ( parentGroup, RelativePath ( path. fileName) )
118125 }
119126
120- private func group( for path: RelativePath ) -> Xcode . Group {
121- if let group = groups [ path] {
122- return group
127+ /// Returns the group for a given path, or `nil` if the path is covered
128+ /// by a parent folder reference.
129+ private func group( for path: RelativePath ) -> Xcode . Group ? {
130+ if let result = groups [ path] {
131+ switch result {
132+ case . covered:
133+ return nil
134+ case . present( let g) :
135+ return g
136+ }
137+ }
138+ guard
139+ files [ path] == nil , let ( parentGroup, childPath) = parentGroup ( for: path)
140+ else {
141+ groups [ path] = . covered
142+ return nil
123143 }
124- let ( parentGroup, childPath) = parentGroup ( for: path)
125144 let group = parentGroup. addGroup (
126145 path: childPath. rawPath, pathBase: . groupDir, name: path. fileName
127146 )
128- groups [ path] = group
147+ groups [ path] = . present ( group)
129148 return group
130149 }
131150
@@ -163,11 +182,12 @@ fileprivate final class ProjectGenerator {
163182 // group there.
164183 if ref. kind == . folder {
165184 guard groups [ path] == nil else {
166- log. warning ( " Skipping blue folder ' \( path) '; already added " )
167185 return nil
168186 }
169187 }
170- let ( parentGroup, childPath) = parentGroup ( for: path)
188+ guard let ( parentGroup, childPath) = parentGroup ( for: path) else {
189+ return nil
190+ }
171191 let file = parentGroup. addFileReference (
172192 path: childPath. rawPath, isDirectory: ref. kind == . folder,
173193 pathBase: . groupDir, name: path. fileName
@@ -178,10 +198,10 @@ fileprivate final class ProjectGenerator {
178198
179199 @discardableResult
180200 private func getOrCreateRepoRef(
181- _ ref: ProjectSpec . PathReference , allowExcluded : Bool = false
201+ _ ref: ProjectSpec . PathReference
182202 ) -> Xcode . FileReference ? {
183203 let path = ref. path
184- guard allowExcluded || checkNotExcluded ( path) else { return nil }
204+ guard checkNotExcluded ( path) else { return nil }
185205 return getOrCreateProjectRef ( ref. withPath ( repoRelativePath. appending ( path) ) )
186206 }
187207
@@ -190,18 +210,35 @@ fileprivate final class ProjectGenerator {
190210 }
191211
192212 func generateBaseTarget(
193- _ name: String , productType : Xcode . Target . ProductType ? ,
194- includeInAllTarget: Bool
213+ _ name: String , at parentPath : RelativePath ? , canUseBuildableFolder : Bool ,
214+ productType : Xcode . Target . ProductType ? , includeInAllTarget: Bool
195215 ) -> Xcode . Target ? {
196216 guard targets [ name] == nil else {
197217 log. warning ( " Duplicate target ' \( name) ', skipping " )
198218 return nil
199219 }
220+ var buildableFolder : Xcode . FileReference ?
221+ if let parentPath, !parentPath. components. isEmpty {
222+ // If we've been asked to use buildable folders, see if we can create
223+ // a folder reference at the parent path. Otherwise, create a group at
224+ // the parent path. If we can't create either a folder or group, this is
225+ // nested in a folder reference and there's nothing we can do.
226+ if spec. useBuildableFolders && canUseBuildableFolder {
227+ buildableFolder = getOrCreateRepoRef ( . folder( parentPath) )
228+ }
229+ guard buildableFolder != nil ||
230+ group ( for: repoRelativePath. appending ( parentPath) ) != nil else {
231+ return nil
232+ }
233+ }
200234 let target = project. addTarget ( productType: productType, name: name)
201235 targets [ name] = target
202236 if includeInAllTarget {
203237 allTarget. addDependency ( on: target)
204238 }
239+ if let buildableFolder {
240+ target. addBuildableFolder ( buildableFolder)
241+ }
205242 target. buildSettings. common. ONLY_ACTIVE_ARCH = " YES "
206243 target. buildSettings. common. USE_HEADERMAP = " NO "
207244 // The product name needs to be unique across every project we generate
@@ -247,8 +284,12 @@ fileprivate final class ProjectGenerator {
247284 }
248285 unbuildableSources += targetInfo. unbuildableSources
249286
250- for header in targetInfo. headers {
251- getOrCreateRepoRef ( . file( header) )
287+ // Need to defer the addition of headers since the target may want to use
288+ // a buildable folder.
289+ defer {
290+ for header in targetInfo. headers {
291+ getOrCreateRepoRef ( . file( header) )
292+ }
252293 }
253294
254295 // If we have no sources, we're done.
@@ -262,8 +303,20 @@ fileprivate final class ProjectGenerator {
262303 }
263304 return
264305 }
306+ // Can only use buildable folders if there are no unique arguments and no
307+ // unbuildable sources.
308+ // TODO: To improve the coverage of buildable folders, we ought to start
309+ // automatically splitting umbrella Clang targets like 'stdlib', since
310+ // they always have files with unique args.
311+ let canUseBuildableFolders =
312+ try spec. useBuildableFolders && targetInfo. unbuildableSources. isEmpty &&
313+ targetInfo. sources. allSatisfy {
314+ try ! buildDir. clangArgs. hasUniqueArgs ( for: $0. path, parent: targetPath)
315+ }
316+
265317 let target = generateBaseTarget (
266- targetInfo. name, productType: . staticArchive,
318+ targetInfo. name, at: targetPath,
319+ canUseBuildableFolder: canUseBuildableFolders, productType: . staticArchive,
267320 includeInAllTarget: includeInAllTarget
268321 )
269322 guard let target else { return }
@@ -437,7 +490,8 @@ fileprivate final class ProjectGenerator {
437490 )
438491 }
439492 let target = generateBaseTarget (
440- targetInfo. name, productType: nil , includeInAllTarget: includeInAllTarget
493+ targetInfo. name, at: nil , canUseBuildableFolder: false , productType: nil ,
494+ includeInAllTarget: includeInAllTarget
441495 )
442496 guard let target else { return nil }
443497
@@ -477,9 +531,11 @@ fileprivate final class ProjectGenerator {
477531 guard checkNotExcluded ( buildRule. parentPath, for: " Swift target " ) else {
478532 return nil
479533 }
534+ // Create the target. Swift targets can always use buildable folders
535+ // since they have a consistent set of arguments.
480536 let target = generateBaseTarget (
481- targetInfo. name, productType : . staticArchive ,
482- includeInAllTarget: includeInAllTarget
537+ targetInfo. name, at : buildRule . parentPath , canUseBuildableFolder : true ,
538+ productType : . staticArchive , includeInAllTarget: includeInAllTarget
483539 )
484540 guard let target else { return nil }
485541
@@ -599,6 +655,11 @@ fileprivate final class ProjectGenerator {
599655 guard !generated else { return }
600656 generated = true
601657
658+ // First add file/folder references.
659+ for ref in spec. referencesToAdd {
660+ getOrCreateRepoRef ( ref)
661+ }
662+
602663 // Gather the Swift targets to generate, including any dependencies.
603664 var swiftTargets : Set < SwiftTarget > = [ ]
604665 for targetSource in spec. swiftTargetSources {
@@ -681,11 +742,6 @@ fileprivate final class ProjectGenerator {
681742 }
682743 }
683744
684- for ref in spec. referencesToAdd {
685- // Allow important references to bypass exclusion checks.
686- getOrCreateRepoRef ( ref, allowExcluded: ref. isImportant)
687- }
688-
689745 // Sort the groups.
690746 sortGroupChildren( project. mainGroup)
691747 }
0 commit comments