]>
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 | |
3b2f2976 XL |
4 | library crate. You may find, as your project develops, that the library crate |
5 | continues to get bigger and you want to split your package up further into | |
6 | multiple library crates. In this situation, Cargo has a feature called | |
7 | *workspaces* that can help manage multiple related packages that are developed | |
8 | in tandem. | |
cc61c64b XL |
9 | |
10 | A *workspace* is a set of packages that will all share the same *Cargo.lock* | |
3b2f2976 XL |
11 | and output directory. Let’s make a project using a workspace, using trivial |
12 | code so we can concentrate on the structure of a workspace. We’ll have a binary | |
13 | that uses two libraries: one library that will provide an `add_one` function | |
14 | and a second library that will provide an `add_two` function. These three | |
15 | crates will all be part of the same workspace. We’ll start by creating a new | |
16 | crate for the binary: | |
cc61c64b XL |
17 | |
18 | ```text | |
19 | $ cargo new --bin adder | |
20 | Created binary (application) `adder` project | |
21 | $ cd adder | |
22 | ``` | |
23 | ||
3b2f2976 XL |
24 | We need to modify the binary package’s *Cargo.toml* and add a `[workspace]` |
25 | section to tell Cargo the `adder` package is a workspace. Add this at the | |
26 | bottom of the file: | |
cc61c64b XL |
27 | |
28 | ```toml | |
29 | [workspace] | |
30 | ``` | |
31 | ||
32 | Like many Cargo features, workspaces support convention over configuration: we | |
3b2f2976 XL |
33 | don’t need to add anything more than this to *Cargo.toml* to define our |
34 | workspace as long as we follow the convention. | |
35 | ||
36 | <!-- Below -- any crates what depends on, specifically? The program? --> | |
37 | <!-- They're all programs. We mean the top-level crate in the workspace here, | |
38 | I've tried to clarify. /Carol --> | |
39 | ||
40 | ### Specifying Workspace Dependencies | |
41 | ||
42 | The workspace convention says any crates in any subdirectories that the | |
43 | top-level crate depends on are part of the workspace. Any crate, whether in a | |
44 | workspace or not, can specify that it has a dependency on a crate in a local | |
45 | directory by using the `path` attribute on the dependency specification in | |
46 | *Cargo.toml*. If a crate has the `[workspace]` key and we specify path | |
47 | dependencies where the paths are subdirectories of the crate’s directory, those | |
48 | dependent crates will be considered part of the workspace. Let’s specify in the | |
49 | *Cargo.toml* for the top-level `adder` crate that it will have a dependency on | |
50 | an `add-one` crate that will be in the `add-one` subdirectory, by changing | |
51 | *Cargo.toml* to look like this: | |
52 | ||
53 | <!-- Above, what is the path dependency actually doing here, can you fill out | |
54 | the paragraph above? --> | |
55 | <!-- done /Carol --> | |
cc61c64b XL |
56 | |
57 | ```toml | |
58 | [dependencies] | |
59 | add-one = { path = "add-one" } | |
60 | ``` | |
61 | ||
3b2f2976 XL |
62 | If we add dependencies to *Cargo.toml* that don’t have a `path` specified, |
63 | those dependencies will be normal dependencies that aren’t in this workspace | |
64 | and are assumed to come from Crates.io. | |
cc61c64b | 65 | |
3b2f2976 XL |
66 | ### Creating the Second Crate in the Workspace |
67 | ||
68 | <!-- You can see I'm adding headings, here, trying to add some more navigable | |
69 | structure -- can you improve these? I'm not sure mine are accurate --> | |
70 | <!-- Yep! /Carol --> | |
71 | ||
72 | Next, while in the `adder` directory, generate an `add-one` crate: | |
cc61c64b XL |
73 | |
74 | ```text | |
75 | $ cargo new add-one | |
76 | Created library `add-one` project | |
77 | ``` | |
78 | ||
79 | Your `adder` directory should now have these directories and files: | |
80 | ||
81 | ```text | |
82 | ├── Cargo.toml | |
83 | ├── add-one | |
84 | │ ├── Cargo.toml | |
85 | │ └── src | |
86 | │ └── lib.rs | |
87 | └── src | |
88 | └── main.rs | |
89 | ``` | |
90 | ||
3b2f2976 | 91 | In *add-one/src/lib.rs*, let’s add an `add_one` function: |
cc61c64b XL |
92 | |
93 | <span class="filename">Filename: add-one/src/lib.rs</span> | |
94 | ||
95 | ```rust | |
96 | pub fn add_one(x: i32) -> i32 { | |
97 | x + 1 | |
98 | } | |
99 | ``` | |
100 | ||
3b2f2976 XL |
101 | <!-- below -- Where are we adding the extern crate line? --> |
102 | <!-- at the top, where all the extern crate lines go and as illustrated by the listing /Carol --> | |
103 | ||
104 | Open up *src/main.rs* for `adder` and add an `extern crate` line at the top of | |
105 | the file to bring the new `add-one` library crate into scope. Then change the | |
106 | `main` function to call the `add_one` function, as in Listing 14-12: | |
cc61c64b XL |
107 | |
108 | ```rust,ignore | |
109 | extern crate add_one; | |
110 | ||
111 | fn main() { | |
112 | let num = 10; | |
113 | println!("Hello, world! {} plus one is {}!", num, add_one::add_one(num)); | |
114 | } | |
115 | ``` | |
116 | ||
3b2f2976 XL |
117 | <span class="caption">Listing 14-12: Using the `add-one` library crate from the |
118 | `adder` crate</span> | |
119 | ||
120 | Let’s build the `adder` crate by running `cargo build` in the *adder* directory! | |
cc61c64b XL |
121 | |
122 | ```text | |
123 | $ cargo build | |
124 | Compiling add-one v0.1.0 (file:///projects/adder/add-one) | |
125 | Compiling adder v0.1.0 (file:///projects/adder) | |
7cac9316 | 126 | Finished dev [unoptimized + debuginfo] target(s) in 0.68 secs |
cc61c64b XL |
127 | ``` |
128 | ||
3b2f2976 XL |
129 | Note that this builds both the `adder` crate and the `add-one` crate in |
130 | *adder/add-one*. Now your *adder* directory should have these files: | |
131 | ||
132 | ```text | |
133 | ├── Cargo.lock | |
134 | ├── Cargo.toml | |
135 | ├── add-one | |
136 | │ ├── Cargo.toml | |
137 | │ └── src | |
138 | │ └── lib.rs | |
139 | ├── src | |
140 | │ └── main.rs | |
141 | └── target | |
142 | ``` | |
cc61c64b | 143 | |
3b2f2976 XL |
144 | The workspace has one *target* directory at the top level; *add-one* doesn’t |
145 | have its own *target* directory. Even if we go into the `add-one` directory and | |
146 | run `cargo build`, the compiled artifacts end up in *adder/target* rather than | |
147 | *adder/add-one/target*. The crates in a workspace depend on each other. If each | |
148 | crate had its own *target* directory, each crate in the workspace would have to | |
149 | recompile each other crate in the workspace in order to have the artifacts in | |
150 | its own *target* directory. By sharing one *target* directory, the crates in | |
151 | the workspace can avoid rebuilding the other crates in the workspace more than | |
152 | necessary. | |
153 | ||
154 | <!-- Above -- I have no idea what this means for our project here, can you put | |
155 | it in more practical terms, or otherwise maybe just explain what this means for | |
156 | the user? --> | |
157 | <!-- I added more explanation for the target directory in this section and | |
158 | added more explanation for the Cargo.lock in the next section, since the | |
159 | Cargo.lock advantages aren't as visible until you start adding dependencies on | |
160 | external crates. What do you think? /Carol --> | |
161 | ||
162 | #### Depending on an External Crate in a Workspace | |
163 | ||
164 | Also notice the workspace only has one *Cargo.lock*, rather than having a | |
165 | top-level *Cargo.lock* and *add-one/Cargo.lock*. This ensures that all crates | |
166 | are using the same version of all dependencies. If we add the `rand` crate to | |
167 | both *Cargo.toml* and *add-one/Cargo.toml*, Cargo will resolve both of those to | |
168 | one version of `rand` and record that in the one *Cargo.lock*. Making all | |
169 | crates in the workspace use the same dependencies means the crates in the | |
170 | workspace will always be compatible with each other. Let’s try this out now. | |
171 | ||
172 | Let’s add the `rand` crate to the `[dependencies]` section in | |
173 | *add-one/Cargo.toml* in order to be able to use the `rand` crate in the | |
174 | `add-one` crate: | |
cc61c64b XL |
175 | |
176 | <span class="filename">Filename: add-one/Cargo.toml</span> | |
177 | ||
178 | ```toml | |
179 | [dependencies] | |
180 | ||
181 | rand = "0.3.14" | |
182 | ``` | |
183 | ||
3b2f2976 XL |
184 | We can now add `extern crate rand;` to *add-one/src/lib.rs*, and building the |
185 | whole workspace by running `cargo build` in the *adder* directory will bring in | |
186 | and compile the `rand` crate: | |
cc61c64b XL |
187 | |
188 | ```text | |
189 | $ cargo build | |
190 | Updating registry `https://github.com/rust-lang/crates.io-index` | |
191 | Downloading rand v0.3.14 | |
192 | ...snip... | |
193 | Compiling rand v0.3.14 | |
194 | Compiling add-one v0.1.0 (file:///projects/adder/add-one) | |
195 | Compiling adder v0.1.0 (file:///projects/adder) | |
7cac9316 | 196 | Finished dev [unoptimized + debuginfo] target(s) in 10.18 secs |
cc61c64b XL |
197 | ``` |
198 | ||
3b2f2976 XL |
199 | The top level *Cargo.lock* now contains information about `add-one`’s |
200 | dependency on `rand`. However, even though `rand` is used somewhere in the | |
201 | workspace, we can’t use it in other crates in the workspace unless we add | |
cc61c64b | 202 | `rand` to their *Cargo.toml* as well. If we add `extern crate rand;` to |
3b2f2976 | 203 | *src/main.rs* for the top level `adder` crate, for example, we’ll get an error: |
cc61c64b XL |
204 | |
205 | ```text | |
206 | $ cargo build | |
207 | Compiling adder v0.1.0 (file:///projects/adder) | |
208 | error[E0463]: can't find crate for `rand` | |
209 | --> src/main.rs:1:1 | |
210 | | | |
211 | 1 | extern crate rand; | |
212 | | ^^^^^^^^^^^^^^^^^^^ can't find crate | |
213 | ``` | |
214 | ||
3b2f2976 XL |
215 | To fix this, edit *Cargo.toml* for the top level `adder` crate and indicate |
216 | that `rand` is a dependency for that crate as well. Building the `adder` crate | |
217 | will add `rand` to the list of dependencies for `adder` in *Cargo.lock*, but no | |
218 | additional copies of `rand` will be downloaded. Cargo has ensured for us that | |
219 | any crate in the workspace using the `rand` crate will be using the same | |
220 | version. Using the same version of `rand` across the workspace saves space | |
221 | since we won’t have multiple copies and ensures that the crates in the | |
222 | workspace will be compatible with each other. | |
cc61c64b | 223 | |
3b2f2976 XL |
224 | #### Adding a Test to a Workspace |
225 | ||
226 | For another enhancement, let’s add a test of the `add_one::add_one` function | |
227 | within the `add_one` crate: | |
cc61c64b XL |
228 | |
229 | <span class="filename">Filename: add-one/src/lib.rs</span> | |
230 | ||
231 | ```rust | |
232 | pub fn add_one(x: i32) -> i32 { | |
233 | x + 1 | |
234 | } | |
235 | ||
236 | #[cfg(test)] | |
237 | mod tests { | |
238 | use super::*; | |
239 | ||
240 | #[test] | |
241 | fn it_works() { | |
242 | assert_eq!(3, add_one(2)); | |
243 | } | |
244 | } | |
245 | ``` | |
246 | ||
247 | Now run `cargo test` in the top-level *adder* directory: | |
248 | ||
249 | ```text | |
250 | $ cargo test | |
251 | Compiling adder v0.1.0 (file:///projects/adder) | |
7cac9316 | 252 | Finished dev [unoptimized + debuginfo] target(s) in 0.27 secs |
cc61c64b XL |
253 | Running target/debug/adder-f0253159197f7841 |
254 | ||
255 | running 0 tests | |
256 | ||
257 | test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured | |
258 | ``` | |
259 | ||
260 | Wait a second, zero tests? We just added one! If we look at the output, we can | |
3b2f2976 XL |
261 | see that `cargo test` in a workspace only runs tests for the top level crate. |
262 | To run tests for all of the crates in the workspace, we need to pass the | |
263 | `--all` flag: | |
264 | ||
265 | ```text | |
266 | $ cargo test --all | |
267 | Finished dev [unoptimized + debuginfo] target(s) in 0.37 secs | |
268 | Running target/debug/deps/add_one-abcabcabc | |
269 | ||
270 | running 1 test | |
271 | test tests::it_works ... ok | |
272 | ||
273 | test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out | |
274 | ||
275 | Running target/debug/deps/adder-abcabcabc | |
276 | ||
277 | running 0 tests | |
278 | ||
279 | test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out | |
280 | ||
281 | Doc-tests add-one | |
282 | ||
283 | running 0 tests | |
284 | ||
285 | test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out | |
286 | ``` | |
287 | ||
288 | When passing `--all`, `cargo test` will run the tests for all of the crates in | |
289 | the workspace. We can also choose to run tests for one particular crate in a | |
290 | workspace from the top level directory by using the `-p` flag and specifying | |
291 | the name of the crate we want to test: | |
cc61c64b XL |
292 | |
293 | ```text | |
294 | $ cargo test -p add-one | |
7cac9316 | 295 | Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs |
3b2f2976 | 296 | Running target/debug/deps/add_one-b3235fea9a156f74 |
cc61c64b XL |
297 | |
298 | running 1 test | |
299 | test tests::it_works ... ok | |
300 | ||
3b2f2976 | 301 | test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out |
cc61c64b XL |
302 | |
303 | Doc-tests add-one | |
304 | ||
305 | running 0 tests | |
306 | ||
3b2f2976 | 307 | test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out |
cc61c64b XL |
308 | ``` |
309 | ||
3b2f2976 XL |
310 | This output shows `cargo test` only ran the tests for the `add-one` crate and |
311 | didn’t run the `adder` crate tests. | |
312 | ||
313 | If you choose to publish the crates in the workspace to crates.io, each crate | |
314 | in the workspace will get published separately. The `cargo publish` command | |
315 | does not have an `--all` flag or a `-p` flag, so it is necessary to change to | |
316 | each crate’s directory and run `cargo publish` on each crate in the workspace | |
317 | in order to publish them. | |
318 | ||
319 | <!-- What does that mean, we have to publish them all one at a time?--> | |
320 | <!-- Yep, we've tried to clarify /Carol --> | |
321 | ||
322 | Now try adding an `add-two` crate to this workspace in a similar way as the | |
323 | `add-one` crate for some more practice! | |
cc61c64b | 324 | |
3b2f2976 XL |
325 | As your project grows, consider using a workspace: smaller components are |
326 | easier to understand individually than one big blob of code. Keeping the crates | |
327 | in a workspace can make coordination among them easier if they work together | |
328 | and are often changed at the same time. |