Skip to content

Commit bc3760f

Browse files
Allen-han21claude
andcommitted
feat: Deprecate dstSubfolderSpec in favor of dstSubfolder
Resolves #1034 ## Changes - Add `dstSubfolder` property to `PBXCopyFilesBuildPhase` (Xcode 16+ format) - Deprecate `dstSubfolderSpec` as a computed property alias - Add `stringValue` to `SubFolder` enum for human-readable output - Add `init(string:)` to `SubFolder` for parsing Xcode 16+ format - Support decoding both legacy integer format and new string format - Serialize using new `dstSubfolder` string format Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 31712ec commit bc3760f

2 files changed

Lines changed: 100 additions & 7 deletions

File tree

Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,56 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase {
1414
case sharedSupport = 12
1515
case plugins = 13
1616
case other
17+
18+
/// Human-readable string representation used in Xcode 16+
19+
public var stringValue: String {
20+
switch self {
21+
case .absolutePath: return "AbsolutePath"
22+
case .productsDirectory: return "ProductsDirectory"
23+
case .wrapper: return "Wrapper"
24+
case .executables: return "Executables"
25+
case .resources: return "Resources"
26+
case .javaResources: return "JavaResources"
27+
case .frameworks: return "Frameworks"
28+
case .sharedFrameworks: return "SharedFrameworks"
29+
case .sharedSupport: return "SharedSupport"
30+
case .plugins: return "Plugins"
31+
case .other: return "Other"
32+
}
33+
}
34+
35+
/// Initialize from string value (Xcode 16+ format)
36+
public init?(string: String) {
37+
switch string {
38+
case "AbsolutePath": self = .absolutePath
39+
case "ProductsDirectory": self = .productsDirectory
40+
case "Wrapper": self = .wrapper
41+
case "Executables": self = .executables
42+
case "Resources": self = .resources
43+
case "JavaResources": self = .javaResources
44+
case "Frameworks": self = .frameworks
45+
case "SharedFrameworks": self = .sharedFrameworks
46+
case "SharedSupport": self = .sharedSupport
47+
case "Plugins": self = .plugins
48+
default: return nil
49+
}
50+
}
1751
}
1852

1953
// MARK: - Attributes
2054

2155
/// Element destination path
2256
public var dstPath: String?
2357

58+
/// Element destination subfolder (Xcode 16+ format, human-readable string)
59+
public var dstSubfolder: SubFolder?
60+
2461
/// Element destination subfolder spec
25-
public var dstSubfolderSpec: SubFolder?
62+
@available(*, deprecated, renamed: "dstSubfolder")
63+
public var dstSubfolderSpec: SubFolder? {
64+
get { dstSubfolder }
65+
set { dstSubfolder = newValue }
66+
}
2667

2768
/// Copy files build phase name
2869
public var name: String?
@@ -37,37 +78,59 @@ public final class PBXCopyFilesBuildPhase: PBXBuildPhase {
3778
///
3879
/// - Parameters:
3980
/// - dstPath: Destination path.
40-
/// - dstSubfolderSpec: Destination subfolder spec.
81+
/// - dstSubfolder: Destination subfolder.
4182
/// - buildActionMask: Build action mask.
4283
/// - files: Build files to copy.
4384
/// - runOnlyForDeploymentPostprocessing: Run only for deployment post processing.
4485
public init(dstPath: String? = nil,
45-
dstSubfolderSpec: SubFolder? = nil,
86+
dstSubfolder: SubFolder? = nil,
4687
name: String? = nil,
4788
buildActionMask: UInt = defaultBuildActionMask,
4889
files: [PBXBuildFile] = [],
4990
runOnlyForDeploymentPostprocessing: Bool = false) {
5091
self.dstPath = dstPath
51-
self.dstSubfolderSpec = dstSubfolderSpec
92+
self.dstSubfolder = dstSubfolder
5293
self.name = name
5394
super.init(files: files,
5495
buildActionMask: buildActionMask,
5596
runOnlyForDeploymentPostprocessing:
5697
runOnlyForDeploymentPostprocessing)
5798
}
5899

100+
/// Initializes the copy files build phase with its attributes (deprecated parameter name).
101+
@available(*, deprecated, renamed: "init(dstPath:dstSubfolder:name:buildActionMask:files:runOnlyForDeploymentPostprocessing:)")
102+
public convenience init(dstPath: String? = nil,
103+
dstSubfolderSpec: SubFolder?,
104+
name: String? = nil,
105+
buildActionMask: UInt = defaultBuildActionMask,
106+
files: [PBXBuildFile] = [],
107+
runOnlyForDeploymentPostprocessing: Bool = false) {
108+
self.init(dstPath: dstPath,
109+
dstSubfolder: dstSubfolderSpec,
110+
name: name,
111+
buildActionMask: buildActionMask,
112+
files: files,
113+
runOnlyForDeploymentPostprocessing: runOnlyForDeploymentPostprocessing)
114+
}
115+
59116
// MARK: - Decodable
60117

61118
fileprivate enum CodingKeys: String, CodingKey {
62119
case dstPath
120+
case dstSubfolder
63121
case dstSubfolderSpec
64122
case name
65123
}
66124

67125
public required init(from decoder: Decoder) throws {
68126
let container = try decoder.container(keyedBy: CodingKeys.self)
69127
dstPath = try container.decodeIfPresent(.dstPath)
70-
dstSubfolderSpec = try container.decodeIntIfPresent(.dstSubfolderSpec).flatMap(SubFolder.init)
128+
// Try to decode dstSubfolder (Xcode 16+ string format) first, fallback to dstSubfolderSpec (legacy integer format)
129+
if let dstSubfolderString: String = try container.decodeIfPresent(.dstSubfolder) {
130+
dstSubfolder = SubFolder(string: dstSubfolderString)
131+
} else {
132+
dstSubfolder = try container.decodeIntIfPresent(.dstSubfolderSpec).flatMap(SubFolder.init)
133+
}
71134
name = try container.decodeIfPresent(.name)
72135
try super.init(from: decoder)
73136
}
@@ -90,8 +153,9 @@ extension PBXCopyFilesBuildPhase: PlistSerializable {
90153
if let name {
91154
dictionary["name"] = .string(CommentedString(name))
92155
}
93-
if let dstSubfolderSpec {
94-
dictionary["dstSubfolderSpec"] = .string(CommentedString("\(dstSubfolderSpec.rawValue)"))
156+
if let dstSubfolder {
157+
// Write using the new Xcode 16+ format (dstSubfolder with string value)
158+
dictionary["dstSubfolder"] = .string(CommentedString(dstSubfolder.stringValue))
95159
}
96160
return (key: CommentedString(reference, comment: name ?? "CopyFiles"), value: .dictionary(dictionary))
97161
}

Tests/XcodeProjTests/Objects/BuildPhase/PBXCopyFilesBuildPhaseTests.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,34 @@ final class PBXCopyFilesBuildPhaseTests: XCTestCase {
102102
XCTAssertEqual(PBXCopyFilesBuildPhase.isa, "PBXCopyFilesBuildPhase")
103103
}
104104

105+
// MARK: - dstSubfolder String Format Tests (Xcode 16+)
106+
107+
func test_subFolder_stringValue_frameworks() {
108+
XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.frameworks.stringValue, "Frameworks")
109+
}
110+
111+
func test_subFolder_stringValue_resources() {
112+
XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder.resources.stringValue, "Resources")
113+
}
114+
115+
func test_subFolder_initFromString_frameworks() {
116+
XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder(string: "Frameworks"), .frameworks)
117+
}
118+
119+
func test_subFolder_initFromString_resources() {
120+
XCTAssertEqual(PBXCopyFilesBuildPhase.SubFolder(string: "Resources"), .resources)
121+
}
122+
123+
func test_subFolder_initFromString_invalidValue() {
124+
XCTAssertNil(PBXCopyFilesBuildPhase.SubFolder(string: "InvalidValue"))
125+
}
126+
127+
func test_decode_dstSubfolder_stringFormat() {
128+
// Test that SubFolder can be initialized from string
129+
let subFolder = PBXCopyFilesBuildPhase.SubFolder(string: "Frameworks")
130+
XCTAssertEqual(subFolder, .frameworks, "Expected dstSubfolder to be .frameworks")
131+
}
132+
105133
func testDictionary() -> [String: Any] {
106134
[
107135
"dstPath": "dstPath",
@@ -112,4 +140,5 @@ final class PBXCopyFilesBuildPhaseTests: XCTestCase {
112140
"reference": "reference",
113141
]
114142
}
143+
115144
}

0 commit comments

Comments
 (0)