Workspaces در Cargo
در فصل 12، ما یک پکیج ساختیم که شامل یک crate باینری و یک crate کتابخانهای بود. همانطور که پروژه شما توسعه مییابد، ممکن است متوجه شوید که crate کتابخانهای همچنان بزرگتر میشود و بخواهید پکیج خود را بیشتر به crateهای کتابخانهای چندگانه تقسیم کنید. Cargo یک ویژگی به نام workspaces ارائه میدهد که میتواند به مدیریت پکیجهای مرتبط که به صورت همزمان توسعه داده میشوند کمک کند.
Creating a Workspace
A workspace is a set of packages that share the same Cargo.lock and output
directory. Let’s make a project using a workspace—we’ll use trivial code so we
can concentrate on the structure of the workspace. There are multiple ways to
structure a workspace, so we’ll just show one common way. We’ll have a
workspace containing a binary and two libraries. The binary, which will provide
the main functionality, will depend on the two libraries. One library will
provide an add_one function, and a second library an add_two function.
These three crates will be part of the same workspace. We’ll start by creating
a new directory for the workspace:
$ mkdir add
$ cd add
Next, in the add directory, we create the Cargo.toml file that will
configure the entire workspace. This file won’t have a [package] section.
Instead, it will start with a [workspace] section that will allow us to add
members to the workspace. We also make a point to use the latest and greatest
version of Cargo’s resolver algorithm in our workspace by setting the
resolver to "2".
Filename: Cargo.toml
[workspace]
resolver = "3"
Next, we’ll create the adder binary crate by running cargo new within the
add directory:
$ cargo new adder
Created binary (application) `adder` package
Adding `adder` as member of workspace at `file:///projects/add`
Running cargo new inside a workspace also automatically adds the newly created
package to the members key in the [workspace] definition in the workspace
Cargo.toml, like this:
[workspace]
resolver = "3"
members = ["adder"]
در این مرحله، میتوانیم workspace را با اجرای دستور cargo build بسازیم. فایلهای موجود در دایرکتوری add شما باید به این صورت باشند:
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
Workspace یک دایرکتوری target در سطح بالا دارد که فایلهای کامپایلشده در آن قرار خواهند گرفت. پکیج adder دایرکتوری target اختصاصی خود را ندارد. حتی اگر دستور cargo build را از داخل دایرکتوری adder اجرا کنیم، فایلهای کامپایلشده همچنان در add/target قرار میگیرند نه در add/adder/target. Cargo دایرکتوری target را در یک workspace به این صورت ساختاردهی میکند زیرا crateهای موجود در یک workspace برای وابستگی به یکدیگر طراحی شدهاند. اگر هر crate دایرکتوری target اختصاصی خود را داشت، هر crate مجبور بود هر کدام از crateهای دیگر را در workspace دوباره کامپایل کند تا فایلهای کامپایلشده را در دایرکتوری target خود قرار دهد. با به اشتراکگذاری یک دایرکتوری target، crateها میتوانند از ساخت مجدد غیرضروری جلوگیری کنند.
ایجاد پکیج دوم در Workspace
Next, let’s create another member package in the workspace and call it
add_one. Change the top-level Cargo.toml to specify the add_one path in
the members list:
Filename: Cargo.toml
[workspace]
resolver = "3"
members = ["adder", "add_one"]
Then generate a new library crate named add_one:
$ cargo new add_one --lib
Created library `add_one` package
Adding `add_one` as member of workspace at `file:///projects/add`
The top-level Cargo.toml will now include the add_one path in the members
list:
Filename: Cargo.toml
[workspace]
resolver = "3"
members = ["adder", "add_one"]
دایرکتوری add شما اکنون باید شامل این دایرکتوریها و فایلها باشد:
├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
در فایل add_one/src/lib.rs، تابعی به نام add_one اضافه کنیم:
Filename: add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
حالا میتوانیم پکیج adder که حاوی باینری ما است را وابسته به پکیج add_one که حاوی کتابخانه ما است کنیم. ابتدا باید یک وابستگی مسیر (path dependency) به add_one در فایل adder/Cargo.toml اضافه کنیم.
Filename: adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }
Cargo فرض نمیکند که crateهای موجود در یک workspace به یکدیگر وابسته هستند، بنابراین ما باید به صراحت روابط وابستگی را مشخص کنیم.
در ادامه، بیایید از تابع add_one (از crate به نام add_one) در crate به نام adder استفاده کنیم. فایل adder/src/main.rs را باز کنید و تابع main را تغییر دهید تا تابع add_one را فراخوانی کند، همانطور که در لیست ۱۴-۷ نشان داده شده است.
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
add_one library crate in the adder crateبیایید workspace را با اجرای دستور cargo build در دایرکتوری سطح بالای add بسازیم!
$ cargo build
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s
برای اجرای crate باینری از دایرکتوری add، میتوانیم با استفاده از آرگومان -p و نام پکیج همراه با دستور cargo run مشخص کنیم کدام پکیج در workspace اجرا شود:
$ cargo run -p adder
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
این کد در فایل adder/src/main.rs را اجرا میکند که به crate add_one وابسته است.
وابستگی به یک پکیج خارجی در یک Workspace
توجه کنید که workspace فقط یک فایل Cargo.lock در سطح بالا دارد، به جای اینکه هر crate دایرکتوری خود فایل Cargo.lock داشته باشد. این اطمینان حاصل میکند که تمام crateها از همان نسخه تمام وابستگیها استفاده میکنند. اگر پکیج rand را به فایلهای adder/Cargo.toml و add_one/Cargo.toml اضافه کنیم، Cargo هر دو را به یک نسخه از rand تبدیل میکند و آن را در فایل Cargo.lock ثبت میکند. اطمینان از اینکه همه crateهای موجود در workspace از همان وابستگیها استفاده میکنند، به این معناست که crateها همیشه با یکدیگر سازگار خواهند بود. بیایید پکیج rand را به بخش [dependencies] در فایل add_one/Cargo.toml اضافه کنیم تا بتوانیم از crate rand در crate add_one استفاده کنیم:
Filename: add_one/Cargo.toml
[dependencies]
rand = "0.8.5"
حالا میتوانیم use rand; را به فایل add_one/src/lib.rs اضافه کنیم و با اجرای دستور cargo build در دایرکتوری add کل workspace را بسازیم، که crate rand را وارد کرده و کامپایل خواهد کرد. یک هشدار دریافت خواهیم کرد زیرا به rand که به محدوده وارد شده است اشارهای نمیکنیم:
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
--snip--
Compiling rand v0.8.5
Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
--> add_one/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1 suggestion)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s
فایل Cargo.lock در سطح بالا اکنون اطلاعاتی درباره وابستگی add_one به rand دارد. با این حال، حتی اگر rand در جایی از workspace استفاده شود، نمیتوانیم از آن در crateهای دیگر workspace استفاده کنیم مگر اینکه rand را به فایلهای Cargo.toml آنها نیز اضافه کنیم. برای مثال، اگر use rand; را به فایل adder/src/main.rs برای پکیج adder اضافه کنیم، با خطا مواجه خواهیم شد:
$ cargo build
--snip--
Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
--> adder/src/main.rs:2:5
|
2 | use rand;
| ^^^^ no external crate `rand`
To fix this, edit the Cargo.toml file for the adder package and indicate
that rand is a dependency for it as well. Building the adder package will
add rand to the list of dependencies for adder in Cargo.lock, but no
additional copies of rand will be downloaded. Cargo will ensure that every
crate in every package in the workspace using the rand package will be using
the same version as long as they specify compatible versions of rand, saving
us space and ensuring that the crates in the workspace will be compatible with
each other.
If crates in the workspace specify incompatible versions of the same dependency, Cargo will resolve each of them, but will still try to resolve as few versions as possible.
افزودن یک تست به یک Workspace
برای یک بهبود دیگر، بیایید یک تست برای تابع add_one::add_one در crate add_one اضافه کنیم:
Filename: add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(3, add_one(2));
}
}
حالا دستور cargo test را در دایرکتوری سطح بالای add اجرا کنید. اجرای دستور cargo test در یک workspace با ساختاری مانند این، تستهای تمام crateهای موجود در workspace را اجرا خواهد کرد:
$ cargo test
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/adder-3a47283c568d2b6a)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
بخش اول خروجی نشان میدهد که تست it_works در crate add_one پاس شده است. بخش بعدی نشان میدهد که هیچ تستی در crate adder پیدا نشده است، و سپس بخش آخر نشان میدهد که هیچ تست مستنداتی در crate add_one پیدا نشده است.
ما همچنین میتوانیم تستهای یک crate خاص در workspace را از دایرکتوری سطح بالا با استفاده از گزینه -p و مشخص کردن نام crateای که میخواهیم تست کنیم، اجرا کنیم:
$ cargo test -p add_one
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
این خروجی نشان میدهد که cargo test فقط تستهای crate add_one را اجرا کرده و تستهای crate adder را اجرا نکرده است.
If you publish the crates in the workspace to crates.io,
each crate in the workspace will need to be published separately. Like cargo test, we can publish a particular crate in our workspace by using the -p
flag and specifying the name of the crate we want to publish.
For additional practice, add an add_two crate to this workspace in a similar
way as the add_one crate!
As your project grows, consider using a workspace: it’s easier to understand smaller, individual components than one big blob of code. Furthermore, keeping the crates in a workspace can make coordination between crates easier if they are often changed at the same time.