diff --git a/CHANGELOG.md b/CHANGELOG.md index f4886713..3ce41097 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## Master + +### Fixed +- Fix nested target attributes (e.g. `attributes.SystemCapabilities`) being serialized as a stringified Swift `Dictionary` description instead of a proper nested plist dictionary, which also caused non-deterministic key ordering in generated `project.pbxproj` files across runs @imadaan + ## 2.46.0 ### Added diff --git a/Sources/ProjectSpec/Settings.swift b/Sources/ProjectSpec/Settings.swift index f0a5344a..05cc48e8 100644 --- a/Sources/ProjectSpec/Settings.swift +++ b/Sources/ProjectSpec/Settings.swift @@ -144,6 +144,14 @@ extension ProjectAttribute { self = .array(array) } else if let object = value as? PBXObject { self = .targetReference(object) + } else if let dictionary = value as? [String: [String: Any]] { + // e.g. `SystemCapabilities`, which is a dictionary of dictionaries. + // Without this case, such values fall through to the `.string("\(value)")` + // branch below, which interpolates the raw Swift `Dictionary`. Its + // `description` iterates in the process's random hash-seed order, so the + // same input can non-deterministically produce a different key order (and + // isn't a valid nested plist dictionary in the first place). + self = .attributeDictionary(dictionary.mapValues { $0.mapValues { ProjectAttribute(any: $0) } }) } else { self = .string("\(value)") } diff --git a/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift b/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift index dd07d770..600e4d61 100644 --- a/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift @@ -324,6 +324,27 @@ class ProjectGeneratorTests: XCTestCase { try expect(targetAttributes[appTarget]?["DevelopmentTeam"]?.stringValue) == "123" } + $0.it("generates nested target attributes such as SystemCapabilities") { + var appTargetWithAttributes = app + appTargetWithAttributes.attributes = [ + "SystemCapabilities": [ + "com.apple.Push": ["enabled": 1], + "com.apple.Keychain": ["enabled": 1], + ], + ] + let project = Project(name: "test", targets: [appTargetWithAttributes, framework]) + let pbxProject = try project.generatePbxProj() + + let targetAttributes = try unwrap(pbxProject.projects.first?.targetAttributes) + let appTarget = try unwrap(pbxProject.targets(named: app.name).first) + + guard case let .attributeDictionary(systemCapabilities)? = targetAttributes[appTarget]?["SystemCapabilities"] else { + throw failure("Expected SystemCapabilities to be a nested attribute dictionary, not a string") + } + try expect(systemCapabilities["com.apple.Push"]?["enabled"]?.stringValue) == "1" + try expect(systemCapabilities["com.apple.Keychain"]?["enabled"]?.stringValue) == "1" + } + $0.it("generates platform version") { let target = Target(name: "Target", type: .application, platform: .watchOS, deploymentTarget: "2.0") let project = Project(name: "", targets: [target], options: .init(deploymentTarget: DeploymentTarget(iOS: "10.0", watchOS: "3.0")))