Blog post: Crate publishing tips and guidelines


Unfortunately unsafe is still needed in rust today

This sounds like an expectation that unsafe will at some point no longer be needed, but it won't just go away.


That's a fair point. I think there was some wishful thinking on my part there, so I rephrased it. I am confident we will still see a fair reduction of the cases where it's needed though.

I wasn't necessarily thinking of special environments like no_std (device driver authors weren't exactly the target audience), but rather of how much it's around today in general purpose libs, networking code, ... often for some (hypothetical) performance gains.

This is a really nice checklist! Thank you for writing about it, I especially agree with the part about doing your own chores and the emphasis that writing a good crate is more than just cobbling together code, it's also about the documentation, tests, dependencies, etc.

I find it funny that in one section you tell people to always run rustfmt, then the first code snippet in the next section contains code that doesn't follow standard rustfmt conventions (note the trailing commas, space before warn and open parens on a newline) :upside_down_face:

#![ warn
   anonymous_parameters          ,
   missing_copy_implementations  ,
   missing_debug_implementations ,
   missing_docs                  ,
   nonstandard_style             ,
   rust_2018_idioms              ,
   single_use_lifetimes          ,
   trivial_casts                 ,
   trivial_numeric_casts         ,
   unreachable_pub               ,
   unused_extern_crates          ,
   unused_qualifications         ,
   variant_size_differences      ,

Notably if I remember correctly, you always have to run cargo commands in the top level of the workspace, since all build artifacts and Cargo.lock are shared.

You shouldn't need to do this.

If you run cargo build while in one sub-crate it'll compile everything needed by that crate and place the artefacts in your top-level target/ directory. If you run cargo build --workpace from a subdirectory it'll compile everything in the workspace.


Thanks for the feedback. If you look carefully, I never wrote that people should always run rustfmt. I just say: "Before making code public, you might want to think about code formatting."

I personally don't use rustfmt because it can't format code in a style that I like. However, there is a merit in the possibility to format code automatically rather than manually, so I felt I had to mention that in the checklist.

Code formatting is also a very much flamewar inducing subject, so I think I didn't want to go into my personal opinions on the matter in that post.

So are you saying you can compile one crate in a workspace when others in that workspace currently don't compile?


$ mkdir /tmp/dummy-workspace && cd /tmp/dummy-workspace
$ cargo new --lib base-lib
$ cargo new --lib some-binary
$ cargo new --lib broken-lib
$ cd some-binary && cargo add ../base-lib && cd ..
    Updating '' index
      Adding base-lib (unknown version) to dependencies
$ cd broken-lib && cargo add ../base-lib && cd ..
    Updating '' index
      Adding base-lib (unknown version) to dependencies
$ tree -I target
β”œβ”€β”€ base-lib
β”‚   β”œβ”€β”€ Cargo.toml
β”‚   └── src
β”‚       └──
β”œβ”€β”€ broken-lib
β”‚   β”œβ”€β”€ Cargo.toml
β”‚   └── src
β”‚       └──
β”œβ”€β”€ Cargo.lock
β”œβ”€β”€ Cargo.toml
└── some-binary
    β”œβ”€β”€ Cargo.toml
    └── src

6 directories, 8 files
$ cat broken-lib/src/
this does not compile
$ cargo build
   Compiling base-lib v0.1.0 (/tmp/dummy-workspace/base-lib)
    Finished dev [unoptimized + debuginfo] target(s) in 0.12s
$ cd ../some-binary
$ cargo build
   Compiling base-lib v0.1.0 (/tmp/dummy-workspace/base-lib)
   Compiling some-binary v0.1.0 (/tmp/dummy-workspace/some-binary)
    Finished dev [unoptimized + debuginfo] target(s) in 0.23s
$  cargo build
   Compiling base-lib v0.1.0 (/tmp/dummy-workspace/base-lib)
   Compiling broken-lib v0.1.0 (/tmp/dummy-workspace/broken-lib)
error: expected one of `!` or `::`, found `does`
 --> broken-lib/src/
1 | this does not compile
  |      ^^^^ expected one of `!` or `::`

error: aborting due to previous error

error: could not compile `broken-lib`.

To learn more, run the command again with --verbose.
1 Like

Thanks a lot for clarifying. I have updated the post to remove that part. I'd have to properly play around with workspaces again to say anything meaningful about them.


Awesome article, I think the ecosystem really needed one like this one! :clap:

Trick: you can dodge it by emiting a "raw --cfg" rather than a cargo feature:

  • Add this to the Cargo.toml to make it work on

    all-features = true
    rustdocflags = ["--cfg", "docs"]
  • Add this to the .cargo/config file (create it if it doesn't exist) to make it work locally:

    rustdocflags = ["--cfg", "docs"]
  • And then the src/ as you suggested; I personally do something like:

        feature(doc_cfg, external_doc),
        doc(include = "../"),
    #![doc = ""] // empty doc line to handle missing doc warning when the feature is missing.
  • Annotating the cfg-gated items with:

    #[cfg(feature = "example")]
    #[cfg_attr(docs, doc(cfg(feature = "example")))]

    But this is repetitive, (plus it requires cargo doc --all-features locally), so a macro can come in handy:

    macro_rules! doc_cfg {(
        #[doc_cfg( $($cfg:tt)* )]
    ) => (
        #[cfg_attr(docs, doc(cfg( $($cfg)* )))]
        #[cfg(any(docs, $($cfg)*))] // also include the item for documentation

    to be used as:

    doc_cfg! {
        #[doc_cfg(feature = "example")]
        pub struct Foo; // etc.

    or, if using #[macro_rules_attribute], this can be made smoother by doing:

    /// At the top level: `src/`
    extern crate macro_rules_attribute;
    #[doc_cfg(feature = "example")]
    pub struct Foo; // etc.

    This, of course, is to have a general tool for any feature / combination of cfgs.
    But usually a crate has one or two recurring cfgs, such as #[cfg(feature = "std")] , for which a more specialized macro is simpler and can handle multiple items at once:

    macro_rules! cfg_std {(
    ) => (
            #[cfg(any(docs, feature = "std"))]
            #[cfg_attr(docs, doc(cfg(feature = "std")))]
    cfg_std! {
        pub struct Foo { ... }
        impl Foo { ... }

Thanks a lot for some great tips. I will update the post or link here.

I am trying out passing the cfg docs directly, but it doesn't seem to solve the problem:

error[E0554]: `#![feature]` may not be used on the stable release channel
 --> src/
3 | #![ cfg_attr( docs, feature(doc_cfg, external_doc) ) ]
  |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The problem is not cfg_attr( feature = "docs", it is feature(doc_cfg, external_doc).
As far as I can tell this is a stable to stable regression, that if I'm not mistaken started appearing with stable 1.41. Before that this compiled fine.

It doesn't seem to be reported. I haven't come round to investigating and reporting it and anyways the damage is done I'm afraid.

I should also double check if doc_cfg is unstable in rustdoc, which might also not work on stable, but I saw my CI starting to break with just external_doc which definitely worked before.

ps: I just found this issue which seems to describe the behavior that no longer works since 1.41:

1 Like

The fact that it requires nightly to build the docs is expected, but does cargo +stable build not work on 1.41.0?

oh yes, if you don't enable the feature, it should work fine. Before though cargo doc did work on stable.

Yh I guess I did not explain the purpose of my changes well enough: for me having to use nightly for the docs is perfectly fine, as it is a "best effort run once" that downstream users do not depend on kind of thing. What I personally find annoying is having to do cargo doc --all-features, which my suggestion "solves" by making all-features be "implicit" when running cargo doc (thanks to the --cfg docs always run and the #[cfg(any(docs, ...)))] tweak). If, on the other hand, would like to have cargo +stable doc work, then my suggestion is indeed of no good to you.

If you want your project to be smart enough to enable features when on +nightly and not on +stable, you could try

Yes, sorry for the confusion. It definitely makes --all-features work on stable again.

Found one more way this breaks. cargo test when running doctests will enable the rustdocflags, so cargo test no longer works on stable.

Only workaround I see is to disable the features by using all( not(doctest), doc ) and run the doc tests specifically in nightly.

1 Like

Super helpful your guide.
One little note about the changelog, you could think about conventional commits and a generated via the rust tool jilu.
At least I find it very handy.

Thanks a lot for your great efforts.

Hi, thanks a lot and welcome to the user forum. Yes, automated changelog was on my todo list. I was going to look into projects like wasm-bindgen to see how they did it.

In the mean time I'm still busy rounding up issues with external docs... It's actually more complicated than I would hope and maybe describing it will be to long to fit in the post inline, but I'll keep updating the information with the best practices I can find, so thanks for sharing jilu. I will look into it.

Using .cargo/config to enable the docs flag locally is interesting, but as mentioned it breaks using non-nightly compilers. As an alternative I just have nightly rustdoc features tested on a nightly CI build using RUSTDOCFLAGS='--cfg=docsrs -Dwarnings' cargo doc --all-features --no-deps, and if I'm changing something relevant and want to see what it looks like I just manually run such a command locally.

1 Like