Layout of a macro crate

I have what is conceptually a single crate that currently works like so:

  • a bunch of proc macros that actually do the work the crate's designed for. The user's code invokes these.
  • a bunch of "ordinary" items, which client code has to be able to see because...
  • the code that the macros generate refers to those items.
  • a bunch of tests of the macros

My current setup is that the ordinary items (A) and macros (B) live in two different crates[1], crate A re-exports the macros from B, and the testing code lives inside crate A. The issue starts with Crate A being marked as a workspace, but doesn't have B as a member for reasons I'll get to in a moment, instead having B as an "ordinary" dependency defined by its path in a subdirectory. Trying to have B as a workspace member doesn't work because (I think) it means A can't re-export the macros from B publicly, but I might have misunderstood that. As is, the testing code falls over because, AFAICT, the generated references to ::A don't resolve from within crate A correctly. (And obviously can't refer to crate:: because in normal use, it won't be)

On top of the tests not working, this is not a great setup because I want to be able to package this whole mess on crates.io, ideally as a single crate rather than having to separately upload the macros and keep the two in sync. AIUI, this is impossible if I have path dependencies.

Another thought was to have the tests in a third crate (C), but I didn't know if it was possible to have that crate as a workspace member and have it depend "up" to B, since B is the conceptually most important. I also considered splitting the three so that they are all equal packages in a virtual workspace, but I don't really know if that'll work either.

Is there any way to make this work without running into inconveniences somewhere?


  1. Because AIUI, a proc-macro crate can only expose macros, and no other kind of item ↩︎

Trying to have B as a workspace member doesn't work because (I think) it means A can't re-export the macros from B publicly, but I might have misunderstood that.

Not true; workspaces don't change what dependencies you can have among their member crates. Workspaces largely only affect how you develop a set of packages, not what they can do.

As is, the testing code falls over because, AFAICT, the generated references to ::A don't resolve from within crate A correctly. (And obviously can't refer to crate:: because in normal use, it won't be)

There's a trick for this: library crate A can declare

extern crate self as a;

so the macro can use ::a::... paths. (But see below.)

I want to be able to package this whole mess on crates.io, ideally as a single crate rather than having to separately upload the macros and keep the two in sync. AIUI, this is impossible if I have path dependencies.

It's impossible regardless: your project necessarily consists of at least two library crates (one for ordinary items and one for proc-macros), and each package can have at most one library crate, so you must publish two packages. You will see this pattern in every library containing proc-macros (except those lucky ones which define only macros).

Another thought was to have the tests in a third crate (C), but I didn't know if it was possible to have that crate as a workspace member and have it depend "up" to B, since B is the conceptually most important.

That will work fine, but you can also write testing code in the tests/ directory of the package A, where it is compiled as separate “integration test” crates that automatically depend on the library. (Doc-tests also have this characteristic.)

I also considered splitting the three so that they are all equal packages in a virtual workspace, but I don't really know if that'll work either.

That doesn't change what's possible. The only difference between a workspace defined by virtual manifest, and a workspace defined by a non-virtual manifest that also defines a package, is the default set of packages operated on by Cargo commands (and you can change even that with workspace.default-members.)