Support for External Dependencies at the Product Level in SwiftPM

Currently, external dependencies are implemented at the package level. This means that it's not possible to dependent on only a specific product of an external package. This becomes an issue when there is an interdependency at the target level between two packages. I ran into this problem here. There's a circular dependency between the packages, but not between any of its targets. This is a totally normal use case and should be possible.

Two possible solutions to this problem:

  1. Add support for external dependencies at the lowest possible level (products).
  2. Check if a dependency is already retrieved or the same as the one we're currently in, and if it is, skip it and continue with the rest of the process. This basically comes down to adding support for circular dependencies between packages but not between targets.

Thoughts?

I don't think its a good idea to support the circular dependency use-case. At the very least, it will require coordination between the packages to release the versions. I doubt there are strong enough use-cases to justify this feature and this will also make the dependency resolution logic even more complex.

Let me give you a concrete example using two of my own packages that have interdependent targets.

The first package, TupleExtensions (this was Foo in the other thread), contains a library that adds operator overloads for all Equatable and Comparable operators where the lhs and rhs are Optionals and Sequences containing tuples with arities 0, 2 to 6. It also adds a test target called TupleExtensionsTests containing the tests for TupleExtensions (this was FooTests in the other thread).

The second package, ZipExtensions (this was Bar in the other thread), contains a library adding free functions for zip(_:_:) for arities 3 to 6 and zipLongest(_:_:) for arities 2 to 6. It also adds a test target called ZipExtensionsTests containing the tests for ZipExtensions (this was BarTests in the other thread).

Dependency-wise, TupleExtensions makes use of zipLongest(_:_:) from ZipExtensions and ZipExtensionsTests makes use of ==(_:_:) from TupleExtensions. Both tests depend on their respective libraries, of course.

Graphically, it looks like this:

Dependencies

To me, this seems like a totally reasonable use case.

I can't make up from your reply if you're opposed to the first solution or not. It could be implemented like this (or some variation of it):

// swift-tools-version:5.0
import PackageDescription

let package = Package(
  name: "TupleExtensions",
  products: [
    .library(name: "TupleExtensions", targets: ["TupleExtensions"])
  ],
  dependencies: [
    .product(name: "ZipExtensions", package: .package(url: "../ZipExtensions", .branch("master")))
  ],
  targets: [
    .target(name: "TupleExtensions", dependencies: ["ZipExtensions"]),
    .testTarget(name: "TupleExtensionsTests", dependencies: ["TupleExtensions"])
  ]
)
// swift-tools-version:5.0
import PackageDescription

let package = Package(
  name: "ZipExtensions",
  products: [
    .library(name: "ZipExtensions", targets: ["ZipExtensions"])
  ],
  dependencies: [
    .product(name: "TupleExtensions", package: .package(url: "../TupleExtensions", .branch("master")))
  ],
  targets: [
    .target(name: "ZipExtensions", dependencies: []),
    .testTarget(name: "ZipExtensionsTests", dependencies: ["ZipExtensions", "TupleExtensions"])
  ]
)

Using this approach, both package and product-level external dependencies can live next to each other (but only if they don't depend on the same package).

Thoughts are welcome!