What happened
XcodeGraphMapper.map(at:) aborts the process with a trap when a project contains a local Swift package that is also present as a folder reference in the project navigator:
Swift/NativeDictionary.swift:792: Fatal error: Duplicate values for key: '/path/to/App/Packages/MyPackage'
Thread 2 crashed:
1 specialized _NativeDictionary.merge<A>(_:isUnique:uniquingKeysWith:)
2 Dictionary.init<A>(uniqueKeysWithValues:)
3 closure #1 in XcodeGraphMapper.extractPackages(from:)
XcodeGraphMapper.swift:237
4 Dictionary.compactMapValues<A>(_:)
5 XcodeGraphMapper.extractPackages(from:) XcodeGraphMapper.swift:235
6 XcodeGraphMapper.buildGraph(from:) XcodeGraphMapper.swift:136
7 XcodeGraphMapper.map(at:) XcodeGraphMapper.swift:82
Because this is a trap rather than a thrown error, consumers cannot recover from it — the whole process dies. In our case that takes an Xcode prebuild phase with it, and the only actionable output the user gets is the line above.
Why
Local packages are collected from two independent places in PBXProjectMapper.map(...):
let localPackages = try pbxProject.localPackages.compactMap {
try packageMapper.map(package: $0, sourceDirectory: sourceDirectory)
} + localPackagePaths.map { .local(path: $0) }
pbxProject.localPackages — the XCLocalSwiftPackageReference entries.
localPackagePaths — from collectAllPackages(from:xcodeProj:), which walks the group tree and treats every PBXFileReference pointing at a directory that contains a Package.swift as a package.
A local package that is declared as a package reference and shown as a folder in the navigator is therefore mapped twice, with the same path. extractPackages then keys packages by URL and requires those keys to be unique:
// XcodeGraphMapper.swift:235
private func extractPackages(from projects: [AbsolutePath: Project]) -> [AbsolutePath: [String: Package]] {
projects.compactMapValues { project in
guard !project.packages.isEmpty else { return nil }
return Dictionary(uniqueKeysWithValues: project.packages.map { ($0.url, $0) })
}
}
Reproduction
Any project that carries both entries for one package. A project generated by XcodeGen from
packages:
MyPackage:
path: Packages/MyPackage
targets:
App:
dependencies:
- package: MyPackage
product: MyPackage
produces exactly that — the XCLocalSwiftPackageReference that links the package, plus a folder reference so it appears in the navigator:
/* Begin PBXFileReference section */
0F5C85DC… /* MyPackage */ = {isa = PBXFileReference; lastKnownFileType = folder; name = MyPackage; path = Packages/MyPackage; sourceTree = SOURCE_ROOT; };
/* End PBXFileReference section */
/* Begin XCLocalSwiftPackageReference section */
FE0DA337… /* XCLocalSwiftPackageReference "Packages/MyPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Packages/MyPackage;
};
/* End XCLocalSwiftPackageReference section */
Removing the folder reference alone makes the crash disappear, which is what pinpoints the duplicate.
Suggested fix
Two places, either of which prevents the trap:
PBXProjectMapper: de-duplicate before constructing the project, e.g. keep the first Package per URL when combining localPackages with localPackagePaths — the two collection paths genuinely describe the same package.
XcodeGraphMapper.extractPackages: use Dictionary(_:uniquingKeysWith:) instead of uniqueKeysWithValues:. Even with the de-duplication above, a project file is user-supplied input; aborting the process on unexpected content is harsh for a library.
Happy to open a PR for either or both if that is welcome.
Environment
- XcodeGraph 1.34.5 (from the archived
tuist/XcodeGraph; the same code is in this repository at
cli/Sources/XcodeGraph/Sources/XcodeGraphMapper/, unchanged as of today — extractPackages at
line 237 and the two collection paths in PBXProjectMapper around line 109)
- macOS 26.6.1, Xcode 27.0
- Reached through Shark, which uses
XcodeGraphMapper to find a target’s resources
Filed here because tuist/XcodeGraph is archived and read-only.