Skip to content

Commit 2199031

Browse files
committed
[xcodegen] Allow buildable folders in more cases
We can define exceptions to handle targets with sources that either have unique arguments or are unbuildable. Eventually this ought to allow us to ditch the "no outside-target source file" rule, but I'm leaving that be for now since ideally we'd handle automatically splitting up umbrella Clang targets such as `stdlib` such that e.g `swiftCore` is its own buildable folder instead of an exception.
1 parent 2a00a65 commit 2199031

File tree

9 files changed

+311
-85
lines changed

9 files changed

+311
-85
lines changed

utils/swift-xcodegen/README.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,7 @@ PROJECT CONFIGURATION:
9494
Requires Xcode 16: Enables the use of "buildable folders", allowing
9595
folder references to be used for compatible targets. This allows new
9696
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: --buildable-folders)
97+
project. (default: --buildable-folders)
10298
10399
--runtimes-build-dir <runtimes-build-dir>
104100
Experimental: The path to a build directory for the new 'Runtimes/'

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ struct ClangTarget {
3131
}
3232

3333
extension RepoBuildDir {
34-
func getCSourceFilePaths(for path: RelativePath) throws -> [RelativePath] {
35-
try getAllRepoSubpaths(of: path).filter(\.isCSourceLike)
34+
func getClangSourceFilePaths(for path: RelativePath) throws -> [RelativePath] {
35+
try getAllRepoSubpaths(of: path).filter(\.isClangSource)
3636
}
3737

3838
func getHeaderFilePaths(for path: RelativePath) throws -> [RelativePath] {
@@ -45,7 +45,7 @@ extension RepoBuildDir {
4545
let path = target.path
4646
let name = target.name
4747

48-
let sourcePaths = try getCSourceFilePaths(for: path)
48+
let sourcePaths = try getClangSourceFilePaths(for: path)
4949
let headers = try getHeaderFilePaths(for: path)
5050
if sourcePaths.isEmpty && headers.isEmpty {
5151
return nil

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

Lines changed: 122 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ fileprivate final class ProjectGenerator {
3535
private var targets: [String: Xcode.Target] = [:]
3636
private var unbuildableSources: [RelativePath] = []
3737
private var runnableBuildTargets: [RunnableTarget: Xcode.Target] = [:]
38+
private var buildableFolders: [RelativePath: Xcode.BuildableFolder] = [:]
3839

3940
/// The group in which external files are stored.
4041
private var externalsGroup: Xcode.Group {
@@ -54,6 +55,13 @@ fileprivate final class ProjectGenerator {
5455
}()
5556
private var includeSubstitutions: Set<BuildArgs.PathSubstitution> = []
5657

58+
private lazy var unbuildablesTarget: Xcode.Target = {
59+
generateBaseTarget(
60+
"Unbuildables", at: ".", canUseBuildableFolder: false,
61+
productType: .staticArchive, includeInAllTarget: false
62+
)!
63+
}()
64+
5765
/// The main repo dir relative to the project.
5866
private lazy var mainRepoDirInProject: RelativePath? =
5967
spec.mainRepoDir.map { repoRelativePath.appending($0) }
@@ -205,6 +213,42 @@ fileprivate final class ProjectGenerator {
205213
return getOrCreateProjectRef(ref.withPath(repoRelativePath.appending(path)))
206214
}
207215

216+
@discardableResult
217+
func getOrCreateRepoBuildableFolder(
218+
at path: RelativePath
219+
) -> Xcode.BuildableFolder? {
220+
guard let ref = getOrCreateRepoRef(.folder(path)) else { return nil }
221+
let folder = ref.getOrCreateBuildableFolder(at: path)
222+
buildableFolders[path] = folder
223+
224+
// Exclude any sources we don't want to handle.
225+
do {
226+
let excluded = try buildDir.getAllRepoSubpaths(of: path)
227+
.filter(\.isExcludedSource)
228+
folder.setTargets([], for: excluded)
229+
} catch {
230+
log.error("\(error)")
231+
}
232+
233+
return folder
234+
}
235+
236+
private func getParentBuildableFolder(
237+
_ path: RelativePath
238+
) -> Xcode.BuildableFolder? {
239+
// First check the mapping directly.
240+
if let buildableFolder = buildableFolders[path] {
241+
return buildableFolder
242+
}
243+
// Then check the parent.
244+
if let parent = path.parentDir,
245+
let buildableFolder = getParentBuildableFolder(parent) {
246+
buildableFolders[path] = buildableFolder
247+
return buildableFolder
248+
}
249+
return nil
250+
}
251+
208252
func getAllRepoSubpaths(of parent: RelativePath) throws -> [RelativePath] {
209253
try buildDir.getAllRepoSubpaths(of: parent)
210254
}
@@ -225,14 +269,15 @@ fileprivate final class ProjectGenerator {
225269
}
226270
return newName
227271
}()
228-
var buildableFolder: Xcode.FileReference?
229-
if let parentPath, !parentPath.components.isEmpty {
272+
var buildableFolder: Xcode.BuildableFolder?
273+
// Note that special targets like "Unbuildables" have an empty parent path.
274+
if let parentPath, !parentPath.isEmpty {
230275
// If we've been asked to use buildable folders, see if we can create
231276
// a folder reference at the parent path. Otherwise, create a group at
232277
// the parent path. If we can't create either a folder or group, this is
233278
// nested in a folder reference and there's nothing we can do.
234279
if spec.useBuildableFolders && canUseBuildableFolder {
235-
buildableFolder = getOrCreateRepoRef(.folder(parentPath))
280+
buildableFolder = getOrCreateRepoBuildableFolder(at: parentPath)
236281
}
237282
guard buildableFolder != nil ||
238283
group(for: repoRelativePath.appending(parentPath)) != nil else {
@@ -262,6 +307,13 @@ fileprivate final class ProjectGenerator {
262307
// The product name needs to be unique across every project we generate
263308
// (to allow the combined workspaces to work), so add in the project name.
264309
target.buildSettings.common.PRODUCT_NAME = "\(self.name)_\(name)"
310+
311+
// Don't optimize or generate debug info, that will only slow down
312+
// compilation; we don't actually care about the binary.
313+
target.buildSettings.common.GCC_OPTIMIZATION_LEVEL = "0"
314+
target.buildSettings.common.GCC_GENERATE_DEBUGGING_SYMBOLS = "NO"
315+
target.buildSettings.common.GCC_WARN_64_TO_32_BIT_CONVERSION = "NO"
316+
265317
return target
266318
}
267319

@@ -298,10 +350,9 @@ fileprivate final class ProjectGenerator {
298350
at parentPath: RelativePath, sources: [RelativePath]
299351
) throws -> Bool {
300352
// To use a buildable folder, all child sources need to be accounted for
301-
// in the target. If we have any stray sources not part of the target,
302-
// attempting to use a buildable folder would incorrectly include them.
303-
// Additionally, special targets like "Unbuildables" have an empty parent
304-
// path, avoid buildable folders for them.
353+
// in the target. Ignore special targets like "Unbuildables" which have an
354+
// empty parent path.
355+
// TODO: We ought to be able to add stray sources as exclusions.
305356
guard spec.useBuildableFolders, !parentPath.isEmpty else { return false }
306357
let sources = Set(sources)
307358
return try getAllRepoSubpaths(of: parentPath)
@@ -311,20 +362,10 @@ fileprivate final class ProjectGenerator {
311362
/// Checks whether a given Clang target can be represented using a buildable
312363
/// folder.
313364
func canUseBuildableFolder(for clangTarget: ClangTarget) throws -> Bool {
314-
// In addition to the standard checking, we also must not have any
315-
// unbuildable sources or sources with unique arguments.
316-
// TODO: To improve the coverage of buildable folders, we ought to start
317-
// automatically splitting umbrella Clang targets like 'stdlib', since
318-
// they currently always have files with unique args.
319-
guard spec.useBuildableFolders, clangTarget.unbuildableSources.isEmpty else {
320-
return false
321-
}
322-
let parent = clangTarget.parentPath
323-
let hasConsistentArgs = try clangTarget.sources.allSatisfy {
324-
try !buildDir.clangArgs.hasUniqueArgs(for: $0, parent: parent)
325-
}
326-
guard hasConsistentArgs else { return false }
327-
return try canUseBuildableFolder(at: parent, sources: clangTarget.sources)
365+
try canUseBuildableFolder(
366+
at: clangTarget.parentPath,
367+
sources: clangTarget.sources + clangTarget.unbuildableSources
368+
)
328369
}
329370

330371
func canUseBuildableFolder(
@@ -336,21 +377,62 @@ fileprivate final class ProjectGenerator {
336377
)
337378
}
338379

339-
func generateClangTarget(
340-
_ targetInfo: ClangTarget, includeInAllTarget: Bool = true
380+
func addSourcesPhaseToClangTarget(
381+
_ target: Xcode.Target, sources: [RelativePath], targetPath: RelativePath
341382
) throws {
383+
let sourcesToBuild = target.addSourcesBuildPhase()
384+
for source in sources {
385+
var fileArgs = try buildDir.clangArgs.getUniqueArgs(
386+
for: source, parent: targetPath, infer: spec.inferArgs
387+
)
388+
if !fileArgs.isEmpty {
389+
applyBaseSubstitutions(to: &fileArgs)
390+
}
391+
// If we're using a buildable folder, the extra arguments are added to it
392+
// directly.
393+
if let buildableFolder = getParentBuildableFolder(source) {
394+
if !fileArgs.isEmpty {
395+
buildableFolder.setExtraCompilerArgs(
396+
fileArgs.printedArgs, for: source, in: target
397+
)
398+
}
399+
continue
400+
}
401+
// Otherwise we add as a file reference and add the arguments to the
402+
// target.
403+
guard let sourceRef = getOrCreateRepoRef(.file(source)) else {
404+
continue
405+
}
406+
let buildFile = sourcesToBuild.addBuildFile(fileRef: sourceRef)
407+
408+
// Add any per-file settings.
409+
buildFile.settings.COMPILER_FLAGS = fileArgs.printed
410+
}
411+
}
412+
413+
func generateClangTarget(_ targetInfo: ClangTarget) throws {
342414
let targetPath = targetInfo.parentPath
343415
guard checkNotExcluded(targetPath, for: "Clang target") else {
344416
return
345417
}
346-
unbuildableSources += targetInfo.unbuildableSources
347418

348-
// Need to defer the addition of headers since the target may want to use
349-
// a buildable folder.
419+
// Need to defer the addition of headers and unbuildable sources since the
420+
// target may want to use a buildable folder.
350421
defer {
351-
for header in targetInfo.headers {
352-
getOrCreateRepoRef(.file(header))
422+
// If we're using a buildable folder, the headers are automatically
423+
// included.
424+
if let buildableFolder = getParentBuildableFolder(targetPath) {
425+
buildableFolder.setTargets(
426+
[unbuildablesTarget], for: targetInfo.unbuildableSources
427+
)
428+
} else {
429+
for header in targetInfo.headers {
430+
getOrCreateRepoRef(.file(header))
431+
}
353432
}
433+
// Add the unbuildable sources regardless of buildable folder since
434+
// we still need the compiler arguments to be set.
435+
unbuildableSources += targetInfo.unbuildableSources
354436
}
355437

356438
// If we have no sources, we're done.
@@ -362,22 +444,20 @@ fileprivate final class ProjectGenerator {
362444
build args
363445
""")
364446
}
447+
// Still create a buildable folder if we can. It won't have an associated
448+
// target, but unbuildable sources may still be added as exceptions.
449+
if try canUseBuildableFolder(for: targetInfo) {
450+
getOrCreateRepoBuildableFolder(at: targetPath)
451+
}
365452
return
366453
}
367454
let target = generateBaseTarget(
368455
targetInfo.name, at: targetPath,
369456
canUseBuildableFolder: try canUseBuildableFolder(for: targetInfo),
370-
productType: .staticArchive,
371-
includeInAllTarget: includeInAllTarget
457+
productType: .staticArchive, includeInAllTarget: true
372458
)
373459
guard let target else { return }
374460

375-
// Don't optimize or generate debug info, that will only slow down
376-
// compilation; we don't actually care about the binary.
377-
target.buildSettings.common.GCC_OPTIMIZATION_LEVEL = "0"
378-
target.buildSettings.common.GCC_GENERATE_DEBUGGING_SYMBOLS = "NO"
379-
target.buildSettings.common.GCC_WARN_64_TO_32_BIT_CONVERSION = "NO"
380-
381461
var libBuildArgs = try buildDir.clangArgs.getArgs(for: targetPath)
382462
applyBaseSubstitutions(to: &libBuildArgs)
383463

@@ -389,23 +469,9 @@ fileprivate final class ProjectGenerator {
389469

390470
target.buildSettings.common.OTHER_CPLUSPLUSFLAGS = libBuildArgs.printedArgs
391471

392-
let sourcesToBuild = target.addSourcesBuildPhase()
393-
394-
for source in targetInfo.sources {
395-
guard let sourceRef = getOrCreateRepoRef(.file(source)) else {
396-
continue
397-
}
398-
let buildFile = sourcesToBuild.addBuildFile(fileRef: sourceRef)
399-
400-
// Add any per-file settings.
401-
var fileArgs = try buildDir.clangArgs.getUniqueArgs(
402-
for: source, parent: targetPath, infer: spec.inferArgs
403-
)
404-
if !fileArgs.isEmpty {
405-
applyBaseSubstitutions(to: &fileArgs)
406-
buildFile.settings.COMPILER_FLAGS = fileArgs.printed
407-
}
408-
}
472+
try addSourcesPhaseToClangTarget(
473+
target, sources: targetInfo.sources, targetPath: targetPath
474+
)
409475
}
410476

411477
/// Record path substitutions for a given target.
@@ -759,14 +825,11 @@ fileprivate final class ProjectGenerator {
759825
try generateClangTarget(target)
760826
}
761827

828+
// Add any unbuildable sources to the special 'Unbuildables' target.
762829
if !unbuildableSources.isEmpty {
763-
let target = ClangTarget(
764-
name: "Unbuildables",
765-
parentPath: ".",
766-
sources: unbuildableSources,
767-
headers: []
830+
try addSourcesPhaseToClangTarget(
831+
unbuildablesTarget, sources: unbuildableSources, targetPath: "."
768832
)
769-
try generateClangTarget(target, includeInAllTarget: false)
770833
}
771834

772835
// Add targets for runnable targets if needed.

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,14 +212,16 @@ extension ProjectSpec {
212212

213213
public mutating func addClangTargets(
214214
below path: RelativePath, addingPrefix prefix: String? = nil,
215-
mayHaveUnbuildableFiles: Bool = false
215+
mayHaveUnbuildableFiles: Bool = false,
216+
excluding excludedChildren: Set<RelativePath> = []
216217
) {
217218
guard addClangTargets else { return }
218219
let originalPath = path
219220
guard let path = mapPath(path, for: "Clang targets") else { return }
220221
let absPath = repoRoot.appending(path)
221222
do {
222-
for child in try absPath.getDirContents() {
223+
for child in try absPath.getDirContents()
224+
where !excludedChildren.contains(child) {
223225
guard absPath.appending(child).isDirectory else {
224226
continue
225227
}

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,19 @@ extension PathProtocol {
129129
return false
130130
}
131131

132-
var isCSourceLike: Bool {
132+
var isClangSource: Bool {
133133
hasExtension(.c, .cpp, .m, .mm)
134134
}
135135

136136
var isSourceLike: Bool {
137-
isCSourceLike || hasExtension(.swift)
137+
isClangSource || hasExtension(.swift)
138+
}
139+
140+
/// Checks whether this file a source file that should be excluded from
141+
/// any generated targets.
142+
var isExcludedSource: Bool {
143+
// We don't get useful build arguments for these.
144+
hasExtension(.asm, .s, .cc, .cl, .inc, .proto)
138145
}
139146

140147
var isDocLike: Bool {

0 commit comments

Comments
 (0)