From ee28305a0cb0427ae3c65dcb1bce823decad4fa0 Mon Sep 17 00:00:00 2001 From: iMostafa Date: Wed, 20 May 2026 16:34:31 +0200 Subject: [PATCH] Buildable folders --- .../ElementCreator/CollectBazelPaths.swift | 16 +++-- .../src/ElementCreator/CreateGroupChild.swift | 61 ++++++++++++---- .../ElementCreator/CreateRootElements.swift | 20 +++--- .../src/ElementCreator/ElementCreator.swift | 6 +- .../src/Generator/CalculatePathTree.swift | 63 ++++++++++++----- .../src/Generator/Generator.swift | 42 ++++++----- .../src/Generator/GeneratorArguments.swift | 9 +++ .../src/Generator/GeneratorEnvironment.swift | 6 +- .../Generator/ReadBuildableFoldersFile.swift | 24 +++++++ .../Generator/CalculatePathTreeTests.swift | 56 ++++++++++++++- .../FilesAndGroupsPartialTests.swift | 1 - .../src/BazelPath+BuildableFolders.swift | 10 +++ .../lib/PBXProj/src/Identifiers.swift | 13 ++++ .../src/Generator/CreateTarget.swift | 42 +++++++---- .../src/Generator/CreateTargetObject.swift | 51 +++++++++++--- .../src/Generator/TargetArguments.swift | 13 +++- .../Generator/CreateTargetObjectTests.swift | 70 +++++++++++++++++-- xcodeproj/internal/pbxproj_partials.bzl | 24 +++++++ .../internal/templates/generator.BUILD.bazel | 1 + xcodeproj/internal/xcodeproj_rule.bzl | 9 +++ xcodeproj/internal/xcodeproj_runner.bzl | 2 + xcodeproj/xcodeproj.bzl | 12 +++- 22 files changed, 447 insertions(+), 104 deletions(-) create mode 100644 tools/generators/files_and_groups/src/Generator/ReadBuildableFoldersFile.swift create mode 100644 tools/generators/lib/PBXProj/src/BazelPath+BuildableFolders.swift diff --git a/tools/generators/files_and_groups/src/ElementCreator/CollectBazelPaths.swift b/tools/generators/files_and_groups/src/ElementCreator/CollectBazelPaths.swift index b9f145d8f1..346c610583 100644 --- a/tools/generators/files_and_groups/src/ElementCreator/CollectBazelPaths.swift +++ b/tools/generators/files_and_groups/src/ElementCreator/CollectBazelPaths.swift @@ -21,9 +21,9 @@ extension ElementCreator { includeSelf: Bool ) -> [BazelPath] { return callable( - /*node:*/ node, - /*bazelPath:*/ bazelPath, - /*includeSelf:*/ includeSelf + /* node: */ node, + /* bazelPath: */ bazelPath, + /* includeSelf: */ includeSelf ) } } @@ -46,7 +46,7 @@ extension ElementCreator.CollectBazelPaths { switch node { case .file: return includeSelf ? [bazelPath] : [] - case .group(_, let children): + case let .group(_, children): var bazelPaths = children.flatMap { node in return handleChildNode(node, parentBazelPath: bazelPath) } @@ -57,6 +57,8 @@ extension ElementCreator.CollectBazelPaths { case .generatedFiles: // Impossible to have generated files under localized or model files fatalError() + case .buildableFolder: + return includeSelf ? [bazelPath] : [] } } @@ -65,13 +67,13 @@ extension ElementCreator.CollectBazelPaths { parentBazelPath: BazelPath ) -> [BazelPath] { switch node { - case .file(let name): + case let .file(name): let bazelPath = BazelPath( parent: parentBazelPath, path: name ) return [bazelPath] - case .group(let name, let children): + case let .group(name, children): let bazelPath = BazelPath( parent: parentBazelPath, path: name @@ -84,6 +86,8 @@ extension ElementCreator.CollectBazelPaths { case .generatedFiles: // Impossible to have generated files under localized or model files fatalError() + case let .buildableFolder(path): + return [path] } } } diff --git a/tools/generators/files_and_groups/src/ElementCreator/CreateGroupChild.swift b/tools/generators/files_and_groups/src/ElementCreator/CreateGroupChild.swift index 60e444e413..f61e8db25e 100644 --- a/tools/generators/files_and_groups/src/ElementCreator/CreateGroupChild.swift +++ b/tools/generators/files_and_groups/src/ElementCreator/CreateGroupChild.swift @@ -38,16 +38,16 @@ extension ElementCreator { parentBazelPathType: BazelPathType ) -> GroupChild { return callable( - /*node:*/ node, - /*parentBazelPath:*/ parentBazelPath, - /*parentBazelPathType:*/ parentBazelPathType, - /*createFile:*/ createFile, - /*createGroup:*/ createGroup, - /*createGroupChild:*/ self, - /*createInlineBazelGeneratedFiles:*/ + /* node: */ node, + /* parentBazelPath: */ parentBazelPath, + /* parentBazelPathType: */ parentBazelPathType, + /* createFile: */ createFile, + /* createGroup: */ createGroup, + /* createGroupChild: */ self, + /* createInlineBazelGeneratedFiles: */ createInlineBazelGeneratedFiles, - /*createLocalizedFiles:*/ createLocalizedFiles, - /*createVersionGroup:*/ createVersionGroup + /* createLocalizedFiles: */ createLocalizedFiles, + /* createVersionGroup: */ createVersionGroup ) } } @@ -82,7 +82,38 @@ extension ElementCreator.CreateGroupChild { createVersionGroup: ElementCreator.CreateVersionGroup ) -> GroupChild { switch node { - case .group(let name, let children): + case let .buildableFolder(path): + let name = path.path.lastPathComponent + let object = Object( + identifier: Identifiers.FilesAndGroups + .synchronizedRootGroup(path.path, name: name), + content: #""" +{ + isa = PBXFileSystemSynchronizedRootGroup; + explicitFileTypes = {}; + explicitFolders = ( + ); + path = \#(name.pbxProjEscaped); + sourceTree = ""; + } +"""# + ) + let element = Element( + name: name, + object: object, + sortOrder: .groupLike + ) + return .elementAndChildren( + .init( + element: element, + transitiveObjects: [object], + bazelPathAndIdentifiers: [], + knownRegions: [], + resolvedRepositories: [] + ) + ) + + case let .group(name, children): let (basenameWithoutExt, ext) = name.splitExtension() switch ext { case "lproj": @@ -130,7 +161,7 @@ extension ElementCreator.CreateGroupChild { ) } - case .file(let name): + case let .file(name): return .elementAndChildren( createFile( name: name, @@ -143,7 +174,7 @@ extension ElementCreator.CreateGroupChild { ) ) - case .generatedFiles(let generatedFiles): + case let .generatedFiles(generatedFiles): return .elementAndChildren( createInlineBazelGeneratedFiles( for: generatedFiles, @@ -154,6 +185,12 @@ extension ElementCreator.CreateGroupChild { } } +private extension String { + var lastPathComponent: String { + return split(separator: "/").last.map(String.init) ?? self + } +} + struct Element: Equatable { enum SortOrder: Comparable { case groupLike diff --git a/tools/generators/files_and_groups/src/ElementCreator/CreateRootElements.swift b/tools/generators/files_and_groups/src/ElementCreator/CreateRootElements.swift index 3d45e31de1..c034fe1553 100644 --- a/tools/generators/files_and_groups/src/ElementCreator/CreateRootElements.swift +++ b/tools/generators/files_and_groups/src/ElementCreator/CreateRootElements.swift @@ -41,15 +41,15 @@ extension ElementCreator { for pathTree: [PathTreeNode] ) -> GroupChildElements { return callable( - /*pathTree:*/ pathTree, - /*includeCompileStub:*/ includeCompileStub, - /*installPath:*/ installPath, - /*workspace:*/ workspace, - /*createExternalRepositoriesGroup:*/ + /* pathTree: */ pathTree, + /* includeCompileStub: */ includeCompileStub, + /* installPath: */ installPath, + /* workspace: */ workspace, + /* createExternalRepositoriesGroup: */ createExternalRepositoriesGroup, - /*createGroupChild:*/ createGroupChild, - /*createGroupChildElements:*/ createGroupChildElements, - /*createInternalGroup:*/ createInternalGroup + /* createGroupChild: */ createGroupChild, + /* createGroupChildElements: */ createGroupChildElements, + /* createInternalGroup: */ createInternalGroup ) } } @@ -86,7 +86,7 @@ extension ElementCreator.CreateRootElements { var groupChildren: [GroupChild] = [] for node in pathTree { switch node { - case .group(let name, let children): + case let .group(name, children): switch name { case "external": groupChildren.append( @@ -120,7 +120,7 @@ extension ElementCreator.CreateRootElements { ) } - case .file, .generatedFiles: + case .file, .generatedFiles, .buildableFolder: groupChildren.append( createGroupChild( for: node, diff --git a/tools/generators/files_and_groups/src/ElementCreator/ElementCreator.swift b/tools/generators/files_and_groups/src/ElementCreator/ElementCreator.swift index 3bb20eb870..4fa4b14a18 100644 --- a/tools/generators/files_and_groups/src/ElementCreator/ElementCreator.swift +++ b/tools/generators/files_and_groups/src/ElementCreator/ElementCreator.swift @@ -16,15 +16,15 @@ struct ElementCreator { arguments.executionRootFile ) - let createRootElements = environment.createCreateRootElements( + let createRootElements = try environment.createCreateRootElements( executionRoot: executionRoot, - externalDir: try environment.externalDir( + externalDir: environment.externalDir( executionRoot: executionRoot ), includeCompileStub: compileStubNeeded, installPath: arguments.installPath, selectedModelVersions: - try environment.readSelectedModelVersionsFile( + environment.readSelectedModelVersionsFile( arguments.selectedModelVersionsFile ), workspace: arguments.workspace diff --git a/tools/generators/files_and_groups/src/Generator/CalculatePathTree.swift b/tools/generators/files_and_groups/src/Generator/CalculatePathTree.swift index f2c3a04f49..3ee433f3be 100644 --- a/tools/generators/files_and_groups/src/Generator/CalculatePathTree.swift +++ b/tools/generators/files_and_groups/src/Generator/CalculatePathTree.swift @@ -3,9 +3,10 @@ import PBXProj extension Generator { static func calculatePathTree( paths: [BazelPath], - generatedPaths: [GeneratedPath] + generatedPaths: [GeneratedPath], + buildableFolders: [BazelPath] = [] ) -> [PathTreeNode] { - /// `[package: [config: [path]]` + // `[package: [config: [path]]` var generatedPathsByPackageAndConfig: [BazelPath: [String: [BazelPath]]] = [:] for generatedPath in generatedPaths { @@ -51,14 +52,14 @@ extension Generator { ( package, .multipleConfigs( - pathsByConfig.sorted { $0.key < $1.key }.map({ config, paths in + pathsByConfig.sorted { $0.key < $1.key }.map { config, paths in return .init( name: config, path: "\(config)/\(packageBin)", children: calculateRootedPathTree(paths: paths) ) - }) + } ) ) ) @@ -67,7 +68,8 @@ extension Generator { return calculateRootedPathTree( paths: paths, - generatedFiles: generatedFiles + generatedFiles: generatedFiles, + buildableFolders: buildableFolders ) } @@ -75,9 +77,10 @@ extension Generator { paths: [BazelPath], generatedFiles: [ (package: BazelPath, generatedFiles: PathTreeNode.GeneratedFiles) - ] = [] + ] = [], + buildableFolders: [BazelPath] = [] ) -> [PathTreeNode] { - guard !paths.isEmpty else { + guard !paths.isEmpty || !generatedFiles.isEmpty || !buildableFolders.isEmpty else { return [] } @@ -87,14 +90,14 @@ extension Generator { nodesByComponentCount[components.count, default: []] .append( PathTreeNodeToVisit( - components: components, - kind: .file - ) + components: components, + kind: .file + ) ) } - for (`package`, generatedFiles) in generatedFiles { - var components = `package`.path.split(separator: "/") + for (package, generatedFiles) in generatedFiles { + var components = package.path.split(separator: "/") components.append("") nodesByComponentCount[components.count, default: []] .append( @@ -105,7 +108,21 @@ extension Generator { ) } - for componentCount in (1...nodesByComponentCount.keys.max()!) + for buildableFolder in buildableFolders { + let components = buildableFolder.path.split(separator: "/") + guard !components.isEmpty else { + continue + } + nodesByComponentCount[components.count, default: []] + .append( + PathTreeNodeToVisit( + components: components, + kind: .buildableFolder(buildableFolder) + ) + ) + } + + for componentCount in (1 ... nodesByComponentCount.keys.max()!) .reversed() { let nodesToVisit = nodesByComponentCount @@ -154,13 +171,15 @@ extension Generator { switch nodeToVisit.kind { case .file: node = .file(String(nodeToVisit.components.last!)) - case .group(let children): + case let .group(children): node = .group( name: String(nodeToVisit.components.last!), children: children ) - case .generatedFiles(let generatedFiles): + case let .generatedFiles(generatedFiles): node = .generatedFiles(generatedFiles) + case let .buildableFolder(buildableFolder): + node = .buildableFolder(buildableFolder) } collectedChildren.append(node) } @@ -203,19 +222,22 @@ enum PathTreeNode: Equatable { case file(String) case group(name: String, children: [PathTreeNode]) case generatedFiles(GeneratedFiles) + case buildableFolder(BazelPath) } extension PathTreeNode { var nameForSpecialGroupChild: String { switch self { - case .file(let name): + case let .file(name): return name - case .group(let name, _): + case let .group(name, _): return name case .generatedFiles: // This is only called from `CreateVerisonGroup` and // `CreateLocalizedFiles` where this case can't be hit fatalError() + case let .buildableFolder(path): + return path.path.lastPathComponent } } } @@ -225,6 +247,7 @@ private class PathTreeNodeToVisit { case file case group(children: [PathTreeNode]) case generatedFiles(PathTreeNode.GeneratedFiles) + case buildableFolder(BazelPath) } let components: [String.SubSequence] @@ -238,3 +261,9 @@ private class PathTreeNodeToVisit { self.kind = kind } } + +private extension String { + var lastPathComponent: String { + return split(separator: "/").last.map(String.init) ?? self + } +} diff --git a/tools/generators/files_and_groups/src/Generator/Generator.swift b/tools/generators/files_and_groups/src/Generator/Generator.swift index 073a40a04a..b6cf7874e1 100644 --- a/tools/generators/files_and_groups/src/Generator/Generator.swift +++ b/tools/generators/files_and_groups/src/Generator/Generator.swift @@ -19,13 +19,19 @@ struct Generator { /// groups `PBXProj` partial, and `RESOLVED_REPOSITORIES` build setting. /// Then it writes them to disk. func generate(arguments: Arguments) async throws { + let buildableFolders = try await environment.readBuildableFoldersFile( + arguments.buildableFoldersFile + ) + // FIXME: Do these in parallel as tasks let pathTree = try await environment.calculatePathTree( - /*paths:*/ - environment.readFilePathsFile(arguments.filePathsFile), - /*generatedPaths:*/ environment.readGeneratedFilePathsFile( + /* paths: */ + environment.readFilePathsFile(arguments.filePathsFile) + .filter { !$0.isContained(in: buildableFolders) }, + /* generatedPaths: */ environment.readGeneratedFilePathsFile( arguments.generatedFilePathsFile - ) + ), + /* buildableFolders: */ buildableFolders ) let elementsCreator = ElementCreator(environment: environment.elements) @@ -39,12 +45,12 @@ struct Generator { } let writeKnownRegionsPartialTask = Task { - return try environment.write( + return try await environment.write( environment.knownRegionsPartial( - /*knownRegions:*/ - try await createElementsTask.value.knownRegions, - /*developmentRegion:*/ arguments.developmentRegion, - /*useBaseInternationalization:*/ + /* knownRegions: */ + createElementsTask.value.knownRegions, + /* developmentRegion: */ arguments.developmentRegion, + /* useBaseInternationalization: */ arguments.useBaseInternationalization ), to: arguments.knownRegionsOutputPath @@ -52,8 +58,8 @@ struct Generator { } let writeFilesAndGroupsPartialTask = Task { - let buildFilesPartial = environment.calculateTargetFilesPartial( - objects: try await environment.createTargetFileObjects( + let buildFilesPartial = try await environment.calculateTargetFilesPartial( + objects: environment.createTargetFileObjects( buildFileSubIdentifierFiles: arguments.buildFileSubIdentifiersFiles, // Because we pass in a task here, @@ -72,21 +78,21 @@ struct Generator { ) ) - return try environment.write( + return try await environment.write( environment.filesAndGroupsPartial( - /*buildFilesPartial:*/ buildFilesPartial, - /*elementsPartial:*/ - try await createElementsTask.value.partial + /* buildFilesPartial: */ buildFilesPartial, + /* elementsPartial: */ + createElementsTask.value.partial ), to: arguments.filesAndGroupsOutputPath ) } let writeResolvedRepositoriesBuildSettingTask = Task { - return try environment.write( + return try await environment.write( environment.resolvedRepositoriesBuildSetting( - /*resolvedRepositories:*/ - try await createElementsTask.value.resolvedRepositories + /* resolvedRepositories: */ + createElementsTask.value.resolvedRepositories ), to: arguments.resolvedRepositoriesOutputPath ) diff --git a/tools/generators/files_and_groups/src/Generator/GeneratorArguments.swift b/tools/generators/files_and_groups/src/Generator/GeneratorArguments.swift index ac0a6e0a92..bf69a1e2c0 100644 --- a/tools/generators/files_and_groups/src/Generator/GeneratorArguments.swift +++ b/tools/generators/files_and_groups/src/Generator/GeneratorArguments.swift @@ -49,6 +49,15 @@ Path to a file that contains generated file paths, split into three components ) var generatedFilePathsFile: URL + @Argument( + help: """ +Path to a file that contains buildable folder paths, relative to the Bazel \ +workspace. +""", + transform: { URL(fileURLWithPath: $0, isDirectory: false) } + ) + var buildableFoldersFile: URL + @Argument(help: "Development region for the project.") var developmentRegion: String diff --git a/tools/generators/files_and_groups/src/Generator/GeneratorEnvironment.swift b/tools/generators/files_and_groups/src/Generator/GeneratorEnvironment.swift index 32ca597f58..236a3f3500 100644 --- a/tools/generators/files_and_groups/src/Generator/GeneratorEnvironment.swift +++ b/tools/generators/files_and_groups/src/Generator/GeneratorEnvironment.swift @@ -12,7 +12,8 @@ extension Generator { let calculatePathTree: ( _ paths: [BazelPath], - _ generatedPaths: [GeneratedPath] + _ generatedPaths: [GeneratedPath], + _ buildableFolders: [BazelPath] ) -> [PathTreeNode] let createTargetFileObjects: CreateTargetFileObjects @@ -34,6 +35,8 @@ extension Generator { let readGeneratedFilePathsFile: ReadGeneratedFilePathsFile + let readBuildableFoldersFile: ReadBuildableFoldersFile + let resolvedRepositoriesBuildSetting: ( _ resolvedRepositories: [ResolvedRepository] ) -> String @@ -59,6 +62,7 @@ extension Generator.Environment { knownRegionsPartial: Generator.knownRegionsPartial, readFilePathsFile: Generator.ReadFilePathsFile(), readGeneratedFilePathsFile: Generator.ReadGeneratedFilePathsFile(), + readBuildableFoldersFile: Generator.ReadBuildableFoldersFile(), resolvedRepositoriesBuildSetting: Generator.resolvedRepositoriesBuildSetting, write: Write() diff --git a/tools/generators/files_and_groups/src/Generator/ReadBuildableFoldersFile.swift b/tools/generators/files_and_groups/src/Generator/ReadBuildableFoldersFile.swift new file mode 100644 index 0000000000..320cb0081f --- /dev/null +++ b/tools/generators/files_and_groups/src/Generator/ReadBuildableFoldersFile.swift @@ -0,0 +1,24 @@ +import Foundation +import PBXProj + +extension Generator { + struct ReadBuildableFoldersFile { + private let callable: Callable + + init(callable: @escaping Callable = Self.defaultCallable) { + self.callable = callable + } + + func callAsFunction(_ url: URL) async throws -> [BazelPath] { + try await callable(url) + } + } +} + +extension Generator.ReadBuildableFoldersFile { + typealias Callable = (_ url: URL) async throws -> [BazelPath] + + static func defaultCallable(_ url: URL) async throws -> [BazelPath] { + return try await Set(url.lines.collect()).map { BazelPath($0) } + } +} diff --git a/tools/generators/files_and_groups/test/Generator/CalculatePathTreeTests.swift b/tools/generators/files_and_groups/test/Generator/CalculatePathTreeTests.swift index b269885738..85f8b62f68 100644 --- a/tools/generators/files_and_groups/test/Generator/CalculatePathTreeTests.swift +++ b/tools/generators/files_and_groups/test/Generator/CalculatePathTreeTests.swift @@ -1,11 +1,9 @@ import CustomDump import PBXProj import XCTest - @testable import files_and_groups final class CalculatePathTreeTests: XCTestCase { - // MARK: - empty func test_empty() { @@ -26,6 +24,58 @@ final class CalculatePathTreeTests: XCTestCase { XCTAssertNoDifference(pathTree, expectedPathTree) } + func test_buildableFolders() { + // Arrange + + let paths: [BazelPath] = [ + "Modules/Feature/OfferFeature/BUILD", + "Modules/Feature/OfferFeature/Sources/Module.swift", + "Modules/Feature/OfferFeature/Tests/OfferFeatureTests.swift", + ] + let generatedPaths: [GeneratedPath] = [] + let buildableFolders: [BazelPath] = [ + "Modules/Feature/OfferFeature/Sources", + "Modules/Feature/OfferFeature/Tests", + ] + + let expectedPathTree: [PathTreeNode] = [ + .group( + name: "Modules", + children: [ + .group( + name: "Feature", + children: [ + .group( + name: "OfferFeature", + children: [ + .file("BUILD"), + .buildableFolder( + "Modules/Feature/OfferFeature/Sources" + ), + .buildableFolder( + "Modules/Feature/OfferFeature/Tests" + ), + ] + ), + ] + ), + ] + ), + ] + + // Act + + let pathTree = Generator.calculatePathTree( + paths: paths.filter { !$0.isContained(in: buildableFolders) }, + generatedPaths: generatedPaths, + buildableFolders: buildableFolders + ) + + // Assert + + XCTAssertNoDifference(pathTree, expectedPathTree) + } + // MARK: - sort func test_sort_children() { @@ -60,7 +110,7 @@ final class CalculatePathTreeTests: XCTestCase { .group( name: "gen", children: [ - .file("folder") + .file("folder"), ] ), ] diff --git a/tools/generators/files_and_groups/test/Generator/FilesAndGroupsPartialTests.swift b/tools/generators/files_and_groups/test/Generator/FilesAndGroupsPartialTests.swift index 8bc0ea6e66..f07b2d1f88 100644 --- a/tools/generators/files_and_groups/test/Generator/FilesAndGroupsPartialTests.swift +++ b/tools/generators/files_and_groups/test/Generator/FilesAndGroupsPartialTests.swift @@ -2,7 +2,6 @@ import CustomDump import Foundation import PBXProj import XCTest - @testable import files_and_groups class FilesAndGroupsPartialTests: XCTestCase { diff --git a/tools/generators/lib/PBXProj/src/BazelPath+BuildableFolders.swift b/tools/generators/lib/PBXProj/src/BazelPath+BuildableFolders.swift new file mode 100644 index 0000000000..9c2587bfed --- /dev/null +++ b/tools/generators/lib/PBXProj/src/BazelPath+BuildableFolders.swift @@ -0,0 +1,10 @@ +public extension BazelPath { + func isContained(in folders: [BazelPath]) -> Bool { + for folder in folders { + if path == folder.path || path.hasPrefix("\(folder.path)/") { + return true + } + } + return false + } +} diff --git a/tools/generators/lib/PBXProj/src/Identifiers.swift b/tools/generators/lib/PBXProj/src/Identifiers.swift index 5a43620184..000f3a6f5f 100644 --- a/tools/generators/lib/PBXProj/src/Identifiers.swift +++ b/tools/generators/lib/PBXProj/src/Identifiers.swift @@ -283,6 +283,19 @@ FF0000000000000000000004 /* Products */ FF0000000000000000000008 /* rules_xcodeproj */ """# + /// Calculates the identifier for a `PBXFileSystemSynchronizedRootGroup` + /// at `path`. + public static func synchronizedRootGroup( + _ path: String, + name: String + ) -> String { + let digest = Insecure.MD5.hash(data: Data("S\(path)".utf8)) + .dropLast(5) + .map { byteHexStrings[$0]! } + .joined() + return #"FD\#(digest) /* \#(name) */"# + } + /// Calculates the identifier for a file or group element at `path`. /// /// - Note: The order that this is called matters. If two `path + type` diff --git a/tools/generators/pbxnativetargets/src/Generator/CreateTarget.swift b/tools/generators/pbxnativetargets/src/Generator/CreateTarget.swift index 586f3fe1d3..689a6adc18 100644 --- a/tools/generators/pbxnativetargets/src/Generator/CreateTarget.swift +++ b/tools/generators/pbxnativetargets/src/Generator/CreateTarget.swift @@ -45,18 +45,18 @@ extension Generator { objects: [Object] ) { return try await callable( - /*consolidationMapEntry:*/ entry, - /*defaultXcodeConfiguration:*/ defaultXcodeConfiguration, - /*shard:*/ shard, - /*targetArguments:*/ targetArguments, - /*topLevelTargetAttributes:*/ topLevelTargetAttributes, - /*unitTestHosts:*/ unitTestHosts, - /*xcodeConfigurations:*/ xcodeConfigurations, - /*calculatePlatformVariants:*/ calculatePlatformVariants, - /*createBuildPhases:*/ createBuildPhases, - /*createProductObject:*/ createProductObject, - /*createTargetObject:*/ createTargetObject, - /*createXcodeConfigurations:*/ createXcodeConfigurations + /* consolidationMapEntry: */ entry, + /* defaultXcodeConfiguration: */ defaultXcodeConfiguration, + /* shard: */ shard, + /* targetArguments: */ targetArguments, + /* topLevelTargetAttributes: */ topLevelTargetAttributes, + /* unitTestHosts: */ unitTestHosts, + /* xcodeConfigurations: */ xcodeConfigurations, + /* calculatePlatformVariants: */ calculatePlatformVariants, + /* createBuildPhases: */ createBuildPhases, + /* createProductObject: */ createProductObject, + /* createTargetObject: */ createTargetObject, + /* createXcodeConfigurations: */ createXcodeConfigurations ) } } @@ -134,7 +134,9 @@ extension Generator.CreateTarget { buildFileObjects, buildPhaseFileSubIdentifiers ) = createBuildPhases( - consolidatedInputs: consolidatedInputs, + consolidatedInputs: consolidatedInputs.filtering( + pathsIn: aTargetArguments.buildableFolders + ), hasCParams: aTargetArguments.hasCParams, hasCxxParams: aTargetArguments.hasCxxParams, hasLinkParams: topLevelTargetAttributes[id]?.linkParams != nil, @@ -178,7 +180,8 @@ extension Generator.CreateTarget { setsProductReference: setsProductReference, dependencySubIdentifiers: entry.dependencySubIdentifiers, buildConfigurationListIdentifier: configurationList.identifier, - buildPhaseIdentifiers: buildPhases.map(\.identifier) + buildPhaseIdentifiers: buildPhases.map(\.identifier), + buildableFolders: aTargetArguments.buildableFolders ) let buildFileSubIdentifiers = @@ -190,6 +193,17 @@ extension Generator.CreateTarget { } } +private extension Target.ConsolidatedInputs { + func filtering(pathsIn buildableFolders: [BazelPath]) -> Self { + return .init( + srcs: srcs.filter { !$0.isContained(in: buildableFolders) }, + nonArcSrcs: nonArcSrcs.filter { + !$0.isContained(in: buildableFolders) + } + ) + } +} + private extension PBXProductType { var isBundle: Bool { switch self { diff --git a/tools/generators/pbxnativetargets/src/Generator/CreateTargetObject.swift b/tools/generators/pbxnativetargets/src/Generator/CreateTargetObject.swift index 12fd71700e..37c7397ef4 100644 --- a/tools/generators/pbxnativetargets/src/Generator/CreateTargetObject.swift +++ b/tools/generators/pbxnativetargets/src/Generator/CreateTargetObject.swift @@ -20,23 +20,31 @@ extension Generator { setsProductReference: Bool, dependencySubIdentifiers: [Identifiers.Targets.SubIdentifier], buildConfigurationListIdentifier: String, - buildPhaseIdentifiers: [String] + buildPhaseIdentifiers: [String], + buildableFolders: [BazelPath] ) -> Object { return callable( - /*identifier:*/ identifier, - /*productType:*/ productType, - /*productName:*/ productName, - /*productSubIdentifier:*/ productSubIdentifier, - /*setsProductReference:*/ setsProductReference, - /*dependencySubIdentifiers:*/ dependencySubIdentifiers, - /*buildConfigurationListIdentifier:*/ + /* identifier: */ identifier, + /* productType: */ productType, + /* productName: */ productName, + /* productSubIdentifier: */ productSubIdentifier, + /* setsProductReference: */ setsProductReference, + /* dependencySubIdentifiers: */ dependencySubIdentifiers, + /* buildConfigurationListIdentifier: */ buildConfigurationListIdentifier, - /*buildPhaseIdentifiers:*/ buildPhaseIdentifiers + /* buildPhaseIdentifiers: */ buildPhaseIdentifiers, + /* buildableFolders: */ buildableFolders ) } } } +private extension String { + var lastPathComponent: String { + return split(separator: "/").last.map(String.init) ?? self + } +} + // MARK: - CreateTargetObject.Callable extension Generator.CreateTargetObject { @@ -48,7 +56,8 @@ extension Generator.CreateTargetObject { _ setsProductReference: Bool, _ dependencySubIdentifiers: [Identifiers.Targets.SubIdentifier], _ buildConfigurationListIdentifier: String, - _ buildPhaseIdentifiers: [String] + _ buildPhaseIdentifiers: [String], + _ buildableFolders: [BazelPath] ) -> Object static func defaultCallable( @@ -59,7 +68,8 @@ extension Generator.CreateTargetObject { setsProductReference: Bool, dependencySubIdentifiers: [Identifiers.Targets.SubIdentifier], buildConfigurationListIdentifier: String, - buildPhaseIdentifiers: [String] + buildPhaseIdentifiers: [String], + buildableFolders: [BazelPath] ) -> Object { let productReference: String if setsProductReference { @@ -73,6 +83,24 @@ extension Generator.CreateTargetObject { productReference = "" } + let fileSystemSynchronizedGroups: String + if buildableFolders.isEmpty { + fileSystemSynchronizedGroups = "" + } else { + let identifiers = buildableFolders.map { + Identifiers.FilesAndGroups.synchronizedRootGroup( + $0.path, + name: $0.path.lastPathComponent + ) + } + fileSystemSynchronizedGroups = #""" + fileSystemSynchronizedGroups = ( +\#(identifiers.map { "\t\t\t\t\($0),\n" }.joined())\# + ); + +"""# + } + // The tabs for indenting are intentional let content = #""" { @@ -100,6 +128,7 @@ extension Generator.CreateTargetObject { .joined() )\# ); +\#(fileSystemSynchronizedGroups)\# name = \#(identifier.pbxProjEscapedName); productName = \#(productName.pbxProjEscaped); \#(productReference)\# diff --git a/tools/generators/pbxnativetargets/src/Generator/TargetArguments.swift b/tools/generators/pbxnativetargets/src/Generator/TargetArguments.swift index 31149ea0e8..1aab59cefb 100644 --- a/tools/generators/pbxnativetargets/src/Generator/TargetArguments.swift +++ b/tools/generators/pbxnativetargets/src/Generator/TargetArguments.swift @@ -27,19 +27,20 @@ struct TargetArguments: Equatable { // FIXME: Extract to `Inputs` type let srcs: [BazelPath] let nonArcSrcs: [BazelPath] + let buildableFolders: [BazelPath] let dSYMPathsBuildSetting: String } -extension Dictionary { +extension [TargetID: TargetArguments] { static func parse(from url: URL) async throws -> Self { - var rawArgs = ArraySlice(try await url.allLines.collect()) + var rawArgs = try await ArraySlice(url.allLines.collect()) let targetCount = try rawArgs.consumeArg("target-count", as: Int.self, in: url) var keysWithValues: [(TargetID, TargetArguments)] = [] - for _ in (0.. { as: BazelPath.self, in: url ) + let buildableFolders = try rawArgs.consumeArgs( + "buildable-folders", + as: BazelPath.self, + in: url + ) let xcodeConfigurations = try rawArgs.consumeArgs( "xcode-configurations", in: url @@ -130,6 +136,7 @@ extension Dictionary { hasCxxParams: hasCxxParams, srcs: srcs, nonArcSrcs: nonArcSrcs, + buildableFolders: buildableFolders, dSYMPathsBuildSetting: dSYMPathsBuildSetting ) ) diff --git a/tools/generators/pbxnativetargets/test/Generator/CreateTargetObjectTests.swift b/tools/generators/pbxnativetargets/test/Generator/CreateTargetObjectTests.swift index d22229a1b9..efc3186875 100644 --- a/tools/generators/pbxnativetargets/test/Generator/CreateTargetObjectTests.swift +++ b/tools/generators/pbxnativetargets/test/Generator/CreateTargetObjectTests.swift @@ -1,6 +1,5 @@ import CustomDump import XCTest - @testable import pbxnativetargets @testable import PBXProj @@ -65,7 +64,8 @@ class CreateTargetObjectTests: XCTestCase { setsProductReference: setsProductReference, dependencySubIdentifiers: dependencySubIdentifiers, buildConfigurationListIdentifier: buildConfigurationListIdentifier, - buildPhaseIdentifiers: buildPhaseIdentifiers + buildPhaseIdentifiers: buildPhaseIdentifiers, + buildableFolders: [] ) // Assert @@ -140,7 +140,8 @@ class CreateTargetObjectTests: XCTestCase { setsProductReference: setsProductReference, dependencySubIdentifiers: dependencySubIdentifiers, buildConfigurationListIdentifier: buildConfigurationListIdentifier, - buildPhaseIdentifiers: buildPhaseIdentifiers + buildPhaseIdentifiers: buildPhaseIdentifiers, + buildableFolders: [] ) // Assert @@ -207,7 +208,68 @@ class CreateTargetObjectTests: XCTestCase { setsProductReference: setsProductReference, dependencySubIdentifiers: dependencySubIdentifiers, buildConfigurationListIdentifier: buildConfigurationListIdentifier, - buildPhaseIdentifiers: buildPhaseIdentifiers + buildPhaseIdentifiers: buildPhaseIdentifiers, + buildableFolders: [] + ) + + // Assert + + XCTAssertNoDifference(object, expectedObject) + } + + func test_buildableFolders() { + // Arrange + + let identifier = Identifiers.Targets.Identifier( + pbxProjEscapedName: "a", + subIdentifier: .init(shard: "A_SHARD", hash: "A_HASH"), + full: "A_ID /* a */", + withoutComment: "A_ID" + ) + let productType = PBXProductType.staticLibrary + let productName = "A" + let productSubIdentifier = Identifiers.BuildFiles.SubIdentifier( + shard: "B_SHARD", + type: .product, + path: "product.basename", + hash: "B_HASH" + ) + let buildableFolder = BazelPath("App/Sources") + + let expectedObject = Object( + identifier: "A_ID /* a */", + content: #""" +{ + isa = PBXNativeTarget; + buildConfigurationList = BCL_ID; + buildPhases = ( + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + \#(Identifiers.FilesAndGroups.synchronizedRootGroup("App/Sources", name: "Sources")), + ); + name = a; + productName = A; + productType = "com.apple.product-type.library.static"; + } +"""# + ) + + // Act + + let object = Generator.CreateTargetObject.defaultCallable( + identifier: identifier, + productType: productType, + productName: productName, + productSubIdentifier: productSubIdentifier, + setsProductReference: false, + dependencySubIdentifiers: [], + buildConfigurationListIdentifier: "BCL_ID", + buildPhaseIdentifiers: [], + buildableFolders: [buildableFolder] ) // Assert diff --git a/xcodeproj/internal/pbxproj_partials.bzl b/xcodeproj/internal/pbxproj_partials.bzl index fb925db83d..2c8acfac36 100644 --- a/xcodeproj/internal/pbxproj_partials.bzl +++ b/xcodeproj/internal/pbxproj_partials.bzl @@ -137,6 +137,7 @@ def _write_consolidation_map_targets( ), colorize, consolidation_map, + buildable_folders_by_label, default_xcode_configuration, generator_name, idx, @@ -309,6 +310,11 @@ def _write_consolidation_map_targets( omit_if_empty = False, terminate_with = "", ) + targets_args.add_all( + buildable_folders_by_label.get(str(label), []), + omit_if_empty = False, + terminate_with = "", + ) targets_args.add_all( xcode_target_configurations[xcode_target.id], @@ -398,6 +404,7 @@ def _write_files_and_groups( *, actions, buildfile_subidentifiers_files, + buildable_folders, colorize, compile_stub_needed, execution_root_file, @@ -468,6 +475,17 @@ def _write_files_and_groups( args.use_param_file("@%s") args.set_param_file_format("multiline") + # buildableFolders + buildable_folders_file = actions.declare_file( + "{}_pbxproj_partials/buildable_folders_file".format( + generator_name, + ), + ) + buildable_folders_args = actions.args() + buildable_folders_args.set_param_file_format("multiline") + buildable_folders_args.add_all(buildable_folders) + actions.write(buildable_folders_file, buildable_folders_args) + # filePaths file_paths_file = actions.declare_file( @@ -543,6 +561,9 @@ def _write_files_and_groups( # generatedFilePathsFile args.add(generated_file_paths_file) + # buildableFoldersFile + args.add(buildable_folders_file) + # developmentRegion args.add(project_options["development_region"]) @@ -571,6 +592,7 @@ def _write_files_and_groups( inputs = [ file_paths_file, generated_file_paths_file, + buildable_folders_file, execution_root_file, selected_model_versions_file, ] + buildfile_subidentifiers_files, @@ -1319,6 +1341,7 @@ def _write_targets( actions, colorize, consolidation_maps, + buildable_folders_by_label, default_xcode_configuration, generator_name, install_path, @@ -1364,6 +1387,7 @@ def _write_targets( actions = actions, colorize = colorize, consolidation_map = consolidation_map, + buildable_folders_by_label = buildable_folders_by_label, default_xcode_configuration = default_xcode_configuration, generator_name = generator_name, idx = consolidation_map.basename, diff --git a/xcodeproj/internal/templates/generator.BUILD.bazel b/xcodeproj/internal/templates/generator.BUILD.bazel index d6f1ba21f1..d0f84f6440 100644 --- a/xcodeproj/internal/templates/generator.BUILD.bazel +++ b/xcodeproj/internal/templates/generator.BUILD.bazel @@ -23,6 +23,7 @@ xcodeproj( ios_device_cpus = "%ios_device_cpus%", ios_simulator_cpus = "%ios_simulator_cpus%", minimum_xcode_version = "%minimum_xcode_version%", + buildable_folders = %buildable_folders%, owned_extra_files = %owned_extra_files%, post_build = """%post_build%""", pre_build = """%pre_build%""", diff --git a/xcodeproj/internal/xcodeproj_rule.bzl b/xcodeproj/internal/xcodeproj_rule.bzl index 9b389363bc..94f9b8bd58 100644 --- a/xcodeproj/internal/xcodeproj_rule.bzl +++ b/xcodeproj/internal/xcodeproj_rule.bzl @@ -313,6 +313,7 @@ def _write_project_contents( install_path, minimum_xcode_version, name, + buildable_folders, owned_extra_files, pbxnativetargets_generator, pbxproj_prefix_generator, @@ -399,6 +400,7 @@ def _write_project_contents( default_xcode_configuration = default_xcode_configuration, generator_name = name, install_path = install_path, + buildable_folders_by_label = buildable_folders, tool = pbxnativetargets_generator, xcode_target_configurations = xcode_target_configurations, xcode_targets = xcode_targets, @@ -412,6 +414,11 @@ def _write_project_contents( ) = pbxproj_partials.write_files_and_groups( actions = actions, buildfile_subidentifiers_files = buildfile_subidentifiers_files, + buildable_folders = [ + folder + for folders in buildable_folders.values() + for folder in folders + ], colorize = colorize, compile_stub_needed = compile_stub_needed, execution_root_file = execution_root_file, @@ -653,6 +660,7 @@ Are you using an `alias`? `xcodeproj.focused_targets` and \ ) ), name = name, + buildable_folders = ctx.attr.buildable_folders, owned_extra_files = ctx.attr.owned_extra_files, pbxnativetargets_generator = ( ctx.executable._pbxnativetargets_generator @@ -793,6 +801,7 @@ def _xcodeproj_attrs( "import_index_build_indexstores": attr.bool(mandatory = True), "install_path": attr.string(mandatory = True), "minimum_xcode_version": attr.string(mandatory = True), + "buildable_folders": attr.string_list_dict(), "owned_extra_files": attr.label_keyed_string_dict(allow_files = True), "post_build": attr.string(mandatory = True), "pre_build": attr.string(mandatory = True), diff --git a/xcodeproj/internal/xcodeproj_runner.bzl b/xcodeproj/internal/xcodeproj_runner.bzl index 13adcff1fa..d8b377b770 100644 --- a/xcodeproj/internal/xcodeproj_runner.bzl +++ b/xcodeproj/internal/xcodeproj_runner.bzl @@ -235,6 +235,7 @@ def _write_generator_build_file( "%ios_simulator_cpus%": attr.ios_simulator_cpus, "%minimum_xcode_version%": attr.minimum_xcode_version, "%name%": name, + "%buildable_folders%": str(attr.buildable_folders), "%owned_extra_files%": str(attr.owned_extra_files), "%post_build%": attr.post_build, "%pre_build%": attr.pre_build, @@ -518,6 +519,7 @@ xcodeproj_runner = rule( "ios_device_cpus": attr.string(mandatory = True), "ios_simulator_cpus": attr.string(), "minimum_xcode_version": attr.string(), + "buildable_folders": attr.string_list_dict(), "owned_extra_files": attr.string_dict(), "post_build": attr.string(), "pre_build": attr.string(), diff --git a/xcodeproj/xcodeproj.bzl b/xcodeproj/xcodeproj.bzl index 2743390f85..2c70b66e7b 100644 --- a/xcodeproj/xcodeproj.bzl +++ b/xcodeproj/xcodeproj.bzl @@ -31,6 +31,7 @@ def xcodeproj( "LANG": "en_US.UTF-8", "PATH": "/bin:/usr/bin", }, + buildable_folders = {}, config = "rules_xcodeproj", default_xcode_configuration = None, extra_files = [], @@ -81,7 +82,7 @@ def xcodeproj( ) ``` - Args: + Args: name: A unique name for this target. associated_extra_files: Optional. A `dict` of files to be added to the project. @@ -90,6 +91,9 @@ def xcodeproj( files should be associated with, and the value is a `list` of `File`s. These files won't be added to the project if the target is unfocused. + buildable_folders: Optional. A `dict` mapping target labels to + workspace-relative folder paths that should be represented as Xcode + buildable folders (PBXFileSystemSynchronizedRootGroup). bazel_env: Optional. A `dict` of environment variables to set when invoking `bazel_path`. @@ -466,6 +470,11 @@ configuration alphabetically ("{default}"). for f, labels in owned_extra_files.items() } + buildable_folders = { + bazel_labels.normalize_string(label): sorted(folders) + for label, folders in buildable_folders.items() + } + unowned_extra_files = [ bazel_labels.normalize_string(f) for f in extra_files @@ -552,6 +561,7 @@ for {configuration} ({new_keys}) do not match keys of other configurations \ ios_device_cpus = ios_device_cpus, ios_simulator_cpus = ios_simulator_cpus, minimum_xcode_version = minimum_xcode_version, + buildable_folders = buildable_folders, owned_extra_files = owned_extra_files, post_build = post_build, pre_build = pre_build,