Dependencies from library and binary crate in a package

I have a package with one library crate and one binary crate (using the library crate), as suggested by the book.

If I would publish this package on crates.io, then people could either depend on it on their projects and use the library crate without getting the binary crate, or use cargo install to build and install the binary crate, right?

The package has some dependencies, among them some that are only used by the binary crate (e.g. clap). Will they who use the package as a library get all those dependencies linked into their programs, even though they are not needed?

Obligatory read on this topic:

Basically, the [dependencies] of a package apply to all crates within that package[1]. In this instance, it means it applies not only to your (main) [[bin]]ary crate, but also to your [lib] crate, that is, the library on which dependents would depend.

So yes, if binary-specific dependencies, such as clap or other CLI helpers, are present under the [dependencies], and are not marked optional = true, then it does mean that dependents (on your [lib]rary) will also be transitively depending on those libraries.

That being said, if they end up never used, then they will very probably not be present in the final linked binary, since they'll probably be elided at link-time.

But since the Cargo pipeline will not know whether they are used or not, it does mean that dependents will have to fetch-at-least-once, and compile, each of these unnecessary dependencies.


The simple solution

Is to make the binary/CLI-specific dependencies optional, behind, e.g., some "cli" Cargo feature:

[dependencies]
# ...

  # Before:
# clap = "x.y.z"
  # Now:
  clap.version = "x.y.z"
  clap.optional = true
  # etc.

# ...

[features]
cli = [ # <- added!
    "dep:clap",
    # etc.
]

From there, to try and get nicer diagnostics when your [[bin]]ary crate is being compiled, you can add "cli" to the set of required-features for your [[bin]]:

[[bin]]
name = "your-package"
path = "src/main.rs"
required-features = ["cli"]

This, alas, does not mean that Cargo is to enable that feature when trying to build the --bin crate, but rather, that Cargo will just complain when trying to build such a crate without passing --features cli.

  • If directly cargo <check|build|run>ning, this complaint will be a nice hard error :white_check_mark:
  • But if a user is cargo installing it, then it will just be a warning, and a silently non-installed binary, which is quite awful UX :x: (this is what the blog post above complains about).

My own take on it

What the blog post fails to suggest, and which I would then personally do instead, would be to, instead of using required-features in the Cargo.toml, to enforce it within the Rust code, ourselves (at least until Cargo decides to properly do it by itself).

For instance, using compile_error!() within the src/main.rs:

  • Instead of:

    # Cargo.toml
    
    [[bin]]
    # …
    required-features = ["cli"]
    
  • do:

    //! `src/main.rs`
    
    #[cfg(not(feature = "cli"))]
    compile_error!("This binary crate requires `--features cli`.");
    
    // rest of the code ...
    

The optimal, albeit cumbersome, solution

which is suggested in that blog post, is simply to forgo the single-package-multiple-crates utopia, and to instead embrace the existence of two Cargo packages, one to be the canonical [lib]rary crate, and the other one, to be the canonical [[bin]]ary crate, with a now necessary explicit dependency on the former by the latter.


  1. More generally, any Cargo.toml which is not scoped under [lib] or [[bin]] is a setting global to the package, and which thus applies to both library and binari(es). ↩︎

6 Likes

IIUC, that Book section was advising a library for the purpose of integration testing a binary. If that's all you want to do, you needn't worry about keeping your library dependencies clean.

Right, but it seems to me that there are other reasons also, such as allowing others to use it as either a library or a binary.

True, and that's a common pattern. But it's not recommended by the book (as far as I'm aware) precisely because it's got problems.