Skip to content

refactor!: strongly typed values in BuildSettings and BuildFileSettings #903

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 41 commits into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e52015a
Introduce `BuildSetting` enum to enable strongly typed setting serial…
waltflanagan Jul 20, 2024
dc0187c
Migrate `BuildSettings` to use `BuildSetting` enum.
waltflanagan Jul 20, 2024
80c6dee
Remove unused method that was the result of another PR’s rebase.
waltflanagan Feb 17, 2025
4c49b9c
Fix equality on `XCBuildConfiguration`
waltflanagan Jul 25, 2024
0d54629
Update `PlistValue` to write `BuildSetting` correctly
waltflanagan Jul 23, 2024
dd33958
Fix Tests
waltflanagan Jul 23, 2024
091d6c4
Fix tests
waltflanagan Jul 25, 2024
2dfe8f2
Fix tests
waltflanagan Feb 17, 2025
9e2a6cb
Linting
waltflanagan Feb 17, 2025
f0b621c
Allow string literal interpolation
waltflanagan Feb 18, 2025
34d0674
Convert `PBXBuildFile` settings to strong types.
waltflanagan Feb 18, 2025
098cdb5
Add tests
waltflanagan Feb 18, 2025
519c511
Linting
waltflanagan Feb 18, 2025
8e32b39
Fix pbxbuildfile equality
waltflanagan Feb 19, 2025
f118244
Add easy access to build setting values
waltflanagan Feb 22, 2025
024210c
Strongly typed classes tho these appear to always be empty
waltflanagan Feb 22, 2025
b72b60e
Strongly typed attributes
waltflanagan Feb 22, 2025
e6c2170
linting
waltflanagan Feb 22, 2025
d3d0bf3
Convenience accessors for BuildFileSetting
waltflanagan Feb 22, 2025
364b8ae
Remove deprecated `parallelizable`
waltflanagan Feb 22, 2025
bc6a9df
Silence sendability warnings
waltflanagan Feb 22, 2025
fd8f898
Conform `BuildSetting` to `CustomStringConvertible`
waltflanagan Feb 27, 2025
fef1fb1
Extract `BuildFileSettings` to own file and convert tests to SwiftTes…
waltflanagan Feb 27, 2025
5ff523d
Extract ProjecteAttributes to own file
waltflanagan Feb 27, 2025
d56c55a
Delete commented code
waltflanagan Feb 27, 2025
7785310
Linting
waltflanagan Feb 27, 2025
f2098b7
Add bool bridging to `BuildSetting`
waltflanagan Feb 27, 2025
6dac103
Update to Swift 6
waltflanagan Feb 27, 2025
2fea426
Update from `#file` to `#filePath` for swift 6
waltflanagan Feb 27, 2025
16e9412
Try to get the right swift version on linux
waltflanagan Feb 27, 2025
0f3a60e
Maybe 6.0.3 fixes the issue
waltflanagan Feb 27, 2025
7f7aaca
Migrate to `XCTUnwrap` to avoid `!` which will crash the test suite a…
waltflanagan Feb 27, 2025
09b3fac
Revert to 6.0.2
waltflanagan Feb 27, 2025
5cf9160
Fix tests
waltflanagan Feb 27, 2025
2461290
Exclude tests that need `plist` serialization from linux
waltflanagan Feb 27, 2025
648909e
Lint fixes
waltflanagan Feb 27, 2025
c4eb844
moar linting
waltflanagan Feb 28, 2025
b8aeb90
Add specific `targetReference` to `ProjectAttribute`
waltflanagan Feb 28, 2025
5d26cf5
Linting
waltflanagan Feb 28, 2025
d04f4ff
Add Hashable and literal expressibility for Tuist Tests
waltflanagan Mar 2, 2025
3945b8c
Linting
waltflanagan Mar 9, 2025
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
4 changes: 4 additions & 0 deletions .github/workflows/xcodeproj.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
- uses: actions/checkout@v3
- uses: jdx/mise-action@v2
- uses: swift-actions/setup-swift@v2
with:
swift-version: "6.0.2"
- name: Build
run: swift build --configuration release
test:
Expand All @@ -49,6 +51,8 @@ jobs:
- uses: actions/checkout@v3
- uses: jdx/mise-action@v2
- uses: swift-actions/setup-swift@v2
with:
swift-version: "6.0.2"
- run: |
git config --global user.email '[email protected]'
git config --global user.name 'xcodeproj'
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.10.0
// swift-tools-version:6.0.0

import PackageDescription

Expand Down
2 changes: 1 addition & 1 deletion Sources/XcodeProj/Extensions/Path+Extras.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ extension Path {
let matchc = gt.gl_pathc
#endif
return (0 ..< Int(matchc)).compactMap { index in
if let path = String(validatingUTF8: gt.gl_pathv[index]!) {
if let path = String(validatingCString: gt.gl_pathv[index]!) {
return Path(path)
}
return nil
Expand Down
55 changes: 55 additions & 0 deletions Sources/XcodeProj/Objects/BuildPhase/BuildFileSetting.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
public enum BuildFileSetting: Sendable, Equatable, Hashable {
case string(String)
case array([String])

public var stringValue: String? {
if case let .string(value) = self {
value
} else {
nil
}
}

public var arrayValue: [String]? {
if case let .array(value) = self {
value
} else {
nil
}
}
}

extension BuildFileSetting: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let string = try container.decode(String.self)
self = .string(string)
} catch {
let array = try container.decode([String].self)
self = .array(array)
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case let .string(string):
try container.encode(string)
case let .array(array):
try container.encode(array)
}
}
}

extension BuildFileSetting: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: String...) {
self = .array(elements)
}
}

extension BuildFileSetting: ExpressibleByStringInterpolation {
public init(stringLiteral value: StringLiteralType) {
self = .string(value)
}
}
24 changes: 21 additions & 3 deletions Sources/XcodeProj/Objects/BuildPhase/PBXBuildFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,25 @@ public final class PBXBuildFile: PBXObject {
}

/// Element settings
public var settings: [String: Any]?
public var settings: [String: BuildFileSetting]?

/// Potentially present for `PBXHeadersBuildPhase` : https://buck.build/javadoc/com/facebook/buck/apple/xcode/xcodeproj/PBXBuildFile.html
public var attributes: [String]? {
if case let .array(attributes) = settings?["ATTRIBUTES"] {
attributes
} else {
nil
}
}

/// Potentially present for `PBXSourcesBuildPhase` : https://buck.build/javadoc/com/facebook/buck/apple/xcode/xcodeproj/PBXBuildFile.html
public var compilerFlags: String? {
if case let .string(compilerFlags) = settings?["COMPILER_FLAGS"] {
compilerFlags
} else {
nil
}
}

/// Platform filter attribute.
/// Introduced in: Xcode 11
Expand All @@ -53,7 +71,7 @@ public final class PBXBuildFile: PBXObject {
/// - settings: build file settings.
public init(file: PBXFileElement? = nil,
product: XCSwiftPackageProductDependency? = nil,
settings: [String: Any]? = nil,
settings: [String: BuildFileSetting]? = nil,
platformFilter: String? = nil,
platformFilters: [String]? = nil) {
fileReference = file?.reference
Expand Down Expand Up @@ -84,7 +102,7 @@ public final class PBXBuildFile: PBXObject {
if let productRefString: String = try container.decodeIfPresent(.productRef) {
productReference = objectReferenceRepository.getOrCreate(reference: productRefString, objects: objects)
}
settings = try container.decodeIfPresent([String: Any].self, forKey: .settings)
settings = try container.decodeIfPresent([String: BuildFileSetting].self, forKey: .settings)
platformFilter = try container.decodeIfPresent(.platformFilter)
platformFilters = try container.decodeIfPresent([String].self, forKey: .platformFilters)
try super.init(from: decoder)
Expand Down
80 changes: 79 additions & 1 deletion Sources/XcodeProj/Objects/Configuration/BuildSettings.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,82 @@
import Foundation

/// Build settings.
public typealias BuildSettings = [String: Any]
public typealias BuildSettings = [String: BuildSetting]

private let yes = "YES"
private let no = "NO"

public enum BuildSetting: Sendable, Equatable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would also add some basic documentation.

case string(String)
case array([String])

public var stringValue: String? {
if case let .string(value) = self {
value
} else {
nil
}
}

public var boolValue: Bool? {
if case let .string(value) = self {
switch value {
case yes: true
case no: false
default: nil
Comment on lines +25 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Xcode's behavior is actually defaulting to false if the value is defined but it's not YES or NO. But conceptually, this feels better, so I'll let you decide what's better here 😅

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the purposes of this utility it strictly checking for boolean build settings as opposed to attempting to resolve a boolean value for the build setting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense!

}
} else {
nil
}
}

public var arrayValue: [String]? {
if case let .array(value) = self {
value
} else {
nil
}
}
}

extension BuildSetting: CustomStringConvertible {
public var description: String {
switch self {
case let .string(string):
string
case let .array(array):
array.joined(separator: " ")
}
}
}

extension BuildSetting: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let string = try container.decode(String.self)
self = .string(string)
} catch {
let array = try container.decode([String].self)
self = .array(array)
}
}
}

extension BuildSetting: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: String...) {
self = .array(elements)
}
}

extension BuildSetting: ExpressibleByStringInterpolation {
public init(stringLiteral value: StringLiteralType) {
self = .string(value)
}
}

extension BuildSetting: ExpressibleByBooleanLiteral {
public init(booleanLiteral value: Bool) {
self = .string(value ? yes : no)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public final class XCBuildConfiguration: PBXObject {
} else {
baseConfigurationReference = nil
}
buildSettings = try container.decode([String: Any].self, forKey: .buildSettings)
buildSettings = try container.decode(BuildSettings.self, forKey: .buildSettings)
name = try container.decode(.name)
try super.init(from: decoder)
}
Expand All @@ -75,16 +75,16 @@ public final class XCBuildConfiguration: PBXObject {
public func append(setting name: String, value: String) {
guard !value.isEmpty else { return }

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

switch existing {
case let string as String where string != value:
case let .string(string) where string != value:
let newValue = [string, value].joined(separator: " ")
buildSettings[name] = newValue
case let array as [String]:
buildSettings[name] = .string(newValue)
case let .array(array):
var newValue = array
newValue.append(value)
buildSettings[name] = newValue.uniqued()
buildSettings[name] = .array(newValue.uniqued())
default:
break
}
Expand Down
14 changes: 7 additions & 7 deletions Sources/XcodeProj/Objects/Project/PBXProj.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public final class PBXProj: Decodable {
public var objectVersion: UInt

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

/// Project root object.
var rootObjectReference: PBXObjectReference?
Expand All @@ -40,7 +41,7 @@ public final class PBXProj: Decodable {
public init(rootObject: PBXProject? = nil,
objectVersion: UInt = Xcode.LastKnown.objectVersion,
archiveVersion: UInt = Xcode.LastKnown.archiveVersion,
classes: [String: Any] = [:],
classes: [String: [String]] = [:],
objects: [PBXObject] = []) {
self.archiveVersion = archiveVersion
self.objectVersion = objectVersion
Expand Down Expand Up @@ -88,7 +89,7 @@ public final class PBXProj: Decodable {
rootObject: PBXProject? = nil,
objectVersion: UInt = Xcode.LastKnown.objectVersion,
archiveVersion: UInt = Xcode.LastKnown.archiveVersion,
classes: [String: Any] = [:],
classes: [String: [String]] = [:],
objects: PBXObjects
) {
self.archiveVersion = archiveVersion
Expand Down Expand Up @@ -116,7 +117,7 @@ public final class PBXProj: Decodable {
self.rootObjectReference = objectReferenceRepository.getOrCreate(reference: rootObjectReference, objects: objects)
objectVersion = try container.decodeIntIfPresent(.objectVersion) ?? 0
archiveVersion = try container.decodeIntIfPresent(.archiveVersion) ?? 1
classes = try container.decodeIfPresent([String: Any].self, forKey: .classes) ?? [:]
classes = try container.decodeIfPresent([String: [String]].self, forKey: .classes) ?? [:]
let objectsDictionary: [String: PBXObjectDictionaryEntry] = try container.decodeIfPresent([String: PBXObjectDictionaryEntry].self, forKey: .objects) ?? [:]

for entry in objectsDictionary {
Expand Down Expand Up @@ -261,10 +262,9 @@ extension PBXProj {

extension PBXProj: Equatable {
public static func == (lhs: PBXProj, rhs: PBXProj) -> Bool {
let equalClasses = NSDictionary(dictionary: lhs.classes).isEqual(to: rhs.classes)
return lhs.archiveVersion == rhs.archiveVersion &&
lhs.archiveVersion == rhs.archiveVersion &&
lhs.objectVersion == rhs.objectVersion &&
equalClasses &&
lhs.classes == rhs.classes &&
lhs.objects == rhs.objects
}
}
Expand Down
37 changes: 20 additions & 17 deletions Sources/XcodeProj/Objects/Project/PBXProject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,20 @@ public final class PBXProject: PBXObject {

/// Project attributes.
/// Target attributes will be merged into this
public var attributes: [String: Any]
public var attributes: [String: ProjectAttribute]

/// Target attribute references.
var targetAttributeReferences: [PBXObjectReference: [String: Any]]
var targetAttributeReferences: [PBXObjectReference: [String: ProjectAttribute]]

/// Target attributes.
public var targetAttributes: [PBXTarget: [String: Any]] {
public var targetAttributes: [PBXTarget: [String: ProjectAttribute]] {
set {
targetAttributeReferences = [:]
for item in newValue {
targetAttributeReferences[item.key.reference] = item.value
}
} get {
var attributes: [PBXTarget: [String: Any]] = [:]
var attributes: [PBXTarget: [String: ProjectAttribute]] = [:]
for targetAttributeReference in targetAttributeReferences {
if let object: PBXTarget = targetAttributeReference.key.getObject() {
attributes[object] = targetAttributeReference.value
Expand Down Expand Up @@ -176,7 +176,7 @@ public final class PBXProject: PBXObject {
/// - Parameters:
/// - attributes: attributes that will be set.
/// - target: target.
public func setTargetAttributes(_ attributes: [String: Any], target: PBXTarget) {
public func setTargetAttributes(_ attributes: [String: ProjectAttribute], target: PBXTarget) {
targetAttributeReferences[target.reference] = attributes
}

Expand Down Expand Up @@ -321,8 +321,8 @@ public final class PBXProject: PBXObject {
projectRoots: [String] = [],
targets: [PBXTarget] = [],
packages: [XCRemoteSwiftPackageReference] = [],
attributes: [String: Any] = [:],
targetAttributes: [PBXTarget: [String: Any]] = [:]) {
attributes: [String: ProjectAttribute] = [:],
targetAttributes: [PBXTarget: [String: ProjectAttribute]] = [:]) {
self.name = name
buildConfigurationListReference = buildConfigurationList.reference
self.compatibilityVersion = compatibilityVersion
Expand Down Expand Up @@ -417,10 +417,12 @@ public final class PBXProject: PBXObject {
let packageRefeferenceStrings: [String] = try container.decodeIfPresent(.packageReferences) ?? []
packageReferences = packageRefeferenceStrings.map { referenceRepository.getOrCreate(reference: $0, objects: objects) }

var attributes = try (container.decodeIfPresent([String: Any].self, forKey: .attributes) ?? [:])
var targetAttributeReferences: [PBXObjectReference: [String: Any]] = [:]
if let targetAttributes = attributes[PBXProject.targetAttributesKey] as? [String: [String: Any]] {
targetAttributes.forEach { targetAttributeReferences[referenceRepository.getOrCreate(reference: $0.key, objects: objects)] = $0.value }
var attributes = try (container.decodeIfPresent([String: ProjectAttribute].self, forKey: .attributes) ?? [:])
var targetAttributeReferences: [PBXObjectReference: [String: ProjectAttribute]] = [:]
if case let .attributeDictionary(targetAttributes) = attributes[PBXProject.targetAttributesKey] {
for targetAttribute in targetAttributes {
targetAttributeReferences[referenceRepository.getOrCreate(reference: targetAttribute.key, objects: objects)] = targetAttribute.value
}
attributes[PBXProject.targetAttributesKey] = nil
}
self.attributes = attributes
Expand Down Expand Up @@ -562,16 +564,17 @@ extension PBXProject: PlistSerializable {
dictionary["packageReferences"] = PlistValue.array(finalPackageReferences)
}

var plistAttributes: [String: Any] = attributes
var plistAttributes: [String: ProjectAttribute] = attributes

// merge target attributes
var plistTargetAttributes: [String: Any] = [:]
var plistTargetAttributes: [String: [String: ProjectAttribute]] = [:]
for (reference, value) in targetAttributeReferences {
plistTargetAttributes[reference.value] = value.mapValues { value in
(value as? PBXObject)?.reference.value ?? value
}
plistTargetAttributes[reference.value] = value
}

if !plistTargetAttributes.isEmpty {
plistAttributes[PBXProject.targetAttributesKey] = .attributeDictionary(plistTargetAttributes)
}
plistAttributes[PBXProject.targetAttributesKey] = plistTargetAttributes.isEmpty ? nil : plistTargetAttributes

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

Expand Down
Loading