Wprowadzenie do Bazel dla programistów Scali

Feb 16 2023
Czas budowy projektu ma znaczący wpływ na efektywność rozwoju zespołu. Im większa baza kodu, tym dłużej trwa budowa.

Czas budowy projektu ma znaczący wpływ na efektywność rozwoju zespołu. Im większa baza kodu, tym dłużej trwa budowa. Im dłuższy czas kompilacji, tym gorsze staje się doświadczenie programisty.

Chociaż SBT jest świetnym narzędziem do budowania, jego konstrukcja (w szczególności brak niezawodnej pamięci podręcznej kompilacji z możliwością adresowania treści) sprawia, że ​​nie nadaje się do dużych projektów.

W tym poście na blogu przedstawiono Bazel, system kompilacji umożliwiający szybkie kompilowanie, nawet w repozytoriach na skalę Google, poprzez tworzenie prostych aplikacji Scala.

Co to jest Bazel?

Przede wszystkim poznajmy koncepcję Bazela. Bazel to system kompilacji, którego motto brzmi: „{Szybko, poprawnie} wybierz dwa”. Aby zrealizować te właściwości, Bazel został zaprojektowany jako „system kompilacji oparty na artefaktach”.

W tej sekcji dowiemy się o naturze Bazel, przyglądając się, czym jest system kompilacji oparty na artefaktach i co oznacza „{Szybko, poprawnie} wybierz dwa”!

System kompilacji oparty na artefaktach

Tradycyjne systemy kompilacji, takie jak Ant i Maven, nazywane są systemami kompilacji opartymi na zadaniach. W konfiguracji kompilacji dla systemów kompilacji opartych na zadaniach opisujemy imperatywny zestaw zadań, takich jak wykonanie zadania A, następnie wykonanie zadania B, a następnie wykonanie zadania C.

Z drugiej strony w systemach kompilacji opartych na artefaktach, takich jak Buck, Pants i Bazel, opisujemy deklaratywny zestaw artefaktów do zbudowania, listę zależności i ograniczone opcje kompilacji .

Tak więc podstawową ideą Bazel jest to, że twoja kompilacja jest czystą funkcją:

  • Źródła i zależności są wprowadzane.
  • Artefakt jest produktem wyjściowym.
  • Nie ma żadnych skutków ubocznych.

{Szybko, Poprawnie} wybierz dwa

Aby lepiej zrozumieć to twierdzenie, konieczne jest zrozumienie właściwości hermetyczności Bazela .

W hermetycznych systemach kompilacji, takich jak Bazel, przy tych samych źródłach wejściowych i konfiguracji zwraca to samo wyjście.

Dzięki hermetyczności Bazel jest w stanie zapewnić powtarzalne kompilacje: biorąc pod uwagę te same dane wejściowe, Bazel zawsze zwraca te same dane wyjściowe na każdym komputerze .

A dzięki powtarzalnej kompilacji Bazel może zapewnić funkcję zdalnej pamięci podręcznej , aby udostępniać pamięć podręczną kompilacji w zespole. Dzięki zdalnej pamięci podręcznej możemy szybko tworzyć duże projekty, korzystając z pamięci podręcznej kompilacji współdzielonej przez członków zespołu. Odtwarzalność konstrukcji jest również krytycznym aspektem bezpieczeństwa łańcucha dostaw, wymaganym do osiągnięcia najwyższego poziomu zgodności z SLSA .

Podstawy Bazela

W tym samouczku budujemy prostą aplikację Scala z Bazel. Cały kod źródłowy jest dostępny tutaj:https://github.com/tanishiking/bazel-tutorial-scala/tree/main/01_scala_tutorial

Struktura projektu wygląda następująco:

|-- WORKSPACE
`-- src
    `-- main
        `-- scala
            |-- cmd
            |   |-- BUILD.bazel
            |   `-- Runner.scala
            `-- lib
                |-- BUILD.bazel
                `-- Greeting.scala

  • Plik WORKSPACE służy do pobierania rzeczy ze świata zewnętrznego do projektu Bazel.
  • Pliki BUILD.bazel dotyczą tego, co dzieje się w twoim projekcie Bazel.

Plik WORKSPACE zawiera zależności zewnętrzne (zarówno dla Bazel, jak i JVM) . Na przykład pobieramy Rules_scala , rozszerzenie Bazel do kompilacji Scali w pliku WORKSPACE .

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") #Import a rule from the Bazel's "standard library"
 ...
http_archive( #This rule can download and import an archived repo
    name = "io_bazel_rules_scala", #The name that will be used to reference the repo
    sha256 = "77a3b9308a8780fff3f10cdbbe36d55164b85a48123033f5e970fdae262e8eb2",
    strip_prefix = "rules_scala-20220201", #Only files from this directory will be unpacked and imported
    type = "zip",
    url = "https://github.com/bazelbuild/rules_scala/releases/download/20220201/rules_scala-20220201.zip",
)

Pliki BUILD.bazel

Aby zdefiniować build w Bazel, piszemy pliki BUILD.bazel .

Zanim przejdziemy do plików BUILD.bazel, rzućmy okiem na pliki Scali do zbudowania. Ten projekt składa się z dwóch pakietów, z których każdy zawiera pojedynczy plik Scali.

// src/main/scala/lib/Greeting.scala
package lib
object Greeting {
  def sayHi = println("Hi!")
}
// src/main/scala/cmd/Runner.scala
package cmd
import lib.Greeting
object Runner {
  def main(args: Array[String]) = {
    Greeting.sayHi
  }
}

Następnie zobaczmy, jak napisać pliki BUILD.bazel , aby zbudować te źródła Scali.

scala_biblioteka

Aby zbudować bibliotekę lib.Greeting w tym przykładzie, umieściliśmy plik BUILD.bazel obok Greeting.scala i zdefiniujemy cel kompilacji za pomocą reguły scala_library dostarczonej przez Rules_scala .

Reguła w Bazel to deklaracja zestawu instrukcji do budowania lub testowania kodu . Na przykład istnieje zestaw reguł tworzenia programów Java (który jest natywnie obsługiwany przez Bazel). Rules_scala zapewnia zestaw reguł do budowania programów Scala.

scala_library kompiluje podane źródła Scali i generuje plik JAR.

# src/main/scala/lib/BUILD.bazel
load("@io_bazel_rules_scala//scala:scala.bzl", "scala_library")
scala_library(
    # unique identifier of this target
    name = "greeting",
    # list of Scala files to build
    srcs = ["Greeting.scala"],
)

  • scala_library to jedna z reguł budowania w Bazel; opisujemy, co zbudować za pomocą reguł.
  • Wymagane atrybuty scala_library to name i srcs .

Teraz mamy źródła Scali do zbudowania i konfigurację BUILD.bazel. Zbudujmy go za pomocą wiersza poleceń Bazel .

$ bazel build //src/main/scala/lib:greeting
...
INFO: Found 1 target...
Target //src/main/scala/lib:greeting up-to-date:
  bazel-bin/src/main/scala/lib/greeting.jar
INFO: Elapsed time: 0.152s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action

Etykieta

//src/main/scala/lib:greeting to coś, co nazywa się etykietą w Bazel i wskazuje na powitanie w src/main/scala/lib/BUILD.baze l. W Bazel używamy etykiety, aby jednoznacznie zidentyfikować cel kompilacji.

Etykieta składa się z 3 elementów. Na przykład w @myrepo//my/app/main:app_binary ,

  • @myrepo // określa nazwę repozytorium . Jeśli pominiemy tę część, etykieta odnosi się do repozytorium, które zawiera ten plik BUILD.bazel. Dlatego możemy pominąć część @myrepo , gdy odnosimy się do etykiety zdefiniowanej w tym samym repozytorium.
  • my/app/main reprezentuje ścieżkę do pakietu ( pliku BUILD.bazel ) względem katalogu głównego repozytorium.
  • :app_binary to nazwa docelowa.

Zależności

Następnie zbudujmy cmd.Runner , który zależy od lib.Greeting . Tym razem cmd.Runner zależy od lib.Greeting , więc wprowadzamy zależność między celami za pomocą atrybutu deps .

# src/main/scala/cmd/BUILD.bazel
load("@io_bazel_rules_scala//scala:scala.bzl", "scala_binary")
scala_binary(
    name = "runner",
    main_class = "cmd.Runner",
    srcs = ["Runner.scala"],
    deps = ["//src/main/scala/lib:greeting"],
)

  • Używamy scala_binary zamiast scala_library :
  • scala_binary to wykonywalna reguła . Reguły plików wykonywalnych określają, jak zbudować plik wykonywalny ze źródeł. Ten proces może zawierać łączenie zależności lub listę ścieżek klas zależności.
  • Na przykład reguła scala_binary tworzy wykonywalny skrypt ze źródeł i zależności.
  • Po zbudowaniu pliku wykonywalnego można go uruchomić za pomocą polecenia bazel run . Spowoduje to wykonanie pliku wykonywalnego.
  • Dodajemy atrybut deps , aby wyświetlić listę wszystkich zależności.
  • W tym przykładzie dodajemy etykietę //src/main/scala/lib:greeting , ponieważ cmd.Runner zależy od lib.Greeting .

$ bazel build //src/main/scala/cmd:runner
ERROR: .../01_scala_tutorial/src/main/scala/cmd/BUILD.bazel:3:13:
in scala_binary rule //src/main/scala/cmd:runner:
target '//src/main/scala/lib:greeting' is not visible from
target '//src/main/scala/cmd:runner'.

Bazel ma koncepcję widoczności . Domyślnie widoczność wszystkich celów jest prywatna , co oznacza, że ​​tylko cele w ramach tego samego pakietu (tj. tego samego pliku BUILD.bazel) mogą uzyskiwać do siebie dostęp.

Aby lib:greeting było widoczne z cmd , dodaj atrybut widoczności do powitania .

scala_library(
     name = "greeting",
     srcs = ["Greeting.scala"],
+    visibility = ["//src/main/scala/cmd:__pkg__"],
 )

Teraz możemy zbudować aplikację:

$ bazel build //src/main/scala/cmd:runner
...
INFO: Found 1 target...
Target //src/main/scala/cmd:runner up-to-date:
  bazel-bin/src/main/scala/cmd/runner.jar
  bazel-bin/src/main/scala/cmd/runner
INFO: Elapsed time: 0.146s, Critical Path: 0.01s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action

$ ./bazel-bin/src/main/scala/cmd/runner
Hi!
$ bazel run //src/main/scala/cmd:runner
Hi!

W powyższych przykładach określamy etykietę celu i budujemy jeden cel, ale czy można zbudować wiele celów budowania jednocześnie?

Odpowiedź brzmi tak. Możemy użyć symbolu wieloznacznego, aby wybrać wiele celów . Na przykład możemy zbudować wszystkie cele według $ bazel build //…

Teraz nauczyliśmy się podstaw Bazel, budując prostą aplikację w Scali, ale jak korzystać z bibliotek innych firm w Bazel?

Zewnętrzne zależności JVM

Nauczmy się teraz, jak korzystać z zewnętrznych bibliotek Mavena, budując prostą aplikację, która analizuje programy Scala za pomocą scalameta i ładnie drukuje AST za pomocą pprint .

W tym przykładzie użyjemy Rules_jvm_external , jednej ze standardowych reguł do zarządzania zewnętrznymi zależnościami JVM.

Uwaga: możemy pobierać słoiki z repozytoriów Maven za pomocą maven_jar, natywnie obsługiwanej reguły przez Bazel. Jednak zalecam używanie Rules_jvm_extenal, ponieważ ma wiele przydatnych funkcji w porównaniu z maven_jar.

Pełny przykład kodu jest dostępny tutaj:https://github.com/tanishiking/bazel-tutorial-scala/tree/main/02_scala_maven.

Ten projekt ma tylko jeden program Scala.

// src/main/scala/example/App.scala
package example
import scala.meta._
object App {
  def main(args: Array[String]) = {
    pprint.pprintln(parse(args.head))
  }
  private def parse(arg: String) = {
    arg.parse[Source].get
  }
}

Aby pobrać Rules_jvm_external , skopiuj i wklej instrukcje konfiguracji ze strony wydania do pliku WORKSPACE w następujący sposób:

http_archive(
    name = "rules_jvm_external",
    strip_prefix = "rules_jvm_external-4.5",
    sha256 = "b17d7388feb9bfa7f2fa09031b32707df529f26c91ab9e5d909eb1676badd9a6",
    url = "https://github.com/bazelbuild/rules_jvm_external/archive/refs/tags/4.5.zip",
)
...

load("@rules_jvm_external//:defs.bzl", "maven_install")
maven_install(
    artifacts = [
        "org.scalameta:scalameta_2.13:4.5.13",
        "com.lihaoyi:pprint_2.13:0.7.3",
    ],
    repositories = [
        "https://repo1.maven.org/maven2",
    ],
)

Aby skorzystać z pobranych zależności, musimy dodać je do atrybutu deps reguł budowania. Rules_jvm_external automatycznie generuje cele dla bibliotek w repozytorium @maven w następującym formacie:

> Domyślna składnia etykiety dla artefaktu foo.bar:baz-qux:1.2.3 to @maven//:foo_bar_baz_qux

https://github.com/bazelbuild/rules_jvm_external#usage

Dlatego możemy odnieść się do com.lihaoyi:pprint_2.13:0.7.3 z etykietą @maven//:com_lihaoyi_pprint_2_13 . Więc umieść następujący plik BUILD.bazel obok App.scala .

# src/main/scala/example/BUILD.bazel
scala_binary(
    name = "app",
    main_class = "example.App",
    srcs = ["App.scala"],
    deps = [
        "@maven//:com_lihaoyi_pprint_2_13",
        "@maven//:org_scalameta_scalameta_2_13",
    ],
)

$ bazel build //src/main/scala/example:app
...
INFO: Found 1 target...
Target //src/main/scala/example:app up-to-date:
  bazel-bin/src/main/scala/example/app.jar
  bazel-bin/src/main/scala/example/app
INFO: Elapsed time: 0.165s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
...
$ bazel-bin/src/main/scala/example/app "object main { println(1) }"
Source(
  stats = List(
    Defn.Object(
      ...
    )
  )
)

Wniosek

W tym artykule pokazaliśmy, jak Bazel umożliwia szybkie budowanie w dużych repozytoriach i przedstawiliśmy podstawowe koncepcje i użycie Bazel, budując prostą aplikację Scala.

Jak widziałeś, Bazel wymaga od użytkowników zarządzania znacznie większą liczbą konfiguracji kompilacji niż inne narzędzia do budowania, takie jak sbt i Maven. Jest to jednak akceptowalny kompromis w przypadku bardzo dużych projektów. biorąc pod uwagę skalowalną szybkość kompilacji dzięki zaletom Bazel, takim jak powtarzalne kompilacje i zdalna pamięć podręczna.

Mam nadzieję, że ten artykuł pomoże Ci podjąć pierwsze kroki w rozpoczęciu pracy z Bazel.

Jeśli chcesz dowiedzieć się więcej o Bazel, polecam przeczytanie oficjalnego przewodnika dla początkujących Bazel i ubrudzenie sobie rąk przy pierwszym projekcie Bazel. W Bazel jest o wiele więcej interesujących tematów do poznania i zbadania!