]>
Commit | Line | Data |
---|---|---|
cc61c64b XL |
1 | ## Cargo Workspaces |
2 | ||
3 | In Chapter 12, we built a package that included both a binary crate and a | |
4 | library crate. But what if the library crate continues to get bigger and we | |
5 | want to split our package up further into multiple library crates? As packages | |
6 | grow, separating out major components can be quite useful. In this situation, | |
7 | Cargo has a feature called *workspaces* that can help us manage multiple | |
8 | related packages that are developed in tandem. | |
9 | ||
10 | A *workspace* is a set of packages that will all share the same *Cargo.lock* | |
11 | and output directory. Let's make a project using a workspace where the code | |
12 | will be trivial so that we can concentrate on the structure of a workspace. | |
13 | We'll have a binary that uses two libraries: one that will provide an `add_one` | |
14 | method and a second that will provide an `add_two` method. Let's start by | |
15 | creating a new crate for the binary: | |
16 | ||
17 | ```text | |
18 | $ cargo new --bin adder | |
19 | Created binary (application) `adder` project | |
20 | $ cd adder | |
21 | ``` | |
22 | ||
23 | We need to modify the binary package's *Cargo.toml* to tell Cargo the `adder` | |
24 | package is a workspace. Add this at the bottom of the file: | |
25 | ||
26 | ```toml | |
27 | [workspace] | |
28 | ``` | |
29 | ||
30 | Like many Cargo features, workspaces support convention over configuration: we | |
31 | don't need to say anything more than this as long as we follow the convention. | |
32 | The convention is that any crates that we depend on as sub-directories will be | |
33 | part of the workspace. Let's add a path dependency to the `adder` crate by | |
34 | changing the `[dependencies]` section of *Cargo.toml* to look like this: | |
35 | ||
36 | ```toml | |
37 | [dependencies] | |
38 | add-one = { path = "add-one" } | |
39 | ``` | |
40 | ||
41 | If we add dependencies that don't have a `path` specified, those will be normal | |
42 | dependencies that aren't in this workspace. | |
43 | ||
44 | Next, generate the `add-one` crate within the `adder` directory: | |
45 | ||
46 | ```text | |
47 | $ cargo new add-one | |
48 | Created library `add-one` project | |
49 | ``` | |
50 | ||
51 | Your `adder` directory should now have these directories and files: | |
52 | ||
53 | ```text | |
54 | ├── Cargo.toml | |
55 | ├── add-one | |
56 | │ ├── Cargo.toml | |
57 | │ └── src | |
58 | │ └── lib.rs | |
59 | └── src | |
60 | └── main.rs | |
61 | ``` | |
62 | ||
63 | In *add-one/src/lib.rs*, let's add an implementation of an `add_one` function: | |
64 | ||
65 | <span class="filename">Filename: add-one/src/lib.rs</span> | |
66 | ||
67 | ```rust | |
68 | pub fn add_one(x: i32) -> i32 { | |
69 | x + 1 | |
70 | } | |
71 | ``` | |
72 | ||
73 | Open up *src/main.rs* for `adder` and add an `extern crate` line to bring the | |
74 | new `add-one` library crate into scope, and change the `main` function to use | |
75 | the `add_one` function: | |
76 | ||
77 | ```rust,ignore | |
78 | extern crate add_one; | |
79 | ||
80 | fn main() { | |
81 | let num = 10; | |
82 | println!("Hello, world! {} plus one is {}!", num, add_one::add_one(num)); | |
83 | } | |
84 | ``` | |
85 | ||
86 | Let's build it! | |
87 | ||
88 | ```text | |
89 | $ cargo build | |
90 | Compiling add-one v0.1.0 (file:///projects/adder/add-one) | |
91 | Compiling adder v0.1.0 (file:///projects/adder) | |
92 | Finished debug [unoptimized + debuginfo] target(s) in 0.68 secs | |
93 | ``` | |
94 | ||
95 | Note that running `cargo build` in the *adder* directory built both that crate | |
96 | and the `add-one` crate in *adder/add-one*, but created only one *Cargo.lock* | |
97 | and one *target* directory, both in the *adder* directory. See if you can add | |
98 | an `add-two` crate in the same way. | |
99 | ||
100 | Let's now say that we'd like to use the `rand` crate in our `add-one` crate. | |
101 | As usual, we'll add it to the `[dependencies]` section in the `Cargo.toml` for | |
102 | that crate: | |
103 | ||
104 | <span class="filename">Filename: add-one/Cargo.toml</span> | |
105 | ||
106 | ```toml | |
107 | [dependencies] | |
108 | ||
109 | rand = "0.3.14" | |
110 | ``` | |
111 | ||
112 | And if we add `extern crate rand;` to *add-one/src/lib.rs* then run `cargo | |
113 | build`, it will succeed: | |
114 | ||
115 | ```text | |
116 | $ cargo build | |
117 | Updating registry `https://github.com/rust-lang/crates.io-index` | |
118 | Downloading rand v0.3.14 | |
119 | ...snip... | |
120 | Compiling rand v0.3.14 | |
121 | Compiling add-one v0.1.0 (file:///projects/adder/add-one) | |
122 | Compiling adder v0.1.0 (file:///projects/adder) | |
123 | Finished debug [unoptimized + debuginfo] target(s) in 10.18 secs | |
124 | ``` | |
125 | ||
126 | The top level *Cargo.lock* now reflects the fact that `add-one` depends | |
127 | on `rand`. However, even though `rand` is used somewhere in the | |
128 | workspace, we can't use it in other crates in the workspace unless we add | |
129 | `rand` to their *Cargo.toml* as well. If we add `extern crate rand;` to | |
130 | *src/main.rs* for the top level `adder` crate, for example, we'll get an error: | |
131 | ||
132 | ```text | |
133 | $ cargo build | |
134 | Compiling adder v0.1.0 (file:///projects/adder) | |
135 | error[E0463]: can't find crate for `rand` | |
136 | --> src/main.rs:1:1 | |
137 | | | |
138 | 1 | extern crate rand; | |
139 | | ^^^^^^^^^^^^^^^^^^^ can't find crate | |
140 | ``` | |
141 | ||
142 | To fix this, edit *Cargo.toml* for the top level and indicate that `rand` is a | |
143 | dependency for the `adder` crate. | |
144 | ||
145 | For another enhancement, let's add a test of the `add_one::add_one` function | |
146 | within that crate: | |
147 | ||
148 | <span class="filename">Filename: add-one/src/lib.rs</span> | |
149 | ||
150 | ```rust | |
151 | pub fn add_one(x: i32) -> i32 { | |
152 | x + 1 | |
153 | } | |
154 | ||
155 | #[cfg(test)] | |
156 | mod tests { | |
157 | use super::*; | |
158 | ||
159 | #[test] | |
160 | fn it_works() { | |
161 | assert_eq!(3, add_one(2)); | |
162 | } | |
163 | } | |
164 | ``` | |
165 | ||
166 | Now run `cargo test` in the top-level *adder* directory: | |
167 | ||
168 | ```text | |
169 | $ cargo test | |
170 | Compiling adder v0.1.0 (file:///projects/adder) | |
171 | Finished debug [unoptimized + debuginfo] target(s) in 0.27 secs | |
172 | Running target/debug/adder-f0253159197f7841 | |
173 | ||
174 | running 0 tests | |
175 | ||
176 | test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured | |
177 | ``` | |
178 | ||
179 | Wait a second, zero tests? We just added one! If we look at the output, we can | |
180 | see that `cargo test` in a workspace only runs the tests for the top level | |
181 | crate. To run tests for the other crates, we need to use the `-p` argument to | |
182 | indicate we want to run tests for a particular package: | |
183 | ||
184 | ```text | |
185 | $ cargo test -p add-one | |
186 | Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs | |
187 | Running target/debug/deps/add_one-abcabcabc | |
188 | ||
189 | running 1 test | |
190 | test tests::it_works ... ok | |
191 | ||
192 | test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured | |
193 | ||
194 | Doc-tests add-one | |
195 | ||
196 | running 0 tests | |
197 | ||
198 | test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured | |
199 | ``` | |
200 | ||
201 | Similarly, if you choose to publish the workspace to crates.io, each crate in | |
202 | the workspace will get published separately. | |
203 | ||
204 | As your project grows, consider a workspace: smaller components are easier to | |
205 | understand individually than one big blob of code. Keeping the crates in a | |
206 | workspace can make coordination among them easier if they work together and are | |
207 | often changed at the same time. |