Swift Package Registry overriding local dependency in Tuist-generated project

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.”

Package Manager Local Dependencies

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.

Hey @fdiaz :waving_hand:

thanks for flagging this. I’m a bit surprised that SwiftPM includes references to both the registry package and the local package, when they are the same package.

But if that’s the case, which it seems to be, then yes, in the PackageInfoMapper, if there are packages with the same name, a local version should take precedence over the registry one.

If you could create a PR for this, that’d be great :folded_hands:

1 Like