]>
Commit | Line | Data |
---|---|---|
13cf67c4 XL |
1 | ## Cargo Workspaces |
2 | ||
3 | In Chapter 12, we built a package that included a binary crate and a library | |
4 | crate. As your project develops, you might find that the library crate | |
5 | continues to get bigger and you want to split up your package further into | |
6 | multiple library crates. In this situation, Cargo offers a feature called | |
7 | *workspaces* that can help manage multiple related packages that are developed | |
8 | in tandem. | |
9 | ||
10 | ### Creating a Workspace | |
11 | ||
12 | A *workspace* is a set of packages that share the same *Cargo.lock* and output | |
13 | directory. Let’s make a project using a workspace—we’ll use trivial code so we | |
14 | can concentrate on the structure of the workspace. There are multiple ways to | |
15 | structure a workspace; we’re going to show one common way. We’ll have a | |
16 | workspace containing a binary and two libraries. The binary, which will provide | |
17 | the main functionality, will depend on the two libraries. One library will | |
18 | provide an `add_one` function, and a second library an `add_two` function. | |
19 | These three crates will be part of the same workspace. We’ll start by creating | |
20 | a new directory for the workspace: | |
21 | ||
22 | ```text | |
23 | $ mkdir add | |
24 | $ cd add | |
25 | ``` | |
26 | ||
27 | Next, in the *add* directory, we create the *Cargo.toml* file that will | |
28 | configure the entire workspace. This file won’t have a `[package]` section or | |
29 | the metadata we’ve seen in other *Cargo.toml* files. Instead, it will start | |
30 | with a `[workspace]` section that will allow us to add members to the workspace | |
31 | by specifying the path to our binary crate; in this case, that path is *adder*: | |
32 | ||
33 | <span class="filename">Filename: Cargo.toml</span> | |
34 | ||
35 | ```toml | |
36 | [workspace] | |
37 | ||
38 | members = [ | |
39 | "adder", | |
40 | ] | |
41 | ``` | |
42 | ||
43 | Next, we’ll create the `adder` binary crate by running `cargo new` within the | |
44 | *add* directory: | |
45 | ||
46 | ```text | |
47 | $ cargo new adder | |
48 | Created binary (application) `adder` project | |
49 | ``` | |
50 | ||
51 | At this point, we can build the workspace by running `cargo build`. The files | |
52 | in your *add* directory should look like this: | |
53 | ||
54 | ```text | |
55 | ├── Cargo.lock | |
56 | ├── Cargo.toml | |
57 | ├── adder | |
58 | │ ├── Cargo.toml | |
59 | │ └── src | |
60 | │ └── main.rs | |
61 | └── target | |
62 | ``` | |
63 | ||
64 | The workspace has one *target* directory at the top level for the compiled | |
65 | artifacts to be placed into; the `adder` crate doesn’t have its own *target* | |
66 | directory. Even if we were to run `cargo build` from inside the *adder* | |
67 | directory, the compiled artifacts would still end up in *add/target* rather | |
68 | than *add/adder/target*. Cargo structures the *target* directory in a workspace | |
69 | like this because the crates in a workspace are meant to depend on each other. | |
70 | If each crate had its own *target* directory, each crate would have to | |
71 | recompile each of the other crates in the workspace to have the artifacts in | |
72 | its own *target* directory. By sharing one *target* directory, the crates can | |
73 | avoid unnecessary rebuilding. | |
74 | ||
75 | ### Creating the Second Crate in the Workspace | |
76 | ||
77 | Next, let’s create another member crate in the workspace and call it `add-one`. | |
78 | Change the top-level *Cargo.toml* to specify the *add-one* path in the | |
79 | `members` list: | |
80 | ||
81 | <span class="filename">Filename: Cargo.toml</span> | |
82 | ||
83 | ```toml | |
84 | [workspace] | |
85 | ||
86 | members = [ | |
87 | "adder", | |
88 | "add-one", | |
89 | ] | |
90 | ``` | |
91 | ||
92 | Then generate a new library crate named `add-one`: | |
93 | ||
94 | ```text | |
95 | $ cargo new add-one --lib | |
96 | Created library `add-one` project | |
97 | ``` | |
98 | ||
99 | Your *add* directory should now have these directories and files: | |
100 | ||
101 | ```text | |
102 | ├── Cargo.lock | |
103 | ├── Cargo.toml | |
104 | ├── add-one | |
105 | │ ├── Cargo.toml | |
106 | │ └── src | |
107 | │ └── lib.rs | |
108 | ├── adder | |
109 | │ ├── Cargo.toml | |
110 | │ └── src | |
111 | │ └── main.rs | |
112 | └── target | |
113 | ``` | |
114 | ||
115 | In the *add-one/src/lib.rs* file, let’s add an `add_one` function: | |
116 | ||
117 | <span class="filename">Filename: add-one/src/lib.rs</span> | |
118 | ||
119 | ```rust | |
120 | pub fn add_one(x: i32) -> i32 { | |
121 | x + 1 | |
122 | } | |
123 | ``` | |
124 | ||
125 | Now that we have a library crate in the workspace, we can have the binary crate | |
126 | `adder` depend on the library crate `add-one`. First, we’ll need to add a path | |
127 | dependency on `add-one` to *adder/Cargo.toml*. | |
128 | ||
129 | <span class="filename">Filename: adder/Cargo.toml</span> | |
130 | ||
131 | ```toml | |
132 | [dependencies] | |
133 | ||
134 | add-one = { path = "../add-one" } | |
135 | ``` | |
136 | ||
137 | Cargo doesn’t assume that crates in a workspace will depend on each other, so | |
138 | we need to be explicit about the dependency relationships between the crates. | |
139 | ||
140 | Next, let’s use the `add_one` function from the `add-one` crate in the `adder` | |
9fa01778 | 141 | crate. Open the *adder/src/main.rs* file and add a `use` line at the top to |
13cf67c4 | 142 | bring the new `add-one` library crate into scope. Then change the `main` |
9fa01778 | 143 | function to call the `add_one` function, as in Listing 14-7. |
13cf67c4 XL |
144 | |
145 | <span class="filename">Filename: adder/src/main.rs</span> | |
146 | ||
147 | ```rust,ignore | |
148 | use add_one; | |
149 | ||
150 | fn main() { | |
151 | let num = 10; | |
152 | println!("Hello, world! {} plus one is {}!", num, add_one::add_one(num)); | |
153 | } | |
154 | ``` | |
155 | ||
156 | <span class="caption">Listing 14-7: Using the `add-one` library crate from the | |
157 | `adder` crate</span> | |
158 | ||
159 | Let’s build the workspace by running `cargo build` in the top-level *add* | |
160 | directory! | |
161 | ||
162 | ```text | |
163 | $ cargo build | |
164 | Compiling add-one v0.1.0 (file:///projects/add/add-one) | |
165 | Compiling adder v0.1.0 (file:///projects/add/adder) | |
166 | Finished dev [unoptimized + debuginfo] target(s) in 0.68 secs | |
167 | ``` | |
168 | ||
169 | To run the binary crate from the *add* directory, we need to specify which | |
170 | package in the workspace we want to use by using the `-p` argument and the | |
171 | package name with `cargo run`: | |
172 | ||
173 | ```text | |
174 | $ cargo run -p adder | |
175 | Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs | |
176 | Running `target/debug/adder` | |
177 | Hello, world! 10 plus one is 11! | |
178 | ``` | |
179 | ||
180 | This runs the code in *adder/src/main.rs*, which depends on the `add-one` crate. | |
181 | ||
182 | #### Depending on an External Crate in a Workspace | |
183 | ||
184 | Notice that the workspace has only one *Cargo.lock* file at the top level of | |
185 | the workspace rather than having a *Cargo.lock* in each crate’s directory. This | |
186 | ensures that all crates are using the same version of all dependencies. If we | |
187 | add the `rand` crate to the *adder/Cargo.toml* and *add-one/Cargo.toml* | |
188 | files, Cargo will resolve both of those to one version of `rand` and record | |
189 | that in the one *Cargo.lock*. Making all crates in the workspace use the same | |
190 | dependencies means the crates in the workspace will always be compatible with | |
191 | each other. Let’s add the `rand` crate to the `[dependencies]` section in the | |
192 | *add-one/Cargo.toml* file to be able to use the `rand` crate in the `add-one` | |
193 | crate: | |
194 | ||
195 | <span class="filename">Filename: add-one/Cargo.toml</span> | |
196 | ||
197 | ```toml | |
198 | [dependencies] | |
199 | ||
200 | rand = "0.3.14" | |
201 | ``` | |
202 | ||
203 | We can now add `use rand;` to the *add-one/src/lib.rs* file, and building the | |
204 | whole workspace by running `cargo build` in the *add* directory will bring in | |
205 | and compile the `rand` crate: | |
206 | ||
207 | ```text | |
208 | $ cargo build | |
209 | Updating registry `https://github.com/rust-lang/crates.io-index` | |
210 | Downloading rand v0.3.14 | |
211 | --snip-- | |
212 | Compiling rand v0.3.14 | |
213 | Compiling add-one v0.1.0 (file:///projects/add/add-one) | |
214 | Compiling adder v0.1.0 (file:///projects/add/adder) | |
215 | Finished dev [unoptimized + debuginfo] target(s) in 10.18 secs | |
216 | ``` | |
217 | ||
218 | The top-level *Cargo.lock* now contains information about the dependency of | |
219 | `add-one` on `rand`. However, even though `rand` is used somewhere in the | |
220 | workspace, we can’t use it in other crates in the workspace unless we add | |
532ac7d7 XL |
221 | `rand` to their *Cargo.toml* files as well. For example, if we add `use rand;` |
222 | to the *adder/src/main.rs* file for the `adder` crate, we’ll get an error: | |
13cf67c4 XL |
223 | |
224 | ```text | |
225 | $ cargo build | |
226 | Compiling adder v0.1.0 (file:///projects/add/adder) | |
227 | error: use of unstable library feature 'rand': use `rand` from crates.io (see | |
228 | issue #27703) | |
229 | --> adder/src/main.rs:1:1 | |
230 | | | |
231 | 1 | use rand; | |
232 | ``` | |
233 | ||
234 | To fix this, edit the *Cargo.toml* file for the `adder` crate and indicate that | |
235 | `rand` is a dependency for that crate as well. Building the `adder` crate will | |
236 | add `rand` to the list of dependencies for `adder` in *Cargo.lock*, but no | |
237 | additional copies of `rand` will be downloaded. Cargo has ensured that every | |
238 | crate in the workspace using the `rand` crate will be using the same version. | |
239 | Using the same version of `rand` across the workspace saves space because we | |
240 | won’t have multiple copies and ensures that the crates in the workspace will be | |
241 | compatible with each other. | |
242 | ||
243 | #### Adding a Test to a Workspace | |
244 | ||
245 | For another enhancement, let’s add a test of the `add_one::add_one` function | |
246 | within the `add_one` crate: | |
247 | ||
248 | <span class="filename">Filename: add-one/src/lib.rs</span> | |
249 | ||
250 | ```rust | |
251 | pub fn add_one(x: i32) -> i32 { | |
252 | x + 1 | |
253 | } | |
254 | ||
255 | #[cfg(test)] | |
256 | mod tests { | |
257 | use super::*; | |
258 | ||
259 | #[test] | |
260 | fn it_works() { | |
261 | assert_eq!(3, add_one(2)); | |
262 | } | |
263 | } | |
264 | ``` | |
265 | ||
266 | Now run `cargo test` in the top-level *add* directory: | |
267 | ||
268 | ```text | |
269 | $ cargo test | |
270 | Compiling add-one v0.1.0 (file:///projects/add/add-one) | |
271 | Compiling adder v0.1.0 (file:///projects/add/adder) | |
272 | Finished dev [unoptimized + debuginfo] target(s) in 0.27 secs | |
273 | Running target/debug/deps/add_one-f0253159197f7841 | |
274 | ||
275 | running 1 test | |
276 | test tests::it_works ... ok | |
277 | ||
278 | test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out | |
279 | ||
280 | Running target/debug/deps/adder-f88af9d2cc175a5e | |
281 | ||
282 | running 0 tests | |
283 | ||
284 | test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out | |
285 | ||
286 | Doc-tests add-one | |
287 | ||
288 | running 0 tests | |
289 | ||
290 | test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out | |
291 | ``` | |
292 | ||
293 | The first section of the output shows that the `it_works` test in the `add-one` | |
294 | crate passed. The next section shows that zero tests were found in the `adder` | |
295 | crate, and then the last section shows zero documentation tests were found in | |
296 | the `add-one` crate. Running `cargo test` in a workspace structured like this | |
297 | one will run the tests for all the crates in the workspace. | |
298 | ||
299 | We can also run tests for one particular crate in a workspace from the | |
300 | top-level directory by using the `-p` flag and specifying the name of the crate | |
301 | we want to test: | |
302 | ||
303 | ```text | |
304 | $ cargo test -p add-one | |
305 | Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs | |
306 | Running target/debug/deps/add_one-b3235fea9a156f74 | |
307 | ||
308 | running 1 test | |
309 | test tests::it_works ... ok | |
310 | ||
311 | test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out | |
312 | ||
313 | Doc-tests add-one | |
314 | ||
315 | running 0 tests | |
316 | ||
317 | test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out | |
318 | ``` | |
319 | ||
320 | This output shows `cargo test` only ran the tests for the `add-one` crate and | |
321 | didn’t run the `adder` crate tests. | |
322 | ||
323 | If you publish the crates in the workspace to *https://crates.io/*, each crate | |
324 | in the workspace will need to be published separately. The `cargo publish` | |
325 | command does not have an `--all` flag or a `-p` flag, so you must change to | |
326 | each crate’s directory and run `cargo publish` on each crate in the workspace | |
327 | to publish the crates. | |
328 | ||
329 | For additional practice, add an `add-two` crate to this workspace in a similar | |
330 | way as the `add-one` crate! | |
331 | ||
332 | As your project grows, consider using a workspace: it’s easier to understand | |
333 | smaller, individual components than one big blob of code. Furthermore, keeping | |
334 | the crates in a workspace can make coordination between them easier if they are | |
335 | often changed at the same time. |