diff --git a/Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift b/Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift index 0fa51e1cf..35d678342 100644 --- a/Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift +++ b/Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift @@ -14,6 +14,40 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase { case sharedSupport = 12 case plugins = 13 case other + + /// Human-readable string representation used in Xcode 16+ + public var stringValue: String { + switch self { + case .absolutePath: return "AbsolutePath" + case .productsDirectory: return "ProductsDirectory" + case .wrapper: return "Wrapper" + case .executables: return "Executables" + case .resources: return "Resources" + case .javaResources: return "JavaResources" + case .frameworks: return "Frameworks" + case .sharedFrameworks: return "SharedFrameworks" + case .sharedSupport: return "SharedSupport" + case .plugins: return "Plugins" + case .other: return "Other" + } + } + + /// Initialize from string value (Xcode 16+ format) + public init?(string: String) { + switch string { + case "AbsolutePath": self = .absolutePath + case "ProductsDirectory": self = .productsDirectory + case "Wrapper": self = .wrapper + case "Executables": self = .executables + case "Resources": self = .resources + case "JavaResources": self = .javaResources + case "Frameworks": self = .frameworks + case "SharedFrameworks": self = .sharedFrameworks + case "SharedSupport": self = .sharedSupport + case "Plugins": self = .plugins + default: return nil + } + } } // MARK: - Attributes @@ -21,8 +55,15 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase { /// Element destination path public var dstPath: String? + /// Element destination subfolder (Xcode 16+ format, human-readable string) + public var dstSubfolder: SubFolder? + /// Element destination subfolder spec - public var dstSubfolderSpec: SubFolder? + @available(*, deprecated, renamed: "dstSubfolder") + public var dstSubfolderSpec: SubFolder? { + get { dstSubfolder } + set { dstSubfolder = newValue } + } /// Copy files build phase name public var name: String? @@ -37,18 +78,18 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase { /// /// - Parameters: /// - dstPath: Destination path. - /// - dstSubfolderSpec: Destination subfolder spec. + /// - dstSubfolder: Destination subfolder. /// - buildActionMask: Build action mask. /// - files: Build files to copy. /// - runOnlyForDeploymentPostprocessing: Run only for deployment post processing. public init(dstPath: String? = nil, - dstSubfolderSpec: SubFolder? = nil, + dstSubfolder: SubFolder? = nil, name: String? = nil, buildActionMask: UInt = defaultBuildActionMask, files: [PBXBuildFile] = [], runOnlyForDeploymentPostprocessing: Bool = false) { self.dstPath = dstPath - self.dstSubfolderSpec = dstSubfolderSpec + self.dstSubfolder = dstSubfolder self.name = name super.init(files: files, buildActionMask: buildActionMask, @@ -56,10 +97,27 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase { runOnlyForDeploymentPostprocessing) } + /// Initializes the copy files build phase with its attributes (deprecated parameter name). + @available(*, deprecated, renamed: "init(dstPath:dstSubfolder:name:buildActionMask:files:runOnlyForDeploymentPostprocessing:)") + public convenience init(dstPath: String? = nil, + dstSubfolderSpec: SubFolder?, + name: String? = nil, + buildActionMask: UInt = defaultBuildActionMask, + files: [PBXBuildFile] = [], + runOnlyForDeploymentPostprocessing: Bool = false) { + self.init(dstPath: dstPath, + dstSubfolder: dstSubfolderSpec, + name: name, + buildActionMask: buildActionMask, + files: files, + runOnlyForDeploymentPostprocessing: runOnlyForDeploymentPostprocessing) + } + // MARK: - Decodable fileprivate enum CodingKeys: String, CodingKey { case dstPath + case dstSubfolder case dstSubfolderSpec case name } @@ -67,7 +125,12 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase { public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) dstPath = try container.decodeIfPresent(.dstPath) - dstSubfolderSpec = try container.decodeIntIfPresent(.dstSubfolderSpec).flatMap(SubFolder.init) + // Try to decode dstSubfolder (Xcode 16+ string format) first, fallback to dstSubfolderSpec (legacy integer format) + if let dstSubfolderString: String = try container.decodeIfPresent(.dstSubfolder) { + dstSubfolder = SubFolder(string: dstSubfolderString) + } else { + dstSubfolder = try container.decodeIntIfPresent(.dstSubfolderSpec).flatMap(SubFolder.init) + } name = try container.decodeIfPresent(.name) try super.init(from: decoder) } @@ -90,8 +153,9 @@ extension PBXCopyFilesBuildPhase: PlistSerializable { if let name { dictionary["name"] = .string(CommentedString(name)) } - if let dstSubfolderSpec { - dictionary["dstSubfolderSpec"] = .string(CommentedString("\(dstSubfolderSpec.rawValue)")) + if let dstSubfolder { + // Write using the new Xcode 16+ format (dstSubfolder with string value) + dictionary["dstSubfolder"] = .string(CommentedString(dstSubfolder.stringValue)) } return (key: CommentedString(reference, comment: name ?? "CopyFiles"), value: .dictionary(dictionary)) } diff --git a/Tests/XcodeProjTests/Objects/BuildPhase/PBXCopyFilesBuildPhaseTests.swift b/Tests/XcodeProjTests/Objects/BuildPhase/PBXCopyFilesBuildPhaseTests.swift index 9d1d6c506..2db1827d5 100644 --- a/Tests/XcodeProjTests/Objects/BuildPhase/PBXCopyFilesBuildPhaseTests.swift +++ b/Tests/XcodeProjTests/Objects/BuildPhase/PBXCopyFilesBuildPhaseTests.swift @@ -102,6 +102,34 @@ final class PBXCopyFilesBuildPhaseTests: XCTestCase { XCTAssertEqual(PBXCopyFilesBuildPhase.isa, "PBXCopyFilesBuildPhase") } + // MARK: - dstSubfolder String Format Tests (Xcode 16+) + + func test_subFolder_stringValue_frameworks() { + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.frameworks.stringValue, "Frameworks") + } + + func test_subFolder_stringValue_resources() { + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.resources.stringValue, "Resources") + } + + func test_subFolder_initFromString_frameworks() { + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder(string: "Frameworks"), .frameworks) + } + + func test_subFolder_initFromString_resources() { + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder(string: "Resources"), .resources) + } + + func test_subFolder_initFromString_invalidValue() { + XCTAssertNil(PBXCopyFilesBuildPhase.SubFolder(string: "InvalidValue")) + } + + func test_decode_dstSubfolder_stringFormat() { + // Test that SubFolder can be initialized from string + let subFolder = PBXCopyFilesBuildPhase.SubFolder(string: "Frameworks") + XCTAssertEqual(subFolder, .frameworks, "Expected dstSubfolder to be .frameworks") + } + func testDictionary() -> [String: Any] { [ "dstPath": "dstPath", @@ -112,4 +140,5 @@ final class PBXCopyFilesBuildPhaseTests: XCTestCase { "reference": "reference", ] } + }