Fix non-deterministic/incorrect serialization of nested target attributes (e.g. SystemCapabilities) - #1639
Open
imadaan wants to merge 3 commits into
Conversation
…utes
ProjectAttribute.init(any:) only handled [String], PBXObject, and fell
back to .string("\(value)") for everything else. Nested dictionary
attributes such as target.attributes.SystemCapabilities therefore never
produced the existing .attributeDictionary case, and instead were
serialized as a raw Swift Dictionary interpolated into a string, e.g.:
SystemCapabilities = "[\"com.apple.Push\": [\"enabled\": 1], ...]";
Dictionary.description iterates in the process's random hash-seed
order, so the exact same input could produce a different key order on
every run, producing spurious project.pbxproj diffs. It also isn't a
valid nested plist dictionary, which some tooling flags as needing a
project 'fix'.
This adds a case that maps [String: [String: Any]] values (the shape
SystemCapabilities and similar nested attributes take) to
.attributeDictionary, so they serialize as a proper nested plist
dictionary. PBXProjEncoder already writes dictionary keys in sorted
order, so this also makes the output deterministic across runs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
target.attributesvalues that are nested dictionaries — most commonlySystemCapabilities(e.g.com.apple.Push,com.apple.Keychain) — are never converted to the existingProjectAttribute.attributeDictionarycase. Instead,ProjectAttribute.init(any:)only handles[String]andPBXObject, and falls back to.string("\(value)")for anything else:For a nested dictionary like:
this produces a
project.pbxprojentry like:i.e. a raw Swift
Dictionaryinterpolated into a string, rather than a proper nested plist dictionary. This has two consequences:SystemCapabilities— Xcode's own tooling expects a nested dictionary here, not a quoted string.Dictionary.descriptioniterates keys in the process's random hash-seed order, so regenerating the exact sameproject.ymlcan flip the key order inside that string between runs (e.g.PushbeforeKeychain, or vice versa), producing spurious diffs in version control on everyxcodegen generate, even though nothing in the spec changed.Fix
Add a case to
ProjectAttribute.init(any:)that recognizes the[String: [String: Any]]shape (which is exactly what.attributeDictionary([String: [String: ProjectAttribute]])expects) and maps it recursively instead of falling through to.string(...):PBXProjEncoder(in XcodeProj) already sorts dictionary keys alphabetically when writing plist dictionaries, so routing through.attributeDictionaryalso makes the output deterministic across runs, in addition to producing correct nested plist structure.Testing
generates nested target attributes such as SystemCapabilities) inProjectGeneratorTests.swiftasserting thatSystemCapabilitiesproduces a.attributeDictionary(not a.string) with the expected nested values.Repro
Given a target with:
Running
xcodegen generaterepeatedly (without pinningSWIFT_DETERMINISTIC_HASHING=1) produces a flip-flopping diff purely in the order of keys inside theSystemCapabilitiesstring value. After this fix, it serializes as a proper nested dictionary and is stable across runs.