From e040520623b64485638ccc2255faab35ed05220b Mon Sep 17 00:00:00 2001 From: fortmarek Date: Thu, 6 Feb 2025 13:01:45 +0100 Subject: [PATCH 1/4] fix: expand synchronized groups when mapping a PBXTarget --- Sources/XcodeGraph/Models/Target.swift | 15 +- .../Mappers/Targets/PBXTargetMapper.swift | 192 +++++++++++++++++- .../Target/PBXTargetMapperTests.swift | 134 +++++++++++- .../TestData/XcodeProj+TestData.swift | 6 +- .../XcodeGraphTests/Models/TargetTests.swift | 9 - 5 files changed, 337 insertions(+), 19 deletions(-) diff --git a/Sources/XcodeGraph/Models/Target.swift b/Sources/XcodeGraph/Models/Target.swift index bd86c180..438f5166 100644 --- a/Sources/XcodeGraph/Models/Target.swift +++ b/Sources/XcodeGraph/Models/Target.swift @@ -8,10 +8,21 @@ public struct Target: Equatable, Hashable, Comparable, Codable, Sendable { // Note: The `.docc` file type is technically both a valid source extension and folder extension // in order to compile the documentation archive (including Tutorials, Articles, etc.) public static let validSourceCompatibleFolderExtensions: [String] = [ - "playground", "rcproject", "mlpackage", "docc", "xcmappingmodel", + "playground", "rcproject", "mlpackage", "docc", "xcmappingmodel", "xcdatamodeld", ] public static let validSourceExtensions: [String] = [ - "m", "swift", "mm", "cpp", "c++", "cc", "c", "d", "s", "intentdefinition", "metal", "mlmodel", + "m", "swift", "mm", "cpp", "c++", "cc", "c", "d", "s", "intentdefinition", "metal", "mlmodel", "clp", + ] + public static let validResourceExtensions: [String] = [ + // Resource + "md", "xcstrings", "plist", "rtf", "tutorial", "sks", "xcprivacy", "gpx", "strings", "stringsdict", "geojson", + // User interface + "storyboard", "xib", + // Other + "xcfilelist", "xcconfig", + ] + public static let validResourceCompatibleFolderExtensions: [String] = [ + "xcassets", "scnassets", "bundle", "xcstickers", "app", ] public static let validFolderExtensions: [String] = [ "framework", "bundle", "app", "xcassets", "appiconset", "scnassets", diff --git a/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift b/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift index 432d0c65..e58c3988 100644 --- a/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift +++ b/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift @@ -64,6 +64,7 @@ struct PBXTargetMapper: PBXTargetMapping { private let frameworksMapper: PBXFrameworksBuildPhaseMapping private let dependencyMapper: PBXTargetDependencyMapping private let buildRuleMapper: BuildRuleMapping + private let fileSystem: FileSysteming init( settingsMapper: SettingsMapping = XCConfigurationMapper(), @@ -75,7 +76,8 @@ struct PBXTargetMapper: PBXTargetMapping { coreDataModelsMapper: PBXCoreDataModelsBuildPhaseMapping = PBXCoreDataModelsBuildPhaseMapper(), frameworksMapper: PBXFrameworksBuildPhaseMapping = PBXFrameworksBuildPhaseMapper(), dependencyMapper: PBXTargetDependencyMapping = PBXTargetDependencyMapper(), - buildRuleMapper: BuildRuleMapping = PBXBuildRuleMapper() + buildRuleMapper: BuildRuleMapping = PBXBuildRuleMapper(), + fileSystem: FileSysteming = FileSystem() ) { self.settingsMapper = settingsMapper self.sourcesMapper = sourcesMapper @@ -87,6 +89,7 @@ struct PBXTargetMapper: PBXTargetMapping { self.frameworksMapper = frameworksMapper self.dependencyMapper = dependencyMapper self.buildRuleMapper = buildRuleMapper + self.fileSystem = fileSystem } func map(pbxTarget: PBXTarget, xcodeProj: XcodeProj) async throws -> Target { @@ -102,18 +105,32 @@ struct PBXTargetMapper: PBXTargetMapping { ) // Build Phases - let sources = try pbxTarget.sourcesBuildPhase().map { + var sources = try pbxTarget.sourcesBuildPhase().map { try sourcesMapper.map($0, xcodeProj: xcodeProj) } ?? [] + sources = try await fileSystemSynchronizedGroupsSources( + from: pbxTarget, + xcodeProj: xcodeProj + ) + sources - let resources = try pbxTarget.resourcesBuildPhase().map { + var resources = try pbxTarget.resourcesBuildPhase().map { try resourcesMapper.map($0, xcodeProj: xcodeProj) } ?? [] + resources = try await fileSystemSynchronizedGroupsResources( + from: pbxTarget, + xcodeProj: xcodeProj + ) + resources - let headers = try pbxTarget.headersBuildPhase().map { + var headers = try pbxTarget.headersBuildPhase().map { try headersMapper.map($0, xcodeProj: xcodeProj) } ?? nil + headers = try await addHeadersFromFileSystemSynchronizedGroups( + from: pbxTarget, + xcodeProj: xcodeProj, + headers: headers + ) + let runScriptPhases = pbxTarget.runScriptBuildPhases() let scripts = try scriptsMapper.map(runScriptPhases, buildPhases: pbxTarget.buildPhases) let rawScriptBuildPhases = scriptsMapper.mapRawScriptBuildPhases(runScriptPhases) @@ -127,10 +144,15 @@ struct PBXTargetMapper: PBXTargetMapping { // Frameworks & libraries let frameworksPhase = try pbxTarget.frameworksBuildPhase() - let frameworks = try frameworksPhase.map { + var frameworks = try frameworksPhase.map { try frameworksMapper.map($0, xcodeProj: xcodeProj) } ?? [] + frameworks = try await fileSystemSynchronizedGroupsFrameworks( + from: pbxTarget, + xcodeProj: xcodeProj + ) + frameworks + // Additional files (not in build phases) let additionalFiles = try mapAdditionalFiles(from: pbxTarget, xcodeProj: xcodeProj) @@ -331,6 +353,166 @@ struct PBXTargetMapper: PBXTargetMapping { return .string(String(describing: value)) } } + + private func fileSystemSynchronizedGroupsSources( + from pbxTarget: PBXTarget, + xcodeProj: XcodeProj + ) async throws -> [SourceFile] { + guard let fileSystemSynchronizedGroups = pbxTarget.fileSystemSynchronizedGroups else { return [] } + var sources: [SourceFile] = [] + for fileSystemSynchronizedGroup in fileSystemSynchronizedGroups { + if let path = fileSystemSynchronizedGroup.path { + let membershipExceptions = Set( + fileSystemSynchronizedGroup.exceptions + .map { $0.compactMap(\.membershipExceptions).flatMap { $0 } } ?? [] + ) + let additionalCompilerFlagsByRelativePath = fileSystemSynchronizedGroup.exceptions? + .reduce([:]) { acc, element in + acc.merging(element.additionalCompilerFlagsByRelativePath ?? [:], uniquingKeysWith: { $1 }) + } + let directory = xcodeProj.srcPath.appending(component: path) + let groupSources = try await fileSystem.glob( + directory: directory, + include: [ + "**/*.{\(Target.validSourceExtensions.joined(separator: ","))}", + "**/*.{\(Target.validSourceCompatibleFolderExtensions.joined(separator: ","))}", + ] + ) + .collect() + .filter { + !membershipExceptions.contains($0.relative(to: directory).pathString) + } + .map { + SourceFile( + path: $0, + compilerFlags: additionalCompilerFlagsByRelativePath?[$0.relative(to: directory).pathString] + ) + } + sources.append(contentsOf: groupSources) + } + } + return sources + } + + private func fileSystemSynchronizedGroupsResources( + from pbxTarget: PBXTarget, + xcodeProj: XcodeProj + ) async throws -> [ResourceFileElement] { + let fileSystemSynchronizedGroups = pbxTarget.fileSystemSynchronizedGroups ?? [] + var resources: [ResourceFileElement] = [] + for fileSystemSynchronizedGroup in fileSystemSynchronizedGroups { + guard let path = fileSystemSynchronizedGroup.path else { continue } + let directory = xcodeProj.srcPath.appending(component: path) + let membershipExceptions = Set( + fileSystemSynchronizedGroup.exceptions + .map { $0.compactMap(\.membershipExceptions).flatMap { $0 } } ?? [] + ) + + let groupResources = try await fileSystem.glob( + directory: directory, + include: [ + "**/*.{\(Target.validResourceExtensions.joined(separator: ","))}", + "**/*.{\(Target.validResourceCompatibleFolderExtensions.joined(separator: ","))}", + ] + ) + .collect() + .filter { + !membershipExceptions.contains($0.relative(to: directory).pathString) + } + .map { + ResourceFileElement(path: $0) + } + resources.append(contentsOf: groupResources) + } + + return resources + } + + private func fileSystemSynchronizedGroupsFrameworks( + from pbxTarget: PBXTarget, + xcodeProj: XcodeProj + ) async throws -> [TargetDependency] { + let fileSystemSynchronizedGroups = pbxTarget.fileSystemSynchronizedGroups ?? [] + var frameworks: [TargetDependency] = [] + for fileSystemSynchronizedGroup in fileSystemSynchronizedGroups { + guard let path = fileSystemSynchronizedGroup.path else { continue } + let directory = xcodeProj.srcPath.appending(component: path) + let membershipExceptions = Set( + fileSystemSynchronizedGroup.exceptions + .map { $0.compactMap(\.membershipExceptions).flatMap { $0 } } ?? [] + ) + + let attributesByRelativePath = fileSystemSynchronizedGroup.exceptions?.reduce([:]) { acc, element in + acc.merging(element.attributesByRelativePath ?? [:], uniquingKeysWith: { $1 }) + } + + let groupFrameworks: [TargetDependency] = try await fileSystem.glob( + directory: directory, + include: [ + "**/*.framework", + ] + ) + .collect() + .filter { + !membershipExceptions.contains($0.relative(to: directory).pathString) + } + .map { + return .framework( + path: $0, + status: attributesByRelativePath?[$0.relative(to: directory).pathString]? + .contains("Weak") == true ? .optional : .required, + condition: nil + ) + } + frameworks.append(contentsOf: groupFrameworks) + } + + return frameworks + } + + private func addHeadersFromFileSystemSynchronizedGroups( + from pbxTarget: PBXTarget, + xcodeProj: XcodeProj, + headers: Headers? + ) async throws -> Headers? { + let fileSystemSynchronizedGroups = pbxTarget.fileSystemSynchronizedGroups ?? [] + var publicHeaders = headers?.public ?? [] + var privateHeaders = headers?.private ?? [] + var projectHeaders = headers?.project ?? [] + for fileSystemSynchronizedGroup in fileSystemSynchronizedGroups { + guard let path = fileSystemSynchronizedGroup.path else { continue } + let directory = xcodeProj.srcPath.appending(component: path) + for synchronizedBuildFileSystemExceptionSet in fileSystemSynchronizedGroup.exceptions ?? [] { + for publicHeader in synchronizedBuildFileSystemExceptionSet.publicHeaders ?? [] { + publicHeaders.append(directory.appending(component: publicHeader)) + } + for privateHeader in synchronizedBuildFileSystemExceptionSet.privateHeaders ?? [] { + privateHeaders.append(directory.appending(component: privateHeader)) + } + } + + let groupHeaders = try await fileSystem.glob( + directory: directory, + include: [ + "**/*.{h,hpp}", + ] + ) + .collect() + .filter { + !privateHeaders.contains($0) && !publicHeaders.contains($0) + } + projectHeaders.append(contentsOf: groupHeaders) + } + if !publicHeaders.isEmpty || !privateHeaders.isEmpty || !projectHeaders.isEmpty || headers != nil { + return Headers( + public: publicHeaders, + private: privateHeaders, + project: projectHeaders + ) + } else { + return headers + } + } } // swiftlint:enable function_body_length diff --git a/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift b/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift index c879b0ce..dca324f0 100644 --- a/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift +++ b/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift @@ -7,7 +7,9 @@ import XcodeProj @testable import XcodeGraphMapper @Suite -struct PBXTargetMapperTests { +struct PBXTargetMapperTests: Sendable { + private let fileSystem = FileSystem() + @Test("Maps a basic target with a product bundle identifier") func testMapBasicTarget() async throws { // Given @@ -135,6 +137,136 @@ struct PBXTargetMapperTests { #expect(mapped.sources[0].path.basename == "ViewController.swift") } + @Test("Maps a target with a buildable group") + func testMapTargetWithBuildableGroup() async throws { + try await fileSystem.runInTemporaryDirectory(prefix: "PBXTargetMapperTests") { appPath in + // Given + let xcodeProj = try await XcodeProj.test( + path: appPath.appending(component: "App.xcodeproj") + ) + let pbxProj = xcodeProj.pbxproj + let sourcesPhase = PBXSourcesBuildPhase(files: []).add(to: pbxProj) + + let target = createTarget( + name: "App", + xcodeProj: xcodeProj, + productType: .application, + buildPhases: [sourcesPhase], + buildSettings: ["PRODUCT_BUNDLE_IDENTIFIER": "com.example.app"] + ) + let exceptionSet = PBXFileSystemSynchronizedBuildFileExceptionSet( + target: target, + membershipExceptions: [ + "Ignored.cpp", + ], + publicHeaders: [ + "Public.h", + ], + privateHeaders: [ + "Private.hpp", + ], + additionalCompilerFlagsByRelativePath: [ + "File.swift": "compiler-flag", + ], + attributesByRelativePath: [ + "Optional.framework": ["Weak"], + ] + ) + let rootGroup = PBXFileSystemSynchronizedRootGroup( + path: "App", + exceptions: [ + exceptionSet, + ] + ) + target.fileSystemSynchronizedGroups = [ + rootGroup, + ] + let buildableGroupPath = appPath.appending(component: "App") + try await fileSystem.makeDirectory(at: buildableGroupPath) + + // Sources + try await fileSystem.touch(buildableGroupPath.appending(component: "File.swift")) + try await fileSystem.touch(buildableGroupPath.appending(component: "File.cpp")) + try await fileSystem.touch(buildableGroupPath.appending(component: "Ignored.cpp")) + try await fileSystem.makeDirectory(at: buildableGroupPath.appending(component: "Nested")) + try await fileSystem.touch(buildableGroupPath.appending(component: "File.c")) + try await fileSystem.makeDirectory(at: buildableGroupPath.appending(component: "App.docc")) + + // Resources + try await fileSystem.makeDirectory(at: buildableGroupPath.appending(component: "Location.geojson")) + try await fileSystem.makeDirectory(at: buildableGroupPath.appending(component: "App.xcassets")) + + // Headers + try await fileSystem.touch(buildableGroupPath.appending(component: "Public.h")) + try await fileSystem.touch(buildableGroupPath.appending(component: "Project.h")) + try await fileSystem.touch(buildableGroupPath.appending(component: "Private.hpp")) + + // Frameworks + try await fileSystem.makeDirectory(at: buildableGroupPath.appending(component: "Framework.framework")) + try await fileSystem.makeDirectory(at: buildableGroupPath.appending(component: "Optional.framework")) + + let mapper = PBXTargetMapper( + fileSystem: fileSystem + ) + + // When + let mapped = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj) + + // Then + #expect( + mapped.sources.sorted(by: { $0.path < $1.path }).map(\.compilerFlags) == [ + nil, + nil, + nil, + "compiler-flag", + ] + ) + #expect( + mapped.sources.map(\.path.basename).sorted() == [ + "App.docc", + "File.c", + "File.cpp", + "File.swift", + ] + ) + #expect( + mapped.resources.resources.map(\.path.basename).sorted() == [ + "App.xcassets", + "Location.geojson", + ] + ) + #expect( + mapped.headers?.private.map(\.basename) == [ + "Private.hpp", + ] + ) + #expect( + mapped.headers?.public.map(\.basename) == [ + "Public.h", + ] + ) + #expect( + mapped.headers?.project.map(\.basename) == [ + "Project.h", + ] + ) + #expect( + mapped.dependencies == [ + .framework( + path: buildableGroupPath.appending(component: "Framework.framework"), + status: .required, + condition: nil + ), + .framework( + path: buildableGroupPath.appending(component: "Optional.framework"), + status: .optional, + condition: nil + ), + ] + ) + } + } + @Test("Maps a target with metadata tags") func testMapTargetWithMetadata() async throws { // Given diff --git a/Tests/XcodeGraphMapperTests/TestData/XcodeProj+TestData.swift b/Tests/XcodeGraphMapperTests/TestData/XcodeProj+TestData.swift index ff173781..b7dd538d 100644 --- a/Tests/XcodeGraphMapperTests/TestData/XcodeProj+TestData.swift +++ b/Tests/XcodeGraphMapperTests/TestData/XcodeProj+TestData.swift @@ -1,6 +1,7 @@ import FileSystem import Foundation import Path +import PathKit import XcodeGraph import XcodeProj @@ -11,7 +12,8 @@ extension XcodeProj { buildConfigurations: [.testDebug(), .testRelease()] ), targets: [PBXTarget] = [], - pbxProj: PBXProj = PBXProj() + pbxProj: PBXProj = PBXProj(), + path: AbsolutePath? = nil ) async throws -> XcodeProj { pbxProj.add(object: configurationList) for config in configurationList.buildConfigurations { @@ -52,7 +54,7 @@ extension XcodeProj { return XcodeProj( workspace: XCWorkspace(), pbxproj: pbxProj, - path: .init("\(sourceDirectory)/\(projectName).xcodeproj") + path: path.map(\.pathString).map { Path($0) } ?? Path("\(sourceDirectory)/\(projectName).xcodeproj") ) } } diff --git a/Tests/XcodeGraphTests/Models/TargetTests.swift b/Tests/XcodeGraphTests/Models/TargetTests.swift index ebbe0927..84ae0f67 100644 --- a/Tests/XcodeGraphTests/Models/TargetTests.swift +++ b/Tests/XcodeGraphTests/Models/TargetTests.swift @@ -12,15 +12,6 @@ final class TargetTests: XCTestCase { XCTAssertCodable(subject) } - func test_validSourceExtensions() { - XCTAssertEqual( - Target.validSourceExtensions, - [ - "m", "swift", "mm", "cpp", "c++", "cc", "c", "d", "s", "intentdefinition", "metal", "mlmodel", - ] - ) - } - func test_sequence_testBundles() { let app = Target.test(product: .app) let tests = Target.test(product: .unitTests) From 212806937f547884df2186e73ae1f6e294d6b5d7 Mon Sep 17 00:00:00 2001 From: fortmarek Date: Fri, 7 Feb 2025 09:08:54 +0100 Subject: [PATCH 2/4] Refactor PBXTargetMapper --- .../Mappers/Targets/PBXTargetMapper.swift | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift b/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift index e58c3988..312af505 100644 --- a/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift +++ b/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift @@ -367,21 +367,20 @@ struct PBXTargetMapper: PBXTargetMapping { .map { $0.compactMap(\.membershipExceptions).flatMap { $0 } } ?? [] ) let additionalCompilerFlagsByRelativePath = fileSystemSynchronizedGroup.exceptions? - .reduce([:]) { acc, element in - acc.merging(element.additionalCompilerFlagsByRelativePath ?? [:], uniquingKeysWith: { $1 }) + .reduce(into: [:]) { acc, element in + acc.merge(element.additionalCompilerFlagsByRelativePath ?? [:], uniquingKeysWith: { $1 }) } let directory = xcodeProj.srcPath.appending(component: path) - let groupSources = try await fileSystem.glob( + let groupSources = try await globFiles( directory: directory, include: [ + // Build glob patterns for source files and source-compatible folders. + // This creates patterns like "**/*.{m,swift,mm,...}". "**/*.{\(Target.validSourceExtensions.joined(separator: ","))}", "**/*.{\(Target.validSourceCompatibleFolderExtensions.joined(separator: ","))}", - ] + ], + membershipExceptions: membershipExceptions ) - .collect() - .filter { - !membershipExceptions.contains($0.relative(to: directory).pathString) - } .map { SourceFile( path: $0, @@ -408,17 +407,16 @@ struct PBXTargetMapper: PBXTargetMapping { .map { $0.compactMap(\.membershipExceptions).flatMap { $0 } } ?? [] ) - let groupResources = try await fileSystem.glob( + let groupResources = try await globFiles( directory: directory, include: [ + // Build glob patterns for resource files and resource-compatible folders. + // This creates patterns like "**/*.{xcassets,png,...}". "**/*.{\(Target.validResourceExtensions.joined(separator: ","))}", "**/*.{\(Target.validResourceCompatibleFolderExtensions.joined(separator: ","))}", - ] + ], + membershipExceptions: membershipExceptions ) - .collect() - .filter { - !membershipExceptions.contains($0.relative(to: directory).pathString) - } .map { ResourceFileElement(path: $0) } @@ -446,16 +444,13 @@ struct PBXTargetMapper: PBXTargetMapping { acc.merging(element.attributesByRelativePath ?? [:], uniquingKeysWith: { $1 }) } - let groupFrameworks: [TargetDependency] = try await fileSystem.glob( + let groupFrameworks: [TargetDependency] = try await globFiles( directory: directory, include: [ "**/*.framework", - ] + ], + membershipExceptions: membershipExceptions ) - .collect() - .filter { - !membershipExceptions.contains($0.relative(to: directory).pathString) - } .map { return .framework( path: $0, @@ -491,6 +486,9 @@ struct PBXTargetMapper: PBXTargetMapping { } } + let publicHeadersSet = Set(publicHeaders) + let privateHeadersSet = Set(privateHeaders) + let groupHeaders = try await fileSystem.glob( directory: directory, include: [ @@ -499,7 +497,7 @@ struct PBXTargetMapper: PBXTargetMapping { ) .collect() .filter { - !privateHeaders.contains($0) && !publicHeaders.contains($0) + !privateHeadersSet.contains($0) && !publicHeadersSet.contains($0) } projectHeaders.append(contentsOf: groupHeaders) } @@ -513,6 +511,21 @@ struct PBXTargetMapper: PBXTargetMapping { return headers } } + + /// Performs a glob search in the given directory using specified patterns, filtering out paths + /// that appear in the membershipExceptions set. + private func globFiles( + directory: AbsolutePath, + include: [String], + membershipExceptions: Set = [] + ) async throws -> [AbsolutePath] { + return try await fileSystem.glob( + directory: directory, + include: include + ) + .collect() + .filter { !membershipExceptions.contains($0.relative(to: directory).pathString) } + } } // swiftlint:enable function_body_length From 9c5526e03171a1d477409327ae97db9ff09298ce Mon Sep 17 00:00:00 2001 From: fortmarek Date: Fri, 7 Feb 2025 17:06:04 +0100 Subject: [PATCH 3/4] Filter out project headers from exception sets --- .../Mappers/Targets/PBXTargetMapper.swift | 27 ++++++++++--------- .../Target/PBXTargetMapperTests.swift | 2 ++ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift b/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift index 312af505..6ba2fbbb 100644 --- a/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift +++ b/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift @@ -362,10 +362,7 @@ struct PBXTargetMapper: PBXTargetMapping { var sources: [SourceFile] = [] for fileSystemSynchronizedGroup in fileSystemSynchronizedGroups { if let path = fileSystemSynchronizedGroup.path { - let membershipExceptions = Set( - fileSystemSynchronizedGroup.exceptions - .map { $0.compactMap(\.membershipExceptions).flatMap { $0 } } ?? [] - ) + let membershipExceptions = membershipExceptions(for: fileSystemSynchronizedGroup) let additionalCompilerFlagsByRelativePath = fileSystemSynchronizedGroup.exceptions? .reduce(into: [:]) { acc, element in acc.merge(element.additionalCompilerFlagsByRelativePath ?? [:], uniquingKeysWith: { $1 }) @@ -435,11 +432,7 @@ struct PBXTargetMapper: PBXTargetMapping { for fileSystemSynchronizedGroup in fileSystemSynchronizedGroups { guard let path = fileSystemSynchronizedGroup.path else { continue } let directory = xcodeProj.srcPath.appending(component: path) - let membershipExceptions = Set( - fileSystemSynchronizedGroup.exceptions - .map { $0.compactMap(\.membershipExceptions).flatMap { $0 } } ?? [] - ) - + let membershipExceptions = membershipExceptions(for: fileSystemSynchronizedGroup) let attributesByRelativePath = fileSystemSynchronizedGroup.exceptions?.reduce([:]) { acc, element in acc.merging(element.attributesByRelativePath ?? [:], uniquingKeysWith: { $1 }) } @@ -485,17 +478,18 @@ struct PBXTargetMapper: PBXTargetMapping { privateHeaders.append(directory.appending(component: privateHeader)) } } + let membershipExceptions = membershipExceptions(for: fileSystemSynchronizedGroup) let publicHeadersSet = Set(publicHeaders) let privateHeadersSet = Set(privateHeaders) - let groupHeaders = try await fileSystem.glob( + let groupHeaders = try await globFiles( directory: directory, include: [ "**/*.{h,hpp}", - ] + ], + membershipExceptions: membershipExceptions ) - .collect() .filter { !privateHeadersSet.contains($0) && !publicHeadersSet.contains($0) } @@ -512,12 +506,19 @@ struct PBXTargetMapper: PBXTargetMapping { } } + private func membershipExceptions(for fileSystemSynchronizedGroup: PBXFileSystemSynchronizedRootGroup) -> Set { + Set( + fileSystemSynchronizedGroup.exceptions + .map { $0.compactMap(\.membershipExceptions).flatMap { $0 } } ?? [] + ) + } + /// Performs a glob search in the given directory using specified patterns, filtering out paths /// that appear in the membershipExceptions set. private func globFiles( directory: AbsolutePath, include: [String], - membershipExceptions: Set = [] + membershipExceptions: Set ) async throws -> [AbsolutePath] { return try await fileSystem.glob( directory: directory, diff --git a/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift b/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift index dca324f0..e8ab4d73 100644 --- a/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift +++ b/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift @@ -158,6 +158,7 @@ struct PBXTargetMapperTests: Sendable { target: target, membershipExceptions: [ "Ignored.cpp", + "Ignored.h", ], publicHeaders: [ "Public.h", @@ -199,6 +200,7 @@ struct PBXTargetMapperTests: Sendable { // Headers try await fileSystem.touch(buildableGroupPath.appending(component: "Public.h")) try await fileSystem.touch(buildableGroupPath.appending(component: "Project.h")) + try await fileSystem.touch(buildableGroupPath.appending(component: "Ignored.h")) try await fileSystem.touch(buildableGroupPath.appending(component: "Private.hpp")) // Frameworks From a291208271df78fce8d2032a298cd7a60713429a Mon Sep 17 00:00:00 2001 From: fortmarek Date: Fri, 7 Feb 2025 17:08:55 +0100 Subject: [PATCH 4/4] Fix lint issue --- Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift b/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift index 6ba2fbbb..692df994 100644 --- a/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift +++ b/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift @@ -49,6 +49,7 @@ protocol PBXTargetMapping { } // swiftlint:disable function_body_length +// swiftlint:disable type_body_length /// A mapper that converts a `PBXTarget` into a domain `Target` model. /// /// `PBXTargetMapper` orchestrates various specialized mappers (e.g., sources, resources, headers) @@ -530,3 +531,4 @@ struct PBXTargetMapper: PBXTargetMapping { } // swiftlint:enable function_body_length +// swiftlint:enable type_body_length