Skip to content

Commit 0af488c

Browse files
myaumurapepicrft
andauthored
feat: Added support for Xcode 26's dstSubfolder (#1038)
* feat: Added dstSubfolder * fix: preserve PBXCopyFilesBuildPhase source compatibility and round-trip --------- Co-authored-by: Pedro Piñera <pedro@pepicrft.me>
1 parent 64d256d commit 0af488c

File tree

3 files changed

+115
-0
lines changed

3 files changed

+115
-0
lines changed

Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,67 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase {
1616
case other
1717
}
1818

19+
public enum DstSubfolder: Equatable, Decodable {
20+
case absolutePath
21+
case productsDirectory
22+
case wrapper
23+
case executables
24+
case resources
25+
case javaResources
26+
case frameworks
27+
case sharedFrameworks
28+
case sharedSupport
29+
case plugins
30+
case other
31+
case product
32+
case none
33+
case unknown(String)
34+
35+
public init(rawValue: String) {
36+
switch rawValue {
37+
case "AbsolutePath": self = .absolutePath
38+
case "ProductsDirectory": self = .productsDirectory
39+
case "Wrapper": self = .wrapper
40+
case "Executables": self = .executables
41+
case "Resources": self = .resources
42+
case "JavaResources": self = .javaResources
43+
case "Frameworks": self = .frameworks
44+
case "SharedFrameworks": self = .sharedFrameworks
45+
case "SharedSupport": self = .sharedSupport
46+
case "PlugIns": self = .plugins
47+
case "Other": self = .other
48+
case "Product": self = .product
49+
case "None": self = .none
50+
default: self = .unknown(rawValue)
51+
}
52+
}
53+
54+
public var rawValue: String {
55+
switch self {
56+
case .absolutePath: "AbsolutePath"
57+
case .productsDirectory: "ProductsDirectory"
58+
case .wrapper: "Wrapper"
59+
case .executables: "Executables"
60+
case .resources: "Resources"
61+
case .javaResources: "JavaResources"
62+
case .frameworks: "Frameworks"
63+
case .sharedFrameworks: "SharedFrameworks"
64+
case .sharedSupport: "SharedSupport"
65+
case .plugins: "PlugIns"
66+
case .other: "Other"
67+
case .product: "Product"
68+
case .none: "None"
69+
case let .unknown(rawValue): rawValue
70+
}
71+
}
72+
73+
public init(from decoder: Decoder) throws {
74+
let container = try decoder.singleValueContainer()
75+
let rawValue = try container.decode(String.self)
76+
self = .init(rawValue: rawValue)
77+
}
78+
}
79+
1980
// MARK: - Attributes
2081

2182
/// Element destination path
@@ -24,6 +85,8 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase {
2485
/// Element destination subfolder spec
2586
public var dstSubfolderSpec: SubFolder?
2687

88+
public var dstSubfolder: DstSubfolder?
89+
2790
/// Copy files build phase name
2891
public var name: String?
2992

@@ -38,17 +101,20 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase {
38101
/// - Parameters:
39102
/// - dstPath: Destination path.
40103
/// - dstSubfolderSpec: Destination subfolder spec.
104+
/// - dstSubfolder: Destination subfolder.
41105
/// - buildActionMask: Build action mask.
42106
/// - files: Build files to copy.
43107
/// - runOnlyForDeploymentPostprocessing: Run only for deployment post processing.
44108
public init(dstPath: String? = nil,
45109
dstSubfolderSpec: SubFolder? = nil,
110+
dstSubfolder: DstSubfolder? = nil,
46111
name: String? = nil,
47112
buildActionMask: UInt = defaultBuildActionMask,
48113
files: [PBXBuildFile] = [],
49114
runOnlyForDeploymentPostprocessing: Bool = false) {
50115
self.dstPath = dstPath
51116
self.dstSubfolderSpec = dstSubfolderSpec
117+
self.dstSubfolder = dstSubfolder
52118
self.name = name
53119
super.init(files: files,
54120
buildActionMask: buildActionMask,
@@ -61,13 +127,15 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase {
61127
fileprivate enum CodingKeys: String, CodingKey {
62128
case dstPath
63129
case dstSubfolderSpec
130+
case dstSubfolder
64131
case name
65132
}
66133

67134
public required init(from decoder: Decoder) throws {
68135
let container = try decoder.container(keyedBy: CodingKeys.self)
69136
dstPath = try container.decodeIfPresent(.dstPath)
70137
dstSubfolderSpec = try container.decodeIntIfPresent(.dstSubfolderSpec).flatMap(SubFolder.init)
138+
dstSubfolder = try container.decodeIfPresent(.dstSubfolder)
71139
name = try container.decodeIfPresent(.name)
72140
try super.init(from: decoder)
73141
}
@@ -93,6 +161,9 @@ extension PBXCopyFilesBuildPhase: PlistSerializable {
93161
if let dstSubfolderSpec {
94162
dictionary["dstSubfolderSpec"] = .string(CommentedString("\(dstSubfolderSpec.rawValue)"))
95163
}
164+
if let dstSubfolder {
165+
dictionary["dstSubfolder"] = .string(CommentedString("\(dstSubfolder.rawValue)"))
166+
}
96167
return (key: CommentedString(reference, comment: name ?? "CopyFiles"), value: .dictionary(dictionary))
97168
}
98169
}

Sources/XcodeProj/Objects/Sourcery/Equality.generated.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ extension PBXCopyFilesBuildPhase {
7575
func isEqual(to rhs: PBXCopyFilesBuildPhase) -> Bool {
7676
if dstPath != rhs.dstPath { return false }
7777
if dstSubfolderSpec != rhs.dstSubfolderSpec { return false }
78+
if dstSubfolder != rhs.dstSubfolder { return false }
7879
if name != rhs.name { return false }
7980
return super.isEqual(to: rhs)
8081
}

Tests/XcodeProjTests/Objects/BuildPhase/PBXCopyFilesBuildPhaseTests.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,29 @@ final class PBXCopyFilesBuildPhaseTests: XCTestCase {
7676
} catch {}
7777
}
7878

79+
func test_init_decodesDstSubfolder() {
80+
var dictionary = testDictionary()
81+
dictionary["dstSubfolder"] = "Frameworks"
82+
let data = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
83+
let decoder = XcodeprojJSONDecoder()
84+
do {
85+
let phase = try decoder.decode(PBXCopyFilesBuildPhase.self, from: data)
86+
XCTAssertEqual(phase.dstSubfolder, .frameworks)
87+
} catch {}
88+
}
89+
90+
func test_init_decodesUnknownDstSubfolder() {
91+
var dictionary = testDictionary()
92+
dictionary["dstSubfolder"] = "InvalidSubfolder"
93+
let data = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
94+
let decoder = XcodeprojJSONDecoder()
95+
do {
96+
let phase = try decoder.decode(PBXCopyFilesBuildPhase.self, from: data)
97+
XCTAssertEqual(phase.dstSubfolder, .unknown("InvalidSubfolder"))
98+
XCTAssertEqual(phase.dstSubfolder?.rawValue, "InvalidSubfolder")
99+
} catch {}
100+
}
101+
79102
func test_init_fails_whenFilesIsMissing() {
80103
var dictionary = testDictionary()
81104
dictionary.removeValue(forKey: "files")
@@ -102,6 +125,26 @@ final class PBXCopyFilesBuildPhaseTests: XCTestCase {
102125
XCTAssertEqual(PBXCopyFilesBuildPhase.isa, "PBXCopyFilesBuildPhase")
103126
}
104127

128+
func test_equal_whenDstSubfolderIsDifferent_returnsFalse() {
129+
let lhs = PBXCopyFilesBuildPhase(dstPath: "dstPath",
130+
dstSubfolderSpec: .frameworks,
131+
dstSubfolder: .frameworks,
132+
name: "Copy")
133+
let rhs = PBXCopyFilesBuildPhase(dstPath: "dstPath",
134+
dstSubfolderSpec: .frameworks,
135+
dstSubfolder: .resources,
136+
name: "Copy")
137+
XCTAssertNotEqual(lhs, rhs)
138+
}
139+
140+
func test_write_preservesUnknownDstSubfolderRawValue() throws {
141+
let subject = PBXCopyFilesBuildPhase(dstSubfolder: .unknown("InvalidSubfolder"))
142+
let proj = PBXProj.fixture()
143+
let (_, plistValue) = try subject.plistKeyAndValue(proj: proj, reference: "ref")
144+
145+
XCTAssertEqual(plistValue.dictionary?["dstSubfolder"]?.string, "InvalidSubfolder")
146+
}
147+
105148
func testDictionary() -> [String: Any] {
106149
[
107150
"dstPath": "dstPath",

0 commit comments

Comments
 (0)