[Pre-RFC] Cargo Crate Parameters

Summary

Cargo should support the idea of a parameterized crate. Library crates would declare usage of a parameter with a given default. Binary crates would then give these parameters values which would affect the full build graph.

Motivation

The crate ecosystem should not need to fragment because of conceptually similar and mutually exclusive crates.

  • tlsnative-tls vs rustls
  • runtimetokio vs async-std
  • http-clientcurl vs hyper

Basically any crate that uses the current feature flags functionality to alter the internals in a mutually exclusive manner would benefit from this.

Guide-level explanation

Cargo.toml would add a section titled params written as follows:

[params]
name = "value"

For a library crate the value is a default if left unspecified by a downstream crate.

For a binary crate the value, is the value, and this applies to all crates in the full graph with a matching parameter name.

This does mean that parameter names are global. I'm optimistic about our ecosystem and think we'll collectively decide parameter names to use that make sense.

Example

Foo Library

# Cargo.toml
# [...]

[params]
tls = "native-tls"
runtime = "async-std"

[target.'cfg(param(tls = "native-tls"))'.dependencies]
native-tls = "*"

[target.'cfg(param(tls = "rustls"))'.dependencies]
rustls = "*"

[target.'cfg(param(runtime = "async-std"))'.dependencies]
async-std = "*"

[target.'cfg(param(runtime = "tokio"))'.dependencies]
tokio = "*"

Bar Application

# Cargo.toml
[dependencies]
async-std = "*"
foo = "*"

[params]
runtime = "async-std"
tls = "native-tls"

Unresolved questions

Can we make declaring parameter-specific dependencies nicer?


I'm not completely sold on most of the details in this. I just think we need something similar to this to address a growing number of areas that need to configure crates in a common and complete fashion.

An existing (postponed) RFC that covers the same use-cases in a quite different way:

(not to say that an approach based on conditional dependencies/compilation shouldn't be explored, but I personally really like how external existential types allow customising crates with no code changes).

3 Likes

IMHO the problem is real and the use-case is strong, but I'm not sure about the proposed solution.

Existing Cargo features suffer from some of these problems (they can't be mutually-exclusive), and need similar fixes (there should be a way to set a feature of a sub-sub-dependency). But this creates a completely new parallel mechanism to the features.

Package managers solve that with a "provides" feature and virtual packages. A TLS implementation defines that it "provides TLS", and an HTTP client specifies that it "needs TLS", and the package manager ensures that there's some package installed that satisfies the need.

4 Likes

I'm definitely not sold on this proposal either. I just want some solution to exist.


@kornel

That sounds like that could work far more transparently.

I'm not sure about the details though. I don't think having the community to create virtual crates can work as the crate sets we're talking about aren't directly API compatible.

Perhaps ...

# lib / Cargo.toml
[dependencies]
async-std = { version = "1.0", optional = true, provides = "runtime" }
tokio = { version = "0.2", optional = true, provides = "runtime" } 
// lib.rs
#[cfg(provided(runtime = "async-std"))]
// [...]
# bin / Cargo.toml
[dependencies]
foo = "*"

# With this, [foo] is now using async-std over tokio
async-std = "1.0"
  • How do we express a "default" choice for binary crates that don't care

Another idea that popped into my head is we could move this into a "peerDependencies" feature of Cargo where a crate expresses that it works with X, Y, and Z crates but those crates must be dependended on by the root crate. The inversion could make sense for other uses cases beyond toggling between impls.

1 Like

If we only have a mechanism to compare the value of a feature to a fixed string, then we might as well just use feature names for this.

The only value of this would be if we could use the string value associated with the feature directly, such as substituting that value in a dependency (as a simple form of dependency injection, for instance).

I'd rather have language-level generic parameters for crates (or modules). Provides-requires relationships would be expressed in terms of traits, so they'd be type-safe and extensible. And you could instantiate the same crate/module multiple times with different parameters.

8 Likes

I think I'd like to have both arbitrary parametrization of crates, and a way to negotiate a one global option for a tree of crates. They're both useful for different cases.

For example, I'd rather define one TLS implementation for the entire binary, even if multiple impls could coexist. Multiple TLS impls don't add functionality, but add bloat and confusion (e.g. different parts of the same program may end up using separate CA certificate stores).

2 Likes