Macro implementation type can't be found on local package when running tests

Hello everyone,

I recently created a peer macro to generate mock code for my protocols. My project is configured as:

  • Main Target
  • Local Packages (Common, Network...)

The local packages are linked into the main target. But I also use SPM to link a local package with one another. (e.g.: Network has Common as dependency). And they are all in the same workspace.

When I use the Macro on a code in the Main Target, it works fine when running the app and running the target's unit tests.

The problem starts when I use the macro on a local package and try to run the unit tests. I get the following error:

External macro implementation type 'Macros.Spy' could not be found for macro 'Spy()'

The Macro can be extended on the local package and can be used when running the app. As shown in the image bellow. But as aforementioned, the unit tests starts failing.


This is the implementation I have for it

Package.swift for Network package
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
  name: "Network",
  platforms: [.iOS(.v15)],
  products: [
    .library(
      name: "NetworkKit",
      targets: ["NetworkKit"]),
  ],
  dependencies: [
    .package(name: "Common", path: "../Common"),
    .package(name: "Macros", path: "../Macros"),
  ],
  targets: [
    .target(
      name: "NetworkKit",
      dependencies: [
        .product(name: "Common", package: "Common"),
        .product(name: "Macros", package: "Macros"),
      ]),
    .testTarget(
      name: "NetworkTests",
      dependencies: ["NetworkKit"]),
  ])

Package.swift for Macros package
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import CompilerPluginSupport
import PackageDescription

let package = Package(
  name: "Macros",
  platforms: [
    .macOS(.v14),
    .iOS(.v15),
    .tvOS(.v13),
    .watchOS(.v6),
    .macCatalyst(.v13),
  ],
  products: [
    .library(
      name: "Macros",
      targets: ["Macros"]),
    .executable(
      name: "MacrosClient",
      targets: ["MacrosClient"]),
  ],
  dependencies: [
    .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
  ],
  targets: [
    .macro(
      name: "MacrosImpl",
      dependencies: [
        .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
        .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
      ]),

    // Library that exposes a macro as part of its API, which is used in client programs.
    .target(name: "Macros", dependencies: ["MacrosImpl"]),

    // A client of the library, which is able to use the macro in its own code.
    .executableTarget(name: "MacrosClient", dependencies: ["Macros"]),

    // A test target used to develop the macro implementation.
    .testTarget(
      name: "MacrosTests",
      dependencies: [
        "MacrosImpl",
        .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
      ]),
  ])

@attached(peer, names: suffixed(Spy))
public macro Spy() = #externalMacro(
  module: "MacrosImpl",
  type: "SpyMacro")

I wonder if this is a limitation. Or if there might be something I might be missing to make it work.

Any suggestion would be really appreciated!

Thanks

One update on this, by setting to static the type of my Macro library, I got it working.

This eventually produced a new error: This will result in duplication of library code.

I suppressed it by setting DISABLE_DIAMOND_PROBLEM_DIAGNOSTIC to YES on build settings. And the tests ran successfully.

But I don't believe this is the best way to fix it, as this is causing undefined behaviour:

Class X is implemented in both (0x108af72d8) and (0x143ca0110). One of the two will be used. Which one is undefined.

I was able to reproduce the issue on an example project. It can be found in here.

The main problem is that on my local package, I have a library called NetworkTestingSupport that depends on Network library. They are both inside the Network package.

NetworkTestingSupport is linked to the Test Target. Which causes the problem. I believe it is linked dynamically, so it can't find the implementation of the macro.

The below represents the dependency graph for the example project shared above. If I remove NetworkTestingSupport and ExampleTests link, it works.

But can't think of a way to fix the issue. Running swift build and swift test on the local packages seems to be working, so I don't think this is related to the SPM.

Looking into old posts in the forum, I could find this topic. Where the setup is similar to mine, and I believe this is related. Both is setting up a TestHelper library, that depends on another library in the package. And this library has dependency of an external package.

It seems Xcode can't link the parent dependencies when running tests.

That’s a very interesting issue you gettin’ here. I will try to investigate the project you linked. Also I would love to hear any updates you have on the issue.

btw. there is a swift macro that do very similar thing you want to achieve: https://github.com/Matejkob/swift-spyable - not sure if you are aware of it :sweat_smile:

Thanks a lot for investigating this!

I am definitely aware of swift-spayable! In fact I studied the way you implemented the macro so I could understand better how macro works! :raised_hands:

I just could not use it as-is because I wanted to follow the same pattern of spies I have on my existing project. So I ended up building my own smaller version.

About the issue, I even tried on my example project to link swift-spayable, instead of a local one. And it failed with the same error.

Things that I also tested and seemed to be a workaround:

  • Have NetworkTestingSupporting as target of Network library. The link succeeds this way.
  • Use the macro only on NetworkTestingSupporting. This is a bit tricky for me, because it means I would not be able to use the protocols from Network.

One update on this, it seems that I need to link the Network library to the Tests Target. So the macro dependency can be found.

I assume there might be a limitation to see beyond a certain level in the dependency tree.

I will probably follow up with this solution for now.