Samouczek Bazel: Tworzenie projektu C++

W tym samouczku omawiamy podstawy tworzenia aplikacji w C++ przy pomocy Bazely. Skonfigurujesz obszar roboczy i utworzysz prosty projekt w C++ zawierający kluczowe pojęcia związane z Bazel, takie jak elementy docelowe i pliki BUILD. Gdy ukończysz ten samouczek, zapoznaj się z artykułem Typowe przypadki użycia C++ Build, aby uzyskać informacje na temat zaawansowanych zagadnień, takich jak pisanie i uruchamianie testów C++.

Szacowany czas ukończenia: 30 minut.

Czego się dowiesz

Tematy w tym samouczku:

  • Tworzenie celu
  • Wizualizacja zależności projektu
  • Podziel projekt na wiele celów i pakietów
  • Kontrolowanie docelowej widoczności w pakietach
  • Tworzenie odwołań do etykiet za pomocą etykiet

Zanim zaczniesz

Aby przygotować się do samouczka, najpierw zainstaluj Bazel, jeśli jeszcze go nie masz. Następnie pobierz przykładowy projekt z repozytorium Bait GitHub:

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

Przykładowy projekt z tego samouczka znajduje się w katalogu 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

Jak widać, w tym samouczku znajdują się 3 zestawy plików, a każdy z nich reprezentuje etap. Na pierwszym etapie stworzysz jeden element docelowy umieszczony w jednym pakiecie. Na drugim etapie podziel projekt na kilka celów i umieść go w jednym pakiecie. Na trzecim i ostatnim etapie podzielisz projekt na kilka pakietów i zbudujesz do nich wiele celów.

Buduj z Bazelem

Konfigurowanie obszaru roboczego

Przed utworzeniem projektu musisz skonfigurować jego obszar roboczy. Obszar roboczy to katalog, w którym znajdują się pliki źródłowe projektu i dane wyjściowe kompilacji Bazelu. Są w nim też pliki, które Bazel rozpoznaje jako specjalne:

  • Plik WORKSPACE, który identyfikuje katalog i jego zawartość jako obszar roboczy Bazela i znajduje się w katalogu głównym struktury katalogu projektu.

  • Jeden lub więcej plików BUILD, które informują Bazela, jak utworzyć różne części projektu. (Katalog w obszarze roboczym, który zawiera plik BUILD, jest pakietem). Więcej informacji o pakietach znajdziesz w dalszej części tego samouczka.

Aby oznaczyć katalog jako obszar roboczy Bazelu, utwórz w nim pusty plik o nazwie WORKSPACE.

Podczas tworzenia projektu Bazel wszystkie dane wejściowe i zależności muszą znajdować się w tym samym obszarze roboczym. Pliki znajdujące się w różnych obszarach roboczych są niezależne od siebie, chyba że znajdują się w zakresie tego samouczka.

Omówienie pliku BUILD

Plik BUILD zawiera różne typy instrukcji dotyczących Bazelu. Najważniejszym typem jest reguła kompilacji, która informuje Bazel, jak utworzyć odpowiednie dane wyjściowe, takie jak pliki binarne lub biblioteki. Każde wystąpienie reguły kompilacji w pliku BUILD jest nazywane celem i wskazuje określony zbiór plików źródłowych i zależności. Cel może też wskazywać inne wartości docelowe.

Spójrz na plik BUILD w katalogu cpp-tutorial/stage1/main:

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

W naszym przykładzie cel hello-world tworzy wbudowaną regułę cc_binary Bazela. Reguła nakazuje Bazelowi utworzenie samodzielnego pliku binarnego z plikiem źródłowym hello-world.cc bez żadnych zależności.

Atrybuty w elemencie docelowym wyraźnie określają jego zależności i opcje. Atrybut name jest wymagany, a wiele z nich jest opcjonalne. Na przykład w celu hello-world obiekt name jest wymagany i nie wymaga wyjaśnienia, a element srcs jest opcjonalny i określa pliki źródłowe, na podstawie których Bazel buduje cel.

Tworzenie projektu

Aby utworzyć przykładowy projekt, przejdź do katalogu cpp-tutorial/stage1 i uruchom polecenie:

bazel build //main:hello-world

W etykiecie docelowej część //main: to lokalizacja pliku BUILD względem katalogu głównego obszaru roboczego, a hello-world jest nazwą celu w BUILD. Więcej informacji o etykietach docelowych znajdziesz w dalszej części tego samouczka.

Bazel generuje wyniki podobne do tych:

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

Gratulacje! Właśnie udało Ci się utworzyć pierwszy cel Bazelu. Miejsca bazel kompilacji danych wyjściowych w katalogu bazel-bin w katalogu głównym obszaru roboczego. Przejrzyj jego zawartość, by zorientować się, jaka będzie struktura wyjściowych Bazela.

Teraz przetestuj nowo utworzony plik binarny:

bazel-bin/main/hello-world

Sprawdź wykres zależności

Pomyślna kompilacja ma wszystkie swoje zależności wyraźnie określone w pliku BUILD. Bazel używa tych instrukcji do tworzenia wykresu zależności projektu, który umożliwia dokładne przyrostowe kompilacje.

Aby zobaczyć zależność zależności od przykładowego projektu, wygeneruj tekstowy wykres zależności, uruchamiając to polecenie w katalogu głównym obszaru roboczego:

bazel query --notool_deps --noimplicit_deps "deps(//main:hello-world)" \
  --output graph

Powyższe polecenie informuje Bazel, że ma wyszukać wszystkie zależności elementu docelowego //main:hello-world (z zależnościem hosta i ujednoliconych zależności) i sformatować dane wyjściowe jako wykres.

Następnie wklej tekst do GraphViz.

W Ubuntu można wyświetlić wykres lokalnie, instalując karty GraphViz i xdot.

sudo apt update && sudo apt install graphviz xdot

Następnie możesz wygenerować i wyświetlić wykres, przypisując tekst wyjściowy nad prosto do xdot:

xdot <(bazel query --notool_deps --noimplicit_deps "deps(//main:hello-world)" \
  --output graph)

Jak widać, pierwszy etap przykładowego projektu ma jedno miejsce docelowe, które tworzy jeden plik źródłowy bez dodatkowych zależności:

Wykres zależności dla „hello-world”

Rysunek 1. Wykres zależności dla raportu hello-world wyświetla jeden cel z pojedynczym plikiem źródłowym.

Gdy skonfigurujesz obszar roboczy, zbudujesz projekt i przeanalizujesz jego zależności, możesz nieco zwiększyć złożoność.

Popraw swoją konstrukcję w Bazelu

Jeden cel jest wystarczający w przypadku małych projektów, ale warto podzielić większe projekty na kilka celów i pakietów, aby przyspieszyć kompilację (czyli ponownie kompilować zmiany), aby przyspieszyć kompilację według i tworzyć wiele części projektu naraz.

Podaj wiele celów kompilacji

Przykładowy kompilację projektu możesz podzielić na 2 cele. Spójrz na plik BUILD w katalogu 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",
    ],
)

Korzystając z tego pliku BUILD, Bazel najpierw tworzy bibliotekę hello-greet (za pomocą wbudowanej reguły cc_library Bazel), a potem hello-world danych Atrybut deps w obiekcie docelowym hello-world wskazuje Bazel, że do utworzenia pliku binarnego hello-world wymagana jest biblioteka hello-greet.

Następnie utwórz nową wersję projektu. Przejdź do katalogu cpp-tutorial/stage2 i uruchom następujące polecenie:

bazel build //main:hello-world

Bazel generuje wyniki podobne do tych:

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

Teraz przetestuj nowo utworzony plik binarny:

bazel-bin/main/hello-world

Jeśli teraz zmodyfikujesz hello-greet.cc i ponownie skompilujesz projekt, Bazel skompiluje tylko ten plik.

Na wykresie zależności widać, że hello-world zależy od tych samych danych wejściowych, co wcześniej, ale struktura kompilacji jest inna:

Wykres zależności dla „hello-world”

Rysunek 2. Wykres zależności hello-world przedstawia zmiany struktury po zmodyfikowaniu pliku.

Udało Ci się utworzyć projekt z 2 celami. Element docelowy hello-world tworzy 1 plik źródłowy i zależy od 1 innego elementu docelowego (//main:hello-greet), który tworzy 2 dodatkowe pliki źródłowe.

Korzystanie z kilku pakietów

Projekt możesz podzielić na kilka pakietów. Spójrz na zawartość katalogu cpp-tutorial/stage3:

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

Widzimy, że istnieją 2 podkatalogi, a każdy z nich zawiera plik BUILD. W Bazelu obszar roboczy zawiera teraz 2 pakiety: lib i main.

Otwórz plik lib/BUILD:

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

W pliku 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",
    ],
)

Jak widać, wartość docelowa hello-world w pakiecie main zależy od wartości docelowej hello-time w pakiecie lib (czyli etykiety docelowej //lib:hello-time) – Bazel wie o tym za pomocą atrybutu deps. Spójrz na wykres zależności:

Wykres zależności dla „hello-world”

Rysunek 3. Wykres zależności hello-world pokazuje, jak wartość docelowa w pakiecie głównym zależy od wartości docelowej w pakiecie lib.

Aby kompilacja się udała, cel //lib:hello-time w: lib/BUILD jest wyraźnie widoczny dla celów w main/BUILD przy użyciu atrybutu visibility. Jest to spowodowane tym, że domyślnie cele są widoczne tylko dla innych celów w tym pliku BUILD. (Bazel korzysta z widoczności docelowej, aby zapobiegać problemom, takim jak biblioteki zawierające szczegóły implementacji wyciekające do publicznych interfejsów API).

Następnie możesz utworzyć ostateczną wersję projektu. Przejdź do katalogu cpp-tutorial/stage3 i uruchom następujące polecenie:

bazel build //main:hello-world

Bazel generuje wyniki podobne do tych:

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

Teraz przetestuj nowo utworzony plik binarny:

bazel-bin/main/hello-world

Udało Ci się utworzyć projekt jako dwa pakiety z trzema wartościami docelowymi i dowiedzieć się o zależnościach między nimi.

Używanie etykiet w odniesieniu do wartości docelowych

W plikach BUILD i w wierszu poleceń Bazel używa etykiet, by odwołać się do wartości docelowych, na przykład //main:hello-world lub //lib:hello-time. Składnia:

//path/to/package:target-name

Jeśli element docelowy jest regułą, topath/to/package to ścieżka do katalogu głównego obszaru roboczego (katalogu zawierającegoWORKSPACE do katalogu zawierającegoBUILD itarget-name nazwana przez Ciebie nazwa celuBUILD plik (name ). Jeśli element docelowy jest docelowym plikiem, path/to/package to ścieżka do katalogu głównego pakietu, a target-name to nazwa pliku docelowego, w tym jego pełna ścieżka do katalogu głównego pakietu (katalogu zawierającego plik BUILD pakietu).

Odwołując się do celów w katalogu głównym repozytorium, ścieżka pakietu jest pusta – wystarczy użyć właściwości //:target-name. Odwołując się do celów w tym samym pliku BUILD, możesz nawet pominąć główny identyfikator obszaru roboczego // i użyć :target-name.

Więcej informacji

Gratulacje! Znasz już podstawy tworzenia projektów C++ w Bazylu. Następnie zapoznaj się z najczęstszymi przypadkami użycia kompilacji C++.

Więcej informacji:

Miłego budowania!