Externe Abhängigkeiten mit Bzlmod verwalten

Bzlmod ist der Codename des neuen externen Abhängigkeitssystems, das in Bazel 5.0 eingeführt wurde. Sie wurde eingeführt, um mehrere Schwachstellen des alten Systems zu bewältigen, die nicht schrittweise behoben werden konnten. Weitere Informationen finden Sie im Abschnitt zur Problembeschreibung des ursprünglichen Designdokuments.

In Bazel 5.0 ist Bzlmod nicht standardmäßig aktiviert. Das Flag --experimental_enable_bzlmod muss angegeben werden, damit Folgendes wirksam wird. Wie der Flag-Name vorschlägt, ist diese Funktion derzeit experimentell. APIs und Verhaltensweisen können sich ändern, bis die Funktion offiziell eingeführt wird.

Bazel-Module

Das alte WORKSPACE-basierte externe Abhängigkeitssystem konzentriert sich auf Repositories (oder Repositories), die über Repository-Regeln erstellt wurden (oder Repository-Regeln. Obwohl Repositories im neuen System weiterhin ein wichtiges Konzept sind, sind Module die Kerneinheiten der Abhängigkeit.

Ein Modul ist im Wesentlichen ein Bazel-Projekt, das mehrere Versionen haben kann. Jedes Projekt veröffentlicht Metadaten zu anderen Modulen, von denen es abhängt. Diese Vorgehensweise ähnelt den bekannten Konzepten in anderen Abhängigkeitsmanagementsystemen: ein Maven-Artefakt, ein npm-Paket und ein Cargo-Crat , ein Go-Modul usw.

Ein Modul gibt seine Abhängigkeiten einfach mit name- und version-Paaren anstelle bestimmter URLs in WORKSPACE an. Die Abhängigkeiten werden dann in einer Bazel Registry gesucht. standardmäßig die Bazel Central Registry. In Ihrem Arbeitsbereich wird jedes Modul in ein Repository umgewandelt.

MODULE.Bazel

Jede Version eines Moduls enthält eine Datei MODULE.bazel, die ihre Abhängigkeiten und andere Metadaten deklariert. Hier ein einfaches Beispiel:

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

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

Die Datei MODULE.bazel sollte sich im Stammverzeichnis des Arbeitsbereichs befinden (neben der Datei WORKSPACE). Im Gegensatz zur Datei WORKSPACE müssen Sie Ihre transitiven Abhängigkeiten nicht angeben. Stattdessen sollten Sie nur direkte Abhängigkeiten angeben und die MODULE.bazel-Dateien Ihrer Abhängigkeiten werden verarbeitet, um automatisch transitive Abhängigkeiten zu erkennen.

Die Datei MODULE.bazel ist mit BUILD-Dateien vergleichbar, da sie keinen Kontrollfluss unterstützt. Außerdem sind load-Anweisungen nicht zulässig. Die Anweisungen der MODULE.bazel-Dateien unterstützen Folgendes:

Versionsformat

Bazel verfügt über ein vielfältiges Netzwerk und in Projekten werden verschiedene Versionsverwaltungsschemas verwendet. Die mit Abstand breiteste Strategie ist SemVer. Es gibt jedoch auch Projekte mit vielen anderen Schemata wie Abseil, deren Versionen datumsbasiert, z. B. 20210324.2).

Aus diesem Grund verwendet Bzlmod eine vereinfachte Version der SemVer-Spezifikation, insbesondere eine beliebige Anzahl von Ziffernfolgen im "release"-Teil der Version (anstatt genau drei, wie SemVer schreibt): MAJOR.MINOR.PATCH). Außerdem wird die Semantik von Haupt-, Neben- und Patchversionen nicht erzwungen. Informationen zur Abwärtskompatibilität finden Sie unter Kompatibilitätsstufe. Andere Teile der SemVer-Spezifikation, z. B. ein Bindestrich, der eine Vorabversion angibt, werden nicht geändert.

Versionsauflösung

Das Problem mit der Rauteabhängigkeit ist ein Grundbestandteil im Bereich der versionierten Abhängigkeitsverwaltung. Angenommen, Sie haben die folgende Abhängigkeitsgrafik:

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

Welche Version D sollte verwendet werden? Zur Beantwortung dieser Frage verwendet Bzlmod den im Go-Modulsystem eingeführten MVS-Algorithmus (Mindestversionsauswahl). MVS geht davon aus, dass alle neuen Versionen eines Moduls abwärtskompatibel sind, und wählt daher einfach die höchste Version aus, die von einem abhängigen Element angegeben wird (in unserem Beispiel D 1.1). Es wird als "minimal" bezeichnet, da D 1.1 hier die minimale Version ist, die unsere Anforderungen erfüllen kann. Selbst wenn D 1.2 oder höher existiert, wählen wir sie nicht aus. Dies bietet jedoch den zusätzlichen Vorteil, dass die Versionsauswahl High Fidelity ist und reproduzierbar ist.

Die Versionsauflösung wird lokal auf Ihrem Computer und nicht in der Registry ausgeführt.

Kompatibilitätsstufe

Die Annahme von MVS zur Abwärtskompatibilität ist machbar, weil abwärtsinkompatible Versionen eines Moduls einfach als separates Modul behandelt werden. In Bezug auf SemVer werden A 1.x und A 2.x als unterschiedliche Module betrachtet und können in der aufgelösten Abhängigkeitsgrafik nebeneinander bestehen. Dies wird wiederum dadurch ermöglicht, dass die Hauptversion im Paketpfad in Go codiert ist, sodass es keine Konflikte hinsichtlich der Kompilierungszeit oder Verknüpfungszeit gibt.

In Bazel gibt es keine solchen Garantien. Daher müssen wir die Hauptversion angeben, um abwärtskompatible Versionen zu erkennen. Diese Nummer wird als Kompatibilitätsstufe bezeichnet und in jeder Modulversion der Anweisung module() angegeben. Anhand dieser Informationen können wir einen Fehler ausgeben, wenn wir feststellen, dass Versionen des gleichen Moduls mit unterschiedlichen Kompatibilitätsstufen in der aufgelösten Abhängigkeitsgrafik vorhanden sind.

Repository-Namen

In Bazel hat jede externe Abhängigkeit einen Repository-Namen. Manchmal wird dieselbe Abhängigkeit über verschiedene Repository-Namen verwendet (z. B. bedeuten @io_bazel_skylib und @bazel_skylib Bazel skylib) oder Der Repository-Name kann für verschiedene Abhängigkeiten in verschiedenen Projekten verwendet werden.

In Bzlmod können Repositories von Bazel-Modulen und Modulerweiterungen generiert werden. Zum Lösen von Konflikten zwischen Repository-Namen verwenden wir den Mechanismus der Repository-Zuordnung im neuen System. Im Folgenden werden zwei wichtige Konzepte erläutert:

  • Name des kanonischen Repositories: Der global eindeutige Repository-Name für jedes Repository. Dies ist der Verzeichnisname, in dem sich das Repository befindet.
    Die Struktur sieht so aus (Warnung: Das kanonische Namensformat ist keine API, von der Sie abhängig sein sollten, sie kann sich jederzeit ändern):

    • Für Bazel-Modul-Repositories: module_name.version
      (Beispiel). @bazel_skylib.1.0.3)
    • Für Repositories von Modulerweiterungen: module_name.version.extension_name.repo_name
      (Beispiel. @rules_cc.0.0.1.cc_configure.local_config_cc)
  • Name des lokalen Repositorys: Der Repository-Name, der in den Dateien BUILD und .bzl in einem Repository verwendet werden soll. Eine Abhängigkeit kann für unterschiedliche Repositories unterschiedliche lokale Namen haben.
    Das Tag wird so bestimmt:

    • Für Bazel-Modul-Repositories: Standardmäßig module_name oder der Name, der durch das Attribut repo_name in bazel_dep angegeben wird.
    • Für Modulerweiterungen: Repository-Name, der über use_repo eingeführt wird.

Jedes Repository verfügt über ein Repository-Zuordnungswörterbuch seiner direkten Abhängigkeiten, d. h. eine Zuordnung vom Namen des lokalen Repositorys zum kanonischen Repository-Namen. Wir verwenden die Repository-Zuordnung, um den Repository-Namen beim Erstellen eines Labels aufzulösen. Beachten Sie, dass es keinen Konflikt mit kanonischen Repository-Namen gibt und die Verwendung von lokalen Repository-Namen durch das Parsen der Datei MODULE.bazel gefunden werden kann. Daher können Konflikte leicht erkannt und gelöst werden, ohne dass Andere Abhängigkeiten.

Strenge Tiefe

Mit dem neuen Format für Abhängigkeitsspezifikationen können wir strengere Prüfungen durchführen. Insbesondere erzwingen wir jetzt, dass ein Modul nur Repositories verwenden kann, die aus seinen direkten Abhängigkeiten erstellt wurden. Dadurch werden versehentliche und schwer zu behebende Fehler verhindert, wenn sich etwas in der transitiven Abhängigkeitsgrafik ändert.

Strikte Deps werden basierend auf der Repository-Zuordnung implementiert. Im Wesentlichen enthält die Repository-Zuordnung für jedes Repository alle direkten Abhängigkeiten. Andere Repositories sind nicht sichtbar. Sichtbare Abhängigkeiten für jedes Repository werden so bestimmt:

  • Ein Bazel-Modul-Repository kann alle in der Datei MODULE.bazel eingeführten Repositories über bazel_dep und use_repo sehen.
  • Ein Modulerweiterungs-Repository kann alle sichtbaren Abhängigkeiten des Moduls sehen, das die Erweiterung bereitstellt, sowie alle anderen Repositories, die von derselben Modulerweiterung generiert wurden.

Registries

Bzlmod ermittelt Abhängigkeiten, indem er seine Informationen von den Bazel-Registries anfordert. Eine Bazel-Registry ist einfach eine Datenbank mit Bazel-Modulen. Die einzige unterstützte Form von Registries ist eine Index-Registry, bei der es sich um ein lokales Verzeichnis oder einen statischen HTTP-Server in einem bestimmten Format handelt. Für die Zukunft planen wir, die Unterstützung für Registries mit einem einzigen Modul zu ergänzen, bei denen es sich einfach um Git-Repositories handelt, die die Quelle und den Verlauf eines Projekts enthalten.

Index-Registry

Eine Index-Registry ist ein lokales Verzeichnis oder ein statischer HTTP-Server mit Informationen zu einer Liste von Modulen, einschließlich ihrer Startseite, der Administratoren, der MODULE.bazel-Datei der einzelnen Versionen und des Abrufs der Quelle jeder Version. Version. Vor allem muss es nicht die Quellarchive selbst bereitstellen.

Eine Index-Registry muss das folgende Format haben:

  • /bazel_registry.json: Eine JSON-Datei mit Metadaten für die Registry. Derzeit enthält er nur den Schlüssel mirrors, der die Liste der Spiegel angibt, die für Quellarchive verwendet werden sollen.
  • /modules: Ein Verzeichnis mit einem Unterverzeichnis für jedes Modul in dieser Registry.
  • /modules/$MODULE: Ein Verzeichnis mit einem Unterverzeichnis für jede Version dieses Moduls sowie der folgenden Datei:
    • metadata.json: Eine JSON-Datei mit Informationen zum Modul mit den folgenden Feldern:
      • homepage: Die URL der Startseite des Projekts.
      • maintainers: Eine Liste von JSON-Objekten, die jeweils den Informationen eines Administrator des Moduls in der Registry entsprechen. Beachten Sie, dass dies nicht unbedingt mit den Autoren des Projekts identisch ist.
      • versions: Eine Liste aller Versionen dieses Moduls, die in dieser Registry gefunden werden.
      • yanked_versions: Eine Liste der eingeschränkten Versionen dieses Moduls. Dies ist derzeit kein operativer Vorgang, aber in Zukunft werden übersprungene Versionen übersprungen oder es wird ein Fehler angezeigt.
  • /modules/$MODULE/$VERSION: Ein Verzeichnis, das die folgenden Dateien enthält:
    • MODULE.bazel: Die Datei MODULE.bazel dieser Modulversion.
    • source.json: Eine JSON-Datei mit Informationen zum Abrufen der Quelle dieser Modulversion mit den folgenden Feldern:
      • url: Die URL des Quellarchivs.
      • integrity: Die Prüfsumme der Subresource Integrity des Archivs.
      • strip_prefix: Ein Verzeichnispräfix, das beim Extrahieren des Quellarchivs entfernt werden soll.
      • patches: Eine Liste von Strings, die jeweils eine Patch-Datei benennen, die auf das extrahierte Archiv angewendet werden soll. Die Patch-Dateien befinden sich im Verzeichnis /modules/$MODULE/$VERSION/patches.
      • patch_strip: Entspricht dem Argument --strip von Unix-Patch.
    • patches/: Ein optionales Verzeichnis, das Patch-Dateien enthält.

Bazel Central-Registry

Bazel Central Registry (BCR) ist eine Index-Registry unter registry.bazel.build. Der Inhalt wird im GitHub-Repository bazelbuild/bazel-central-registry unterstützt.

Der BCR wird von der Bazel-Community verwaltet. Mitwirkende können Pull-Anfragen senden. Weitere Informationen finden Sie in den Bazel Central Registry-Richtlinien und -Verfahren.

Neben dem Format einer normalen Index-Registry erfordert der BCR auch diepresubmit.yml Datei für jede Modulversion (/modules/$MODULE/$VERSION/presubmit.yml ). Diese Datei gibt einige grundlegende Build- und Testziele an, mit denen die Gültigkeit dieser Modulversion überprüft werden kann. Sie wird von den CI-Pipelines des BCR verwendet, um die Interoperabilität zwischen den Modulen im BCR zu gewährleisten. aus.

Registries auswählen

Mit dem wiederholbaren Bazel-Flag --registry können Sie die Liste der Registries angeben, aus denen Module angefordert werden sollen. Damit können Sie Ihr Projekt so einrichten, dass Abhängigkeiten von einer Drittanbieter- oder internen Registry abgerufen werden. Frühere Registries haben Vorrang. Der Einfachheit halber können Sie in der Datei .bazelrc Ihres Projekts eine Liste von --registry-Flags einfügen.

Modulerweiterungen

Mit Modulerweiterungen können Sie das Modulsystem erweitern, indem Sie Eingabedaten aus Modulen in der Abhängigkeitsgrafik lesen, die erforderliche Logik zum Auflösen von Abhängigkeiten ausführen und schließlich Repositories durch Aufrufen von Repository-Regeln erstellen. Sie ähneln den WORKSPACE-Makros von heute, eignen sich jedoch vor allem für Module und transitive Abhängigkeiten.

Modulerweiterungen werden in .bzl-Dateien wie Repository-Regeln oder WORKSPACE-Makros definiert. Sie werden nicht direkt aufgerufen. Stattdessen kann jedes Modul Datenelemente, sogenannte Tags, angeben, damit Erweiterungen gelesen werden können. Anschließend werden die Modulerweiterungen ausgeführt. Jede Erweiterung wird nach der Modulauflösung einmal ausgeführt, noch bevor ein Build tatsächlich ausgeführt wird, und ruft alle zugehörigen Tags in der gesamten Abhängigkeitsgrafik ab.

          [ 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) ]

Im obigen Abhängigkeitsdiagramm sind die Felder A 1.1 und B 1.2 Bazel-Module. Sie können sich jedes Modul als MODULE.bazel-Datei vorstellen. In jedem Modul können einige Tags für Modulerweiterungen angegeben werden. Dann werden einige für die Erweiterung mamaven“ und andere für carcargo“ angegeben. Wenn diese Abhängigkeitsgrafik final ist (z. B. hat B 1.2 möglicherweise bazel_dep auf D 1.3, wurde aber aufgrund von C auf D 1.4 aktualisiert), wird "maven" ausgeführt und es werden alle maven.*-Tags gelesen. Anhand der darin enthaltenen Informationen wird entschieden, welche Repositories erstellt werden. Entsprechend für die Erweiterung "Cargo".

Nutzung der Erweiterungen

Erweiterungen werden in den Bazel-Modulen selbst gehostet. Wenn Sie also eine Erweiterung in Ihrem Modul verwenden möchten, fügen Sie zuerst ein bazel_dep in dieses Modul ein und rufen Sie dann use_extension integrierte Funktion, um sie in den Geltungsbereich zu bringen. Das folgende Beispiel zeigt ein Snippet aus einer MODULE.bazel-Datei zur Verwendung einer hypothetischen Maven-Erweiterung, die im Modul rules_jvm_external definiert ist:

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

Nachdem Sie die Erweiterung in Betracht gezogen haben, können Sie mit der Punktsyntax Tags dafür festlegen. Beachten Sie, dass die Tags dem Schema entsprechen müssen, das in den entsprechenden Tag-Klassen definiert ist (siehe Definition der Erweiterung unten). In diesem Beispiel werden einige maven.dep- und maven.pom-Tags angegeben.

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

Wenn die Erweiterung Repositories generiert, die Sie in Ihrem Modul verwenden möchten, verwenden Sie die Anweisung use_repo, um sie zu deklarieren. Dies dient der strengen Bedingung für Deps und vermeidet Konflikte mit lokalen Repository-Namen.

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

Die von einer Erweiterung generierten Repositories sind Teil ihrer API. Daher wird anhand der von Ihnen angegebenen Tags ermittelt, dass die Erweiterung "maven" ein Repository mit dem Namen "org_junit_junit" und eines mit dem Namen "com_google_guava_guava" generiert. " Mit use_repo können Sie sie optional im Bereich Ihres Moduls umbenennen, wie hier "guava".

Definition der Erweiterung

Modulerweiterungen werden mit der Funktion module_extension ähnlich wie Repository-Regeln definiert. Beide haben eine Implementierungsfunktion. Während Repository-Regeln eine Reihe von Attributen haben, haben Modulerweiterungen eine Reihe von tag_classes, die jeweils eine Reihe von Attributen haben. Mit den Tag-Klassen werden Schemas für Tags definiert, die von dieser Erweiterung verwendet werden. Fortsetzung unseres Beispiels für die hypothetische Erweiterung "maven" oben:

# @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},
)

Aus diesen Deklarationen geht klar hervor, dass die Tags maven.dep und maven.pom mit dem oben definierten Attributschema angegeben werden können.

Die Implementierungsfunktion ähnelt einem WORKSPACE-Makro, mit dem Unterschied, dass sie ein module_ctx-Objekt erhält, das Zugriff auf die Abhängigkeitsgrafik und alle entsprechenden Tags gewährt. Die Implementierungsfunktion sollte dann Repository-Regeln aufrufen, um Repositories zu generieren:

# @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]

Im obigen Beispiel durchlaufen wir alle Module in der Abhängigkeitsgrafik (ctx.modules), die jeweils ein bazel_module-Objekt sind, dessen tags alle maven.*-Tags im Modul verfügbar. Anschließend rufen wir das Befehlszeilendienstprogramm "Coursier" auf, um Maven zu kontaktieren und die Lösung auszuführen. Zuletzt verwenden wir das Auflösungsergebnis, um mit der hypothetischen maven_single_jar-Repository-Regel eine Reihe von Repositories zu erstellen.