Skip to content

Commit a9eb6af

Browse files
authored
refactor!: strongly typed values in BuildSettings and BuildFileSettings (#903)
* Introduce `BuildSetting` enum to enable strongly typed setting serialization * Migrate `BuildSettings` to use `BuildSetting` enum. * Remove unused method that was the result of another PR’s rebase. * Fix equality on `XCBuildConfiguration` * Update `PlistValue` to write `BuildSetting` correctly * Fix Tests * Fix tests This was fun. `NSDictionary(buildSettings)` is not able to be compared because we wrap swift values so we must use `==` on the swift types since they are not concrete and equatable. * Fix tests * Linting * Allow string literal interpolation * Convert `PBXBuildFile` settings to strong types. The settings here are constrained to two cases, one as a string and one as a string array as defined here: https://buck.build/javadoc/com/facebook/buck/apple/xcode/xcodeproj/PBXBuildFile.html Given the narrow use case we should constraint the available types here to fit the need. * Add tests * Linting * Fix pbxbuildfile equality * Add easy access to build setting values * Strongly typed classes tho these appear to always be empty * Strongly typed attributes XcodeGen introduced the ability to include whole `PBXObject` values within these dictionaries which end up being written to the project as their reference string value. In order to simplify the attributes interface, i’m removing that capability and consumers will need to do their own unwrapping. * linting * Convenience accessors for BuildFileSetting * Remove deprecated `parallelizable` * Silence sendability warnings * Conform `BuildSetting` to `CustomStringConvertible` * Extract `BuildFileSettings` to own file and convert tests to SwiftTesting * Extract ProjecteAttributes to own file * Delete commented code * Linting * Add bool bridging to `BuildSetting` * Update to Swift 6 * Update from `#file` to `#filePath` for swift 6 * Try to get the right swift version on linux * Maybe 6.0.3 fixes the issue * Migrate to `XCTUnwrap` to avoid `!` which will crash the test suite and not give an accurate failure count * Revert to 6.0.2 6.0.3 is not supported and also doesnt have a fix we need. * Fix tests * Exclude tests that need `plist` serialization from linux This need a fix that is in swift 6.1 to pass on linux: swiftlang/swift-foundation#1002 * Lint fixes * moar linting * Add specific `targetReference` to `ProjectAttribute` Also removed `Encodable` conformance as we have a custom `plist` method that is used for writing. * Linting * Add Hashable and literal expressibility for Tuist Tests * Linting
1 parent b1caa06 commit a9eb6af

35 files changed

+1585
-1242
lines changed

.github/workflows/xcodeproj.yml

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ jobs:
3131
- uses: actions/checkout@v3
3232
- uses: jdx/mise-action@v2
3333
- uses: swift-actions/setup-swift@v2
34+
with:
35+
swift-version: "6.0.2"
3436
- name: Build
3537
run: swift build --configuration release
3638
test:
@@ -49,6 +51,8 @@ jobs:
4951
- uses: actions/checkout@v3
5052
- uses: jdx/mise-action@v2
5153
- uses: swift-actions/setup-swift@v2
54+
with:
55+
swift-version: "6.0.2"
5256
- run: |
5357
git config --global user.email '[email protected]'
5458
git config --global user.name 'xcodeproj'

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.10.0
1+
// swift-tools-version:6.0.0
22

33
import PackageDescription
44

Sources/XcodeProj/Extensions/Path+Extras.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ extension Path {
4343
let matchc = gt.gl_pathc
4444
#endif
4545
return (0 ..< Int(matchc)).compactMap { index in
46-
if let path = String(validatingUTF8: gt.gl_pathv[index]!) {
46+
if let path = String(validatingCString: gt.gl_pathv[index]!) {
4747
return Path(path)
4848
}
4949
return nil
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
public enum BuildFileSetting: Sendable, Equatable, Hashable {
2+
case string(String)
3+
case array([String])
4+
5+
public var stringValue: String? {
6+
if case let .string(value) = self {
7+
value
8+
} else {
9+
nil
10+
}
11+
}
12+
13+
public var arrayValue: [String]? {
14+
if case let .array(value) = self {
15+
value
16+
} else {
17+
nil
18+
}
19+
}
20+
}
21+
22+
extension BuildFileSetting: Codable {
23+
public init(from decoder: Decoder) throws {
24+
let container = try decoder.singleValueContainer()
25+
do {
26+
let string = try container.decode(String.self)
27+
self = .string(string)
28+
} catch {
29+
let array = try container.decode([String].self)
30+
self = .array(array)
31+
}
32+
}
33+
34+
public func encode(to encoder: Encoder) throws {
35+
var container = encoder.singleValueContainer()
36+
switch self {
37+
case let .string(string):
38+
try container.encode(string)
39+
case let .array(array):
40+
try container.encode(array)
41+
}
42+
}
43+
}
44+
45+
extension BuildFileSetting: ExpressibleByArrayLiteral {
46+
public init(arrayLiteral elements: String...) {
47+
self = .array(elements)
48+
}
49+
}
50+
51+
extension BuildFileSetting: ExpressibleByStringInterpolation {
52+
public init(stringLiteral value: StringLiteralType) {
53+
self = .string(value)
54+
}
55+
}

Sources/XcodeProj/Objects/BuildPhase/PBXBuildFile.swift

+21-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,25 @@ public final class PBXBuildFile: PBXObject {
3131
}
3232

3333
/// Element settings
34-
public var settings: [String: Any]?
34+
public var settings: [String: BuildFileSetting]?
35+
36+
/// Potentially present for `PBXHeadersBuildPhase` : https://buck.build/javadoc/com/facebook/buck/apple/xcode/xcodeproj/PBXBuildFile.html
37+
public var attributes: [String]? {
38+
if case let .array(attributes) = settings?["ATTRIBUTES"] {
39+
attributes
40+
} else {
41+
nil
42+
}
43+
}
44+
45+
/// Potentially present for `PBXSourcesBuildPhase` : https://buck.build/javadoc/com/facebook/buck/apple/xcode/xcodeproj/PBXBuildFile.html
46+
public var compilerFlags: String? {
47+
if case let .string(compilerFlags) = settings?["COMPILER_FLAGS"] {
48+
compilerFlags
49+
} else {
50+
nil
51+
}
52+
}
3553

3654
/// Platform filter attribute.
3755
/// Introduced in: Xcode 11
@@ -53,7 +71,7 @@ public final class PBXBuildFile: PBXObject {
5371
/// - settings: build file settings.
5472
public init(file: PBXFileElement? = nil,
5573
product: XCSwiftPackageProductDependency? = nil,
56-
settings: [String: Any]? = nil,
74+
settings: [String: BuildFileSetting]? = nil,
5775
platformFilter: String? = nil,
5876
platformFilters: [String]? = nil) {
5977
fileReference = file?.reference
@@ -84,7 +102,7 @@ public final class PBXBuildFile: PBXObject {
84102
if let productRefString: String = try container.decodeIfPresent(.productRef) {
85103
productReference = objectReferenceRepository.getOrCreate(reference: productRefString, objects: objects)
86104
}
87-
settings = try container.decodeIfPresent([String: Any].self, forKey: .settings)
105+
settings = try container.decodeIfPresent([String: BuildFileSetting].self, forKey: .settings)
88106
platformFilter = try container.decodeIfPresent(.platformFilter)
89107
platformFilters = try container.decodeIfPresent([String].self, forKey: .platformFilters)
90108
try super.init(from: decoder)
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,82 @@
11
import Foundation
22

33
/// Build settings.
4-
public typealias BuildSettings = [String: Any]
4+
public typealias BuildSettings = [String: BuildSetting]
5+
6+
private let yes = "YES"
7+
private let no = "NO"
8+
9+
public enum BuildSetting: Sendable, Equatable {
10+
case string(String)
11+
case array([String])
12+
13+
public var stringValue: String? {
14+
if case let .string(value) = self {
15+
value
16+
} else {
17+
nil
18+
}
19+
}
20+
21+
public var boolValue: Bool? {
22+
if case let .string(value) = self {
23+
switch value {
24+
case yes: true
25+
case no: false
26+
default: nil
27+
}
28+
} else {
29+
nil
30+
}
31+
}
32+
33+
public var arrayValue: [String]? {
34+
if case let .array(value) = self {
35+
value
36+
} else {
37+
nil
38+
}
39+
}
40+
}
41+
42+
extension BuildSetting: CustomStringConvertible {
43+
public var description: String {
44+
switch self {
45+
case let .string(string):
46+
string
47+
case let .array(array):
48+
array.joined(separator: " ")
49+
}
50+
}
51+
}
52+
53+
extension BuildSetting: Decodable {
54+
public init(from decoder: Decoder) throws {
55+
let container = try decoder.singleValueContainer()
56+
do {
57+
let string = try container.decode(String.self)
58+
self = .string(string)
59+
} catch {
60+
let array = try container.decode([String].self)
61+
self = .array(array)
62+
}
63+
}
64+
}
65+
66+
extension BuildSetting: ExpressibleByArrayLiteral {
67+
public init(arrayLiteral elements: String...) {
68+
self = .array(elements)
69+
}
70+
}
71+
72+
extension BuildSetting: ExpressibleByStringInterpolation {
73+
public init(stringLiteral value: StringLiteralType) {
74+
self = .string(value)
75+
}
76+
}
77+
78+
extension BuildSetting: ExpressibleByBooleanLiteral {
79+
public init(booleanLiteral value: Bool) {
80+
self = .string(value ? yes : no)
81+
}
82+
}

Sources/XcodeProj/Objects/Configuration/XCBuildConfiguration.swift

+6-6
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public final class XCBuildConfiguration: PBXObject {
5959
} else {
6060
baseConfigurationReference = nil
6161
}
62-
buildSettings = try container.decode([String: Any].self, forKey: .buildSettings)
62+
buildSettings = try container.decode(BuildSettings.self, forKey: .buildSettings)
6363
name = try container.decode(.name)
6464
try super.init(from: decoder)
6565
}
@@ -75,16 +75,16 @@ public final class XCBuildConfiguration: PBXObject {
7575
public func append(setting name: String, value: String) {
7676
guard !value.isEmpty else { return }
7777

78-
let existing: Any = buildSettings[name] ?? "$(inherited)"
78+
let existing: BuildSetting = buildSettings[name] ?? "$(inherited)"
7979

8080
switch existing {
81-
case let string as String where string != value:
81+
case let .string(string) where string != value:
8282
let newValue = [string, value].joined(separator: " ")
83-
buildSettings[name] = newValue
84-
case let array as [String]:
83+
buildSettings[name] = .string(newValue)
84+
case let .array(array):
8585
var newValue = array
8686
newValue.append(value)
87-
buildSettings[name] = newValue.uniqued()
87+
buildSettings[name] = .array(newValue.uniqued())
8888
default:
8989
break
9090
}

Sources/XcodeProj/Objects/Project/PBXProj.swift

+7-7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ public final class PBXProj: Decodable {
1414
public var objectVersion: UInt
1515

1616
/// Project classes.
17-
public var classes: [String: Any]
17+
/// This appears to always be empty as defined here: http://www.monobjc.net/xcode-project-file-format.html
18+
public var classes: [String: [String]]
1819

1920
/// Project root object.
2021
var rootObjectReference: PBXObjectReference?
@@ -40,7 +41,7 @@ public final class PBXProj: Decodable {
4041
public init(rootObject: PBXProject? = nil,
4142
objectVersion: UInt = Xcode.LastKnown.objectVersion,
4243
archiveVersion: UInt = Xcode.LastKnown.archiveVersion,
43-
classes: [String: Any] = [:],
44+
classes: [String: [String]] = [:],
4445
objects: [PBXObject] = []) {
4546
self.archiveVersion = archiveVersion
4647
self.objectVersion = objectVersion
@@ -88,7 +89,7 @@ public final class PBXProj: Decodable {
8889
rootObject: PBXProject? = nil,
8990
objectVersion: UInt = Xcode.LastKnown.objectVersion,
9091
archiveVersion: UInt = Xcode.LastKnown.archiveVersion,
91-
classes: [String: Any] = [:],
92+
classes: [String: [String]] = [:],
9293
objects: PBXObjects
9394
) {
9495
self.archiveVersion = archiveVersion
@@ -116,7 +117,7 @@ public final class PBXProj: Decodable {
116117
self.rootObjectReference = objectReferenceRepository.getOrCreate(reference: rootObjectReference, objects: objects)
117118
objectVersion = try container.decodeIntIfPresent(.objectVersion) ?? 0
118119
archiveVersion = try container.decodeIntIfPresent(.archiveVersion) ?? 1
119-
classes = try container.decodeIfPresent([String: Any].self, forKey: .classes) ?? [:]
120+
classes = try container.decodeIfPresent([String: [String]].self, forKey: .classes) ?? [:]
120121
let objectsDictionary: [String: PBXObjectDictionaryEntry] = try container.decodeIfPresent([String: PBXObjectDictionaryEntry].self, forKey: .objects) ?? [:]
121122

122123
for entry in objectsDictionary {
@@ -261,10 +262,9 @@ extension PBXProj {
261262

262263
extension PBXProj: Equatable {
263264
public static func == (lhs: PBXProj, rhs: PBXProj) -> Bool {
264-
let equalClasses = NSDictionary(dictionary: lhs.classes).isEqual(to: rhs.classes)
265-
return lhs.archiveVersion == rhs.archiveVersion &&
265+
lhs.archiveVersion == rhs.archiveVersion &&
266266
lhs.objectVersion == rhs.objectVersion &&
267-
equalClasses &&
267+
lhs.classes == rhs.classes &&
268268
lhs.objects == rhs.objects
269269
}
270270
}

Sources/XcodeProj/Objects/Project/PBXProject.swift

+20-17
Original file line numberDiff line numberDiff line change
@@ -108,20 +108,20 @@ public final class PBXProject: PBXObject {
108108

109109
/// Project attributes.
110110
/// Target attributes will be merged into this
111-
public var attributes: [String: Any]
111+
public var attributes: [String: ProjectAttribute]
112112

113113
/// Target attribute references.
114-
var targetAttributeReferences: [PBXObjectReference: [String: Any]]
114+
var targetAttributeReferences: [PBXObjectReference: [String: ProjectAttribute]]
115115

116116
/// Target attributes.
117-
public var targetAttributes: [PBXTarget: [String: Any]] {
117+
public var targetAttributes: [PBXTarget: [String: ProjectAttribute]] {
118118
set {
119119
targetAttributeReferences = [:]
120120
for item in newValue {
121121
targetAttributeReferences[item.key.reference] = item.value
122122
}
123123
} get {
124-
var attributes: [PBXTarget: [String: Any]] = [:]
124+
var attributes: [PBXTarget: [String: ProjectAttribute]] = [:]
125125
for targetAttributeReference in targetAttributeReferences {
126126
if let object: PBXTarget = targetAttributeReference.key.getObject() {
127127
attributes[object] = targetAttributeReference.value
@@ -176,7 +176,7 @@ public final class PBXProject: PBXObject {
176176
/// - Parameters:
177177
/// - attributes: attributes that will be set.
178178
/// - target: target.
179-
public func setTargetAttributes(_ attributes: [String: Any], target: PBXTarget) {
179+
public func setTargetAttributes(_ attributes: [String: ProjectAttribute], target: PBXTarget) {
180180
targetAttributeReferences[target.reference] = attributes
181181
}
182182

@@ -321,8 +321,8 @@ public final class PBXProject: PBXObject {
321321
projectRoots: [String] = [],
322322
targets: [PBXTarget] = [],
323323
packages: [XCRemoteSwiftPackageReference] = [],
324-
attributes: [String: Any] = [:],
325-
targetAttributes: [PBXTarget: [String: Any]] = [:]) {
324+
attributes: [String: ProjectAttribute] = [:],
325+
targetAttributes: [PBXTarget: [String: ProjectAttribute]] = [:]) {
326326
self.name = name
327327
buildConfigurationListReference = buildConfigurationList.reference
328328
self.compatibilityVersion = compatibilityVersion
@@ -417,10 +417,12 @@ public final class PBXProject: PBXObject {
417417
let packageRefeferenceStrings: [String] = try container.decodeIfPresent(.packageReferences) ?? []
418418
packageReferences = packageRefeferenceStrings.map { referenceRepository.getOrCreate(reference: $0, objects: objects) }
419419

420-
var attributes = try (container.decodeIfPresent([String: Any].self, forKey: .attributes) ?? [:])
421-
var targetAttributeReferences: [PBXObjectReference: [String: Any]] = [:]
422-
if let targetAttributes = attributes[PBXProject.targetAttributesKey] as? [String: [String: Any]] {
423-
targetAttributes.forEach { targetAttributeReferences[referenceRepository.getOrCreate(reference: $0.key, objects: objects)] = $0.value }
420+
var attributes = try (container.decodeIfPresent([String: ProjectAttribute].self, forKey: .attributes) ?? [:])
421+
var targetAttributeReferences: [PBXObjectReference: [String: ProjectAttribute]] = [:]
422+
if case let .attributeDictionary(targetAttributes) = attributes[PBXProject.targetAttributesKey] {
423+
for targetAttribute in targetAttributes {
424+
targetAttributeReferences[referenceRepository.getOrCreate(reference: targetAttribute.key, objects: objects)] = targetAttribute.value
425+
}
424426
attributes[PBXProject.targetAttributesKey] = nil
425427
}
426428
self.attributes = attributes
@@ -562,16 +564,17 @@ extension PBXProject: PlistSerializable {
562564
dictionary["packageReferences"] = PlistValue.array(finalPackageReferences)
563565
}
564566

565-
var plistAttributes: [String: Any] = attributes
567+
var plistAttributes: [String: ProjectAttribute] = attributes
566568

567569
// merge target attributes
568-
var plistTargetAttributes: [String: Any] = [:]
570+
var plistTargetAttributes: [String: [String: ProjectAttribute]] = [:]
569571
for (reference, value) in targetAttributeReferences {
570-
plistTargetAttributes[reference.value] = value.mapValues { value in
571-
(value as? PBXObject)?.reference.value ?? value
572-
}
572+
plistTargetAttributes[reference.value] = value
573+
}
574+
575+
if !plistTargetAttributes.isEmpty {
576+
plistAttributes[PBXProject.targetAttributesKey] = .attributeDictionary(plistTargetAttributes)
573577
}
574-
plistAttributes[PBXProject.targetAttributesKey] = plistTargetAttributes.isEmpty ? nil : plistTargetAttributes
575578

576579
dictionary["attributes"] = plistAttributes.plist()
577580

0 commit comments

Comments
 (0)