Workspaces در Cargo
در فصل 12، ما یک پکیج ساختیم که شامل یک crate باینری و یک crate کتابخانهای بود. همانطور که پروژه شما توسعه مییابد، ممکن است متوجه شوید که crate کتابخانهای همچنان بزرگتر میشود و بخواهید پکیج خود را بیشتر به crateهای کتابخانهای چندگانه تقسیم کنید. Cargo یک ویژگی به نام workspaces ارائه میدهد که میتواند به مدیریت پکیجهای مرتبط که به صورت همزمان توسعه داده میشوند کمک کند.
ایجاد یک Workspace
یک workspace مجموعهای از پکیجها است که یک فایل Cargo.lock و دایرکتوری خروجی مشترک دارند. بیایید یک پروژه با استفاده از workspace ایجاد کنیم—ما از کد سادهای استفاده خواهیم کرد تا بتوانیم بر ساختار workspace تمرکز کنیم. راههای متعددی برای ساختن یک workspace وجود دارد، بنابراین فقط یک روش رایج را نشان خواهیم داد. ما یک workspace شامل یک باینری و دو کتابخانه خواهیم داشت. باینری که عملکرد اصلی را فراهم خواهد کرد، به دو کتابخانه وابسته خواهد بود. یک کتابخانه تابع add_one
و کتابخانه دیگر تابع add_two
ارائه خواهد داد. این سه crate بخشی از یک workspace خواهند بود. ابتدا با ایجاد یک دایرکتوری جدید برای workspace شروع میکنیم:
$ mkdir add
$ cd add
سپس، در دایرکتوری add، فایل Cargo.toml را ایجاد میکنیم که کل workspace را پیکربندی میکند. این فایل بخش [package]
نخواهد داشت. در عوض، با یک بخش [workspace]
شروع میشود که به ما اجازه میدهد اعضا را به workspace اضافه کنیم. همچنین نسخه جدیدتر الگوریتم resolver Cargo را با تنظیم resolver
به "2"
استفاده میکنیم.
Filename: Cargo.toml
[workspace]
resolver = "2"
سپس، crate باینری adder
را با اجرای cargo new
در دایرکتوری add ایجاد میکنیم:
$ cargo new adder
Creating binary (application) `adder` package
Adding `adder` as member of workspace at `file:///projects/add`
اجرای cargo new
داخل یک workspace به صورت خودکار پکیج تازه ایجاد شده را به کلید members
در تعریف [workspace]
در فایل Cargo.toml
workspace اضافه میکند، به این صورت:
[workspace]
resolver = "2"
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
حالا، بیایید یک پکیج عضو دیگر در workspace ایجاد کنیم و آن را add_one
بنامیم. فایل Cargo.toml در سطح بالا را تغییر دهید تا مسیر add_one را در لیست members
مشخص کنید:
Filename: Cargo.toml
[workspace]
resolver = "2"
members = ["adder", "add_one"]
سپس یک crate کتابخانهای جدید به نام add_one
ایجاد کنید:
$ cargo new add_one --lib
Creating library `add_one` package
Adding `add_one` as member of workspace at `file:///projects/add`
دایرکتوری 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`
برای رفع این مشکل، فایل Cargo.toml پکیج adder
را ویرایش کرده و مشخص کنید که rand
برای آن نیز یک وابستگی است. ساختن پکیج adder
، rand
را به لیست وابستگیهای adder
در فایل Cargo.lock اضافه میکند، اما هیچ نسخه اضافی از rand
دانلود نخواهد شد. Cargo اطمینان حاصل میکند که هر crate در هر پکیجی از workspace که از پکیج rand
استفاده میکند، از همان نسخه استفاده کند، به شرطی که نسخههای سازگار از rand
را مشخص کنند. این کار فضای ما را ذخیره کرده و تضمین میکند که crateهای workspace با یکدیگر سازگار خواهند بود.
اگر crateهای workspace نسخههای ناسازگار از یک وابستگی را مشخص کنند، Cargo هر یک از آنها را جداگانه حل خواهد کرد، اما همچنان تلاش میکند که تعداد نسخههای حلشده را به حداقل برساند.
افزودن یک تست به یک 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-f0253159197f7841)
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-49979ff40686fa8e)
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-b3235fea9a156f74)
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
را اجرا نکرده است.
اگر crateهای موجود در workspace را در crates.io منتشر کنید، هر crate در workspace باید به صورت جداگانه منتشر شود. مشابه با cargo test
، میتوانیم یک crate خاص را در workspace خود با استفاده از گزینه -p
و مشخص کردن نام crateای که میخواهیم منتشر کنیم، منتشر کنیم.
برای تمرین بیشتر، یک crate جدید به نام add_two
به این workspace اضافه کنید، به شیوهای مشابه crate add_one
!
همانطور که پروژه شما رشد میکند، استفاده از یک workspace را در نظر بگیرید: فهمیدن اجزای کوچکتر و جداگانه آسانتر از کار کردن با یک کد بزرگ و یکپارچه است. علاوه بر این، نگه داشتن crateها در یک workspace میتواند هماهنگی بین آنها را آسانتر کند، به خصوص اگر crateها اغلب به طور همزمان تغییر کنند.