บทแนะนำ Bazel: สร้างโปรเจ็กต์ C++

เกริ่นนำ

หากเพิ่งเริ่มใช้ Bazel คุณมาถูกที่แล้ว ทำตามบทแนะนำ First Build นี้เพื่อดูบทแนะนำง่ายๆ ในการใช้ Bazel บทแนะนำนี้จะให้คำจำกัดความคำสำคัญต่างๆ เนื่องจากเป็นคำที่ใช้ในบริบทของ Bazel และจะแนะนำพื้นฐานต่างๆ เกี่ยวกับเวิร์กโฟลว์ของ Bazel เริ่มต้นด้วยเครื่องมือที่จำเป็นต้องใช้ คุณจะสร้างและเรียกใช้ 3 โครงการที่มีความซับซ้อนมากขึ้นเรื่อยๆ และเรียนรู้ว่าโครงการเหล่านั้นซับซ้อนขึ้นได้อย่างไรและเพราะอะไร

แม้ว่า Bazel จะเป็นระบบสร้างที่รองรับบิลด์หลายภาษา แต่บทแนะนำนี้ใช้โปรเจ็กต์ C++ เป็นตัวอย่าง และให้หลักเกณฑ์และขั้นตอนทั่วไปที่ใช้ได้กับภาษาส่วนใหญ่

เวลาที่ใช้ดำเนินการจนเสร็จโดยประมาณ: 30 นาที

ข้อกำหนดเบื้องต้น

เริ่มด้วยการติดตั้ง Bazel หากยังไม่ได้ทำ บทแนะนำนี้ใช้ Git สำหรับการควบคุมแหล่งที่มา ดังนั้นเพื่อผลลัพธ์ที่ดีที่สุด ให้ติดตั้ง Git

ถัดไป ให้เรียกข้อมูลโปรเจ็กต์ตัวอย่างจากที่เก็บ GitHub ของ Bazel โดยเรียกใช้การดำเนินการต่อไปนี้ในเครื่องมือบรรทัดคำสั่งที่ต้องการ

git clone https://github.com/bazelbuild/examples

โปรเจ็กต์ตัวอย่างสำหรับบทแนะนำนี้อยู่ในไดเรกทอรี examples/cpp-tutorial

โปรดดูโครงสร้างด้านล่าง

examples
└── cpp-tutorial
    ├──stage1
    │  ├── main
    │  │   ├── BUILD
    │  │   └── hello-world.cc
    │  └── WORKSPACE
    ├──stage2
    │  ├── main
    │  │   ├── BUILD
    │  │   ├── hello-world.cc
    │  │   ├── hello-greet.cc
    │  │   └── hello-greet.h
    │  └── WORKSPACE
    └──stage3
       ├── main
       │   ├── BUILD
       │   ├── hello-world.cc
       │   ├── hello-greet.cc
       │   └── hello-greet.h
       ├── lib
       │   ├── BUILD
       │   ├── hello-time.cc
       │   └── hello-time.h
       └── WORKSPACE

ไฟล์มีอยู่ด้วยกัน 3 ชุด โดยแต่ละชุดจะแสดงขั้นตอนในบทแนะนำนี้ ในระยะแรก คุณจะต้องสร้างเป้าหมายรายการเดียวในแพ็กเกจเดียว ในขั้นตอนที่ 2 คุณจะสร้างทั้งไบนารีและไลบรารีจากแพ็กเกจเดียว ในขั้นตอนที่ 3 ซึ่งเป็นขั้นตอนสุดท้าย คุณจะสร้างโปรเจ็กต์ที่มีแพ็กเกจหลายรายการ และสร้างโดยมีเป้าหมายหลายรายการ

สรุป: บทนำ

ด้วยการติดตั้ง Bazel (และ Git) และโคลนที่เก็บสำหรับบทแนะนำนี้ คุณจะได้วางรากฐานสำหรับบิลด์แรกของคุณด้วย Bazel ไปยังส่วนถัดไปเพื่อระบุข้อกำหนดบางอย่างและตั้งค่าพื้นที่ทำงาน

เริ่มต้นใช้งาน

ตั้งค่าพื้นที่ทำงาน

คุณต้องตั้งค่าพื้นที่ทำงานของโปรเจ็กต์ก่อนจึงจะสร้างโปรเจ็กต์ได้ พื้นที่ทำงานคือไดเรกทอรีที่เก็บไฟล์ต้นฉบับของโปรเจ็กต์และเอาต์พุตบิลด์ของ Bazel ซึ่งยังมีไฟล์สำคัญต่อไปนี้ด้วย

  • WORKSPACE file ซึ่งระบุว่าไดเรกทอรีและเนื้อหาในไดเรกทอรีเป็นพื้นที่ทำงานของ Bazel และยังคงอยู่ที่รูทของโครงสร้างไดเรกทอรีของโปรเจ็กต์
  • BUILD files อย่างน้อย 1 รายการ ซึ่งบอกวิธีการสร้างส่วนต่างๆ ของโปรเจ็กต์ที่ Bazel ไดเรกทอรีภายในพื้นที่ทำงานที่มีไฟล์ BUILD คือแพ็กเกจ (ดูเพิ่มเติมเกี่ยวกับแพ็กเกจได้ ภายหลังในบทแนะนำนี้)

ในโปรเจ็กต์ในอนาคต หากต้องการกำหนดไดเรกทอรีเป็นพื้นที่ทำงาน Bazel ให้สร้างไฟล์เปล่าชื่อ WORKSPACE ในไดเรกทอรีนั้น เพื่อวัตถุประสงค์ของบทแนะนำนี้ จะมีไฟล์ WORKSPACE อยู่ในแต่ละขั้นตอนแล้ว

หมายเหตุ: เมื่อ Bazel สร้างโปรเจ็กต์ อินพุตทั้งหมดต้องอยู่ในพื้นที่ทำงานเดียวกัน ไฟล์ที่อยู่ภายในพื้นที่ทำงานต่างๆ จะไม่เป็นอิสระจากกัน เว้นแต่จะลิงก์ไว้ ดูข้อมูลโดยละเอียดเพิ่มเติมเกี่ยวกับกฎพื้นที่ทำงานได้ในคู่มือนี้

ทำความเข้าใจไฟล์ BUILD

ไฟล์ BUILD มีวิธีการประเภทต่างๆ สำหรับ Bazel ไฟล์ BUILD แต่ละไฟล์ต้องมีกฎอย่างน้อย 1 กฎเป็นชุดคำสั่ง ซึ่งจะบอก Bazel วิธีสร้างเอาต์พุตที่ต้องการ เช่น ไบนารีหรือไลบรารีที่สั่งการได้ อินสแตนซ์แต่ละรายการของกฎบิลด์ในไฟล์ BUILD จะเรียกว่าเป้าหมาย และชี้ไปยังชุดไฟล์ต้นฉบับและทรัพยากร Dependency ที่เฉพาะเจาะจง เป้าหมายยังชี้ไปยังเป้าหมายอื่นๆ ได้ด้วย

ดูไฟล์ BUILD ในไดเรกทอรี cpp-tutorial/stage1/main

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
)

ในตัวอย่างของเรา เป้าหมาย hello-world จะสร้างอินสแตนซ์ในตัวของ Bazel cc_binary rule กฎนี้จะบอกให้ Bazel สร้างไบนารีที่ดำเนินการได้ในตัวจากไฟล์ต้นฉบับ hello-world.cc ที่ไม่มีทรัพยากร Dependency

สรุป: การเริ่มต้นใช้งาน

ตอนนี้คุณก็คุ้นเคยกับคำศัพท์สำคัญและความหมายของคำเหล่านั้นในบริบทของโปรเจ็กต์นี้และ Bazel โดยทั่วไปแล้ว ในหัวข้อถัดไป คุณจะได้สร้างและทดสอบ ระยะที่ 1 ของโปรเจ็กต์

ขั้นที่ 1: เป้าหมายเดียว แพ็กเกจเดียว

ถึงเวลาสร้างส่วนแรกของโปรเจ็กต์แล้ว สำหรับการอ้างอิงภาพ โครงสร้างของส่วนระยะที่ 1 ของโครงการคือ

examples
└── cpp-tutorial
    └──stage1
       ├── main
       │   ├── BUILD
       │   └── hello-world.cc
       └── WORKSPACE

เรียกใช้คำสั่งต่อไปนี้เพื่อย้ายไปยังไดเรกทอรี cpp-tutorial/stage1

cd cpp-tutorial/stage1

ต่อไป ให้เรียกใช้

bazel build //main:hello-world

ในป้ายกำกับเป้าหมาย ส่วน //main: คือตำแหน่งของไฟล์ BUILD ที่สัมพันธ์กับรูทของพื้นที่ทำงาน และ hello-world เป็นชื่อเป้าหมายในไฟล์ BUILD

Bazel ผลิตบางสิ่งที่มีลักษณะดังนี้

INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 2.267s, Critical Path: 0.25s

คุณเพิ่งสร้างเป้าหมาย Bazel แรก Bazel จะวางเอาต์พุตของบิลด์ในไดเรกทอรี bazel-bin ที่รูทของพื้นที่ทำงาน

ตอนนี้ให้ทดสอบไบนารีที่สร้างใหม่ซึ่งได้แก่

bazel-bin/main/hello-world

ซึ่งจะได้รับข้อความ "Hello world" ที่พิมพ์ออกมา

กราฟการอ้างอิงของระยะที่ 1 มีดังนี้

กราฟการขึ้นต่อกันของ hello-world แสดงเป้าหมายเดียวที่มีไฟล์แหล่งที่มาไฟล์เดียว

สรุป: ขั้นที่ 1

ตอนนี้คุณก็ได้สร้างงานสร้างชิ้นแรกเสร็จเรียบร้อยแล้ว คุณก็มีแนวคิดเบื้องต้นว่าสิ่งปลูกสร้างนั้นมีโครงสร้างอย่างไร ในขั้นถัดไป คุณจะเพิ่มความซับซ้อนโดยการเพิ่มเป้าหมายอื่น

ขั้นที่ 2: มีเป้าหมายบิลด์หลายรายการ

แม้ว่าเป้าหมายเดียวก็เพียงพอแล้วสําหรับโปรเจ็กต์ขนาดเล็ก แต่คุณก็อาจต้องแบ่งโปรเจ็กต์ขนาดใหญ่ออกเป็นเป้าหมายและแพ็กเกจหลายรายการ วิธีนี้ทำให้งานสร้างเพิ่มขึ้นอย่างรวดเร็ว กล่าวคือ Bazel จะสร้างเฉพาะสิ่งที่เปลี่ยนแปลงเท่านั้น และเพิ่มความเร็วในการสร้างด้วยการสร้างหลายๆ ส่วนของโปรเจ็กต์พร้อมกัน ขั้นตอนนี้ของบทแนะนำจะเพิ่มเป้าหมาย และขั้นตอนถัดไปจะเพิ่มแพ็กเกจ

นี่คือไดเรกทอรีที่คุณกำลังดำเนินการในขั้นที่ 2:

    ├──stage2
    │  ├── main
    │  │   ├── BUILD
    │  │   ├── hello-world.cc
    │  │   ├── hello-greet.cc
    │  │   └── hello-greet.h
    │  └── WORKSPACE

ดูไฟล์ BUILD ในไดเรกทอรี cpp-tutorial/stage2/main ที่ด้านล่าง

cc_library(
    name = "hello-greet",
    srcs = ["hello-greet.cc"],
    hdrs = ["hello-greet.h"],
)

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
    deps = [
        ":hello-greet",
    ],
)

ด้วยไฟล์ BUILD นี้ บาเซลจะสร้างไลบรารี hello-greet ขึ้นก่อน (โดยใช้ cc_library rule ในตัวของ Bazel) จากนั้นจึงสร้างไบนารี hello-world แอตทริบิวต์ deps ในเป้าหมาย hello-world บอก Bazel ว่าไลบรารี hello-greet จำเป็นต้องสร้างไบนารี hello-world

ก่อนที่จะสร้างโปรเจ็กต์เวอร์ชันใหม่นี้ได้ คุณต้องเปลี่ยนไดเรกทอรีโดยเปลี่ยนไปใช้ไดเรกทอรี cpp-tutorial/stage2 ด้วยการเรียกใช้คำสั่งต่อไปนี้

cd ../stage2

ตอนนี้คุณสามารถสร้างไบนารีใหม่โดยใช้คำสั่งที่คุ้นเคยต่อไปนี้

bazel build //main:hello-world

ขอแนะนำตัวอีกครั้งว่า Bazel ได้สร้างบางสิ่งที่มีลักษณะดังนี้

INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 2.399s, Critical Path: 0.30s

ตอนนี้คุณสามารถทดสอบไบนารีที่สร้างใหม่ ซึ่งจะแสดงผล "Hello world" อีกรายการหนึ่ง:

bazel-bin/main/hello-world

หากตอนนี้หากคุณแก้ไข hello-greet.cc และสร้างโปรเจ็กต์อีกครั้ง Bazel จะคอมไพล์เฉพาะไฟล์นั้นอีกครั้ง

เมื่อดูกราฟการอ้างอิง คุณจะเห็นว่า hello-world ขึ้นอยู่กับอินพุตเดียวกันกับที่ใช้ก่อนหน้านี้ แต่โครงสร้างของบิลด์จะแตกต่างกัน ดังนี้

กราฟการขึ้นต่อกันของ "hello-world" แสดงการเปลี่ยนแปลงโครงสร้างหลังจากแก้ไขไฟล์

สรุป: ขั้นที่ 2

ตอนนี้คุณได้สร้างโปรเจ็กต์ที่มี 2 เป้าหมายแล้ว เป้าหมาย hello-world จะสร้างไฟล์ต้นฉบับ 1 ไฟล์และขึ้นอยู่กับเป้าหมายอีก 1 อย่าง (//main:hello-greet) ซึ่งสร้างไฟล์ต้นฉบับเพิ่มเติม 2 ไฟล์ ในส่วนถัดไป ให้ก้าวไปอีกขั้น และเพิ่มแพ็กเกจอีกแบบหนึ่ง

ขั้นที่ 3: หลายแพ็กเกจ

ขั้นตอนต่อไปนี้จะเพิ่มข้อมูลแทรกอีกชั้นหนึ่งและสร้างโปรเจ็กต์ที่มีแพ็กเกจหลายแพ็กเกจ ลองดูโครงสร้างและเนื้อหาของไดเรกทอรี cpp-tutorial/stage3 ที่ด้านล่าง

└──stage3
   ├── main
   │   ├── BUILD
   │   ├── hello-world.cc
   │   ├── hello-greet.cc
   │   └── hello-greet.h
   ├── lib
   │   ├── BUILD
   │   ├── hello-time.cc
   │   └── hello-time.h
   └── WORKSPACE

คุณจะเห็นว่าตอนนี้มีไดเรกทอรีย่อย 2 รายการ และแต่ละไดเรกทอรีย่อยมีไฟล์ BUILD ดังนั้นสำหรับ Bazel พื้นที่ทำงานจึงมี 2 แพ็กเกจ ได้แก่ lib และ main

ดูไฟล์ lib/BUILD

cc_library(
    name = "hello-time",
    srcs = ["hello-time.cc"],
    hdrs = ["hello-time.h"],
    visibility = ["//main:__pkg__"],
)

และที่ไฟล์ main/BUILD:

cc_library(
    name = "hello-greet",
    srcs = ["hello-greet.cc"],
    hdrs = ["hello-greet.h"],
)

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
    deps = [
        ":hello-greet",
        "//lib:hello-time",
    ],
)

เป้าหมาย hello-world ในแพ็กเกจหลักจะขึ้นอยู่กับเป้าหมาย hello-time ในแพ็กเกจ lib (ป้ายกำกับเป้าหมาย //lib:hello-time) โดย Bazel รู้เรื่องนี้ผ่านแอตทริบิวต์ deps คุณจะดูข้อมูลนี้ได้ในกราฟการอ้างอิง

กราฟการขึ้นต่อกันของ "hello-world" จะแสดงวิธีที่เป้าหมายในแพ็กเกจหลักขึ้นอยู่กับเป้าหมายในแพ็กเกจ "lib"

หากต้องการให้บิลด์ประสบความสำเร็จ คุณต้องทำให้เป้าหมาย //lib:hello-time ใน lib/BUILD ปรากฏอย่างชัดแจ้งต่อเป้าหมายใน main/BUILD โดยใช้แอตทริบิวต์ระดับการเข้าถึง เนื่องจากโดยค่าเริ่มต้น เป้าหมายอื่นจะมองเห็นได้ในไฟล์ BUILD เดียวกันเท่านั้น Bazel ใช้ระดับการเข้าถึงเป้าหมายเพื่อป้องกันปัญหาต่างๆ เช่น ไลบรารีที่มีรายละเอียดการใช้งานรั่วไหลใน API สาธารณะ

ตอนนี้ให้สร้างโปรเจ็กต์เวอร์ชันสุดท้ายนี้ เปลี่ยนเป็นไดเรกทอรี cpp-tutorial/stage3 โดยเรียกใช้

cd  ../stage3

เรียกใช้คำสั่งต่อไปนี้อีกครั้ง

bazel build //main:hello-world

Bazel ผลิตบางสิ่งที่มีลักษณะดังนี้

INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 0.167s, Critical Path: 0.00s

ตอนนี้ให้ทดสอบไบนารีสุดท้ายของบทแนะนำนี้สำหรับข้อความ Hello world สุดท้าย

bazel-bin/main/hello-world

สรุป: ขั้นที่ 3

ตอนนี้คุณได้สร้างโปรเจ็กต์เป็น 2 แพ็กเกจที่มี 3 เป้าหมายและเข้าใจทรัพยากร Dependency แล้ว ซึ่งจะช่วยให้คุณมองหาและสร้างโปรเจ็กต์ในอนาคตกับ Bazel ในส่วนถัดไปจะมาดูวิธีไปต่อ ในบาซาลกันต่อ

ขั้นตอนถัดไป

ตอนนี้คุณได้สร้างพื้นฐานแรกด้วย Bazel จนเสร็จสมบูรณ์แล้ว แต่นี่เป็นเพียงการเริ่มต้นเท่านั้น แหล่งข้อมูลเพิ่มเติมเพื่อการเรียนรู้กับ Bazel ต่อไปมีดังนี้

ขอให้สนุกกับการสร้าง