diff --git a/Sources/XcodeGraphMapper/Mappers/Graph/XcodeGraphMapper.swift b/Sources/XcodeGraphMapper/Mappers/Graph/XcodeGraphMapper.swift index 46122f82..380f0028 100644 --- a/Sources/XcodeGraphMapper/Mappers/Graph/XcodeGraphMapper.swift +++ b/Sources/XcodeGraphMapper/Mappers/Graph/XcodeGraphMapper.swift @@ -320,7 +320,10 @@ public struct XcodeGraphMapper: XcodeGraphMapping { paths.append(refPath) } case let .group(group): - let nestedPaths = try await extractProjectPaths(from: group.children, srcPath: srcPath) + // Set a new src root to account for projects in nested directories + let recursiveRoot = srcPath.appending(component: group.location.path) + + let nestedPaths = try await extractProjectPaths(from: group.children, srcPath: recursiveRoot) paths.append(contentsOf: nestedPaths) } } diff --git a/Tests/XcodeGraphMapperTests/MapperTests/Graph/XcodeGraphMapperTests.swift b/Tests/XcodeGraphMapperTests/MapperTests/Graph/XcodeGraphMapperTests.swift index 1d356f7a..bd696ed0 100644 --- a/Tests/XcodeGraphMapperTests/MapperTests/Graph/XcodeGraphMapperTests.swift +++ b/Tests/XcodeGraphMapperTests/MapperTests/Graph/XcodeGraphMapperTests.swift @@ -202,6 +202,81 @@ struct XcodeGraphMapperTests { #expect(graph.dependencyConditions.isEmpty == true) } + @Test("Maps a workspace with multiple projects in different directories into a single graph") + func testWorkspaceGraphMultipleProjectsInDifferentDirectories() async throws { + // Given + // + // A project structure like this: + // . + // ├── Workspace.xcworkspace + // ├── App + // │ └── ProjectA.xcodeproj + // └── Modules + // └── ProjectB.xcodeproj + // + + let pbxProjA = PBXProj() + let pbxProjB = PBXProj() + + let debug: XCBuildConfiguration = .testDebug().add(to: pbxProjA).add(to: pbxProjB) + let releaseConfig: XCBuildConfiguration = .testRelease().add(to: pbxProjA).add(to: pbxProjB) + let configurationList: XCConfigurationList = .test( + buildConfigurations: [debug, releaseConfig] + ) + .add(to: pbxProjA) + .add(to: pbxProjB) + + let projectA = try await XcodeProj.test( + projectName: "ProjectA", + configurationList: configurationList, + pbxProj: pbxProjA, + path: "/tmp/App/ProjectA.xcodeproj" + ) + + let projectB = try await XcodeProj.test( + projectName: "ProjectB", + configurationList: configurationList, + pbxProj: pbxProjB, + path: "/tmp/Modules/ProjectB/ProjectB.xcodeproj" + ) + + let projectAPath = try #require(projectA.path?.string) + let projectBPath = try #require(projectB.path?.string) + + let xcworkspace = XCWorkspace( + data: XCWorkspaceData( + children: [ + .group(.init(location: .group("App"), name: "App", children: [ + .file(.init(location: .absolute(projectAPath))), + ])), + .group(.init(location: .group("Modules"), name: "Modules", children: [ + .file(.init(location: .absolute(projectBPath))), + ])), + ] + ), + path: .init("/tmp/Workspace.xcworkspace") + ) + + try projectA.write(path: projectA.path!) + try projectB.write(path: projectB.path!) + let mapper = XcodeGraphMapper() + + // When + let graph = try await mapper.buildGraph(from: .workspace(xcworkspace)) + + // Then + + // General workspace checks + #expect(graph.workspace.name == "Workspace") + #expect(graph.workspace.projects.contains(projectA.projectPath) == true) + #expect(graph.workspace.projects.contains(projectB.projectPath) == true) + #expect(graph.projects.count == 2) + + // Nested paths are correct + #expect(graph.projects["/tmp/App"] != nil) + #expect(graph.projects["/tmp/Modules/ProjectB"] != nil) + } + @Test("Maps a project graph with dependencies between targets") func testGraphWithDependencies() async throws { // Given