Zarządzanie zależnościami zewnętrznymi za pomocą Bzlmod

Bzlmod to nazwa kodowa nowego systemu zewnętrznej zależności wprowadzonej w Bazelu 5.0. Wprowadziliśmy tę funkcję w celu rozwiązania kilku problemów (starych systemu), których nie da się stopniowo wdrożyć. więcej informacji znajdziesz w sekcji opisującej problem w oryginalnym dokumencie projektu.

W Bazel 5.0 Bzlmod nie jest domyślnie włączony. Aby flaga zaczęła obowiązywać, należy określić flagę --experimental_enable_bzlmod. Jak sugeruje nazwa flagi, ta funkcja jest obecnie eksperymentalna. Interfejsy API i zachowania mogą ulec zmianie, dopóki funkcja nie zostanie oficjalnie uruchomiona.

Moduły bazelowe

Dawny zewnętrzny system zależności oparty na WORKSPACE jest oparty na repozytoriach (lub repozytorium), które zostaje utworzony przy użyciu reguł repozytorium (lub reguły repozytorium). Repozytoria to nadal ważny element systemu, a moduły są podstawowymi jednostkami zależności.

Moduł to zasadniczo projekt Bazel, który może mieć wiele wersji. Każda z nich publikuje metadane o innych modułach, od których zależy. To działanie znane w przypadku innych systemów zarządzania zależnością: artefaktów Maven, pakietu npm i skrzyni ładunkowej Cargo. , moduł Go itp.

Moduł określa swoje zależności przy użyciu par name i version, a nie określonych adresów URL w WORKSPACE. Zależności są następnie wyszukiwane w rejestrze Bazela. domyślnie w Bazel Central Registry. W Twoim obszarze roboczym każdy moduł staje się repozytorium.

MODULE.bazel

Każda wersja każdego modułu ma plik MODULE.bazel z deklaracją zależności i innymi metadanymi. Oto podstawowy przykład:

module(
    name = "my-module",
    version = "1.0",
)

bazel_dep(name = "rules_cc", version = "0.0.1")
bazel_dep(name = "protobuf", version = "3.19.0")

Plik MODULE.bazel powinien znajdować się w katalogu głównym katalogu obszaru roboczego (obok pliku WORKSPACE). W przeciwieństwie do pliku WORKSPACE nie musisz określać zależności przechodniowych. Zamiast tego określaj tylko zależności bezpośrednie, a pliki MODULE.bazel zależności są automatycznie przetwarzane, aby możliwe było automatyczne zależności.

Plik MODULE.bazel jest podobny do plików BUILD, ponieważ nie obsługuje żadnej formy kontroli. zakazuje też instrukcji load. Dyrektywy MODULE.bazel obsługują pliki:

Format wersji

Bazel ma zróżnicowany ekosystem i projekty używają różnych schematów obsługi wersji. Najpopularniejszy jest SemVer, ale istnieją też duże projekty wykorzystujące różne schematy, np. Abseil, których wersje na podstawie daty, np. 20210324.2).

Z tego powodu Bzlmod stosuje bardziej spokojną wersję specyfikacji SemVer, w szczególności zezwalającą na dowolną liczbę sekwencji cyfr w sekcji „release” wersji (zamiast dokładnie 3 zgodnie z przepisami SemVer): MAJOR.MINOR.PATCH. Ponadto nie wymuszane są semantyki wersji głównych i nieletnich oraz poprawek. (Szczegółowe informacje o odwrotności zgodności znajdziesz w artykule Poziom zgodności). Inne części specyfikacji SemVer, takie jak łącznik oznaczający wersję przedpremierową, nie są modyfikowane.

Rozdzielczość wersji

Problem z zależnością diamentów jest podstawowym zpoziomu w zarządzanej zależności. Załóżmy, że masz taki wykres zależności:

       A 1.0
      /     \
   B 1.0    C 1.1
     |        |
   D 1.0    D 1.1

Której wersji D należy użyć? Aby rozwiązać ten problem, Bzlmod używa algorytmu Minimalnego wyboru wersji (MVS) wprowadzonego w systemie modułu Go. MVS zakłada, że wszystkie nowe wersje modułu są zgodne wstecz, po prostu wybiera najwyższą wersję określoną przez dowolny zależny (w tym przykładzie 1.1). To tzw. „minimalna”, ponieważ D 1.1 jest tutaj minimalną wersją, która spełnia nasze wymagania. Nawet jeśli D 1.2 lub nowsza istnieje, nie wybieramy ich. Dodatkową zaletą jest to, że wybór wersji to wysoka wierność i powtarzalność.

Rozwiązywanie wersji odbywa się lokalnie na komputerze, a nie w rejestrze.

Poziom zgodności

Założenie, że MVS przyjmuje zgodność wsteczną, jest możliwe, ponieważ po prostu traktuje niezgodne wersje wstecz jako odrębny moduł. W kontekście SemVer oznacza to, że wartości 1.x i 2.x są uważane za oddzielne moduły i mogą współistnieć na wykresie zależności. Jest to możliwe dzięki temu, że wersja główna jest zakodowana w ścieżce pakietu w Go, więc nie ma sprzeczności w czasie kompilacji ani czasu połączenia.

W Bazelu nie mamy takich gwarancji. Dlatego potrzebujemy sposobu na oznaczenie liczby „głównej”, by wykrywać niezgodne wstecz wersje. Liczba ta nosi nazwę poziomu zgodności i jest określana przez poszczególne wersje modułów w dyrektywie module(). Biorąc pod uwagę te informacje, możemy zgłosić błąd, gdy wykryjemy, że w wersji z zależnościami znajdują się wersje tego samego modułu z różnymi poziomami zgodności.

Nazwy repozytoriów

W Bazelu każda zależność zewnętrzna ma nazwę repozytorium. Ta sama zależność może być czasem wykorzystywana przez różne nazwy repozytoriów (np. @io_bazel_skylib i @bazel_skylib oznaczają bazel skylib) lub to samo Nazwa repozytorium może być używana dla różnych zależności w różnych projektach.

W Bzlmod repozytoria można generować za pomocą modułów Bazel i rozszerzeń. Aby rozwiązać konflikty nazw folderów, uwzględniamy w nowym systemie mechanizm mapowania repozytorium. Oto 2 ważne kwestie:

  • Kanoniczna nazwa repozytorium: globalnie unikalna nazwa repozytorium dla każdego repozytorium. Będzie to nazwa katalogu, w którym znajduje się repozytorium.
    Jest on skonstruowany w następujący sposób (Ostrzeżenie: format kanonicznej nie jest zależnym interfejsem API i może się w każdej chwili zmienić):

    • W przypadku modułu modułu Bazel: module_name.version
      (Przykład). @bazel_skylib.1.0.3)
    • Dotyczy rozszerzenia modułu: module_name.version.extension_name.repo_name
      (Przykład). @rules_cc.0.0.1.cc_configure.local_config_cc)
  • Nazwa repozytorium lokalnego: nazwa repozytorium, która ma być używana w plikach BUILD i .bzl w repozytorium. Ta sama zależność może mieć różne nazwy lokalne dla różnych repozytoriów.
    Jest określany jako:

    • W przypadku modułu bazy bazy: domyślne dane: module_name lub nazwa podana w atrybucie repo_name w bazel_dep.
    • Na potrzeby rozszerzenia modułu: nazwę repozytorium wpisana za pomocą use_repo.

Każde repozytorium ma słownik mapowania repozytorium jego bezpośrednich zależności, czyli mapowanie z nazwy lokalnego repozytorium na kanoniczną nazwę repozytorium. Używamy mapowania repozytorium, aby ustalić nazwę repozytorium podczas tworzenia etykiety. Pamiętaj, że kanoniczne nazwy repozytoriów nie są sprzeczne z plikami repozytorium, a lokalne repozytoria można rozpoznać po przeanalizowaniu pliku MODULE.bazel. Dzięki temu można je łatwo wykryć i rozwiązać bez wpływu na ich nazwy. innych zależności.

Dokładne dochody

Nowy format specyfikacji zależności pozwala nam przeprowadzać bardziej rygorystyczne testy. Szczególnie wymagamy, by moduł mógł korzystać z repozytoriów utworzonych na podstawie jego bezpośrednich zależności. Pomaga to zapobiegać przypadkowym i trudnym do debugowania uszkodzeniam, gdy coś się zmienia na wykresie zależności zależności.

Implementacja w trybie rygorystycznego wdrożenia jest wdrażana na podstawie mapowania repozytorium. Zasadniczo mapowanie repozytorium w przypadku każdego repozytorium zawiera wszystkie jego zależności bezpośrednie, żadne inne repozytorium nie jest widoczne. Widoczne zależności dla każdego repozytorium są określane w następujący sposób:

  • Repozytorium modułu Bazel może wyświetlać wszystkie repliki wprowadzone w pliku MODULE.bazel za pomocą bazel_dep i use_repo.
  • Repozytorium rozszerzenia modułu może zobaczyć wszystkie widoczne zależności modułu, który zawiera dane rozszerzenie, oraz wszystkie inne repozytoria wygenerowane przez to samo rozszerzenie modułu.

Rejestry

Bzlmod odkrywa zależności, żądając informacji od rejestrów Bazel. Rejestr Baze to po prostu baza danych modułów Bazel. Jedyną obsługiwaną formą rejestru jest rejestr indeksu, który jest lokalnym katalogiem lub statycznym serwerem HTTP zgodnym z określonym formatem. W przyszłości planujemy dodać obsługę rejestrów z jednym modułem, które są po prostu repozytorium git z źródła i historią projektu.

Rejestr indeksu

Rejestr indeksu jest lokalnym katalogiem lub statycznym serwerem HTTP zawierającym informacje o listie modułów, w tym o ich stronie głównej, obsługę, plik MODULE.bazel każdej wersji i informacje o pobieraniu źródła każdego z nich. Pamiętaj, że nie musi ono obsługiwać archiwów źródłowych.

Rejestr indeksu musi mieć następujący format:

  • /bazel_registry.json: plik JSON zawierający metadane rejestru. Obecnie zawiera tylko 1 klucz mirrors, który określa listę lustr, których chcesz użyć w archiwach źródłowych.
  • /modules: katalog zawierający podkatalog dla każdego modułu w tym rejestrze.
  • /modules/$MODULE: katalog zawierający podkatalog dla każdej wersji tego modułu i następujący plik:
    • metadata.json: plik JSON z informacjami o module z następującymi polami:
      • homepage: adres URL strony głównej projektu.
      • maintainers: lista obiektów JSON, z których każdy odpowiada informacjom modułu obsługi w rejestrze. Pamiętaj, że niekoniecznie to samo co autorzy projektu.
      • versions: lista wszystkich wersji tego modułu, które znajdują się w tym rejestrze.
      • yanked_versions: lista usuniętych wersji tego modułu. Na razie nie jest to specjalny przeszkodę, ale w przyszłości w jego zbiorowych wersjach zostanie pominięta lub zostanie wyświetlony komunikat o błędzie.
  • /modules/$MODULE/$VERSION: katalog zawierający te pliki:
    • MODULE.bazel: plik MODULE.bazel wersji tego modułu.
    • source.json: plik JSON z informacjami o pobieraniu źródła tej wersji modułu, wraz z tymi polami:
      • url: adres URL archiwum źródłowego.
      • integrity: suma kontrolna archiwum z podzasobem.
      • strip_prefix: prefiks katalogu do wyodrębnienia podczas wyodrębniania archiwum źródłowego.
      • patches: lista ciągów tekstowych, z których każdy ma nazwę pliku poprawki zastosowanego do wyodrębnionego archiwum. Pliki poprawek znajdują się w katalogu /modules/$MODULE/$VERSION/patches.
      • patch_strip: taki sam jak argument --strip poprawki.
    • patches/: opcjonalny katalog zawierający pliki poprawek.

Rejestr Central Bazel

Bazel Central Registry (BCR) to rejestr indeksu działający pod adresem registry.bazel.build. Jego zawartość jest tworzona przez repozytorium GitHub bazelbuild/bazel-central-registry.

BCR zajmuje się społeczność Bazela. zachęcamy do przesyłania próśb o pobranie danych. Zobacz zasady i procedury postępowania w Bazel Central.

Poza formatem zwykłego rejestru indeksów plik BCR wymaga pliku presubmit.yml dla każdej wersji modułu (/modules/$MODULE/$VERSION/presubmit.yml). Ten plik określa kilka podstawowych celów kompilacji i testowania, które mogą służyć do sprawdzania poprawności tej wersji modułu i używane przez potoki CI BCR w celu zapewnienia współdziałania między modułami w BCR danych

Wybieranie rejestrów

Możesz użyć powtarzalnej flagi Bazel --registry, aby określić listę rejestrów, z których chcesz żądać modułów. Możesz tak skonfigurować projekt, aby pobierał zależności z rejestru zewnętrznego lub wewnętrznego. Wcześniejsze rejestry mają pierwszeństwo. Dla ułatwienia możesz umieścić listę flag --registry w pliku .bazelrc projektu.

Rozszerzenia modułów

Rozszerzenia modułów umożliwiają rozszerzenie systemu modułów przez czytanie danych wejściowych z modułów na wykresie zależności, wykonanie niezbędnych działań logicznych w celu rozwiązania zależności, a na koniec utworzenie repozytorium przez wywołanie reguł repozytorium. Są one podobne do dzisiejszych makr WORKSPACE, ale bardziej sprawdzają się w świecie modułów i zależności przechodnich.

Rozszerzenia modułu definiuje się w plikach .bzl, tak jak reguły repozytorium lub makra WORKSPACE. Nie są one wywoływane bezpośrednio. Każdy moduł może przy tym wskazywać fragmenty danych nazywane tagami, które mają być odczytywane przez rozszerzenia. Następnie po rozwiązaniu wersji modułu uruchamia się rozszerzenia modułu. Każde rozszerzenie jest uruchamiane raz po rozwiązaniu modułu (nadal przed faktyczną kompilacją) i odczytuje wszystkie należące do niego tagi na całym wykresie zależności.

          [ A 1.1                ]
          [   * maven.dep(X 2.1) ]
          [   * maven.pom(...)   ]
              /              \
   bazel_dep /                \ bazel_dep
            /                  \
[ B 1.2                ]     [ C 1.0                ]
[   * maven.dep(X 1.2) ]     [   * maven.dep(X 2.1) ]
[   * maven.dep(Y 1.3) ]     [   * cargo.dep(P 1.1) ]
            \                  /
   bazel_dep \                / bazel_dep
              \              /
          [ D 1.4                ]
          [   * maven.dep(Z 1.4) ]
          [   * cargo.dep(Q 1.1) ]

W podanym wyżej wykresie zależności A 1.1 i B 1.2 itd. są modułami Bazela. Każdy z nich możesz określić jako plik MODULE.bazel. Każdy moduł może mieć pewne tagi rozszerzeń W tym przypadku niektóre dotyczą rozszerzenia „maven”, a inne „cargo”. Po sfinalizowaniu tego wykresu zależności (np. B 1.2 ma bazel_dep z D 1.3, ale ze względu na C został zmieniony na D 1.4) rozszerzenie „maven” i odczytuje wszystkie tagi maven.*, korzystając z zawartych w nim informacji przy podejmowaniu decyzji, które repozytorium utworzyć. Podobnie jest w przypadku rozszerzenia „cargo”.

Wykorzystanie rozszerzenia

Rozszerzenia są hostowane w modułach Bazel, więc aby użyć rozszerzenia w tym module, musisz najpierw dodać w nim obiekt bazel_dep, a następnie wywołać funkcję use_extension/ – wchodzi w zakres. Skorzystaj z poniższego przykładowego fragmentu kodu z pliku MODULE.bazel, aby użyć hipotetycznego rozszerzenia „maven” zdefiniowanego w module rules_jvm_external:

bazel_dep(name = "rules_jvm_external", version = "1.0")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

Po dodaniu rozszerzenia do zakresu możesz użyć kropki kroczącej, aby określić dla niego tagi. Pamiętaj, że tagi muszą być zgodne ze schematem zdefiniowanym przez odpowiednie klasy tagów (patrz definicja rozszerzenia poniżej). Oto przykład określający tagi maven.dep i maven.pom.

maven.dep(coord="org.junit:junit:3.0")
maven.dep(coord="com.google.guava:guava:1.2")
maven.pom(pom_xml="//:pom.xml")

Jeśli rozszerzenie generuje repozytorium, którego chcesz używać w module, zadeklaruj je za pomocą dyrektywy use_repo. Ma to na celu spełnienie ścisłego warunku deps, a także uniknięcie konfliktu z lokalną nazwą repozytorium.

use_repo(
    maven,
    "org_junit_junit",
    guava="com_google_guava_guava",
)

Repozytoria wygenerowane przez rozszerzenie są częścią interfejsu API, więc na podstawie podanych przez Ciebie tagów musisz wiedzieć, że rozszerzenie „maven” wygeneruje repozytorium o nazwie „org_junit_junit” i „com_google_guava_guava”. ”. W use_repo możesz opcjonalnie zmienić ich nazwy w zakresie modułu, np. „guava”.

Definicja rozszerzenia

Rozszerzenia modułu definiuje się podobnie do reguł repozytorium za pomocą funkcji module_extension. Każda z nich ma funkcję implementacji. Mimo że reguły repozytorium zawierają kilka atrybutów, rozszerzenia modułu mają liczbę atrybutów tag_classes, z których każdy ma pewną liczbę atrybutów. Klasy tagów definiują schematy tagów używanych przez to rozszerzenie. Kontynuując nasz przykład hipotetycznego rozszerzenia „maven”:

# @rules_jvm_external//:extensions.bzl
maven_dep = tag_class(attrs = {"coord": attr.string()})
maven_pom = tag_class(attrs = {"pom_xml": attr.label()})
maven = module_extension(
    implementation=_maven_impl,
    tag_classes={"dep": maven_dep, "pom": maven_pom},
)

Te deklaracje wyraźnie wskazują, że tagi maven.dep i maven.pom można określić za pomocą zdefiniowanego powyżej atrybutu.

Funkcja implementacji jest podobna do makra WORKSPACE, z tą różnicą, że pobiera obiekt module_ctx, który przyznaje dostęp do wykresu zależności i wszystkich powiązanych tagów. Funkcja implementacji powinna następnie wywołać reguły repozytorium, aby wygenerować repozytorium:

# @rules_jvm_external//:extensions.bzl
load("//:repo_rules.bzl", "maven_single_jar")
def _maven_impl(ctx):
  coords = []
  for mod in ctx.modules:
    coords += [dep.coord for dep in mod.tags.dep]
  output = ctx.execute(["coursier", "resolve", coords])  # hypothetical call
  repo_attrs = process_coursier(output)
  [maven_single_jar(**attrs) for attrs in repo_attrs]

W powyższym przykładzie omawiamy wszystkie moduły na wykresie zależności (ctx.modules), z których każdy jest obiektem bazel_module, którego tags zawiera wszystkie tagi maven.* modułu. Następnie wywołujemy narzędzie wiersza poleceń, by skontaktować się z Maven i rozwiązać problem. Rezultat rozwiązania służy do utworzenia liczby repozytoriów przy użyciu hipotetycznej reguły maven_single_jar.