Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

There is currently no way to specify features automatically when using cargo doc #8905

Open
GuillaumeGomez opened this issue Nov 27, 2020 · 33 comments
Labels
A-features Area: features — conditional compilation C-feature-request Category: proposal for a feature. Before PR, ping rust-lang/cargo if this is not `Feature accepted` Command-doc

Comments

@GuillaumeGomez
Copy link
Member

I thought at first about using [profile.doc], but it is doing nothing (as displayed by cargo itself) and there are no other way to do so.

Then I looked a bit deeper and noticed that you can't set features under profiles, making this not the right approach.

Therefore, I suggest adding a new section simply called "rustdoc" which would allow for now:

[rustdoc]
features = ["whatever"]
@GuillaumeGomez GuillaumeGomez added the C-feature-request Category: proposal for a feature. Before PR, ping rust-lang/cargo if this is not `Feature accepted` label Nov 27, 2020
@ehuss ehuss added A-features Area: features — conditional compilation Command-doc labels Nov 30, 2020
@Andlon
Copy link

Andlon commented Feb 16, 2022

In an attempt to motivate this issue, I'd like to bring up the example of proc macros that add or modify documentation, of the form

/// Foobaring a Baz.
#[modify_docs]
fn foobar(baz: Baz) {}

or

//! Crate docs
#![doc = generate_super_nice_html!()]

Or, for a more realistic example, consider the embed-doc-image crate:

//! A collection of crustaceans.
//!
//! # Ferris
//! ![Ferris](ferris)
//!
//! Ferris is a crab, as you can see in the above image.
#![doc = embed_image!("ferris", "images/ferris.png")]

A bit of a problem with these solutions is that - although the macros shown in the above examples perhaps only modify documentation, they still need to run for normal compilation. This means that:

  • they might need to pull in dependencies (not dev-dependencies) that are not needed by the code in a non-documentation setting
  • they generally still need to run in a non-documentation setting

For example, embed_image! will load an image from disk and encode it as base64, which might be expensive depending on the image and hardware, and as such it's conceivable that it might impact compile times in non-documentation code.

There is no truly good workaround for these kind of macros at the moment. Sure, you can define a feature that you need to manually enable when calling rustdoc, but this:

  • is really annoying.
  • the feature might be needed by a dependency and not something you can change from the top-level crate. So this will generally tend to break documentation for dependencies.

If we could define features that should be enabled when rustdoc is generating documentation, then we could make sure that we have zero associated costs in non-documentation settings, for example by enabling an enabled feature in the case of embed-doc-image.

@joshtriplett
Copy link
Member

joshtriplett commented Mar 1, 2022

We talked about this at length in today's @rust-lang/cargo meeting. Copying some text from #10435 to move the discussion here:

Currently, Rust has three kinds of dependencies: dependencies, dev-dependencies, and build-dependencies. Adding a fourth kind of dependency would be a substantial increase in complexity.

This issue started out talking about using feature flags for this, and in our discussions today we felt like that approach would be much simpler and make more use of existing Cargo facilities.

Crates today already use features for this, and then add docs.rs metadata telling docs.rs to use those features. And features can already control dependencies or features of dependencies.

We're open to a new mechanism to automatically enable a feature flag on doc builds and doctests, so that cargo doc and cargo test work automatically. We'd be happy to discuss different ways to make that work.

Also, separately, this is something we'd consider to be a large feature, for which we'd like to see an RFC design rather than going directly to a PR. Doing design iteration in a PR is much higher bandwidth for us than reviewing an RFC.

We're also open to just installing dev-dependencies automatically when building docs. That would be a much simpler change, and (in my opinion, not speaking for the whole team here) might not require a full RFC.

@GuillaumeGomez
Copy link
Member Author

The feature approach is unfortunately not sustainable as soon as the crate is a dependency because it then requires for the crate using it as a dependency to handle the feature as well. So we need a way to have dependencies and features specific to a rustdoc run. As for how, I don't have a preference. doc-dependencies seemed like the right approach but any other allowing this would be nice too.

@Andlon
Copy link

Andlon commented Mar 2, 2022

It seems to me that this issue stems from the fact that documentation and standard builds have different sets of desirable default feature sets. For example, a crate might only want to use a minimal set of features for a dependency, but when building documentation for a crate higher in the dependency tree, we still want to have the "proper" documentation for the dependencies.

However, currently there is only a single mechanism for specifying default features, with no way to distinguish between standard and doc builds.

With that in mind, it seems at first glance to me that something like the following would be ideal:

[features]
default = ["my", "default", "features"]

# Document all the default stuff, but also make sure that we document serde impls
doc-default = [ "default", "serde" ]

# If not specified, implicitly use default features, i.e.
doc-default = [ "default" ]

I think doc-default should ignore --no-default-features (command-line) and default-features = false (Cargo.toml). Otherwise documentation of dependencies might be incomplete when not using default features in the build.

Instead, we would need a separate doc-default-features = false flag (and command-line argument) so that dependencies can fine tune what they want to document from dependencies.

Certainly the latter is a breaking change: having doc builds treat default-features = false differently is a breaking change in cargo's behavior when building documentation. I'm not able to see the complete ramifications of this at the moment, and it would be great if perhaps someone from the @rust-lang/cargo team could comment on that.

@GuillaumeGomez
Copy link
Member Author

Having doc-default would also allow to handle doc dependencies pretty easily. I like this suggestion!

@joshtriplett
Copy link
Member

What is the advantage of having a doc-default feature, rather than just enabling dev-dependencies for all doc builds? doctests already have to install dev-dependencies, so this would just enable those for docs as well.

@GuillaumeGomez
Copy link
Member Author

Because I assume you don't need the latex proc-macro when running your tests?

@joshtriplett
Copy link
Member

joshtriplett commented Mar 2, 2022

You do need it to build your docs, which you have to do to as part of cargo test to run doctests. And dev-dependencies typically includes many heavier-weight things that are left out of ordinary dependencies; for instance, an async crate might pull in a full async runtime and all its dependencies for testing.

As long as they're not pulled in for ordinary builds, I don't think we need to worry much about the weight of dev-dependencies. That doesn't seem worth making complexity tradeoffs, compared to just saying all doc/doctest builds pull in dev-dependencies.

@Andlon
Copy link

Andlon commented Mar 2, 2022

You do need it to build your docs, which you have to do to as part of cargo test to run doctests. And dev-dependencies typically includes many heavier-weight things that are left out of ordinary dependencies; for instance, an async crate might pull in a full async runtime and all its dependencies for testing.

As long as they're not pulled in for ordinary builds, I don't think we need to worry much about the weight of dev-dependencies.

I don't immediately see how making doc builds use dev dependencies solves the problem here. Consider my motivating example for embed-doc-image:

//! ![Ferris](ferris)
#![doc = embed_image!("ferris", "images/ferris.png")]

Here we'll need the embed_image! macro in all builds to make this compile at all. So that means that we can not simply make embed-doc-image a dev-dependency: it needs to be present in normal builds as well.

In addition to this, there may be a compile-time cost associated with invoking the macros as well. For example, with embed-doc-image you would need, for each image you use in your documentation, to:

  • load a file (possibly "large") from the file system to memory
  • encode the image as base64

So even if this would only happen in test settings, it could perhaps add a sizeable cost to the debug-compile-run cycle (depending on how well incremental compilation does its job).

@Andlon
Copy link

Andlon commented Mar 2, 2022

cfg(doc) would seem like the ideal solution for the above problem, except for the fact that it doesn't play well with dependencies: roughly speaking, cfg(doc) only applies to the top-level crate but not for the documentation generated for dependencies.

@Urgau
Copy link
Member

Urgau commented Mar 2, 2022

@Andlon in your case I think you could do this:

//! ![Ferris](ferris)
#![cfg_attr(doc, doc = embed_image!("ferris", "images/ferris.png"))]

It's a bit ugly but I think that would work with dev-dependencies being applied to doc.

Their is also the possibility to not expand the doc attr unless rustdoc (or even cfg(doc)) request it.

@Andlon
Copy link

Andlon commented Mar 2, 2022

@Urgau: I agree that would be almost ideal if it worked as you would expect (see my above comment, which I slipped in just before you posted yours!).

Their is also the possibility to not expand the doc attr unless rustdoc (or even cfg(doc)) request it.

This suggestion is interesting... I guess someone with knowledge of how this propagates through cargo/rustdoc would have to chime in to say if that is workable.

@Urgau
Copy link
Member

Urgau commented Mar 2, 2022

@Urgau: I agree that would be almost ideal if it worked as you would expect (see my above comment, which I slipped in just before you posted yours!).

You're right, that's weird. Is this a feature ? because I would consider this as a bug.

Their is also the possibility to not expand the doc attr unless rustdoc (or even cfg(doc)) request it.

This suggestion is interesting... I guess someone with knowledge of how this propagates through cargo/rustdoc would have to chime in to say if that is workable.

This would be more of rustc problem, as rustdoc use it for basically anything related to the source code.

@GuillaumeGomez
Copy link
Member Author

You're right, that's weird. Is this a feature ? because I would consider this as a bug.

This is a bug and the main motivation behind this issue. :)

@Andlon
Copy link

Andlon commented Mar 2, 2022

You're right, that's weird. Is this a feature ? because I would consider this as a bug.

This is a bug and the main motivation behind this issue. :)

Sorry, but I believe this statement might confound this issue somewhat: even if cfg(doc) worked the way you might expect (propagates to dependencies during doc compilation etc.), it still wouldn't allow us to enable certain features or dependencies only for doc builds.

@GuillaumeGomez
Copy link
Member Author

You are right on that. This is why I was hoping for doc-default to kinda fix both. But yes, currently we can't specify with cargo doc dependencies nor features.

@Urgau
Copy link
Member

Urgau commented Mar 2, 2022

Yes but coupled with doc-default or dev-dependencies for doc, It should work.

Btw I just locally experimented with some changes to cargo to build the dependencies when documenting with --cfg doc, it seems to work perfectly fine. Except that now cargo can't reuse the previously build deps when doing check or build but that's already the case when doing check --test or test so it's not really a problem.

@GuillaumeGomez
Copy link
Member Author

Yes, it's as expected and why I think @joshtriplett is suggesting to re-use dev-dependencies.

@Andlon
Copy link

Andlon commented Mar 2, 2022

@Urgau: interesting. I must admit I haven't tested this myself - I was mainly made aware of the problems of cfg(doc) by @Nemo157 on Discord. I wonder if perhaps if they could chime in with an example of when it doesn't work?

@Andlon
Copy link

Andlon commented Mar 2, 2022

I will also add that I think if dev-dependencies + cfg(doc) actually work out, then I believe this might be better than to add additional complexity with e.g. the doc-default suggestion. I think we should really not encourage crates to further complicate builds with complex behavior when building docs.

@GuillaumeGomez
Copy link
Member Author

It's not always an option unfortunately. For example, in gtk-rs we have a dox feature which updates the source code to add the (LGPL) documentation. If we don't have a way to enable this feature only on doc build, then it means your code will be updated every time you run cargo test. Or what we did: we now generate empty documentation on docs.rs and host the documentation ourselves.

@Andlon
Copy link

Andlon commented Mar 2, 2022

@GuillaumeGomez: forgive me if I'm being too naive here, but wouldn't cfg(doc) solve that problem?

@Urgau
Copy link
Member

Urgau commented Mar 2, 2022

Okay, so if I take everything in account I propose having [dev-dependencies] + cfg(doc) (for every deps), this would cover most of the cases.

And then using [profile.doc] to have features:

[profile.doc]
features = ["feature", "automatically", "activated", "for", "doc"]

Even trough I think that this would better suited for something like rustdoc-stripper directly in rustdoc but that's out of reach for the time being and also a bit inrelevent.

@GuillaumeGomez
Copy link
Member Author

GuillaumeGomez commented Mar 2, 2022

@GuillaumeGomez: forgive me if I'm being too naive here, but wouldn't cfg(doc) solve that problem?

Sorry but I'm the one who's lost: how cfg(doc) would fix the re-compilation issue if the source code is updated?

EDIT: if you're talking about #![cfg_attr(doc, doc = include_str!("LGPL.md")], it's just not possible for another simple reason: each item in gtk-rs crates (struct/enum/union/function/method/const) has its documentation generated. It would mean having thousands of markdown files around. Not really the best...

@Andlon
Copy link

Andlon commented Mar 2, 2022

@GuillaumeGomez: I think I didn't understand what you meant. But couldn't cargo doc --features dox be replaced with cfg(doc) directives? For example to do the codegen in a build script when cfg(doc)? Or is cfg(doc) set when running cargo test as well?

@GuillaumeGomez
Copy link
Member Author

No because we need to do things in build.rs (downloading docs and putting it back into the source code). But I think it's not an issue. The big issue in gtk-rs currently is that we handle versions with features and we need dependencies to have been built with them and with the dox feature to make it work. So if they are built with cfg(doc), almost all of our problems are solved.

Being able to have doc specific features would be a big plus too.

Or is cfg(doc) set when running cargo test as well?

It is when you're running cargo test --doc with cfg(doctest) too. But it's only for the current crate, not its dependencies.

@Manishearth
Copy link
Member

What is the advantage of having a doc-default feature, rather than just enabling dev-dependencies for all doc builds? doctests already have to install dev-dependencies, so this would just enable those for docs as well.

Because enabling dev-deps for all dependencies will probably pull in all kinds of stuff that in aggregate is likely to have build issues. dev-dependencies is currently quite squarely in the zone of "users of this crate don't need to care about this", and this changes that.

@joshtriplett
Copy link
Member

joshtriplett commented Mar 2, 2022

cfg(doc) would seem like the ideal solution for the above problem, except for the fact that it doesn't play well with dependencies: roughly speaking, cfg(doc) only applies to the top-level crate but not for the documentation generated for dependencies.

That does indeed seem like a bug. Unless I'm misunderstanding how cfg(doc) is supposed to work. Regardless of anything else, fixing this seems like it'll make more cases work well.

@Urgau
Copy link
Member

Urgau commented Mar 2, 2022

@joshtriplett The problem with cfg(doc) not being applied is because when a crate has a dependency this dependency first needs to be check (cargo term) then passed to rustdoc BUT when checking cargo doesn't add --cfg doc.

@Nemo157
Copy link
Member

Nemo157 commented Mar 2, 2022

Changing cfg(doc) to be passed down to all dependencies is very likely to be a breaking change. I know async-std in particular is doing some very cursed things based on cfg(doc) so I would not be surprised if it broke doc builds of its dependents. A quick test of RUSTFLAGS=--cfg=doc cargo check actually reveals an earlier failure when checking socket2:

error[E0599]: no method named `_set_nosigpipe` found for reference `&socket::Socket` in the current scope
    --> /home/nemo157/.cargo/registry/src/github.com-1ecc6299db9ec823/socket2-0.4.4/src/sys/unix.rs:1123:14

Maybe it would be possible to tie this to an edition change or some other opt-in to the breakage.

One other big consideration is that rustdoc is not the only consumer of the doc attributes. These are also consumed by other development tools such as rust-analyzer. For those tools it's actually better if during development all builds get the "complete docs", not just when the crate is built with cfg(doc) or dev-dependencies.

@Nemo157
Copy link
Member

Nemo157 commented Mar 2, 2022

roughly speaking, cfg(doc) only applies to the top-level crate but not for the documentation generated for dependencies.

To make this a little more precise, cfg(doc) applies only during the rustdoc build for a crate, so when you cargo doc and it documents all crates in your dependency tree they will each have --cfg=doc implied while their docs are built; but when your crates documentation is built it uses the standard rmeta from each of its dependencies that was built without --cfg=doc.

You can see the difference when inlining documentation, if you inline documentation from a dependency that does something like #[cfg_attr(doc, doc = "foo")] then foo will appear in the docs generated for their crate but not yours. (Based on a quick grep of all crates I have locally, this is not something that happens, but it is likely to be one outcome of allowing the sort of features that are wanted here).

@Nemo157
Copy link
Member

Nemo157 commented Mar 2, 2022

There was also some prior discussion of a different usecase of something like doc-dependencies in rust-lang/rust#74481.

@sam0x17
Copy link

sam0x17 commented Aug 28, 2023

It's a bit ugly but I think that would work with dev-dependencies being applied to doc.

The problem is it does not. Just hit this issue with my docify crate. If you try to build something like this with docify as only a dev-dependency, build will fail:

[dev-dependencies]
docify = "0.2"
//! ### Interned Example
#![cfg_attr(doc, doc = docify::embed_run!("tests/tests.rs", test_interned_showcase))]
error[E0433]: failed to resolve: use of undeclared crate or module `docify`
  --> src/lib.rs:38:24
   |
38 | #![cfg_attr(doc, doc = docify::embed_run!("tests/tests.rs", test_interned_showcase))]
   |                        ^^^^^^ use of undeclared crate or module `docify`

Thus users of docify are forced to add it as a direct dependency, which is not ideal.

If we've decided adding doc-dependencies would increase complexity too much, we should at least make cargo doc respect dev-dependencies the way it is supposed to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-features Area: features — conditional compilation C-feature-request Category: proposal for a feature. Before PR, ping rust-lang/cargo if this is not `Feature accepted` Command-doc
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants