Skip to content

Commit 9a53db6

Browse files
committed
[xcodegen] Add --buildable-folders
This enables the use of folder references for compatible targets, allowing new source files to be added without needing to regenerate the project. Currently disabled by default; I'd like to get some living-on before enabling.
1 parent a596880 commit 9a53db6

File tree

7 files changed

+86
-15
lines changed

7 files changed

+86
-15
lines changed

utils/swift-xcodegen/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ PROJECT CONFIGURATION:
9090
--prefer-folder-refs/--no-prefer-folder-refs
9191
Whether to prefer folder references for groups containing non-source
9292
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)
93102
94103
MISC:
95104
--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: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,6 @@ fileprivate final class ProjectGenerator {
182182
// group there.
183183
if ref.kind == .folder {
184184
guard groups[path] == nil else {
185-
log.warning("Skipping blue folder '\(path)'; already added")
186185
return nil
187186
}
188187
}
@@ -211,24 +210,35 @@ fileprivate final class ProjectGenerator {
211210
}
212211

213212
func generateBaseTarget(
214-
_ name: String, at parentPath: RelativePath?,
213+
_ name: String, at parentPath: RelativePath?, canUseBuildableFolder: Bool,
215214
productType: Xcode.Target.ProductType?, includeInAllTarget: Bool
216215
) -> Xcode.Target? {
217216
guard targets[name] == nil else {
218217
log.warning("Duplicate target '\(name)', skipping")
219218
return nil
220219
}
221-
// Make sure we can create a group for the parent path, otherwise
222-
// this is nested in a folder reference and there's nothing we can do.
223-
if let parentPath, !parentPath.components.isEmpty,
224-
group(for: repoRelativePath.appending(parentPath)) == nil {
225-
return nil
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+
}
226233
}
227234
let target = project.addTarget(productType: productType, name: name)
228235
targets[name] = target
229236
if includeInAllTarget {
230237
allTarget.addDependency(on: target)
231238
}
239+
if let buildableFolder {
240+
target.addBuildableFolder(buildableFolder)
241+
}
232242
target.buildSettings.common.ONLY_ACTIVE_ARCH = "YES"
233243
target.buildSettings.common.USE_HEADERMAP = "NO"
234244
// The product name needs to be unique across every project we generate
@@ -274,8 +284,12 @@ fileprivate final class ProjectGenerator {
274284
}
275285
unbuildableSources += targetInfo.unbuildableSources
276286

277-
for header in targetInfo.headers {
278-
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+
}
279293
}
280294

281295
// If we have no sources, we're done.
@@ -289,8 +303,20 @@ fileprivate final class ProjectGenerator {
289303
}
290304
return
291305
}
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+
292317
let target = generateBaseTarget(
293-
targetInfo.name, at: targetInfo.parentPath, productType: .staticArchive,
318+
targetInfo.name, at: targetPath,
319+
canUseBuildableFolder: canUseBuildableFolders, productType: .staticArchive,
294320
includeInAllTarget: includeInAllTarget
295321
)
296322
guard let target else { return }
@@ -464,7 +490,7 @@ fileprivate final class ProjectGenerator {
464490
)
465491
}
466492
let target = generateBaseTarget(
467-
targetInfo.name, at: nil, productType: nil,
493+
targetInfo.name, at: nil, canUseBuildableFolder: false, productType: nil,
468494
includeInAllTarget: includeInAllTarget
469495
)
470496
guard let target else { return nil }
@@ -505,9 +531,11 @@ fileprivate final class ProjectGenerator {
505531
guard checkNotExcluded(buildRule.parentPath, for: "Swift target") else {
506532
return nil
507533
}
534+
// Create the target. Swift targets can always use buildable folders
535+
// since they have a consistent set of arguments.
508536
let target = generateBaseTarget(
509-
targetInfo.name, at: buildRule.parentPath, productType: .staticArchive,
510-
includeInAllTarget: includeInAllTarget
537+
targetInfo.name, at: buildRule.parentPath, canUseBuildableFolder: true,
538+
productType: .staticArchive, includeInAllTarget: includeInAllTarget
511539
)
512540
guard let target else { return nil }
513541

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ public struct ProjectSpec {
4040
/// files.
4141
public var preferFolderRefs: Bool
4242

43+
/// Whether to enable the use of buildable folders for targets.
44+
public var useBuildableFolders: Bool
45+
4346
/// If provided, the paths added will be implicitly appended to this path.
4447
let mainRepoDir: RelativePath?
4548

@@ -55,7 +58,7 @@ public struct ProjectSpec {
5558
addClangTargets: Bool, addSwiftTargets: Bool,
5659
addSwiftDependencies: Bool, addRunnableTargets: Bool,
5760
addBuildForRunnableTargets: Bool, inferArgs: Bool, preferFolderRefs: Bool,
58-
mainRepoDir: RelativePath? = nil
61+
useBuildableFolders: Bool, mainRepoDir: RelativePath? = nil
5962
) {
6063
self.name = name
6164
self.buildDir = buildDir
@@ -67,6 +70,7 @@ public struct ProjectSpec {
6770
self.addBuildForRunnableTargets = addBuildForRunnableTargets
6871
self.inferArgs = inferArgs
6972
self.preferFolderRefs = preferFolderRefs
73+
self.useBuildableFolders = useBuildableFolders
7074
self.mainRepoDir = mainRepoDir
7175
}
7276

utils/swift-xcodegen/Sources/swift-xcodegen/Options.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,21 @@ struct ProjectOptions: ParsableArguments {
202202
)
203203
var preferFolderRefs: Bool = false
204204

205+
@Flag(
206+
name: .customLong("buildable-folders"), inversion: .prefixedNo,
207+
help: """
208+
Requires Xcode 16: Enables the use of "buildable folders", allowing
209+
folder references to be used for compatible targets. This allows new
210+
source files to be added to a target without needing to regenerate the
211+
project.
212+
213+
Only supported for targets that have no per-file build settings. This
214+
unfortunately means some Clang targes such as 'lib/Basic' and 'stdlib'
215+
cannot currently use buildable folders.
216+
"""
217+
)
218+
var useBuildableFolders: Bool = false
219+
205220
@Option(help: .hidden)
206221
var blueFolders: String = ""
207222
}

utils/swift-xcodegen/Sources/swift-xcodegen/SwiftXcodegen.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ struct SwiftXcodegen: AsyncParsableCommand, Sendable {
7272
addRunnableTargets: false,
7373
addBuildForRunnableTargets: self.addBuildForRunnableTargets,
7474
inferArgs: self.inferArgs, preferFolderRefs: self.preferFolderRefs,
75-
mainRepoDir: mainRepoDir
75+
useBuildableFolders: self.useBuildableFolders, mainRepoDir: mainRepoDir
7676
)
7777
}
7878

0 commit comments

Comments
 (0)