พื้นที่ทำงานของ Cargo (Cargo Workspaces)
ในบทที่ 12 เราได้สร้างแพ็กเกจที่ประกอบด้วยไบนารี crate และไลบรารี crate เมื่อโปรเจกต์ของคุณได้รับการพัฒนาอย่างต่อเนื่อง คุณอาจพบว่าไลบรารี crate มีขนาดใหญ่ขึ้นเรื่อย ๆ และคุณต้องการแบ่งแพ็กเกจของคุณออกเป็นหลาย ๆ ไลบรารี crate Cargo จึงมีฟีเจอร์ที่เรียกว่า พื้นที่ทำงาน (workspaces) ซึ่งสามารถช่วยจัดการหลาย ๆ แพ็กเกจที่เกี่ยวข้องกันและถูกพัฒนาไปพร้อมกันได้
การสร้างพื้นที่ทำงาน (Workspace)
พื้นที่ทำงาน (workspace) คือชุดของแพ็กเกจที่ใช้ไฟล์ Cargo.lock และไดเรกทอรีผลลัพธ์ (output directory) ร่วมกัน มาลองสร้างโปรเจกต์โดยใช้พื้นที่ทำงานกัน โดยเราจะใช้โค้ดแบบง่าย ๆ เพื่อให้สามารถมุ่งเน้นไปที่โครงสร้างของพื้นที่ทำงานได้ มีหลายวิธีในการจัดโครงสร้างพื้นที่ทำงาน ดังนั้นเราจะแสดงวิธีที่พบบ่อยวิธีหนึ่ง โดยเราจะมีพื้นที่ทำงานที่ประกอบด้วยหนึ่งไบนารีและสองไลบรารี ไบนารีซึ่งจะทำหน้าที่หลักจะพึ่งพาไลบรารีทั้งสองนั้น ไลบรารีหนึ่งจะให้บริการฟังก์ชัน add_one และอีกไลบรารีหนึ่งจะให้บริการฟังก์ชัน add_two ทั้งสาม crate นี้จะอยู่ภายใต้พื้นที่ทำงานเดียวกัน เราจะเริ่มจากการสร้างไดเรกทอรีใหม่สำหรับพื้นที่ทำงาน:
$ mkdir add
$ cd add
ถัดไป ภายในไดเรกทอรี add เราจะสร้างไฟล์ Cargo.toml ที่จะกำหนดโครงร่างพื้นที่ทำงานทั้งหมด ไฟล์นี้จะไม่มีส่วน [package] แต่จะเริ่มต้นด้วยส่วน [workspace] ซึ่งจะอนุญาตให้เราเพิ่มสมาชิกเข้าไปในพื้นที่ทำงานได้ เรายังระบุให้ใช้อัลกอริทึม Resolver เวอร์ชันล่าสุดและดีที่สุดของ Cargo ในพื้นที่ทำงานของเราโดยตั้งค่า resolver เป็น "3":
ชื่อไฟล์: Cargo.toml
[workspace]
resolver = "3"
ถัดไป เราจะสร้างไบนารี crate ชื่อ adder โดยการรัน cargo new ภายในไดเรกทอรี add:
$ cargo new adder
Created binary (application) `adder` package
Adding `adder` as member of workspace at `file:///projects/add`
การรัน cargo new ภายในพื้นที่ทำงานจะเพิ่มแพ็กเกจที่สร้างขึ้นใหม่เข้าไปในคีย์ members ของส่วน [workspace] ในไฟล์ Cargo.toml ของพื้นที่ทำงานโดยให้อัตโนมัติ เช่นนี้:
[workspace]
resolver = "3"
members = ["adder"]
ณ จุดนี้ เราสามารถบิลด์พื้นที่ทำงานได้โดยการรัน cargo build ไฟล์ในไดเรกทอรี add ของคุณควรจะดูเป็นดังนี้:
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
พื้นที่ทำงานมีไดเรกทอรี target เพียงอันเดียวที่ระดับบนสุด ซึ่งชิ้นงานที่คอมไพล์แล้ว (compiled artifacts) จะถูกจัดวางไว้ในนั้น แพ็กเกจ adder จึงไม่มีไดเรกทอรี target เป็นของตัวเอง แม้ว่าเราจะรัน cargo build จากภายในไดเรกทอรี adder ชิ้นงานที่คอมไพล์แล้วก็ยังคงไปอยู่ที่ add/target แทนที่จะเป็น add/adder/target Cargo จัดโครงสร้างไดเรกทอรี target ในพื้นที่ทำงานในลักษณะนี้เพราะ crate ในพื้นที่ทำงานถูกออกแบบมาให้พึ่งพากัน หากแต่ละ crate มีไดเรกทอรี target ของตนเอง แต่ละ crate จะต้องคอมไพล์ crate อื่น ๆ ทั้งหมดในพื้นที่ทำงานซ้ำเพื่อวางชิ้นงานไว้ในไดเรกทอรี target ของตนเอง การแชร์ไดเรกทอรี target ร่วมกันจึงช่วยให้ crate ต่าง ๆ หลีกเลี่ยงการบิลด์ซ้ำโดยไม่จำเป็นได้
การสร้างแพ็กเกจที่สองในพื้นที่ทำงาน
ถัดไป มาสร้างแพ็กเกจสมาชิกตัวที่สองในพื้นที่ทำงานโดยตั้งชื่อว่า add_one ให้สร้างไลบรารี crate ใหม่ชื่อ add_one:
$ cargo new add_one --lib
Created library `add_one` package
Adding `add_one` as member of workspace at `file:///projects/add`
ไฟล์ Cargo.toml ที่ระดับบนสุดจะรวมพาธ add_one เข้าไปในรายการ members แล้ว:
ชื่อไฟล์: 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 กัน:
ชื่อไฟล์: add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
ตอนนี้เราสามารถกำหนดให้แพ็กเกจ adder ซึ่งมีไบนารีของเรา พึ่งพาแพ็กเกจ add_one ซึ่งมีไลบรารีของเราได้แล้ว อันดับแรก เราต้องเพิ่มพาธพึ่งพา (path dependency) บน add_one ลงใน adder/Cargo.toml
ชื่อไฟล์: adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }
Cargo ไม่ได้ทัศนคติล่วงหน้าว่า crate ต่าง ๆ ในพื้นที่ทำงานจะพึ่งพากันเอง ดังนั้นเราจึงต้องระบุความสัมพันธ์ของการพึ่งพาให้ชัดเจน
ถัดไป มาใช้ฟังก์ชัน add_one (จาก crate add_one) ใน crate adder ให้เปิดไฟล์ adder/src/main.rs แล้วเปลี่ยนฟังก์ชัน main เพื่อเรียกใช้ฟังก์ชัน add_one ดังในโค้ดตัวอย่างที่ 14-7
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
มาบิลด์พื้นที่ทำงานโดยการรัน 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:
$ 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
การพึ่งพาแพ็กเกจภายนอก
สังเกตว่าพื้นที่ทำงานมีไฟล์ Cargo.lock เพียงไฟล์เดียวที่ระดับบนสุด แทนที่จะมีไฟล์ Cargo.lock ในแต่ละไดเรกทอรีของ crate สิ่งนี้ช่วยให้มั่นใจได้ว่าทุก crate จะใช้เวอร์ชันเดียวกันของทรัพยากรภายนอกทั้งหมด หากเราเพิ่มแพ็กเกจ rand ลงในไฟล์ adder/Cargo.toml และ add_one/Cargo.toml Cargo จะแก้ไขค่าทรัพยากรภายนอกทั้งสองนั้นให้เป็น rand เวอร์ชันเดียว และบันทึกค่านั้นไว้ในไฟล์ Cargo.lock เพียงไฟล์เดียว การทำให้ทุก crate ในพื้นที่ทำงานใช้ทรัพยากรภายนอกเดียวกันหมายความว่า crate จะสามารถทำงานร่วมกันได้อย่างสมบูรณ์เสมอ ให้เราเพิ่ม crate rand ลงในส่วน [dependencies] ในไฟล์ add_one/Cargo.toml เพื่อให้เราสามารถใช้ crate rand ใน crate add_one ได้:
ชื่อไฟล์: add_one/Cargo.toml
[dependencies]
rand = "0.10.1"
ตอนนี้เราสามารถเพิ่ม use rand; ลงในไฟล์ add_one/src/lib.rs ได้แล้ว และการบิลด์พื้นที่ทำงานทั้งหมดโดยการรัน cargo build ในไดเรกทอรี add จะดึงและคอมไพล์ crate rand เข้ามา เราจะได้คำเตือนหนึ่งคำเตือนเพราะเรายังไม่ได้อ้างอิงถึง rand ที่เรานำเข้ามาใน scope:
$ cargo build
Updating crates.io index
Downloaded rand v0.10.1
--snip--
Compiling rand v0.10.1
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)]` (part of `#[warn(unused)]`) 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 จะถูกใช้ที่ใดที่หนึ่งในพื้นที่ทำงาน แต่เราก็ไม่สามารถใช้มันใน crate อื่น ๆ ในพื้นที่ทำงานได้ เว้นแต่เราจะเพิ่ม rand ลงในไฟล์ Cargo.toml ของ crate เหล่านั้นด้วย ตัวอย่างเช่น หากเราเพิ่ม 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 ในทุกแพ็กเกจของพื้นที่ทำงานที่ใช้แพ็กเกจ rand จะใช้เวอร์ชันเดียวกันตราบใดที่พวกมันระบุเวอร์ชันของ rand ที่เข้ากันได้ ซึ่งช่วยประหยัดพื้นที่และมั่นใจได้ว่า crate ในพื้นที่ทำงานจะเข้ากันได้ซึ่งกันและกัน
หาก crate ในพื้นที่ทำงานระบุเวอร์ชันของทรัพยากรพึ่งพาเดียวกันที่ไม่เข้ากันได้ Cargo จะแก้ปัญหาของแต่ละ crate แต่จะยังคงพยายามแก้ปัญหาให้ใช้เวอร์ชันน้อยที่สุดเท่าที่จะเป็นไปได้
การเพิ่มการทดสอบลงในพื้นที่ทำงาน
สำหรับการปรับปรุงเพิ่มเติม มาเพิ่มการทดสอบฟังก์ชัน add_one::add_one ภายใน crate add_one:
ชื่อไฟล์: 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 ในพื้นที่ทำงานที่จัดโครงสร้างเหมือนอันนี้ จะรันการทดสอบสำหรับทุก crate ในพื้นที่ทำงาน:
$ 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 เฉพาะเจาะจงในพื้นที่ทำงานจากไดเรกทอรีระดับบนสุดได้โดยใช้แฟล็ก -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
หากคุณเผยแพร่ crate ในพื้นที่ทำงานไปยัง crates.io แต่ละ crate ในพื้นที่ทำงานจะต้องถูกเผยแพร่แยกกัน เช่นเดียวกับ cargo test เราสามารถเผยแพร่ crate เฉพาะเจาะจงในพื้นที่ทำงานของเราได้โดยใช้แฟล็ก -p และระบุชื่อของ crate ที่เราต้องการเผยแพร่
สำหรับการฝึกฝนเพิ่มเติม ลองเพิ่ม crate add_two ลงในพื้นที่ทำงานนี้ในลักษณะที่คล้ายกับ crate add_one ดูสิ!
เมื่อโปรเจกต์ของคุณเติบโตขึ้น ให้พิจารณาใช้พื้นที่ทำงาน: มันช่วยให้คุณทำงานกับส่วนประกอบที่เล็กลงและเข้าใจได้ง่ายกว่าโค้ดก้อนใหญ่เพียงก้อนเดียว ยิ่งไปกว่านั้น การเก็บ crate ไว้ในพื้นที่ทำงานยังช่วยให้การประสานงานระหว่าง crate ต่าง ๆ ง่ายขึ้นหากพวกมันมักถูกเปลี่ยนแปลงไปพร้อม ๆ กัน