From e15187b2103e874e9b59200ee77d9cd88dbbe02a Mon Sep 17 00:00:00 2001 From: Kirill Gusev Date: Sun, 28 Dec 2025 14:39:17 +0300 Subject: [PATCH 1/2] feat: Added dstSubfolder --- .../BuildPhase/PBXCopyFilesBuildPhase.swift | 35 ++++++++++++++++--- .../PBXCopyFilesBuildPhaseTests.swift | 20 +++++------ 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift b/Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift index 0fa51e1cf..635b6923d 100644 --- a/Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift +++ b/Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift @@ -2,7 +2,9 @@ import Foundation /// This is the element for the copy file build phase. public final class PBXCopyFilesBuildPhase: PBXBuildPhase { - public enum SubFolder: UInt, Decodable { + + @available(*, deprecated, renamed: "SubFolder", message: "May become obsolete in the future in favor of dstSubfolder") + public enum SubFolderSpec: UInt, Decodable { case absolutePath = 0 case productsDirectory = 16 case wrapper = 1 @@ -15,6 +17,22 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase { case plugins = 13 case other } + + public enum SubFolder: String, Decodable { + case absolutePath = "AbsolutePath" + case productsDirectory = "ProductsDirectory" + case wrapper = "Wrapper" + case executables = "Executables" + case resources = "Resources" + case javaResources = "JavaResources" + case frameworks = "Frameworks" + case sharedFrameworks = "SharedFrameworks" + case sharedSupport = "SharedSupport" + case plugins = "PlugIns" + case other = "Other" + case product = "Product" + case none = "None" + } // MARK: - Attributes @@ -22,7 +40,9 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase { public var dstPath: String? /// Element destination subfolder spec - public var dstSubfolderSpec: SubFolder? + public var dstSubfolderSpec: SubFolderSpec? + + public var dstSubfolder: SubFolder? /// Copy files build phase name public var name: String? @@ -42,13 +62,15 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase { /// - files: Build files to copy. /// - runOnlyForDeploymentPostprocessing: Run only for deployment post processing. public init(dstPath: String? = nil, - dstSubfolderSpec: SubFolder? = nil, + dstSubfolderSpec: SubFolderSpec? = 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, @@ -61,13 +83,15 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase { fileprivate enum CodingKeys: String, CodingKey { case dstPath case dstSubfolderSpec + case dstSubfolder case name } 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) + dstSubfolderSpec = try container.decodeIntIfPresent(.dstSubfolderSpec).flatMap(SubFolderSpec.init) + dstSubfolder = try container.decodeIfPresent(.dstSubfolder) name = try container.decodeIfPresent(.name) try super.init(from: decoder) } @@ -93,6 +117,9 @@ extension PBXCopyFilesBuildPhase: PlistSerializable { if let dstSubfolderSpec { dictionary["dstSubfolderSpec"] = .string(CommentedString("\(dstSubfolderSpec.rawValue)")) } + if let dstSubfolder { + dictionary["dstSubfolder"] = .string(CommentedString("\(dstSubfolder.rawValue)")) + } 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..36a68c2bf 100644 --- a/Tests/XcodeProjTests/Objects/BuildPhase/PBXCopyFilesBuildPhaseTests.swift +++ b/Tests/XcodeProjTests/Objects/BuildPhase/PBXCopyFilesBuildPhaseTests.swift @@ -4,43 +4,43 @@ import XCTest final class PBXCopyFilesBuildPhaseTests: XCTestCase { func test_subFolder_Path_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.absolutePath.rawValue, 0) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.absolutePath.rawValue, 0) } func test_subFolder_producsDirectory_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.productsDirectory.rawValue, 16) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.productsDirectory.rawValue, 16) } func test_subFolder_wrapper_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.wrapper.rawValue, 1) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.wrapper.rawValue, 1) } func test_subFolder_executables_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.executables.rawValue, 6) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.executables.rawValue, 6) } func test_subFolder_resources_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.resources.rawValue, 7) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.resources.rawValue, 7) } func test_subFolder_javaResources_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.javaResources.rawValue, 15) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.javaResources.rawValue, 15) } func test_subFolder_frameworks_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.frameworks.rawValue, 10) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.frameworks.rawValue, 10) } func test_subFolder_sharedFrameworks_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.sharedFrameworks.rawValue, 11) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.sharedFrameworks.rawValue, 11) } func test_subFolder_sharedSupport_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.sharedSupport.rawValue, 12) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.sharedSupport.rawValue, 12) } func test_subFolder_plugins_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.plugins.rawValue, 13) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.plugins.rawValue, 13) } func test_init_fails_whenDstPathIsMissing() { From 40250993ad23778cfff91db4625bb241f6e20da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Pi=C3=B1era?= Date: Thu, 26 Feb 2026 17:40:44 +0100 Subject: [PATCH 2/2] fix: preserve PBXCopyFilesBuildPhase source compatibility and round-trip --- .../BuildPhase/PBXCopyFilesBuildPhase.swift | 92 ++++++++++++++----- .../Objects/Sourcery/Equality.generated.swift | 1 + .../PBXCopyFilesBuildPhaseTests.swift | 63 +++++++++++-- 3 files changed, 122 insertions(+), 34 deletions(-) diff --git a/Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift b/Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift index 635b6923d..58f8817bf 100644 --- a/Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift +++ b/Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift @@ -2,9 +2,7 @@ import Foundation /// This is the element for the copy file build phase. public final class PBXCopyFilesBuildPhase: PBXBuildPhase { - - @available(*, deprecated, renamed: "SubFolder", message: "May become obsolete in the future in favor of dstSubfolder") - public enum SubFolderSpec: UInt, Decodable { + public enum SubFolder: UInt, Decodable { case absolutePath = 0 case productsDirectory = 16 case wrapper = 1 @@ -17,21 +15,66 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase { case plugins = 13 case other } - - public enum SubFolder: String, Decodable { - case absolutePath = "AbsolutePath" - case productsDirectory = "ProductsDirectory" - case wrapper = "Wrapper" - case executables = "Executables" - case resources = "Resources" - case javaResources = "JavaResources" - case frameworks = "Frameworks" - case sharedFrameworks = "SharedFrameworks" - case sharedSupport = "SharedSupport" - case plugins = "PlugIns" - case other = "Other" - case product = "Product" - case none = "None" + + public enum DstSubfolder: Equatable, Decodable { + case absolutePath + case productsDirectory + case wrapper + case executables + case resources + case javaResources + case frameworks + case sharedFrameworks + case sharedSupport + case plugins + case other + case product + case none + case unknown(String) + + public init(rawValue: String) { + switch rawValue { + 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 + case "Other": self = .other + case "Product": self = .product + case "None": self = .none + default: self = .unknown(rawValue) + } + } + + public var rawValue: String { + switch self { + case .absolutePath: "AbsolutePath" + case .productsDirectory: "ProductsDirectory" + case .wrapper: "Wrapper" + case .executables: "Executables" + case .resources: "Resources" + case .javaResources: "JavaResources" + case .frameworks: "Frameworks" + case .sharedFrameworks: "SharedFrameworks" + case .sharedSupport: "SharedSupport" + case .plugins: "PlugIns" + case .other: "Other" + case .product: "Product" + case .none: "None" + case let .unknown(rawValue): rawValue + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(String.self) + self = .init(rawValue: rawValue) + } } // MARK: - Attributes @@ -40,9 +83,9 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase { public var dstPath: String? /// Element destination subfolder spec - public var dstSubfolderSpec: SubFolderSpec? - - public var dstSubfolder: SubFolder? + public var dstSubfolderSpec: SubFolder? + + public var dstSubfolder: DstSubfolder? /// Copy files build phase name public var name: String? @@ -58,12 +101,13 @@ 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: SubFolderSpec? = nil, - dstSubfolder: SubFolder? = nil, + dstSubfolderSpec: SubFolder? = nil, + dstSubfolder: DstSubfolder? = nil, name: String? = nil, buildActionMask: UInt = defaultBuildActionMask, files: [PBXBuildFile] = [], @@ -90,7 +134,7 @@ 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(SubFolderSpec.init) + dstSubfolderSpec = try container.decodeIntIfPresent(.dstSubfolderSpec).flatMap(SubFolder.init) dstSubfolder = try container.decodeIfPresent(.dstSubfolder) name = try container.decodeIfPresent(.name) try super.init(from: decoder) diff --git a/Sources/XcodeProj/Objects/Sourcery/Equality.generated.swift b/Sources/XcodeProj/Objects/Sourcery/Equality.generated.swift index cd69699a9..f7c86b149 100644 --- a/Sources/XcodeProj/Objects/Sourcery/Equality.generated.swift +++ b/Sources/XcodeProj/Objects/Sourcery/Equality.generated.swift @@ -75,6 +75,7 @@ extension PBXCopyFilesBuildPhase { func isEqual(to rhs: PBXCopyFilesBuildPhase) -> Bool { if dstPath != rhs.dstPath { return false } if dstSubfolderSpec != rhs.dstSubfolderSpec { return false } + if dstSubfolder != rhs.dstSubfolder { return false } if name != rhs.name { return false } return super.isEqual(to: rhs) } diff --git a/Tests/XcodeProjTests/Objects/BuildPhase/PBXCopyFilesBuildPhaseTests.swift b/Tests/XcodeProjTests/Objects/BuildPhase/PBXCopyFilesBuildPhaseTests.swift index 36a68c2bf..e809f2952 100644 --- a/Tests/XcodeProjTests/Objects/BuildPhase/PBXCopyFilesBuildPhaseTests.swift +++ b/Tests/XcodeProjTests/Objects/BuildPhase/PBXCopyFilesBuildPhaseTests.swift @@ -4,43 +4,43 @@ import XCTest final class PBXCopyFilesBuildPhaseTests: XCTestCase { func test_subFolder_Path_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.absolutePath.rawValue, 0) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.absolutePath.rawValue, 0) } func test_subFolder_producsDirectory_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.productsDirectory.rawValue, 16) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.productsDirectory.rawValue, 16) } func test_subFolder_wrapper_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.wrapper.rawValue, 1) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.wrapper.rawValue, 1) } func test_subFolder_executables_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.executables.rawValue, 6) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.executables.rawValue, 6) } func test_subFolder_resources_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.resources.rawValue, 7) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.resources.rawValue, 7) } func test_subFolder_javaResources_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.javaResources.rawValue, 15) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.javaResources.rawValue, 15) } func test_subFolder_frameworks_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.frameworks.rawValue, 10) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.frameworks.rawValue, 10) } func test_subFolder_sharedFrameworks_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.sharedFrameworks.rawValue, 11) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.sharedFrameworks.rawValue, 11) } func test_subFolder_sharedSupport_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.sharedSupport.rawValue, 12) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.sharedSupport.rawValue, 12) } func test_subFolder_plugins_hasTheCorrectValue() { - XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolderSpec.plugins.rawValue, 13) + XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.plugins.rawValue, 13) } func test_init_fails_whenDstPathIsMissing() { @@ -76,6 +76,29 @@ final class PBXCopyFilesBuildPhaseTests: XCTestCase { } catch {} } + func test_init_decodesDstSubfolder() { + var dictionary = testDictionary() + dictionary["dstSubfolder"] = "Frameworks" + let data = try! JSONSerialization.data(withJSONObject: dictionary, options: []) + let decoder = XcodeprojJSONDecoder() + do { + let phase = try decoder.decode(PBXCopyFilesBuildPhase.self, from: data) + XCTAssertEqual(phase.dstSubfolder, .frameworks) + } catch {} + } + + func test_init_decodesUnknownDstSubfolder() { + var dictionary = testDictionary() + dictionary["dstSubfolder"] = "InvalidSubfolder" + let data = try! JSONSerialization.data(withJSONObject: dictionary, options: []) + let decoder = XcodeprojJSONDecoder() + do { + let phase = try decoder.decode(PBXCopyFilesBuildPhase.self, from: data) + XCTAssertEqual(phase.dstSubfolder, .unknown("InvalidSubfolder")) + XCTAssertEqual(phase.dstSubfolder?.rawValue, "InvalidSubfolder") + } catch {} + } + func test_init_fails_whenFilesIsMissing() { var dictionary = testDictionary() dictionary.removeValue(forKey: "files") @@ -102,6 +125,26 @@ final class PBXCopyFilesBuildPhaseTests: XCTestCase { XCTAssertEqual(PBXCopyFilesBuildPhase.isa, "PBXCopyFilesBuildPhase") } + func test_equal_whenDstSubfolderIsDifferent_returnsFalse() { + let lhs = PBXCopyFilesBuildPhase(dstPath: "dstPath", + dstSubfolderSpec: .frameworks, + dstSubfolder: .frameworks, + name: "Copy") + let rhs = PBXCopyFilesBuildPhase(dstPath: "dstPath", + dstSubfolderSpec: .frameworks, + dstSubfolder: .resources, + name: "Copy") + XCTAssertNotEqual(lhs, rhs) + } + + func test_write_preservesUnknownDstSubfolderRawValue() throws { + let subject = PBXCopyFilesBuildPhase(dstSubfolder: .unknown("InvalidSubfolder")) + let proj = PBXProj.fixture() + let (_, plistValue) = try subject.plistKeyAndValue(proj: proj, reference: "ref") + + XCTAssertEqual(plistValue.dictionary?["dstSubfolder"]?.string, "InvalidSubfolder") + } + func testDictionary() -> [String: Any] { [ "dstPath": "dstPath",