Swift Package Registry overriding local dependency in Tuist-generated project
Hi folks — we’re running into an issue where a Swift Package Registry dependency appears to be taking precedence over a local package dependency when generating an Xcode project with Tuist.
I’m trying to determine whether:
-
this is expected behavior (and we should adjust our setup), or
-
this is a Tuist bug (happy to open a PR if so).
Background
We use a Swift Package Registry
We publish private dependencies through our own Swift Package Registry to improve package resolution and avoid git-based dependencies.
Repo/module structure
Our convention is 1 repo == 1 Swift package/module. Each repo has a standard Package.swift at the root:
import PackageDescription
let package = Package(
name: "MyModule",
platforms: [
.iOS(.v17)
],
products: [
.library(
name: "MyModule",
targets: ["MyModule"]
),
],
dependencies: [ /* ... */ ],
targets: [
.target(
name: "MyModule",
dependencies: [ /* ... */ ]
)
]
)
Demo app
Each module repo also contains a demo app target used for local development, so contributors don’t need to build/run the full app.
The demo app depends on MyModule locally, plus additional packages. We express demo-app-only dependencies via Tuist/Package.swift:
import PackageDescription
let package = Package(
name: "MyModuleExtraPackages",
platforms: [
.iOS(.v17)
],
dependencies: [
.package(path: "../"), // local MyModule
.package(id: "company.MyDependency", from: "1.0.0"),
// more demo app deps...
]
)
Folder layout:
├── MyModule
│ └── Project.swift
├── MyModuleDemo
│ └── Project.swift
├── Package.swift
├── Tuist
│ └── Package.swift
├── Tuist.swift
└── Workspace.swift
The problem
If MyDependency (fetched from the registry) itself depends on MyModule, the generated Xcode project for MyModuleDemo ends up using the registry version of MyModule instead of the local path ../.
In other words: even though the demo app explicitly includes MyModule as a local package, the resolved graph uses the registry copy.
What I’m seeing
SPM produces a workspace-state.json that includes both the local and registry references, roughly like:
{
"dependencies": [
{
"packageRef": {
"identity": "ios-mymodule",
"kind": "fileSystem",
"location": "/Users/fdiaz/ios-MyModule",
"name": "MyModule"
}
},
{
"packageRef": {
"identity": "company.MyModule",
"kind": "registry",
"location": "company.MyModule",
"name": "company.MyModule"
},
"state": {
"name": "registryDownload",
"version": "1.30.0"
}
}
]
}
My expectation is that the local dependency should override the non-local one when they refer to the same package/module, based on SwiftPM’s stated behavior:
“A local package dependency will override any regular dependency in the package graph that has the same package name.”
Why I think Tuist may be involved
Looking at Tuist’s package graph loader, it seems to prioritize registry packages above other kinds, which would explain why the registry version wins in our case:
Question
Is this expected behavior on Tuist’s side, or does this look like a bug where local packages should take precedence over registry packages when there’s a name collision / override scenario?
If it’s a bug, I’m happy to help with a PR — just want to confirm the intended behavior first.