Repository navigation
fix: resolve the references project.xcproj left undefined - #1184
Merged
Merged
Conversation
Four ways an encoded project could refer to something the file never defines, each of which makes Xcode refuse to open it: - A name path built through a group that shares its name with a sibling. The ambiguity now travels down, so descendants fall back to identifiers the same way their ancestor does. - A products group that is the main group. The root carries no identifier, so it is addressed by the empty name path, which is what Xcode writes. - An element no group in the tree holds, such as a subproject's file reference. These are collected into a "Recovered References" group, the same as Xcode's own converter does. - The decoder could not resolve the empty name path either, so the products group was dropped on the way back in. Apple's schema validator and the round trip both resolve a reference against the graph that produced it, so neither noticed. Verified by opening the generated projects with Xcode 27.2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Four ways a
project.xcprojcould come out referring to something the file never defines, so Xcode refused to open it. I hit these while checking #1179, #1180, #1181 and #1182 against a local Xcode 27.2, by writing projects with the library and opening each one withxcodebuild -list.None of them are caught by what we have today. Apple's schema validator accepts every one of these files, and our own round trip resolves a reference against the same graph that produced it, so neither ever looks at whether the reference resolves within the file. Only Xcode does.
What was wrong
1. A name path through an ambiguous group.
indexNamePathsalready notices that two sibling groups spelled the same way cannot be told apart, and gives them identifiers. It then kept walking and used that same ambiguous name as the prefix for their children, so a child came out as"Products/App.app"and resolved to nothing:A name path is only as good as every component in it, so the ambiguity now travels down and everything below an ambiguous group falls back to an identifier too. This is not exotic: a project with several subproject references has one group named
Productsper subproject, which is exactly the shapeXcode16ProjectReferenceOrder/Wrong.xcodeprojhas.2. A products group that is the main group.
productRefGroup = mainGroupis how a project without a separate products group spells it, andFixtures/iOS/ProjectWithoutProductsGroup.xcodeprojis one. We wrote"products-group": "id:<mainGroup>", but the main group is the root of the tree rather than an entry in it, so it never carries an identifier and nothing resolved:I checked what Xcode itself writes by converting the fixture with
xcodebuild -convert-project "Xcode Project", and it writes"products-group": []. The empty name path is how the root group is addressed, so that is what we write now.3. An element no group in the tree holds. A subproject's file reference is named by
projectReferencesand by a dependency's container portal, but nothing requires a group to list it as a child, and inXcode16ProjectReferenceOrder/{Test,Wrong}.xcodeprojnone does. We referred to it by identifier and then never wrote the element, so again nothing resolved.Xcode's own converter collects these into a group called
Recovered Referencesand addresses them by name through it. I went with the same thing rather than inventing our own spelling, and our output for that group is now byte identical to Xcode's. Worth a second opinion on this one, since synthesizing a group does add structure theproject.pbxprojnever had. The alternative I considered was throwing instead of writing a file we know Xcode will reject, but that trades a bad file for a hard failure on projects that Xcode is perfectly happy to open once the reference is recovered.Because that group is synthesized rather than held by the tree, the sibling collision count never sees it, so a project that already has a top level group of that name would have hit problem 1. Both sides now fall back to identifiers in that case.
4. The decoder could not resolve the empty name path either.
lookUp(namePath:)started with no current element, so a path with no components returned nil andproductsGroupwas quietly dropped on the way back in. It now starts at the root group.That one mattered more than it looks. Before this change, a
preserveAllround trip ofProjectWithoutProductsGroupdid not just produce a file Xcode would reject, it threw:So that shape was broken in both directions and we could not read back our own output.
How it was verified
Everything below is against Xcode 27.2, opening real generated projects with
xcodebuild -list, since that is the only thing that catches any of this.Fixtures/that Xcode can read now opens.ProjectWithoutProductsGroup,Xcode16ProjectReferenceOrder/TestandXcode16ProjectReferenceOrder/Wrongwere rejected before and open now. The fourProjectWith*SwiftPackage*fixtures still stop at package resolution, which is a missing sibling directory in my scratch copy and not a project read.Recovered Referencesgroup. All three were rejected before and open now.id:reference in the encoded file is defined somewhere in the same file. It fails on all three fixtures without the source change and would have caught all four of these.ProjectWithoutProductsGroupis added to thepreserveAllidentifier round trip, where it used to throw.mise run lintpasses.One thing I deliberately left alone:
Wrong.xcodeprojlosesApp-Info.pliston a round trip. It sits in anInfoPlistsgroup that nothing references, and Xcode's own conversion drops it too, so an unreachable element simply has no place in a format that stores the reachable tree. That is why that fixture is not in the strict identifier round trip.馃 Generated with Claude Code