I've been running into a number of frustrations with how profiles are handled in Rust build tooling and its ecosystem. I think this frustration can be illustrated with this set of features in the log crate.
To explain, the log crate (and similar crates like tracing) have two sets of compile-time log configuration features, max_level_* and release_max_level_*. These are branched on whether or not debug_assertions are enabled in the build. In general, the status of debug_assertions is used throughout the Rust ecosystem to identify a debug build. This creates a binary of choice in build profiles (only debug vs. release) and couples debug assertions with other debug features.
Coming from a game development perspective, I find this rather limiting. In game development, in large engines, there are typically many gradients between debug/development builds and release builds. Unreal Engine has five such profiles out of the box and the handful of proprietary AAA engines I've worked with have this many or more. Part of the reason is that large games often can't be run at playable framerates in debug mode, and so various flavors of release builds are used in debugging contexts. It isn't unusual to have a debug build, a profiling build, a release build, and a retail build for example, where release and retail differ in things like telemetry, symbol availability, and logging.
None of this can be easily expressed with conditional compilation. There is no #[cfg(profile="...")] nor any way to enable features in specific profiles, and past discussion was somewhat ambivalent on the idea. Coming back to the log example, I may want a profiling or design-review build that has debug assertions enabled but not logging. Or I may want to make my Debug derives gated by a cfg_attr attribute so that debug information doesn't appear in retail build profiles and expose data to cheaters. None of this can be controlled at the profile level right now.
Of course, there are always custom conditionals, but those are not nearly as first-class as profiles and features are. Similarly, there is no way to specify custom rustflags or custom conditionals in cargo profiles. The only widely agreed-upon switch used in the ecosystem that I'm aware of is whether or not debug assertions are enabled.
Is there a better way here? What can I propose here that has likelihood of acceptance? I'd like to understand the tooling design constraints that make these things difficult.
Yes, what is currently provided does not give sufficient flexibility.
The fundamental problem with profile names is, outside of built-in names, the user can define whatever they want and using them would need ecosystem wide conventions.
What CAD97 said in the other thread (detect settings within a profile rather than the profile itself) has recently been discussed by the Cargo team and it is their official stance on how this problem should be solved. So now we just need custom settings.
We have unstable support for profile rustflags but that will likely stay permamently unstable.
Setting feature flags within profiles would probably work better for the ecosystem than the inverse (detecting profiles in #[cfg] in code). If that was the case, then the log crate could just have one set of static max_level_* flags that could be set in profiles.
[profile.play]
debug_assertions = true
features = ["log/max_level_info"]
[profile.profiling]
debug_assertions = false
features = ["log/max_level_error"]
[profile.retail]
debug_assertions = false
features = ["log/max_level_off"]
This avoids the problem of the library itself needing to be aware of profile names (or global features) or trying to infer profiles by exposed settings (as is currently done with debug_assertions).
You mentioned in the other thread that workspace-level features are limited, but I'm already doing this in my workspace right now:
[workspace.dependencies]
log = { version = "0.4.28", features = ["max_level_debug", "release_max_level_error"] }
Having profiles activate a feature is an interesting idea. I thought we had an issue for this but can't find it. If someone were to develop this idea, they'd need to consider
Workspaces and features being package specific
Interactions with the command line, like --no-defaut-features
What this means to be set in a config profilt as those are detatched from workspaces
per package profile overrides
In this example I believe enabling a feature in a profile would be equivalent to enabling the feature at the workspace level while that profile is active (as in, it would be treated as if it was appended to workspace.dependencies.NAME.features). Could you help me understand what some of the consequences of this nuance are?
Workspace dependencies are something that you can inherit but have no impact on their own. You can generally only enable features in direct dependencies, so what happens when:
the dep isn't present
dep is present but is transitive
There are also the other parts
profile overrides (what we do for lto might set precendence)
I see. That makes sense. I think I'd like to proceed with a pre-RFC for this ask. With that in mind, loosely speaking, I believe the answers are:
If the dep isn't present (i.e. the workspace member crate hasn't pulled in the dependency from the workspace) then the feature controls for it specified in the workspace-level profile features value is ignored.
For now I wouldn't ask to provide support for [patch] here, but there are interesting use cases to consider in the future.
I believe this would work for profile overrides. You could do
[profile.dev.package.foo]
features = ["some_feature"]
to enable a given feature in the foo package in dev builds. I'm not sure why you would necessarily want to though. Does that seem incorrect or problematic?
For config files, I would propose that the features value could be overridden the same as any other value for a given profile.
I think one more controversial proposal would be for what to do about features in the top-level member crate being built (e.g. cargo build -p top_crate), for that I would propose also being able to specify features in profiles for this top crate e.g.
[profile.play]
features = ["top_crate/some_feature"]
that would be ignored if the top crate isn't being built. I'm not sure if there is a better way to do this, though.
I suppose another alternative would be to have cfg profile flag support in Cargo.toml (even if not available in Rust code) and have logic similar to target-based dependencies. So you could do something like [target.'cfg(profile = "retail")'.dependencies] in either workspaces or in member crates. I believe that would look like:
[dependencies]
log = { version = "0.4.28" }
[target.'cfg(profile = "play")'.dependencies.log]
features = ["max_level_warn"]
[target.'cfg(profile = "retail")'.dependencies.log]
features = ["max_level_off"]
Then in the member crate, if you pull in the feature via workspace = true it would take the appropriate feature(s).
That doesn't solve the problem of enabling/disabling features in the workspace's member crates themselves though. Maybe being able to override [target.'cfg(profile = "retail")'.features.default] in the member crate (not workspace) Cargo.toml?
Be sure to evaluate each of the points that I listed. For config, the concern is that the config is not tied to your project but your environment. For instance, what if you set this in ~/.cargo/config.toml? For myself, my main concern will be whether this is worth it for features or if it should wait on globals. If its done with features, what would most likely go through is having something similar --features semantics / syntax, including being limited to only workspace members. While you can use --features with non-workspace members, that is unspecified behavior that is likely a gap in validation and only works if it doesn't need deps out of your lockfile.
As for target enabling of features, that is not supported today and can enable features that need cfg profile in source which makes it incomplete (and we wouldn't complete it). Also I worry about overloading dependencies like that for enabling features. Doing this with full dependencier would be worse.
Hm, honestly having featureflags = field of a profile with the same semantics of --features might be sufficient. The problem there is that it would be top-level only and so you'd have the classic problem of having to feed features through to various levels of dependencies. Now I understand why you suggest linking it to global features.