Repository navigation
fix: identify nameless copy files and script phases in project.xcproj - #1181
Merged
pepicrft merged 1 commit intoSep 18, 2026
Merged
Conversation
freddi-kit
approved these changes
Sep 18, 2026
pepicrft
force-pushed
the
fix/xcproj-nameless-copy-and-script-phases
branch
from
September 18, 2026 08:26
63d1c5e to
2badffe
Compare
Contributor
|
Heads up that I rebased this onto main and force pushed, since #1182 landed first and the two tests were added at the same spot in I also validated this against Xcode 27.2 here, since you mentioned you had 27.0. Your table holds exactly: with a single nameless phase of each kind, 27.2 rejects Waiting on CI and then I will merge. Thanks for digging into this one. |
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.
There is no issue for this one either. I hit it while adding
project.xcprojsupport to XcodeGen.Short description 📝
A target with a copy files or script build phase that has no name produces a project Xcode refuses to open:
A file's membership is written as a reference to the phase that owns it. For a phase with no name that reference is the kind on its own,
"App/copy". Xcode resolves a bare kind against the one phase of that kind a target holds, and a target can hold any number of copy files and script phases, so it rejects the reference. It does so even when the target has exactly one copy phase, which is what makes this easy to misread as an ambiguity problem.I went through the kinds one at a time, each on a target holding a single nameless phase of that kind with one file in it:
"App/<kind>"compile-sourcesframeworksresourcesheaderscopyscriptExactly the two kinds a target can hold more than one of. Adding a name by hand to the rejected file makes it open, so a name or an identifier is what Xcode wants, and
PBXCopyFilesBuildPhase.nameis optional and usually nil in aproject.pbxproj.Both Apple's schema validator and this library's round trip accept the output, so nothing catches it before Xcode does.
Whether a generator hits this comes down to whether it names its phases. Tuist does not:
copyFilesandscriptsboth take a required name in its manifest, all ten phase constructions inBuildPhaseGeneratorpass one, and a project generated with tuist 4.210.0-canary.9 writes"App/copy/Copy Headers"and opens. XcodeGen does, because the copy files phase it builds for acopyFilessource has no name at all.Solution 📦
indexBuildPhasesalready collects the phases that need an identifier because a name would not pick them out. A nameless phase of a repeatable kind joins them:isRepeatableKindswitches overBuildPhaseexhaustively rather than falling through adefault, so a kind added later has to be classified instead of silently treated as unique.Nothing else changes.
needsObjectIDstill requires the phase to be one something points at, so a nameless script phase that owns no files stays inline with no identifier, which is whataScriptPhaseWithoutANameStaysNamelesscovers. Phases that already resolve by name keep doing so, and the output for the four unique kinds is untouched.I considered giving a nameless copy phase a default name, the way Xcode labels one in its UI. That invents data the property list never had, and it would come back through the decoder as a named phase, so the round trip would no longer describe the same project.
Implementation 👩💻👨💻
XCProjEncoder.indexBuildPhases.isRepeatableKind, an exhaustive switch overBuildPhase.namelessRepeatablePhasesCarryAnIdentifiertoXCProjEncoderTests.Testing
The new test is parameterized over the six kinds this library can encode. For each one it builds a target holding a single nameless phase of that kind with one file, encodes, and checks whether the membership came out as
"App/<kind>"or as an identifier. It expects an identifier forcopyandscriptand a name for the other four, then decodes the result and checks the membership survived either way. Onmainit fails oncopyandscriptand passes on the other four, so it pins the boundary and not just the fix.The schema validator accepts both spellings, so the unit test cannot tell us whether Xcode agrees. I checked the output against Xcode 27.0 as well:
.xcodeprojper kind, each written throughXcodeProjwith a single nameless phase. Before the changexcodebuild -listrejectscopyandscript; after it, all six open.TestProjectfixture, which is what led me here, regenerated asproject.xcproj. Every "Could not uniquely resolve the build phase name" error is gone. It now stops on an unrelatedInvalid reference: “Projects/AnotherProject”from a cross project target dependency, which I am looking at separately.383 XCTest and 84 Swift Testing tests pass on macOS, and
mise run lintpasses. I did not run the Linux or iOS jobs locally, and I have Xcode 27.0 here rather than 27.2, so the table above is what 27.0 accepts and rejects.