It is currently the case that every cargo package corresponds to exactly 0 or 1 library crates. Its not possible to use cargo to create a package containing multiple libraries. @alexcrichton and @wycats discussed the motivation for this back in 2015:
These comments enumerate some of the design constraints and drawbacks of allowing a single package to contain multiple libraries, but they also contain some motivations for the limitation that I think might be reasonable to challenge at this point.
That is, I think there are good reasons to want to have multiple libraries in a package that are uploaded to crates.io as a single unit:
- Proc macro crates: Proc macros have to be contained in a special crate, leading to splits like
serde
vsserde_derive
. With macro re-exporting, though, its possible to expose proc macros through the non-macro crate. In theory, the "derive" crate could be completely eliminated by having it subsumed as a "sub-library" of the main crate. - Internal privacy boundaries: Often, a project will have a subsection which exposes a simple interface for very complex internals. Many items inside that submodule will want to be exposed throughout it but not outside it, leading to a lot of
pub(in module)
declarations. These could be abbreviated topub(crate)
(or even further if we stabilizecrate
visibility or similar) by making that submodule its own crate. - Improved internal dependency organization: Crates are required to form a DAG. By breaking off subcrates, you guarantee a certain relationship of dependencies between them, helping you maintain a certain order within your project.
- Improved compile times: Crates are compiled separately and in parallel; it can be worth making a module its own crate for that reason alone.
I don't have a complete design, but I think a solution in this space is worth pursuing.
Constraints
Here's a list of design constraints I've come up with:
- These libraries should all be versioned and packaged together. When uploaded to crates.io, they form a single entry. If you want them to be separate, you want a workspace, not this feature.
- Largely as an implication of the first item, the design is constrained to having a "main" library (unless its a binary project), which is probably treated specially. I'm going to refer to the non-main libraries as "sublibraries."
- There should be an easy and automatic way to do this without enumerating them all in your Cargo.toml, so that creating a new sublibrary is as easy as creating a new submodule. Ideally, each library need not have its own
Cargo.toml
either. - It should be possible (with annotations) for these subliraries to depend on one another.
- It should be possible for sublibraries to have dependencies that your other libraries don't, but they should also have automatic access to the dependencies of the main library.
Initial sketch
Underlying mechanism ([[sublib]]
).
Cargo.toml gains a new section [[sublib]]
, which is just like [[bin]]
et al. Every sublibrary is available as a dependency to the main library be default. A sublibrary does not need to have a Cargo.toml
.
The [[sublib]]
section has a new entry the other target entries don't have: manifest-path
, which points to a manifest for that sublibrary. A manifest for a sublibrary contains only a subset of Cargo.toml
.
The sublibrary manifest contains the dependencies table. Somehow, through the dependencies table, a sublibrary can depend both on external packages and on other sublibraries (is a path
dependency adequate for the latter case or do we need a new type of dependency?).
The sublibrary manifest does not generate its own lockfile: all external dependencies are versioned in the main Cargo.lock
for the package.
Sublibrary manifests are optional: without one, that sublibrary has access to all the dependencies in the main library's Cargo.toml
and none of the other sublibraries.
Additionally, the sublibrary manifest contains a [lib]
table, which has all the options that the main [lib]
table would have. Having a [lib]
table in the sublibrary manifest as well as a [[sublib]]
section is an error: only the implicit form, described below, uses the [lib]
section.
Automated implicit form
The src/lib/
directory acts a lot like the src/bin
directory. Every subdirectory of src/lib
is an automatic sublib with the name of that directory, rooted at src/lib/$name/lib.rs
. That directory can also contain a toml file for the sublibrary manifest path, maybe named Cargo.toml
but maybe named something else like Sublibrary.toml
or something?
As a result, a user can create a new sublibrary by creating a new directory under src/lib
, no other work necessary. When they want more complexity, they can create a toml file in that directory. Only if they want a different file structure do they need to move into the [[sublib]]
form.
Having a [[sublib]]
section in your Cargo.toml turns off the implicit form (just like [[bin]]
does).
Backward compatibility
I'm sure there are already some projects with a toplevel module named lib
. Not sure the best way to handle this. Maybe we can act fast to reserve that directory name in the edition for now?
Revisiting Alex & Yehuda's design problems:
- Building separate libraries: since sublibraries are targets, they can be built the same way any other target can; when building the main target, sublibraries will be built since they are dependencies of it.
- Specifying dependencies among them: handled by the sublibraries' manifest file.
- Build scripts: Open question! I haven't tried to solve this yet.
- Yehuda's "semantic gap:" I think this comment refers to a version in which multiple libraries are exposed from a single crates.io package. My proposal avoids this problem by having a single "main" package that is exposed, and sublibraries are just for internal organization.
cc the cargo team not previously mentioned: @aturon @matklad @ag_dubs @nrc @Eh2406