Swift Package Manager Deduplication#
Overview#
File: cli/Sources/TuistLoader/Loaders/SwiftPackageManagerGraphLoader.swift
When SPM resolves a workspace, the same package can appear from multiple sources β a local path checkout, a Swift Package Registry entry, and a remote source-control (SCM) URL can all resolve to what is logically the same dependency. SwiftPackageManagerGraphLoader contains explicit deduplication logic to collapse these duplicates into a single entry before building the DependenciesGraph.
How Deduplication Works#
After loading all resolved dependencies from workspace-state.json into [SwiftPackageManagerResolvedPackageInfo], the loader groups packages by a computed key and then picks one winner per group .
Grouping Key#
The grouping key is computed at lines 214β221:
- Registry packages (
kind == "registry"): key is the last component ofname, split on.and lowercased. Registry packages use a scoped identifier likescope.package-name, soAlamofire.Alamofireβalamofire. - All other packages (local, SCM): key is
name.lowercased().
Precedence Order#
Within each group, the winner is selected by this priority :
| Priority | Kinds |
|---|---|
| 1 β Local | local, fileSystem, localSourceControl |
| 2 β Registry | registry |
| 3 β SCM | remoteSourceControl, remote, others |
The local-over-registry behavior matches the Swift Package Manager's own stated semantics: "A local package dependency will override any regular dependency in the package graph that has the same package name."
The helper isLocalDependencyKind(_:) centralizes which string values count as "local."
Package Identity vs. Package Name#
There is an important distinction in how packages are identified in this code:
id(line 192):dependency.packageRef.identity.lowercased()β this is the URL-based identity assigned by SPM, unique to a specific remote URL or registry coordinate .name(line 193):dependency.packageRef.nameβ this is the human-readable name declared inPackage.swift; it is not guaranteed to be unique across different packages.
The deduplication groups by name, not by URL identity. This is intentional for the registry/SCM case (both refer to the same logical package from different transports), but it creates a known limitation described below.
Known Limitation: Same Name, Different URLs (Issue #11867)#
Because grouping uses name.lowercased(), two different packages that happen to declare the same name: field in their Package.swift will be incorrectly merged, silently dropping one of them. The discarded package's products become unreachable, surfacing as:
<ProductName> is not a valid configured external dependency
A real-world example is migrating from danielgindi/Charts (v5, name "DGCharts") alongside ChartsOrg/Charts (v6, also name "DGCharts") β both share the same name: field but are distinct packages at different URLs.
The winner within a same-name group is nondeterministic because the upstream concurrentMap does not preserve insertion order . The suggested fix is to include URL identity in the grouping key so only packages from the same source are merged.
History#
- PR #7518 β Introduced the original registry-vs-SCM deduplication to prevent the same package from appearing twice when resolved through both transports. Used a filter-based approach (keep non-registry if it exists).
- PR #9540 (fix(cli): Prioritize local packages over registry versions) β Extended the logic to cover local packages. Replaced the binary registry-filter with the current three-tier grouping+precedence approach. Motivated by a community report where a registry package was overriding a local path dependency in the generated project.
- Issue #11867 β Open bug: name-based grouping incorrectly deduplicates distinct packages that share a
name:field.
Key Entry Points#
| Symbol | Location | Purpose |
|---|---|---|
SwiftPackageManagerGraphLoader.loadUnsafe(...) | lines 119β334 | Main loading logic; contains deduplication block |
| Deduplication block | lines 202β232 | Grouping + precedence selection |
isLocalDependencyKind(_:) | lines 336β338 | Classifies local, fileSystem, localSourceControl as local |
SwiftPackageManagerResolvedPackageInfo | lines 446β454 | Internal model: holds id, name, kind, folder, hash |