In general, whenever possible, you should design a library’s features so that it is not undesirable for the feature to be enabled. Features can add support for new behavior, but the library should not perform that new behavior until it is requested at run time by a specific usage of the library.
Sometimes this is not practical, but it’s what you should aim for.
So, you’re suggesting that in addition to cfg!(feature = "logs"), I should also add a runtime check (for example, via a CLI argument) so that logs are enabled explicitly during runtime tests, but skipped when the macros are expanded at compile time?
The thing is, I was hoping for conditional compilation and more flexible dependency management. Especially since the issue only occurs with macros.
The problem is that #[cfg(feature = "logs")] eliminates a lot of code that exists solely for console output. Even if I remove the console output from the macros using runtime flags, that still wouldn’t solve the whole issue: all the code behind #[cfg(feature = "logs")] would still be compiled whenever the feature is enabled
I second @kpreid. Even with the new resolver, features are not nearly as customizable as one may want/think. For example optional dependencies always create an implied feature that applies to all targets/platforms no matter if the optional dependency is a build dependency or a normal dependency unless one defines at least one feature that depends on that dependency feature via "dep:<dependency_name>". As an explicit example:
[dependencies]
foo = "0.0.0"
[target.'cfg(all(unix, not(unix))'.build-dependencies]
foo = { version = "0.0.0", optional = true }
[target.jibberish.dependencies]
bar = { version = "0.0.0", optional = true }
will automatically expose features foo and bar for all platform/targets. Notice that foo is an unconditional dependency, and that it's impossible for it to be a build dependency (let alone an optional one) since it's obviously impossible for a platform to be both unix and not(unix). Additionally even though jibberish isn't a recognized target thus will never be built, bar is an exposed feature.
I realize this isn't directly related to your problem since you're not talking about optional dependencies, but I'm merely trying to support the stance that features should almost always work unconditionally. You can always use compile_error to try and detect certain combinations of features and targets that you want to forbid.
This has obvious consequences. For example let's say I have the following in Cargo.toml:
Then I decide to conditionally configure certain code to only work on Windows platforms that have enabled foo. It's not sufficient to have #[cfg(feature = "foo")] since that code will also be compiled for non-Windows platforms. Of course this will normally cause a compilation error when compiled on a non-Windows platform since it won't find the dependency foo, but it may not or at least provide a not very helpful compilation error message. Instead you really should have #[cfg(all(feature = "foo", windows))] that way the code doesn't exist at all for non-Windows platforms.
Cargo features are designed to allow skipping code that is completely unused (or can't be compiled in the current environment), not to precisely select whether it is enabled in each specific case the library is used within a single workspace. If you want that precision, you'll need an explicit configuration mechanism, but that doesn’t mean it has to be purely run-time; it can be based on generics, or calling a different entry point to the library, or an attribute in a macro’s input.
What happens when you use --target to specify your host platform? Without --target, cargo unifies the "host" and "target" in various ways and I'm guessing that applies to features too. resolver = "2" was largely intended to avoid host/target feature unification from cross compiling so host std deps didn't cause problems for a no_std target.