Repository navigation
fix: generate deterministic references for build phase membership exception sets - #1201
Merged
Merged
Conversation
…eption sets A PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet's reference is derived from its build phase's reference, but exception sets were fixed while walking the groups, before the targets pass makes build phase references permanent. A build phase with a temporary reference therefore fed a random TEMP_ UUID into the hash, so the same project got different references on every write. Exception sets of that kind are now fixed after the targets. When the build phase already has a permanent reference the identifiers are the same as before, so existing output doesn't change. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a0d7-4cfc-76a2-8b2d-344510b78c8c Co-authored-by: Amp <amp@ampcode.com>
pepicrft
force-pushed
the
fix/deterministic-synchronized-group-references
branch
from
November 4, 2026 15:52
87713df to
5f74120
Compare
pepicrft
added a commit
to tuist/tuist
that referenced
this pull request
Nov 4, 2026
9.17.7 makes writing pbxproj files cheaper (tuist/XcodeProj#1200) and 9.17.8 makes build phase membership exception set references deterministic (tuist/XcodeProj#1201). AEXML moves to 4.7.3, the minimum XcodeProj now requires.
pepicrft
added a commit
to tuist/tuist
that referenced
this pull request
Nov 4, 2026
9.17.7 makes writing pbxproj files cheaper (tuist/XcodeProj#1200) and 9.17.8 makes build phase membership exception set references deterministic (tuist/XcodeProj#1201). AEXML moves to 4.7.3, the minimum XcodeProj now requires.
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.
What changed
ReferenceGeneratornow generates the references ofPBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSetobjects after the targets pass instead of while walking the groups. The PR also adds a regression test showing that the same project gets the same references when generated twice. The fixture round-trip test from #1200 now also checks that writing each fixture twice gives the same bytes; that check had to be left out of #1200 because of this bug.Root cause
An exception set's reference is an MD5 of its identifiers, and those include its build phase's reference value. The generator fixes synchronized root groups and their exception sets while walking the main group, but build phases only get permanent references later, in
generateTargetReferences. When the build phase was still temporary, the hash input contained its randomTEMP_<UUID>. That made the exception set's reference, and every list pointing at it, different on every write.It shows up whenever such an exception set and its build phase are new: a project built in code, or one written after
invalidateUUIDs(). For example, writingFixtures/SynchronizedRootGroupsafterinvalidateUUIDs()gave a different reference for its build phase exception set on every run.Approach
When the generator reaches a build phase membership exception set, it records the set along with the identifiers it would have used. Once all targets and their build phases have permanent references, it fixes them.
I preferred this to dropping the build phase reference from the identifiers. That would have changed the reference of every existing exception set, while deferring leaves the inputs unchanged whenever the build phase was already permanent. Build-file exception sets already depend on their target's reference, which is fixed before the groups are walked, so they were deterministic and are left alone.
Impact
.pbxprojfiles see no change: their build phases already have permanent references, so the identifiers are the same as before.invalidateUUIDs().Validation
swift test: 384 XCTest and 103 Swift Testing tests pass.SynchronizedRootGroupswithout the fix and passes on all six fixtures with it.test_projectWithFilesystemSynchronizedRootGroupWithBuildPhaseException_generatesDeterministicIdentifiersfails without the fix and passes with it.invalidateUUIDs()and got identical bytes every time:SynchronizedRootGroups,iOS/Project,Xcode16/Test,AppWithExtensionsandTargetWithCustomBuildRules. Before the fix,SynchronizedRootGroupsdiffered on every run.mise run lint: SwiftFormat reports 0 files to format, and SwiftLint has no new warnings.I found this while writing tests for #1200.