- 7 Managing Growing Projects with Packages Crates and Modules
- 14 More About Cargo and Crates io
- 14-1 Customizing Builds with Release Profiles
- 14-2 Publishing a Crate to Crates io
- 14-2-1 Making Useful Documentation Comments
- 14-2-2 Exporting a Convenient Public API with
pub use
- 14-2-3 Setting Up a Crates io Account
- 14-2-4 Adding Metadata to a New Crate
- 14-2-5 Publishing to Crates io
- 14-2-6 Publishing a New Version of an Existing Crate
- 14-2-7 Removing Versions from Crates.io with
cargo yank
- 14-3 Cargo Workspaces
- 14-4 Installing Binaries from Crates io with
cargo install
- 14-5 Extending Cargo with Custom Commands
In addition to grouping functionality, encapsulating implementation details lets you reuse code at a higher level: once you’ve implemented an operation, other code can call that code via the code’s public interface without knowing how the implementation works. The way you write code defines which parts are public for other code to use and which parts are private implementation details that you reserve the right to change. This is another way to limit the amount of detail you have to keep in your head.
- Packages: A Cargo feature that lets you build, test, and share crates
- Crates: A tree of modules that produces a library or executable
- Modules and use: Let you control the organization, scope, and privacy of paths
- Paths: A way of naming an item, such as a struct, function, or module
7-1 Packages and Crates
- A crate is a binary or library.
- The crate root is a source file that is used to know how to build a crate.
- A package is one or more crates that provide a set of functionality. A package contains a Cargo.toml file that describes how to build those crates.
- A package must contain zero or one library crates, and no more. It can contain as many binary crates as you’d like, but it must contain at least one crate (either library or binary).
Cargo’s conventions are that if you have a src directory containing main.rs in the same directory as a package’s Cargo.toml, Cargo knows this package contains a binary crate with the same name as the package, and src/main.rs is its crate root. Another convention of Cargo’s is that if the package directory contains src/lib.rs, the package contains a library crate with the same name as the package, and src/lib.rs is its crate root.
If a package contains both src/main.rs and src/lib.rs, then it has two crates: a library and a binary, both with the same name. If we only had one of the two, the package would have either a single library or binary crate. A package can have multiple binary crates by placing files in the src/bin directory: each file will be a separate binary crate.
A crate will group related functionality together in a scope so the functionality is easy to share between multiple projects.
7-2 Defining Modules to Control Scope and Privacy
Modules let us organize code within a crate into groups for readability and easy reuse. Modules also control the privacy of items, which is whether an item can be used by outside code (public) or is an internal implementation detail and not available for outside use (private).
1 | // cargo new --lib restaurant |
We define a module by starting with the mod
keyword and then specify the name of the module and place curly brackets around the body of the module. Inside modules, we can have other modules, as in this case with the modules hosting
and serving
. Modules can also hold definitions for other items, such as structs, enums, constants, traits, or functions.
7-3 Paths for Referring to an Item in the Module Tree
A path can take two forms:
- An absolute path starts from a crate root by using a crate name or a literal
crate
. - A relative path starts from the current module and uses
self
,super
, or an identifier in the current module.
1 | mod front_of_house { |
Using the crate
name to start from the crate root is like using /
to start from the filesystem root in your shell.
7-3-1 Modules as the Privacy Boundary
If you want to make an item like a function or struct private, you put it in a module. Here are the privacy rules:
- All items (functions, methods, structs, enums, modules, annd constants) are private by default.
- You can use the
pub
keyword to make an item public. - You aren’t allowed to use private code defined in modules that are children of the current module.
- You are allowed to use any code defined in ancestor modules or the current module.
7-3-2 Exposing Paths with the pub
Keyword
1 | // two pubs |
The front_of_house
module isn’t public, but because the eat_at_restaurant
function is defined in the same module as front_of_house
(that is, eat_at_restaurant
and front_of_house
are siblings), we can refer to front_of_house
from eat_at_restaurant
.
7-3-3 Starting Relative Paths with super
We can also construct relative paths that begin in the parent module by using super
at the start of the path. This is like starting a filesystem path with the ..
syntax.
1 | fn serve_order() {} |
The fix_incorrect_order
function is in the back_of_house
module, so we can use super
to go to the parent module of back_of_house
, which in this case is crate
, the root. From there, we look for serve_order
and find it.
7-3-4 Making Structs and Enums Public
We can also use pub
to designate structs and enums as public, but there are a few extra details. If we use pub
before a struct definition, we make the struct public, but the struct’s fields will still be private. We can make each field public or not on a case-by-case basis.
1 | mod back_of_house { |
Note that because back_of_house::Breakfast
has a private field, the struct needs to provide a public associated function that constructs an instance of Breakfast
(we’ve named it summer
here). If Breakfast
didn’t have such a function, we couldn’t create an instance of Breakfast
in eat_at_restaurant
because we couldn’t set the value of the private seasonal_fruit
field in eat_at_restaurant
.
In contrast, if we make an enum public, all of its variants are then public. We only need the pub
before the enum
keyword
1 | mod back_of_house { |
Enums aren’t very useful unless their variants are public; it would be annoying to have to annotate all enum variants with pub
in every case, so the default for enum variants is to be public. Structs are often useful without their fields being public, so struct fields follow the general rule of everything being private by default unless annotated with pub
.
7-4 Bringing Paths into Scope with the use
Keyword
It might seem like the paths we’ve written to call functions so far are inconveniently long and repetitive. We can bring a path into a scope once and then call the items in that path as if they’re local items with the use
keyword.
1 | mod front_of_house { |
Adding use
and a path in a scope is similar to creating a symbolic link in the filesystem. You can also bring an item into scope with use
and a relative path.
7-4-1 Creating Idiomatic use
Paths
For functions, it’s considered idiomatic to specify the function’s parent module with use
, and then specify the parent module when calling the function.
For structs, enums, and other items, specifying the full path to the item with use
is idiomatic. The exception to this idiom is if the use
statements would bring two items with the same name into scope, which isn’t allowed.
1 | use std::collections::HashMap; |
7-4-2 Providing New Names with the as
Keyword
1 | use std::fmt::Result; |
7-4-3 Re-exporting Names with pub use
When we bring a name into scope with the use
keyword, the name available in the new scope is private. To enable the code that calls our code to refer to that name as if it had been defined in that code’s scope, we can combine pub
and use
. This technique is called re-exporting because we’re bringing an item into scope but also making that item available for others to bring into their scope.
1 | mod front_of_house { |
By using pub use
, external code can now call the add_to_waitlist
function using hosting::add_to_waitlist
. If we hadn’t specified pub use
, the eat_at_restaurant
function could call hosting::add_to_waitlist
in its scope, but external code couldn’t take advantage of this new path.
Re-exporting is useful when the internal structure of your code is different from how programmers calling your code would think about the domain. For example, in this restaurant metaphor, the people running the restaurant think about “front of house” and “back of house.” But customers visiting a restaurant probably won’t think about the parts of the restaurant in those terms. With pub use
, we can write our code with one structure but expose a different structure. Doing so makes our library well organized for programmers working on the library and programmers calling the library.
7-4-4 Using External Package
1 | // Cargo.toml |
7-4-5 Nested Paths for Cleaning Up Large use
Lists
1 | use std::cmp::Ordering; |
7-4-6 The Glob Operator
1 | // bring all public items defined in a path |
The glob operator is often used when testing to bring everything under test into the tests
module; The glob operator is also sometimes used as part of the prelude pattern.
7-5 Separating Modules into Different Files
1 | // src/lib.rs |
Using a semicolon after mod front_of_house
instead of a block tells Rust to load the contents of the module from another file with the same name as the module.
1 | // src/front_of_house.rs |
The module tree remains the same:
1 | // src/front_of_house.rs |
14 More About Cargo and Crates io
14-1 Customizing Builds with Release Profiles
In Rust, release profiles are predefined and customizable profiles with different configurations that allow a programmer to have more control over various options for compiling code. Each profile is configured independently of the others.
1 | $ cargo build |
The dev
profile is defined with good defaults for development, and the release
profile has good defaults for release builds.
Cargo has default settings for each of the profiles that apply when there aren’t any [profile.*]
sections in the project’s Cargo.toml file. By adding [profile.*]
sections for any profile you want to customize, you can override any subset of the default settings below.
1 | [profile.dev] |
The opt-level
setting controls the number of optimizations Rust will apply to your code, with a range of 0 to 3.
14-2 Publishing a Crate to Crates io
14-2-1 Making Useful Documentation Comments
Rust also has a particular kind of comment for documentation, known conveniently as a documentation comment, that will generate HTML documentation. Documentation comments use three slashes, ///
, instead of two and support Markdown notation for formatting the text.
1 | /// Adds one to the number given. |
Then run cargo doc
, the document will be in the target/doc
directory.
14-2-1-1 Commonly Used Sections
-
Examples
-
Panics: The scenarios in which the function being documented could panic. Callers of the function who don’t want their programs to panic should make sure they don’t call the function in these situations.
-
Erros: If the function returns a
Result
, describing the kinds of errors that might occur and what conditions might cause those errors to be returned can be helpful to callers so they can write code to handle the different kinds of errors in different ways. -
Safety: If the function is
unsafe
to call (we discuss unsafety in Chapter 19), there should be a section explaining why the function is unsafe and covering the invariants that the function expects callers to uphold.
Most documentation comments don’t need all of these sections, but this is a good checklist to remind you of the aspects of your code that people calling your code will be interested in knowing about.
14-2-1-2 Documentation Comments as Tests
Running cargo test
will run the code examples in your documentation as tests!
14-2-1-3 Commenting Contained Items
Another style of doc comment, //!
, we typically use these doc comments inside the crate root file (src/lib.rs by convention) or inside a module to document the crate or the module as a whole. Documentation comments within items are useful for describing crates and modules especially. Use them to explain the overall purpose of the container to help your users understand the crate’s organization.
14-2-2 Exporting a Convenient Public API with pub use
If the structure isn’t convenient for others to use from another library, you don’t have to rearrange your internal organization: instead, you can re-export items to make a public structure that’s different from your private structure by using pub use
.
Filename: src/lib.rs
1 | //! # Art |
Filename: src/main.rs
1 | use art::kinds::PrimaryColor; |
To remove the internal organization from the public API, we can modify the art
crate code to add pub use
statements to re-export the items at the top level.
1 | //! # Art |
Filename: src/main.rs
1 | use art::PrimaryColor; |
14-2-3 Setting Up a Crates io Account
1 | $ cargo login api_key |
This command will inform Cargo of your API token and store it locally in ~/.cargo/credentials.
14-2-4 Adding Metadata to a New Crate
Before publishing, you’ll need to add some metadata to your crate by adding it to the [package]
section of the crate’s Cargo.toml file.
Step1: A unique name
1 | [package] |
Step2: Add a description and license
1 | [package] |
14-2-5 Publishing to Crates io
Be careful when publishing a crate because a publish is permanent. The version can never be overwritten, and the code cannot be deleted.
1 | $ cargo publish |
14-2-6 Publishing a New Version of an Existing Crate
When you’ve made changes to your crate and are ready to release a new version, you change the version
value specified in your Cargo.toml file and republish. Use the Semantic Versioning rules to decide what an appropriate next version number is based on the kinds of changes you’ve made.
14-2-7 Removing Versions from Crates.io with cargo yank
Although you can’t remove previous versions of a crate, you can prevent any future projects from adding them as a new dependency.
Yanking a version prevents new projects from starting to depend on that version while allowing all existing projects that depend on it to continue to download and depend on that version. Essentially, a yank means that all projects with a Cargo.lock will not break, and any future Cargo.lock files generated will not use the yanked version.
1 | $ cargo yank --vers 1.0.1 |
14-3 Cargo Workspaces
As your project develops, you might find that the library crate continues to get bigger and you want to split up your package further into multiple library crates. Cargo offers a feature called workspaces that can help manage multiple related packages that are developed in tandem.
14.3.1 Creating a Workspace
A workspace is a set of packages that share the same Cargo.lock and output directory.
-
touch
Cargo.toml
, add[workspace]
1
2
3
4
5[workspace]
members = [
"adder",
] -
cargo new adder
-
cargo build
1 | $ tree add -L 2 |
14-3-2 Creating the Second Crate in the Workspace
Step1: Edit Cargo.toml
1 | [workspace] |
Step2: Generate a new library crate
1 | $ cargo new add-one --lib |
Step3: Add function add_one
to add-one/src/lib.rs
1 | pub fn add_one(x: i32) -> i32 { |
Step4: Add a dependency on add-one
to adder/Cargo.toml
1 | [dependencies] |
Step5: Use the add_one
function from the add-one
crate in the adder
crate
1 | // adder/src/main.rs |
Step6: cargo build
Step7: cargo run -p adder
This runs the code in adder/src/main.rs, which depends on the add-one
crate.
14-3-2-1 Depending on an External Crate in a Workspace
Notice that the workspace has only one Cargo.lock file at the top level of the workspace rather than having a Cargo.lock in each crate’s directory. This ensures that all crates are using the same version of all dependencies. If we add the rand
crate to the adder/Cargo.toml and add-one/Cargo.toml files, Cargo will resolve both of those to one version of rand
and record that in the one Cargo.lock.
However, even though rand
is used somewhere in the workspace, we can’t use it in other crates in the workspace unless we add rand
to their Cargo.toml files as well.
To fix this, edit the Cargo.toml file for the adder
crate and indicate that rand
is a dependency for that crate as well. Building the adder
crate will add rand
to the list of dependencies for adder
in Cargo.lock, but no additional copies of rand
will be downloaded. Cargo has ensured that every crate in the workspace using the rand
crate will be using the same version.
14-3-2-2 Adding a Test to a Workspace
Running cargo test
in a workspace structured like this one will run the tests for all the crates in the workspace.
1 | # only run tests for the add-one |
If you publish the crates in the workspace to crates.io, each crate in the workspace will need to be published separately. The cargo publish
command does not have an --all
flag or a -p
flag, so you must change to each crate’s directory and run cargo publish
on each crate in the workspace to publish the crates.
The whole code can be found here.
14-4 Installing Binaries from Crates io with cargo install
Note that you can only install packages that have binary targets. A binary target is the runnable program that is created if the crate has a src/main.rs file or another file specified as a binary, as opposed to a library target that isn’t runnable on its own but is suitable for including within other programs.
All binaries installed with cargo install
are stored in the installation root’s bin folder (~/.cargo/bin
).
14-5 Extending Cargo with Custom Commands
Cargo is designed so you can extend it with new subcommands without having to modify Cargo. If a binary in your $PATH
is named cargo-something
, you can run it as if it was a Cargo subcommand by running cargo something
. Custom commands like this are also listed when you run cargo --list
. Being able to use cargo install
to install extensions and then run them just like the built-in Cargo tools is a super convenient benefit of Cargo’s design!