Circular Dependencies in SwiftPM

I have two packages, Foo and Bar. Both are in the same directory:

.
├─ Bar/
│  ├─ Sources/
│  │  └─ Bar/
│  │     └─ Bar.swift
│  ├─ Tests/
│  │  └─ BarTests/
│  │     └─ BarTests.swift
│  └─ Package.swift
└─ Foo/
   ├─ Sources/
   │  └─ Foo/
   │     └─ Foo.swift
   ├─ Tests/
   │  └─ FooTests/
   │     └─ FooTests.swift
   └─ Package.swift

Both define two targets; a library and tests:

// swift-tools-version:4.2
import PackageDescription

let package = Package(
  name: "Foo",
  products: [
    .library(name: "Foo", targets: ["Foo"])
  ],
  dependencies: [
    .package(url: "../Bar", .branch("master"))
  ],
  targets: [
    .target(name: "Foo", dependencies: []),
    .testTarget(name: "FooTests", dependencies: ["Bar"])
  ]
)
// swift-tools-version:4.2
import PackageDescription

let package = Package(
  name: "Bar",
  products: [
    .library(name: "Bar", targets: ["Bar"])
  ],
  dependencies: [
    .package(url: "../Foo", .branch("master"))
  ],
  targets: [
    .target(name: "Bar", dependencies: ["Foo"]),
    .testTarget(name: "BarTests", dependencies: ["Bar"])
  ]
)

Running swift build, swift package or any of its variants on either package will cause it to segfault:

$ cd Foo
$ swift build
Updating /Absolute/Path/To/Bar
[1]    10067 segmentation fault  swift build

There's a circular dependency between the packages, but I was hoping we'd be safe since there are no circular dependencies between any of the targets:

            ┌─ FooTests
Foo ── Bar ─┤
            └─ BarTests

Is this currently possible without creating a third package containing shared code? Note that it should also work when both are in separate remote repositories.

2 Likes

SwiftPM doesn't support this and you'll need to create another package to put the shared code. However, it shouldn't seg. fault! Do you mind filing a JIRA?

@Nicole_Jacque can you move this post to using Swift section?

Thanks. I'll look into filing a bug report.

Here are the instructions: https://github.com/apple/swift-package-manager/blob/master/Documentation/Resources.md#reporting-a-good-swiftpm-bug

1 Like

Filed as SR-7979 (for posterity's sake).

I did some more thinking.

One way of getting around this issue is to split Bar into two separate packages, Bar and BarTests, containing the library and the tests respectively.

So now we have three packages, Foo, Bar and BarTests:

.
├─ Bar/
│  ├─ Sources/
│  │  └─ Bar/
│  │     └─ Bar.swift
│  └─ Package.swift
├─ BarTests/
│  ├─ Tests/
│  │  └─ BarTests/
│  │     └─ BarTests.swift
│  └─ Package.swift
└─ Foo/
   ├─ Sources/
   │  └─ Foo/
   │     └─ Foo.swift
   ├─ Tests/
   │  └─ FooTests/
   │     └─ FooTests.swift
   └─ Package.swift

The manifest files for all packages:

// swift-tools-version:4.2
import PackageDescription

let package = Package(
  name: "Foo",
  products: [
    .library(name: "Foo", targets: ["Foo"])
  ],
  dependencies: [
    .package(url: "../Bar", .branch("master"))
  ],
  targets: [
    .target(name: "Foo", dependencies: ["Bar"]),
    .testTarget(name: "FooTests", dependencies: ["Foo"])
  ]
)
// swift-tools-version:4.2
import PackageDescription

let package = Package(
  name: "Bar",
  products: [
    .library(name: "Bar", targets: ["Bar"])
  ],
  dependencies: [],
  targets: [
    .target(name: "Bar", dependencies: [])
  ]
)
// swift-tools-version:4.2
import PackageDescription

let package = Package(
  name: "BarTests",
  products: [],
  dependencies: [
    .package(url: "../Bar", .branch("master")),
    .package(url: "../Foo", .branch("master"))
  ],
  targets: [
    .testTarget(name: "BarTests", dependencies: ["Bar", "Foo"])
  ]
)

It's not the most elegant solution, but it works. One downside is that you now have to keep Bar and BarTests in sync. Another downside is that tests must be run from BarTests.

(Can test-only dependencies, a feature that has been proposed before, have a role in solving this issue?)

I don't really see how test-only dependency feature will help in your case.

We have the same issue.

There is a "common test utils" package, which is a collection of all kinds of mocks for other packages used in the project. It's quite handy to plug these common test utils in the test target.

For example, let's say we have Accounts package, that has the Accounts and AccountsTests targets. The there's TestUtils package with TestUtils and TestUtilsTests targets.

TestUtils target depends on Accounts target.
AccountsTests target depends on TestUtils target.

In terms of targets there's no circular dependencies as such.
🎯AccountsTests ---> 🎯TestUtils ---> 🎯Accounts

But in terms of packages, there is a circular dependency:
📦Accounts (🎯AccountsTests) ---> 📦TestUtils (🎯TestUtils) ---> 📦Accounts (🎯Accounts)

Swift PM only support package-level dependencies.
This seems to be the design philosophy or an oversight, I can't really tell.

We've been building these packages so far using:

  • Just good old Xcode projects created manually
  • Buck build system
  • CocoaPods

Both Buck and CocoaPods (and I assume Bazel too) allow this kind of dependencies.

With Buck I'd have to use apple_library and apple_test rules, each of those rules would define a target.

apple_test will depend on apple_library.
These 2 targets together are part of one logical package, but both targets specify the dependencies they require.
There's no package-level dependency specification.

# Modified example, not actual Buck rules, but an example.

apple_library(
    name = "Accounts",
    deps = [
        library("SomeOtherDependency"),
        # ---> Does not depend on TestUtils!
    ],
    tests = [
        dependency("AccountsTests"),
    ],
    frameworks = [
        system_framework("Foundation"),
    ],
)

apple_test(
    name = "AccountsTests",
    deps = [
        library("Accounts"),
        library("TestUtils"), # <--- Depends on TestUtils
    ],
    frameworks = [
        developer_framework("XCTest"),
        system_framework("Foundation"),
    ],
)

apple_library(
    name = "TestUtils",
    deps = [
        library("Accounts"), # <--- Depends on Accounts
    ],
    frameworks = [
        developer_framework("XCTest"),
        system_framework("Foundation"),
    ],
)

CocoaPods, even though we no longer use it, allows similar target-level dependency specification, for example:

  s.test_spec "AccountsTests" do |test_spec|
    test_spec.source_files = "Tests/**/*.{h,m,swift}"
    test_spec.resources = ["Tests/**/Fixtures/*"]
    test_spec.dependency "TestUtils" # <--- Like this
  end

# TestUtils will depend on "Accounts" in its turn.

Both Buck and CocoaPods can generate an Xcode project, that can be used to build and test all the targets.

Swift PM allows only package-level dependency.
It looks at packages only and doesn't take targets into account.

Also, with Xcode 11.2.1 it still crashes while trying to resolve circular dependency.

5 Likes