diff --git a/Package.resolved b/Package.resolved index 9b9bd94d..633726a1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "252c1ee702d5bd9c4155485fe13cdca178e46fc497b3f48683ff138bb43d7492", + "originHash" : "377d0890e78ed330d863806cbf4cdc28ce4dc14faeb68c692140ca84153f49ff", "pins" : [ { "identity" : "aexml", @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tuist/Command.git", "state" : { - "revision" : "9d03a95faa94b961edc1cf2c5f4379b0108ee97a", - "version" : "0.12.1" + "revision" : "07846291097a593de29846c0083b758471071cdf", + "version" : "0.12.2" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tuist/FileSystem.git", "state" : { - "revision" : "267329f17e523575162f0887d20f2b78f609585a", - "version" : "0.7.6" + "revision" : "1bff6d54e90a79706a4a9d77cd081104de430361", + "version" : "0.7.7" } }, { @@ -132,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio", "state" : { - "revision" : "dff45738d84a53dbc8ee899c306b3a7227f54f89", - "version" : "2.80.0" + "revision" : "c51907a839e63ebf0ba2076bba73dd96436bd1b9", + "version" : "2.81.0" } }, { @@ -168,8 +168,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tuist/XcodeProj", "state" : { - "revision" : "6f90427e172da66336739801c84b9cef3e17367b", - "version" : "8.26.6" + "revision" : "142b7ea0a087eabf4d12207302a185a0e9d4659b", + "version" : "8.27.0" } }, { diff --git a/Package.swift b/Package.swift index 1d5b404b..225a81c9 100644 --- a/Package.swift +++ b/Package.swift @@ -80,7 +80,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/Flight-School/AnyCodable", .upToNextMajor(from: "0.6.7")), .package(url: "https://github.com/tuist/Path.git", .upToNextMajor(from: "0.3.8")), - .package(url: "https://github.com/tuist/XcodeProj", from: "8.26.7"), + .package(url: "https://github.com/tuist/XcodeProj", .upToNextMajor(from: "8.27.0")), .package(url: "https://github.com/tuist/Command.git", from: "0.12.2"), .package(url: "https://github.com/tuist/FileSystem.git", .upToNextMajor(from: "0.7.7")), .package(url: "https://github.com/apple/swift-service-context", .upToNextMajor(from: "1.2.0")), diff --git a/Sources/XcodeGraphMapper/Mappers/Phases/PBXCopyFilesBuildPhaseMapper.swift b/Sources/XcodeGraphMapper/Mappers/Phases/PBXCopyFilesBuildPhaseMapper.swift index 1c01c216..c5e281a7 100644 --- a/Sources/XcodeGraphMapper/Mappers/Phases/PBXCopyFilesBuildPhaseMapper.swift +++ b/Sources/XcodeGraphMapper/Mappers/Phases/PBXCopyFilesBuildPhaseMapper.swift @@ -11,15 +11,29 @@ protocol PBXCopyFilesBuildPhaseMapping { /// - xcodeProj: The `XcodeProj` containing project configuration and file references. /// - Returns: An array of mapped `CopyFilesAction`s. /// - Throws: If any file paths are invalid or cannot be resolved. - func map(_ copyFilesPhases: [PBXCopyFilesBuildPhase], xcodeProj: XcodeProj) throws -> [CopyFilesAction] + func map( + _ copyFilesPhases: [PBXCopyFilesBuildPhase], + fileSystemSynchronizedGroups: [PBXFileSystemSynchronizedRootGroup], + xcodeProj: XcodeProj + ) throws -> [CopyFilesAction] } /// A mapper that converts `PBXCopyFilesBuildPhase` objects into `CopyFilesAction` domain models. struct PBXCopyFilesBuildPhaseMapper: PBXCopyFilesBuildPhaseMapping { /// Maps the provided copy files phases to sorted `CopyFilesAction` models. - func map(_ copyFilesPhases: [PBXCopyFilesBuildPhase], xcodeProj: XcodeProj) throws -> [CopyFilesAction] { + func map( + _ copyFilesPhases: [PBXCopyFilesBuildPhase], + fileSystemSynchronizedGroups: [PBXFileSystemSynchronizedRootGroup], + xcodeProj: XcodeProj + ) throws -> [CopyFilesAction] { try copyFilesPhases - .compactMap { try mapCopyFilesPhase($0, xcodeProj: xcodeProj) } + .compactMap { + try mapCopyFilesPhase( + $0, + fileSystemSynchronizedGroups: fileSystemSynchronizedGroups, + xcodeProj: xcodeProj + ) + } .sorted { $0.name < $1.name } } @@ -33,6 +47,7 @@ struct PBXCopyFilesBuildPhaseMapper: PBXCopyFilesBuildPhaseMapping { /// - Throws: If file paths are invalid or unresolved. private func mapCopyFilesPhase( _ phase: PBXCopyFilesBuildPhase, + fileSystemSynchronizedGroups: [PBXFileSystemSynchronizedRootGroup], xcodeProj: XcodeProj ) throws -> CopyFilesAction? { let files = try (phase.files ?? []) @@ -50,15 +65,47 @@ struct PBXCopyFilesBuildPhaseMapper: PBXCopyFilesBuildPhaseMapping { return .file(path: absolutePath, condition: nil, codeSignOnCopy: codeSignOnCopy) } .sorted { $0.path < $1.path } + let groupsFiles = fileSystemSynchronizedGroupsFiles( + phase, + fileSystemSynchronizedGroups: fileSystemSynchronizedGroups, + xcodeProj: xcodeProj + ) return CopyFilesAction( name: phase.name ?? BuildPhaseConstants.copyFilesDefault, destination: mapDstSubfolderSpec(phase.dstSubfolderSpec), subpath: (phase.dstPath?.isEmpty == true) ? nil : phase.dstPath, - files: files + files: files + groupsFiles ) } + private func fileSystemSynchronizedGroupsFiles( + _ phase: PBXCopyFilesBuildPhase, + fileSystemSynchronizedGroups: [PBXFileSystemSynchronizedRootGroup], + xcodeProj: XcodeProj + ) -> [CopyFileElement] { + var files: [CopyFileElement] = [] + for fileSystemSynchronizedGroup in fileSystemSynchronizedGroups { + if let path = fileSystemSynchronizedGroup.path { + let buildPhaseExceptions = fileSystemSynchronizedGroup.exceptions? + .compactMap { $0 as? PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet } + .filter { $0.buildPhase == phase } ?? [] + let groupFiles = buildPhaseExceptions.compactMap { + $0.membershipExceptions?.map { + return CopyFileElement.file( + path: xcodeProj.srcPath.appending(component: path).appending(RelativePath($0)), + condition: nil, + codeSignOnCopy: true + ) + } + } + .flatMap { $0 } + files.append(contentsOf: groupFiles) + } + } + return files + } + /// Maps a `PBXCopyFilesBuildPhase.SubFolder` to a `CopyFilesAction.Destination`. private func mapDstSubfolderSpec( _ subfolderSpec: PBXCopyFilesBuildPhase.SubFolder? diff --git a/Sources/XcodeGraphMapper/Mappers/Project/PBXProjectMapper.swift b/Sources/XcodeGraphMapper/Mappers/Project/PBXProjectMapper.swift index 820c8913..42ad8b6f 100644 --- a/Sources/XcodeGraphMapper/Mappers/Project/PBXProjectMapper.swift +++ b/Sources/XcodeGraphMapper/Mappers/Project/PBXProjectMapper.swift @@ -175,7 +175,7 @@ struct PBXProjectMapper: PBXProjectMapping { let pathString = try file.fullPath(sourceRoot: xcodeProj.srcPathString) { let path = try AbsolutePath(validating: pathString) - if try await fileSystem.exists(path, isDirectory: true), + if (try? await fileSystem.exists(path, isDirectory: true)) ?? false, try await fileSystem.exists(path.appending(component: "Package.swift")) { packages.insert(path) diff --git a/Sources/XcodeGraphMapper/Mappers/Targets/PBXTarget+GraphMapping.swift b/Sources/XcodeGraphMapper/Mappers/Targets/PBXTarget+GraphMapping.swift index 98d22726..2de70f79 100644 --- a/Sources/XcodeGraphMapper/Mappers/Targets/PBXTarget+GraphMapping.swift +++ b/Sources/XcodeGraphMapper/Mappers/Targets/PBXTarget+GraphMapping.swift @@ -8,8 +8,9 @@ extension PBXTarget { func bundleIdentifier() throws -> String { if let bundleId = debugBuildSettings.string(for: .productBundleIdentifier) { return bundleId + } else { + return "Unknown" } - throw PBXTargetMappingError.missingBundleIdentifier(targetName: name) } /// Returns an array of all `PBXCopyFilesBuildPhase` instances for this target. diff --git a/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift b/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift index bb5ce10c..5b1d7412 100644 --- a/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift +++ b/Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift @@ -10,7 +10,6 @@ enum PBXTargetMappingError: LocalizedError, Equatable { case noProjectsFound(path: String) case missingFilesGroup(targetName: String) case invalidPlist(path: String) - case missingBundleIdentifier(targetName: String) var errorDescription: String? { switch self { @@ -20,8 +19,6 @@ enum PBXTargetMappingError: LocalizedError, Equatable { return "The files group is missing for the target '\(targetName)'." case let .invalidPlist(path): return "Failed to read a valid plist dictionary from file at: \(path)." - case let .missingBundleIdentifier(targetName): - return "The bundle identifier is missing for the target '\(targetName)'." } } } @@ -150,7 +147,11 @@ struct PBXTargetMapper: PBXTargetMapping { let rawScriptBuildPhases = scriptsMapper.mapRawScriptBuildPhases(runScriptPhases) let copyFilesPhases = pbxTarget.copyFilesBuildPhases() - let copyFiles = try copyFilesMapper.map(copyFilesPhases, xcodeProj: xcodeProj) + let copyFiles = try copyFilesMapper.map( + copyFilesPhases, + fileSystemSynchronizedGroups: pbxTarget.fileSystemSynchronizedGroups ?? [], + xcodeProj: xcodeProj + ) // Core Data models let resourceFiles = try pbxTarget.resourcesBuildPhase()?.files ?? [] @@ -390,8 +391,9 @@ struct PBXTargetMapper: PBXTargetMapping { for fileSystemSynchronizedGroup in fileSystemSynchronizedGroups { if let path = fileSystemSynchronizedGroup.path { let membershipExceptions = membershipExceptions(for: fileSystemSynchronizedGroup) - let additionalCompilerFlagsByRelativePath = fileSystemSynchronizedGroup.exceptions? + let additionalCompilerFlagsByRelativePath: [String: String]? = fileSystemSynchronizedGroup.exceptions? .reduce(into: [:]) { acc, element in + guard let element = element as? PBXFileSystemSynchronizedBuildFileExceptionSet else { return } acc.merge(element.additionalCompilerFlagsByRelativePath ?? [:], uniquingKeysWith: { $1 }) } let directory = xcodeProj.srcPath.appending(component: path) @@ -429,10 +431,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 groupResources = try await globFiles( directory: directory, @@ -463,9 +462,10 @@ struct PBXTargetMapper: PBXTargetMapping { guard let path = fileSystemSynchronizedGroup.path else { continue } let directory = xcodeProj.srcPath.appending(component: path) let membershipExceptions = membershipExceptions(for: fileSystemSynchronizedGroup) - let attributesByRelativePath = fileSystemSynchronizedGroup.exceptions?.reduce([:]) { acc, element in - acc.merging(element.attributesByRelativePath ?? [:], uniquingKeysWith: { $1 }) - } + let attributesByRelativePath = fileSystemSynchronizedGroup.exceptions? + .compactMap { $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet }.reduce([:]) { acc, element in + acc.merging(element.attributesByRelativePath ?? [:], uniquingKeysWith: { $1 }) + } let groupFrameworks: [TargetDependency] = try await globFiles( directory: directory, @@ -500,7 +500,9 @@ struct PBXTargetMapper: PBXTargetMapping { for fileSystemSynchronizedGroup in fileSystemSynchronizedGroups { guard let path = fileSystemSynchronizedGroup.path else { continue } let directory = xcodeProj.srcPath.appending(component: path) - for synchronizedBuildFileSystemExceptionSet in fileSystemSynchronizedGroup.exceptions ?? [] { + for synchronizedBuildFileSystemExceptionSet in fileSystemSynchronizedGroup.exceptions? + .compactMap({ $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet }) ?? [] + { for publicHeader in synchronizedBuildFileSystemExceptionSet.publicHeaders ?? [] { publicHeaders.append(directory.appending(component: publicHeader)) } @@ -539,6 +541,7 @@ struct PBXTargetMapper: PBXTargetMapping { private func membershipExceptions(for fileSystemSynchronizedGroup: PBXFileSystemSynchronizedRootGroup) -> Set { Set( fileSystemSynchronizedGroup.exceptions + .map { $0.compactMap { $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet } } .map { $0.compactMap(\.membershipExceptions).flatMap { $0 } } ?? [] ) } diff --git a/Tests/XcodeGraphMapperTests/MapperTests/Phases/PBXCopyFilesBuildPhaseMapperTests.swift b/Tests/XcodeGraphMapperTests/MapperTests/Phases/PBXCopyFilesBuildPhaseMapperTests.swift index 1149c452..2a761848 100644 --- a/Tests/XcodeGraphMapperTests/MapperTests/Phases/PBXCopyFilesBuildPhaseMapperTests.swift +++ b/Tests/XcodeGraphMapperTests/MapperTests/Phases/PBXCopyFilesBuildPhaseMapperTests.swift @@ -43,7 +43,11 @@ struct PBXCopyFilesBuildPhaseMapperTests { let mapper = PBXCopyFilesBuildPhaseMapper() // When - let copyActions = try mapper.map([copyFilesPhase], xcodeProj: xcodeProj) + let copyActions = try mapper.map( + [copyFilesPhase], + fileSystemSynchronizedGroups: [], + xcodeProj: xcodeProj + ) // Then #expect(copyActions.count == 1) @@ -58,4 +62,70 @@ struct PBXCopyFilesBuildPhaseMapperTests { #expect(fileAction.codeSignOnCopy == true) #expect(fileAction.path.basename == "MyLibrary.dylib") } + + @Test("Maps copy files actions with a synchronized group") + func testMapCopyFilesWithSynchronizedGroup() async throws { + // Given + let xcodeProj = try await XcodeProj.test( + path: "/tmp/TestProject/Project.xcodeproj" + ) + let pbxProj = xcodeProj.pbxproj + + let copyFilesPhase = PBXCopyFilesBuildPhase( + dstPath: "XPC Services", + dstSubfolderSpec: .productsDirectory, + name: "Copy files", + files: [] + ) + .add(to: pbxProj) + + try PBXNativeTarget.test( + name: "App", + buildPhases: [copyFilesPhase], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let mapper = PBXCopyFilesBuildPhaseMapper() + let exceptionSet = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet( + buildPhase: copyFilesPhase, + membershipExceptions: [ + "XCPService.xpc", + ], + attributesByRelativePath: nil + ) + let rootGroup = PBXFileSystemSynchronizedRootGroup( + path: "SynchronizedRootGroup", + exceptions: [ + exceptionSet, + ] + ) + + // When + let copyActions = try mapper.map( + [copyFilesPhase], + fileSystemSynchronizedGroups: [ + rootGroup, + ], + xcodeProj: xcodeProj + ) + + // Then + #expect(copyActions.count == 1) + + let action = try #require(copyActions.first) + #expect(action.name == "Copy files") + #expect(action.destination == .productsDirectory) + #expect(action.subpath == "XPC Services") + #expect( + action.files == [ + .file( + path: "/tmp/TestProject/SynchronizedRootGroup/XCPService.xpc", + condition: nil, + codeSignOnCopy: true + ), + ] + ) + } } diff --git a/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift b/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift index c63962c6..e18de05e 100644 --- a/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift +++ b/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift @@ -40,7 +40,7 @@ struct PBXTargetMapperTests: Sendable { #expect(mapped.bundleId == "com.example.app") } - @Test("Throws an error if the target is missing a bundle identifier") + @Test("Defaults to unknown if the target is missing a bundle identifier") func testMapTargetWithMissingBundleId() async throws { // Given let xcodeProj = try await XcodeProj.test() @@ -53,15 +53,16 @@ struct PBXTargetMapperTests: Sendable { ) let mapper = PBXTargetMapper() - // When / Then - await #expect(throws: PBXTargetMappingError.missingBundleIdentifier(targetName: "App")) { - _ = try await mapper.map( - pbxTarget: target, - xcodeProj: xcodeProj, - projectNativeTargets: [:], - packages: [] - ) - } + // When + let mapped = try await mapper.map( + pbxTarget: target, + xcodeProj: xcodeProj, + projectNativeTargets: [:], + packages: [] + ) + + // Then + #expect(mapped.bundleId == "Unknown") } @Test("Maps a target with environment variables")