Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 71 additions & 7 deletions Sources/XcodeProj/Objects/BuildPhase/PBXCopyFilesBuildPhase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,56 @@ 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

/// 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?
Expand All @@ -37,37 +78,59 @@ 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,
runOnlyForDeploymentPostprocessing:
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
}

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)
}
Expand All @@ -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))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -112,4 +140,5 @@ final class PBXCopyFilesBuildPhaseTests: XCTestCase {
"reference": "reference",
]
}

}