Skip to content

Commit 3793c37

Browse files
authored
Merge pull request #77515 from hamishknight/buildable-folders
[xcodegen] Add buildable folder support
2 parents 17ab897 + 9a53db6 commit 3793c37

File tree

10 files changed

+249
-109
lines changed

10 files changed

+249
-109
lines changed

utils/swift-xcodegen/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ PROJECT CONFIGURATION:
8787
on the build arguments of surrounding files. This is mainly useful for
8888
files that aren't built in the default config, but are still useful to
8989
edit (e.g sourcekitdAPI-InProc.cpp). (default: --infer-args)
90+
--prefer-folder-refs/--no-prefer-folder-refs
91+
Whether to prefer folder references for groups containing non-source
92+
files (default: --no-prefer-folder-refs)
93+
--buildable-folders/--no-buildable-folders
94+
Requires Xcode 16: Enables the use of "buildable folders", allowing
95+
folder references to be used for compatible targets. This allows new
96+
source files to be added to a target without needing to regenerate the
97+
project.
98+
99+
Only supported for targets that have no per-file build settings. This
100+
unfortunately means some Clang targes such as 'lib/Basic' and 'stdlib'
101+
cannot currently use buildable folders. (default: --no-buildable-folders)
90102
91103
MISC:
92104
--project-root-dir <project-root-dir>

utils/swift-xcodegen/Sources/SwiftXcodeGen/BuildArgs/ClangBuildArgsProvider.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ struct ClangBuildArgsProvider {
8383
return .init(for: .clang, args: fileArgs.sorted())
8484
}
8585

86+
/// Whether the given path has any unique args not covered by `parent`.
87+
func hasUniqueArgs(for path: RelativePath, parent: RelativePath) -> Bool {
88+
args.hasUniqueArgs(for: path, parent: parent)
89+
}
90+
8691
/// Whether the given file has build arguments.
8792
func hasBuildArgs(for path: RelativePath) -> Bool {
8893
!args.getArgs(for: path).isEmpty

utils/swift-xcodegen/Sources/SwiftXcodeGen/BuildArgs/CommandArgTree.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,14 @@ struct CommandArgTree {
4646
) -> Set<Command.Argument> {
4747
getArgs(for: path).subtracting(getArgs(for: parent))
4848
}
49+
50+
/// Whether the given path has any unique args not covered by `parent`.
51+
func hasUniqueArgs(for path: RelativePath, parent: RelativePath) -> Bool {
52+
let args = getArgs(for: path)
53+
guard !args.isEmpty else { return false }
54+
// Assuming `parent` is an ancestor of path, the arguments for parent is
55+
// guaranteed to be a subset of the arguments for `path`. As such, we
56+
// only have to compare sizes here.
57+
return args.count != getArgs(for: parent).count
58+
}
4959
}

utils/swift-xcodegen/Sources/SwiftXcodeGen/Generator/ProjectGenerator.swift

Lines changed: 81 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

utils/swift-xcodegen/Sources/SwiftXcodeGen/Generator/ProjectSpec.swift

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ public struct ProjectSpec {
3636
/// on the build arguments of surrounding files.
3737
public var inferArgs: Bool
3838

39+
/// Whether to prefer using folder references for groups containing non-source
40+
/// files.
41+
public var preferFolderRefs: Bool
42+
43+
/// Whether to enable the use of buildable folders for targets.
44+
public var useBuildableFolders: Bool
45+
3946
/// If provided, the paths added will be implicitly appended to this path.
4047
let mainRepoDir: RelativePath?
4148

@@ -50,8 +57,8 @@ public struct ProjectSpec {
5057
_ name: String, for buildDir: RepoBuildDir, runnableBuildDir: RepoBuildDir,
5158
addClangTargets: Bool, addSwiftTargets: Bool,
5259
addSwiftDependencies: Bool, addRunnableTargets: Bool,
53-
addBuildForRunnableTargets: Bool, inferArgs: Bool,
54-
mainRepoDir: RelativePath? = nil
60+
addBuildForRunnableTargets: Bool, inferArgs: Bool, preferFolderRefs: Bool,
61+
useBuildableFolders: Bool, mainRepoDir: RelativePath? = nil
5562
) {
5663
self.name = name
5764
self.buildDir = buildDir
@@ -62,6 +69,8 @@ public struct ProjectSpec {
6269
self.addRunnableTargets = addRunnableTargets
6370
self.addBuildForRunnableTargets = addBuildForRunnableTargets
6471
self.inferArgs = inferArgs
72+
self.preferFolderRefs = preferFolderRefs
73+
self.useBuildableFolders = useBuildableFolders
6574
self.mainRepoDir = mainRepoDir
6675
}
6776

@@ -83,14 +92,11 @@ extension ProjectSpec {
8392
var kind: Kind
8493
var path: RelativePath
8594

86-
/// Whether this reference should bypass exclusion checks.
87-
var isImportant: Bool
88-
89-
static func file(_ path: RelativePath, isImportant: Bool = false) -> Self {
90-
.init(kind: .file, path: path, isImportant: isImportant)
95+
static func file(_ path: RelativePath) -> Self {
96+
.init(kind: .file, path: path)
9197
}
92-
static func folder(_ path: RelativePath, isImportant: Bool = false) -> Self {
93-
.init(kind: .folder, path: path, isImportant: isImportant)
98+
static func folder(_ path: RelativePath) -> Self {
99+
.init(kind: .folder, path: path)
94100
}
95101

96102
func withPath(_ newPath: RelativePath) -> Self {
@@ -145,24 +151,18 @@ extension ProjectSpec {
145151
self.knownUnbuildables.insert(path)
146152
}
147153

148-
public mutating func addReference(
149-
to path: RelativePath, isImportant: Bool = false
150-
) {
154+
public mutating func addReference(to path: RelativePath) {
151155
guard let path = mapPath(path, for: "file") else { return }
152-
if repoRoot.appending(path).isDirectory {
153-
if isImportant {
154-
// Important folder references should block anything being added under
155-
// them.
156-
excludedPaths.append(.init(path: path))
157-
}
158-
referencesToAdd.append(.folder(path, isImportant: isImportant))
159-
} else {
160-
referencesToAdd.append(.file(path, isImportant: isImportant))
161-
}
156+
let isDir = repoRoot.appending(path).isDirectory
157+
referencesToAdd.append(isDir ? .folder(path) : .file(path))
162158
}
163159

164160
public mutating func addHeaders(in path: RelativePath) {
165161
guard let path = mapPath(path, for: "headers") else { return }
162+
if preferFolderRefs {
163+
referencesToAdd.append(.folder(path))
164+
return
165+
}
166166
do {
167167
for header in try buildDir.getHeaderFilePaths(for: path) {
168168
referencesToAdd.append(.file(header))
@@ -184,6 +184,10 @@ extension ProjectSpec {
184184

185185
public mutating func addDocsGroup(at path: RelativePath) {
186186
guard let path = mapPath(path, for: "docs") else { return }
187+
if preferFolderRefs {
188+
referencesToAdd.append(.folder(path))
189+
return
190+
}
187191
do {
188192
for doc in try buildDir.getAllRepoSubpaths(of: path) where doc.isDocLike {
189193
referencesToAdd.append(.file(doc))

utils/swift-xcodegen/Sources/SwiftXcodeGen/Path/PathProtocol.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,11 @@ extension PathProtocol {
127127
}
128128

129129
extension Collection where Element: PathProtocol {
130+
/// Computes the common parent for a collection of paths. If there is only
131+
/// a single unique path, this returns the parent for that path.
130132
var commonAncestor: Element? {
131133
guard let first = self.first else { return nil }
132-
return dropFirst().reduce(first, { $0.commonAncestor(with: $1) })
134+
let result = dropFirst().reduce(first, { $0.commonAncestor(with: $1) })
135+
return result == first ? result.parentDir : result
133136
}
134137
}

0 commit comments

Comments
 (0)