Summary
Calling removeFromProject() on a PBXNativeTarget does not cascade to PBXFileSystemSynchronizedBuildFileExceptionSet objects that reference that target. The orphaned exception sets retain a nil target reference, which crashes during serialization when generating the plist annotation (target.name on nil).
Minimal reproduction
Setup (Xcode 16+):
- Create a project with two app targets: AppA and AppB
- Create a folder
SharedSrc/ containing Shared.swift and Info.plist
- In Xcode, drag
SharedSrc/ into the project as a synchronized folder reference, with both targets checked
- Select
Info.plist in the navigator, open the File Inspector, and uncheck AppB from Target Membership
This creates a PBXFileSystemSynchronizedBuildFileExceptionSet with target = AppB inside the SharedSrc synchronized root group — meaning "exclude Info.plist from AppB's build."
Reproduction (Ruby xcodeproj gem v1.27.0):
require "xcodeproj"
proj = Xcodeproj::Project.open("Repro.xcodeproj")
# Removing AppA works fine (not referenced by any exception set)
proj.targets.find { |t| t.name == "AppA" }.remove_from_project
proj.save # => OK
# Removing AppB crashes (referenced by the exception set)
proj.targets.find { |t| t.name == "AppB" }.remove_from_project
proj.save # => CRASH
Crash:
PBXFileSystemSynchronizedBuildFileExceptionSet#display_name: undefined method 'name' for nil (NoMethodError)
"Exceptions for \"#{GroupableHelper.parent(self).display_name}\" in \"#{target.name}\" target"
What happens
removeFromProject() on the target removes the PBXNativeTarget object, its build configuration list, and its build phases
- It does not remove
PBXFileSystemSynchronizedBuildFileExceptionSet objects whose target property pointed to the removed target
- On save, the serializer calls
display_name on each exception set, which accesses target.name — now nil — and crashes
What should happen
removeFromProject() on a PBXNativeTarget should also remove any PBXFileSystemSynchronizedBuildFileExceptionSet whose target references the removed target. Xcode's UI does this cleanup when deleting a target — the programmatic API should match.
Additional incomplete cascading
Beyond exception sets, removeFromProject() also leaves behind:
PBXFileReference for the target's product (.app, .xctest)
- The product reference in the Products group
PBXGroup entries for the target's source directory
These don't crash serialization but leave orphaned entries in the project file.
Workaround
Clean up orphaned exception sets manually before saving:
proj.objects.select { |o|
o.isa == "PBXFileSystemSynchronizedBuildFileExceptionSet" &&
o.respond_to?(:target) && o.target.nil?
}.each { |o| o.remove_from_project }
Environment
- Ruby xcodeproj gem 1.27.0
- Project created with Xcode 26 beta (uses synchronized folders /
PBXFileSystemSynchronizedRootGroup)
- Also reproduced via xcodeproj-mcp-server v1.5.0, which uses the Swift tuist/XcodeProj library — same crash (exit code 133 / SIGTRAP, the Swift equivalent of nil access)
Summary
Calling
removeFromProject()on aPBXNativeTargetdoes not cascade toPBXFileSystemSynchronizedBuildFileExceptionSetobjects that reference that target. The orphaned exception sets retain a niltargetreference, which crashes during serialization when generating the plist annotation (target.nameon nil).Minimal reproduction
Setup (Xcode 16+):
SharedSrc/containingShared.swiftandInfo.plistSharedSrc/into the project as a synchronized folder reference, with both targets checkedInfo.plistin the navigator, open the File Inspector, and uncheck AppB from Target MembershipThis creates a
PBXFileSystemSynchronizedBuildFileExceptionSetwithtarget = AppBinside theSharedSrcsynchronized root group — meaning "exclude Info.plist from AppB's build."Reproduction (Ruby xcodeproj gem v1.27.0):
Crash:
What happens
removeFromProject()on the target removes thePBXNativeTargetobject, its build configuration list, and its build phasesPBXFileSystemSynchronizedBuildFileExceptionSetobjects whosetargetproperty pointed to the removed targetdisplay_nameon each exception set, which accessestarget.name— now nil — and crashesWhat should happen
removeFromProject()on aPBXNativeTargetshould also remove anyPBXFileSystemSynchronizedBuildFileExceptionSetwhosetargetreferences the removed target. Xcode's UI does this cleanup when deleting a target — the programmatic API should match.Additional incomplete cascading
Beyond exception sets,
removeFromProject()also leaves behind:PBXFileReferencefor the target's product (.app,.xctest)PBXGroupentries for the target's source directoryThese don't crash serialization but leave orphaned entries in the project file.
Workaround
Clean up orphaned exception sets manually before saving:
Environment
PBXFileSystemSynchronizedRootGroup)