Regeln

Eine Regel definiert eine Reihe von Aktionen, die Bazel für Eingaben ausführt, um eine Reihe von Ausgaben zu erstellen, auf die in Die von der Implementierungsfunktion der Regel zurückgegebenen Anbieter. Eine C++-Binärdatei kann zum Beispiel so aussehen:

  1. Nehmen Sie eine Reihe von .cpp-Quelldateien (Eingaben).
  2. Führen Sie g++ für die Quelldateien (Aktion) aus.
  3. Der DefaultInfo-Anbieter mit der ausführbaren Ausgabe und anderen Dateien zurückgeben, die zur Laufzeit verfügbar gemacht werden sollen
  4. Geben Sie den CcInfo-Anbieter mit C++-spezifischen Informationen zurück, die vom Ziel und seinen Abhängigkeiten erfasst wurden.

Aus Sicht von Bazel sind g++ und die C++-Standardbibliotheken ebenfalls Eingaben für diese Regel. Als Regelautor müssen Sie nicht nur die vom Nutzer bereitgestellten Eingaben für eine Regel berücksichtigen, sondern auch alle Tools und Bibliotheken, die zum Ausführen der Aktionen erforderlich sind.

Bevor Sie eine Regel erstellen oder ändern, müssen Sie mit den Build-Phasen von Bazel vertraut sein. Es ist wichtig, die drei Phasen eines Builds (Laden, Analysieren und Ausführen) zu verstehen. Außerdem erfahren Sie mehr über Makros, um den Unterschied zwischen Regeln und Makros zu verstehen. Lesen Sie als Erstes die Anleitung zu Regeln. Verwenden Sie diese Seite dann als Referenz.

Einige Regeln sind in Bazel selbst enthalten. Diese nativen Regeln wie cc_library und java_binary bieten grundlegende Unterstützung für bestimmte Sprachen. Wenn Sie Ihre eigenen Regeln definieren, können Sie ähnliche Unterstützung für Sprachen und Tools hinzufügen, die Bazel nicht nativ unterstützt.

Bazel bietet ein Erweiterbarkeitsmodell für das Schreiben von Regeln mit der Sprache Starlark. Diese Regeln sind in .bzl-Dateien geschrieben, die direkt aus BUILD-Dateien geladen werden können.

Beim Definieren Ihrer eigenen Regel können Sie entscheiden, welche Attribute sie unterstützt und wie ihre Ausgaben erzeugt werden.

Die implementation-Funktion der Regel definiert ihr genaues Verhalten während der Analysephase. Diese Funktion führt keine externen Befehle aus. Stattdessen werden Aktionen registriert, die später während der Ausführungsphase verwendet werden, um die Ausgaben der Regel zu erstellen, wenn sie benötigt werden.

Regelerstellung

Verwenden Sie in einer .bzl-Datei die Funktion rule, um eine neue Regel zu definieren und das Ergebnis in einer globalen Variablen zu speichern. Der Aufruf von rule gibt Attribute und eine Implementierungsfunktion an:

example_library = rule(
    implementation = _example_library_impl,
    attrs = {
        "deps": attr.label_list(),
        ...
    },
)

Dies definiert eine Art der Regel mit dem Namen example_library.

Der Aufruf von rule muss auch angeben, ob die Regel eine ausführbare Ausgabe (mit executable=True) oder speziell eine ausführbare Testdatei (mit test=True) erstellt. aus. Wenn letztere eine Regel ist, ist sie eine Testregel. Der Name der Regel muss auf _test enden.

Zielinstanziierung

Regeln können geladen und in BUILD-Dateien aufgerufen werden:

load('//some/pkg:rules.bzl', 'example_library')

example_library(
    name = "example_target",
    deps = [":another_target"],
    ...
)

Jeder Aufruf einer Build-Regel gibt keinen Wert zurück. Allerdings hat dies die Auswirkungen auf die Definition eines Ziels. Das wird als Instanziierung der Regel bezeichnet. Hier werden ein Name für das neue Ziel und Werte für die Attribute des Ziels angegeben.

Regeln können auch über Starlark-Funktionen aufgerufen und in .bzl-Dateien geladen werden. Starlark-Funktionen, die Regeln aufrufen, werden als Starlark-Makros bezeichnet. Starlark-Makros müssen letztendlich aus BUILD-Dateien abgerufen werden. Sie können nur in der Ladephase aufgerufen werden, wenn BUILD-Dateien zur Instanziierung von Zielen ausgewertet werden.

Attribute

Ein Attribut ist ein Regelargument. Attribute können bestimmte Werte für die Implementierung eines Ziels bereitstellen oder auf andere Ziele verweisen, wodurch ein Diagramm von Abhängigkeiten erstellt wird.

Regelspezifische Attribute wie srcs oder deps werden definiert, indem eine Zuordnung von Attributnamen zu Schemas übergeben wird, die mit dem Modul attr erstellt wurden. ) zum Parameter attrs von rule hinzu. Allgemeine Attribute wie name und visibility werden implizit allen Regeln hinzugefügt. Zusätzliche Attribute werden implizit den ausführbaren und Testregeln hinzugefügt. Attribute, die implizit einer Regel hinzugefügt werden, können nicht in dem an attrs übergebenen Wörterbuch enthalten sein.

Abhängigkeitsattribute

Regeln, die Quellcode verarbeiten, definieren normalerweise die folgenden Attribute, um verschiedene Arten von Abhängigkeiten zu verarbeiten:

  • srcs gibt die Quelldateien an, die von den Aktionen eines Ziels verarbeitet werden. Häufig gibt das Attributschema an, welche Dateierweiterungen für die Sortierung der von der Regel verarbeiteten Quelldatei erwartet werden. Regeln für Sprachen mit Header-Dateien geben in der Regel ein separates hdrs-Attribut für Header an, die von einem Ziel und dessen Nutzern verarbeitet werden.
  • deps gibt Codeabhängigkeiten für ein Ziel an. Das Attributschema sollte angeben, welche Anbieter diese Abhängigkeiten bereitstellen müssen. Beispiel: cc_library stellt CcInfo bereit.
  • data gibt Dateien an, die zur Laufzeit für eine ausführbare Datei verfügbar sein sollen, die von einem Ziel abhängt. Dadurch sollten beliebige Dateien angegeben werden können.
example_library = rule(
    implementation = _example_library_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = [".example"]),
        "hdrs": attr.label_list(allow_files = [".header"]),
        "deps": attr.label_list(providers = [ExampleInfo]),
        "data": attr.label_list(allow_files = True),
        ...
    },
)

Dies sind Beispiele für Abhängigkeitsattribute. Alle Attribute, die ein Eingabelabel angeben (die mitattr.label_list ,attr.label oderattr.label_keyed_string_dict definiert Abhängigkeiten eines bestimmten Typs zwischen einem Ziel und den Zielen, deren Labels (oder die entsprechenden Label Objekte), wenn das Ziel definiert ist. Das Repository und eventuell der Pfad für diese Labels wird relativ zum definierten Ziel aufgelöst.

example_library(
    name = "my_target",
    deps = [":other_target"],
)

example_library(
    name = "other_target",
    ...
)

In diesem Beispielother_target ist eine Abhängigkeit vonmy_target und daherother_target zuerst analysiert wird. Es gilt als Fehler, wenn die Abhängigkeitsgrafik der Ziele einen Zyklus enthält.

Private Attribute und implizite Abhängigkeiten

Ein Abhängigkeitsattribut mit einem Standardwert erstellt eine implizite Abhängigkeit. Sie ist implizit, da sie Teil der Zielgrafik ist, die der Nutzer nicht in einer BUILD-Datei angibt. Implizite Abhängigkeiten sind nützlich, um eine Beziehung zwischen einer Regel und einem Tool (einer Build-Abhängigkeit, z. B. einer Kompilierung) hartzucodieren, da ein Nutzer in den meisten Fällen Sie möchten nicht angeben, welches Tool die Regel verwendet. Innerhalb der Implementierungsfunktion der Regel wird dies wie andere Abhängigkeiten behandelt.

Wenn Sie eine implizite Abhängigkeit bereitstellen möchten, ohne dass der Nutzer diesen Wert überschreibt, können Sie das AttributPrivat Geben Sie ihm einen Namen, der mit einem Unterstrich (_ ). Private Attribute müssen Standardwerte haben. Im Allgemeinen ist es nur sinnvoll, private Attribute für implizite Abhängigkeiten zu verwenden.

example_library = rule(
    implementation = _example_library_impl,
    attrs = {
        ...
        "_compiler": attr.label(
            default = Label("//tools:example_compiler"),
            allow_single_file = True,
            executable = True,
            cfg = "exec",
        ),
    },
)

In diesem Beispiel hat jedes Ziel vom Typ example_library eine implizite Abhängigkeit vom Compiler //tools:example_compiler. Dadurch kann die Implementierungsfunktion von example_library Aktionen generieren, die den Compiler aufrufen, obwohl der Nutzer sein Label nicht als Eingabe übergeben hat. Da _compiler ein privates Attribut ist, weist ctx.attr._compiler darauf hin, dass in allen Zielen dieses Regeltyps immer auf //tools:example_compiler verwiesen wird. Alternativ können Sie das Attribut compiler ohne Unterstrich benennen und den Standardwert beibehalten. Dadurch können Nutzer bei Bedarf einen anderen Compiler ersetzen, aber es ist keine Kenntnis des Compilers-Labels erforderlich.

Implizite Abhängigkeiten werden im Allgemeinen für Tools verwendet, die sich im selben Repository wie die Regelimplementierung befinden. Wenn das Tool stattdessen von der Ausführungsplattform oder einem anderen Repository stammt, sollte die Regel dieses Tool aus einer Toolchain abrufen.

Ausgabeattribute

Ausgabeattribute wie attr.output und attr.output_list deklarieren eine Ausgabedatei, die vom Ziel generiert wird. aus. Diese unterscheiden sich in zweierlei Hinsicht von Abhängigkeitsattributen:

  • Sie definieren Ziele der Ausgabedatei, anstatt auf Ziele zu verweisen, die an anderer Stelle definiert sind.
  • Die Ziele der Ausgabedatei hängen vom Ziel der instanziierten Regel ab und nicht umgekehrt.

Normalerweise werden Ausgabeattribute nur verwendet, wenn eine Regel Ausgaben mit benutzerdefinierten Namen erstellen muss, die nicht auf dem Zielnamen basieren können. Wenn eine Regel ein Ausgabeattribut hat, heißt sie normalerweise out oder outs.

Ausgabeattribute sind die bevorzugte Methode zum Erstellen vorab deklarierter Ausgaben, die speziell von der Befehlszeile abhängig sind.

Implementierungsfunktion

Jede Regel erfordert eine implementation-Funktion. Diese Funktionen werden in der Analysephase strikt ausgeführt und transformieren die Grafik der in der Ladephase generierten Ziele in eine Grafik mit Aktionen, um in der Ausführungsphase durchgeführt. Daher können Implementierungsfunktionen keine Dateien lesen oder schreiben.

Funktionen zur Regelimplementierung sind normalerweise privat (mit einem führenden Unterstrich benannt). In der Regel werden sie mit der Regel benannt, aber mit dem Suffix _impl.

Implementierungsfunktionen verwenden genau einen Parameter: einen Regelkontext, üblicherweise ctx genannt. Sie geben eine Liste von Anbietern zurück.

Ziele

Abhängigkeiten werden zum Zeitpunkt der Analyse als Target-Objekte dargestellt. Diese Objekte enthalten die Anbieter, die beim Ausführen der Implementierungsfunktion des Ziels generiert wurden.

ctx.attr enthält Felder, die den Namen der einzelnen Abhängigkeitsattribute entsprechen, einschließlich Target-Objekten, die jede direkte Abhängigkeit über dieses Attribut darstellen. Für label_list-Attribute ist dies eine Liste von Targets. Bei label-Attributen ist dies ein einzelner Target oder None.

Eine Liste von Anbieterobjekten wird von der Implementierungsfunktion eines Ziels zurückgegeben:

return [ExampleInfo(headers = depset(...))]

Auf diese kann mit der Indexschreibweise ([]) zugegriffen werden, wobei der Anbietertyp als Schlüssel verwendet wird. Diese können benutzerdefinierte Anbieter sein, die in Starlark definiert sind, oder Anbieter für native Regeln, die als globale Starlark-Variablen verfügbar sind.

Wenn eine Regel beispielsweise Headerdateien über ein hdrs-Attribut übernimmt und sie den Kompilierungsaktionen des Ziels und seiner Nutzer zur Verfügung stellt, könnte sie so erfasst werden:

def _example_library_impl(ctx):
    ...
    transitive_headers = [dep[ExampleInfo].headers for dep in ctx.attr.deps]

Für den alten Stil, in dem ein struct von der Implementierungsfunktion eines Ziels anstelle einer Liste von Anbieterobjekten zurückgegeben wird:

return struct(example_info = struct(headers = depset(...)))

Anbieter können aus dem entsprechenden Feld des Objekts Target abgerufen werden:

transitive_headers = [dep.example_info.headers for dep in ctx.attr.deps]

Wir empfehlen, diesen Stil nicht zu verwenden und die Regeln sollten von ihm migriert werden.

Dateien

Dateien werden durch File-Objekte dargestellt. Da Bazel während der Analyse keine E/A-Vorgänge für die Datei durchführt, können diese Objekte nicht zum direkten Lesen oder Schreiben von Dateiinhalten verwendet werden. Stattdessen werden sie an Aktionen gesendet, die Aktionen auslösen (siehe ctx.actions), um Teile der Aktionsgrafik zu erstellen.

Eine File kann entweder eine Quelldatei oder eine generierte Datei sein. Jede generierte Datei muss eine Ausgabe von genau einer Aktion sein. Quelldateien können die Ausgabe einer Aktion nicht sein.

Für jedes Abhängigkeitsattribut enthält das entsprechende Feld von ctx.files eine Liste der Standardausgaben aller Abhängigkeiten über dieses Attribut:

def _example_library_impl(ctx):
    ...
    headers = depset(ctx.files.hdrs, transitive=transitive_headers)
    srcs = ctx.files.srcs
    ...

ctx.file enthält einen einzelnen File oder einen None für Abhängigkeitsattribute, deren Spezifikation den Wert allow_single_file=True enthält. ctx.executable verhält sich genauso wie ctx.file, enthält jedoch nur Felder für Abhängigkeitsattribute, deren Spezifikation executable=True festgelegt hat.

Ausgaben deklarieren

Während der Analyse kann die Implementierungsfunktion einer Regel Ausgaben erstellen. Da alle Labels während der Ladephase bekannt sein müssen, haben diese zusätzlichen Ausgaben keine Labels. File-Objekte für Ausgaben können mit ctx.actions.declare_file und ctx.actions.declare_directory erstellt werden. Oft basieren die Namen der Ausgaben auf dem Namen des Ziels, ctx.label.name:

def _example_library_impl(ctx):
  ...
  output_file = ctx.actions.declare_file(ctx.label.name + ".output")
  ...

Fürdeklarierte Ausgaben , wie die zum Erstellen vonAusgabeattribute .File können stattdessen aus den entsprechenden Feldern vonctx.outputs aus.

Aktionen

Eine Aktion beschreibt, wie eine Reihe von Ausgaben aus einer Reihe von Eingaben generiert wird, z. B. "run gcc on hello.c and get hello.o". Wenn eine Aktion erstellt wird, führt Bazel den Befehl nicht sofort aus. Sie registriert sich in einem Diagramm von Abhängigkeiten, da eine Aktion von der Ausgabe einer anderen Aktion abhängen kann. In C muss die Verknüpfung beispielsweise nach dem Compiler aufgerufen werden.

Allgemeine Funktionen, die Aktionen erstellen, sind in ctx.actions definiert:

Mit ctx.actions.args können die Argumente für Aktionen effizient angesammelt werden. Es wird dabei verhindert, dass Duplikate bis zur Ausführungszeit reduziert werden:

def _example_library_impl(ctx):
    ...

    transitive_headers = [dep[ExampleInfo].headers for dep in ctx.attr.deps]
    headers = depset(ctx.files.hdrs, transitive=transitive_headers)
    srcs = ctx.files.srcs
    inputs = depset(srcs, transitive=[headers])
    output_file = ctx.actions.declare_file(ctx.label.name + ".output")

    args = ctx.actions.args()
    args.add_joined("-h", headers, join_with=",")
    args.add_joined("-s", srcs, join_with=",")
    args.add("-o", output_file)

    ctx.actions.run(
        mnemonic = "ExampleCompile",
        executable = ctx.executable._compiler,
        arguments = [args],
        inputs = inputs,
        outputs = [output_file],
    )
    ...

Aktionen übernehmen eine Liste oder Depset der Eingabedateien und generieren eine (nicht leere) Liste von Ausgabedateien. Die Gruppe von Eingabe- und Ausgabedateien muss in der Analysephase bekannt sein. Dies kann vom Wert von Attributen abhängen, einschließlich Anbietern von Abhängigkeiten, kann jedoch nicht vom Ergebnis der Ausführung abhängen. Wenn Sie mit der Aktion beispielsweise den Befehl zum Entpacken ausführen, müssen Sie angeben, welche Dateien vor dem Ausführen der ZIP-Datei erhöht werden sollen. Aktionen, die intern eine variable Anzahl von Dateien erstellen, können diese in einer einzigen Datei zusammenfassen, z. B. als ZIP-, TAR- oder anderes Archivformat.

Aktionen müssen alle Eingaben enthalten. Das Auflisten von Eingaben, die nicht verwendet werden, ist zulässig, aber ineffizient.

Aktionen müssen alle ihre Ausgaben erstellen. Sie können andere Dateien schreiben, aber alles, was nicht in den Ausgaben enthalten ist, steht den Nutzern nicht zur Verfügung. Alle deklarierten Ausgaben müssen von einer Aktion geschrieben werden.

Aktionen sind mit reinen Funktionen vergleichbar: Sie sollten nur von den bereitgestellten Eingaben abhängen und dürfen nicht auf Computerinformationen, Nutzernamen, Uhren, Netzwerke oder E/A-Geräte zugreifen (mit Ausnahme von Lese- und Schreibvorgängen). Dies ist wichtig, da die Ausgabe im Cache gespeichert und wiederverwendet wird.

Abhängigkeiten werden von Bazel aufgelöst und entscheiden, welche Aktionen ausgeführt werden. Es ist ein Fehler aufgetreten, wenn in der Abhängigkeitsgrafik ein Zyklus vorhanden ist. Das Erstellen einer Aktion garantiert nicht, dass sie ausgeführt wird. Das hängt davon ab, ob ihre Ausgaben für den Build benötigt werden.

Anbieter

Anbieter sind Informationen, die eine Regel für andere Regeln freigibt. Diese Daten können Ausgabedateien, Bibliotheken, Parameter zur Übergabe an die Befehlszeile eines Tools oder alles andere enthalten, was ein Zielnutzer wissen sollte.

Da die Implementierungsfunktion einer Regel nur Anbieter aus den unmittelbaren Abhängigkeiten des instanziierten Ziels lesen kann, müssen Regeln alle Informationen von den Abhängigkeiten eines Ziels weiterleiten, die den Nutzern eines Ziels bekannt sein müssen. In der Regel werden diese Ein depset.

Die Anbieter eines Ziels werden durch eine Liste von Provider-Objekten angegeben, die von der Implementierungsfunktion zurückgegeben werden.

Alte Implementierungsfunktionen können auch in einem Legacy-Stil geschrieben werden, wobei die Implementierungsfunktion ein struct anstelle einer Liste mit Anbieterobjekten zurückgibt. Wir empfehlen, diesen Stil nicht zu verwenden und die Regeln sollten von ihm migriert werden.

Standardausgaben

Die Standardausgaben eines Ziels sind die Ausgaben, die standardmäßig angefordert werden, wenn das Ziel in der Befehlszeile für den Build angefordert wird. Beispiel: Das java_library-Ziel //pkg:foo hat foo.jar als Standardausgabe. Diese wird mit dem Befehl bazel build //pkg:foo erstellt.

Standardausgaben werden durch den Parameter files von DefaultInfo angegeben:

def _example_library_impl(ctx):
    ...
    return [
        DefaultInfo(files = depset([output_file]), ...),
        ...
    ]

Wenn DefaultInfo von einer Regelimplementierung nicht zurückgegeben wird oder der Parameter files nicht angegeben ist, werden in DefaultInfo.files standardmäßig alle vorgegebenen Ausgaben verwendet (im Allgemeinen diejenigen, die erstellt wurden). nach Ausgabeattributen).

Regeln zum Ausführen von Aktionen sollten Standardausgaben liefern, auch wenn diese Ausgaben nicht direkt verwendet werden sollten. Aktionen, die nicht im Diagramm der angeforderten Ausgaben enthalten sind, werden bereinigt. Wenn eine Ausgabe nur von den Nutzern eines Ziels verwendet wird, werden diese Aktionen nicht ausgeführt, wenn das Ziel isoliert erstellt wird. Dies erschwert die Fehlerbehebung, da eine Neuerstellung nur des fehlgeschlagenen Ziels nicht zum Ausfall führt.

Runfiles

Runfiles sind eine Reihe von Dateien, die zur Laufzeit von einem Ziel verwendet werden (im Gegensatz zur Build-Zeit). Während der Ausführungsphase erstellt Bazel einen Verzeichnisbaum, der Symlinks enthält, die auf die Runfiles verweisen. Dadurch wird die Umgebung für die Binärdatei bereitgestellt, damit sie während der Laufzeit auf die Runfiles zugreifen kann.

Runfiles können bei der Regelerstellung manuell hinzugefügt werden. runfiles-Objekte können von der Methode runfiles im Regelkontext ctx.runfiles erstellt und an die runfiles-Parameter im DefaultInfo. Die ausführbare Ausgabe von ausführbaren Regeln wird implizit den Runfiles hinzugefügt.

Einige Regeln geben Attribute an, die im Allgemeinen data genannt werden und deren Ausgaben den Runfiles eines Ziels hinzugefügt werden. Runfiles sollten auch über den folgenden Befehl zusammengeführt werden:data sowie Attribute, die Code für eine spätere Ausführung liefern können.srcs (kann Folgendes enthalten:filegroup Ziele mit verknüpftendata ) und deps aus.

def _example_library_impl(ctx):
    ...
    runfiles = ctx.runfiles(files = ctx.files.data)
    transitive_runfiles = []
    for runfiles_attr in (
        ctx.attr.srcs,
        ctx.attr.hdrs,
        ctx.attr.deps,
        ctx.attr.data,
    ):
        for target in runfiles_attr:
            transitive_runfiles.append(target[DefaultInfo].default_runfiles)
    runfiles = runfiles.merge_all(transitive_runfiles)
    return [
        DefaultInfo(..., runfiles = runfiles),
        ...
    ]

Benutzerdefinierte Anbieter

Anbieter können mit der Funktion provider definiert werden, um regelspezifische Informationen bereitzustellen:

ExampleInfo = provider(
    "Info needed to compile/link Example code.",
    fields={
        "headers": "depset of header Files from transitive dependencies.",
        "files_to_link": "depset of Files from compilation.",
    })

Mit Funktionen zur Regelimplementierung können dann Anbieterinstanzen erstellt und zurückgegeben werden:

def _example_library_impl(ctx):
  ...
  return [
      ...
      ExampleInfo(
          headers = headers,
          files_to_link = depset(
              [output_file],
              transitive = [
                  dep[ExampleInfo].files_to_link for dep in ctx.attr.deps
              ],
          ),
      )
  ]
Benutzerdefinierte Initialisierung von Anbietern

Es ist möglich, die Instanziierung eines Anbieters mit benutzerdefinierter Vorverarbeitung und Validierungslogik zu schützen. Dadurch können Sie dafür sorgen, dass alle Anbieterinstanzen bestimmte Invarianten befolgt oder Nutzern eine saubere API zum Abrufen einer Instanz zur Verfügung gestellt wird.

Dazu wird ein init-Callback an die Funktion provider übergeben. Wenn dieser Callback angegeben ist, ändert sich der Rückgabetyp von provider() in ein Tupel von zwei Werten: das Anbietersymbol, das der normale Rückgabewert ist, wenn init nicht verwendet wird, und ein " Rohkonstruktor".

In diesem Fall leitet das Anbietersymbol statt direkt eine neue Instanz zurück, sondern die Argumente an den init-Callback weiter. Der Rückgabewert des Callbacks muss ein dict sein, das Feldnamen (Strings) den Werten zuordnet. Damit werden die Felder der neuen Instanz initialisiert. Der Callback kann eine beliebige Signatur haben. Wenn die Argumente nicht mit der Signatur übereinstimmen, wird ein Fehler gemeldet, als würde er direkt aufgerufen.

Der Rohkonstruktor wird im Gegensatz dazu den Callback init umgehen.

Im folgenden Beispiel wird init zur Vorverarbeitung und Validierung der Argumente verwendet:

# //pkg:exampleinfo.bzl

_core_headers = [...]  # private constant representing standard library files

# It's possible to define an init accepting positional arguments, but
# keyword-only arguments are preferred.
def _exampleinfo_init(*, files_to_link, headers = None, allow_empty_files_to_link = False):
    if not files_to_link and not allow_empty_files_to_link:
        fail("files_to_link may not be empty")
    all_headers = depset(_core_headers, transitive = headers)
    return {'files_to_link': files_to_link, 'headers': all_headers}

ExampleInfo, _new_exampleinfo = provider(
    ...
    init = _exampleinfo_init)

export ExampleInfo

Eine Regelimplementierung kann dann den Anbieter so instanziieren:

    ExampleInfo(
        files_to_link=my_files_to_link,  # may not be empty
        headers = my_headers,  # will automatically include the core headers
    )

Mit dem Rohkonstruktor können alternative öffentliche Factory-Funktionen definiert werden, die nicht die init-Logik durchlaufen. In infobeispielinfo.bzl“ könnte beispielsweise Folgendes definiert werden:

def make_barebones_exampleinfo(headers):
    """Returns an ExampleInfo with no files_to_link and only the specified headers."""
    return _new_exampleinfo(files_to_link = depset(), headers = all_headers)

In der Regel ist der unbearbeitete Konstruktor an eine Variable gebunden, deren Name mit einem Unterstrich (_new_exampleinfo oben) beginnt, damit der Nutzercode sie nicht laden und keine beliebigen Anbieterinstanzen generieren kann.

Eine weitere Verwendung für init besteht darin, den Nutzer einfach daran zu hindern, das Anbietersymbol komplett aufzurufen, und die Verwendung einer Factory-Funktion zu erzwingen:

def _exampleinfo_init_banned(*args, **kwargs):
    fail("Do not call ExampleInfo(). Use make_exampleinfo() instead.")

ExampleInfo, _new_exampleinfo = provider(
    ...
    init = _exampleinfo_init_banned)

def make_exampleinfo(...):
    ...
    return _new_exampleinfo(...)

Ausführbare Regeln und Testregeln

Ausführbare Regeln definieren Ziele, die mit einem bazel run-Befehl aufgerufen werden können. Testregeln sind eine spezielle Art von ausführbarer Regel, deren Ziele auch mit dem Befehl bazel test aufgerufen werden können. Ausführbare und Testregeln werden erstellt, indem die entsprechendeexecutable odertest Argument fürTrue im Anruf anrule:

example_binary = rule(
   implementation = _example_binary_impl,
   executable = True,
   ...
)

example_test = rule(
   implementation = _example_binary_impl,
   test = True,
   ...
)

Testregeln müssen Namen haben, die auf _test enden. Testnamen für Ziele enden häufig auch mit _test, was aber nicht erforderlich ist. Nicht-Testregeln dürfen dieses Suffix nicht haben.

Beide Arten von Regeln müssen eine ausführbare Ausgabedatei erzeugen, die von den Befehlen run oder test aufgerufen werden kann. Diese Datei kann aber nicht angegeben werden. Wenn Sie Bayern mitteilen möchten, welche Ausgabe einer Regel als ausführbare Datei verwendet werden soll, übergeben Sie sie als executable-Argument eines zurückgegebenen DefaultInfo-Anbieters. Dieser executable wird den Standardausgaben der Regel hinzugefügt, sodass Sie ihn nicht sowohl an executable als auch an files übergeben müssen. Sie wird auch implizit den Runfiles hinzugefügt:

def _example_binary_impl(ctx):
    executable = ctx.actions.declare_file(ctx.label.name)
    ...
    return [
        DefaultInfo(executable = executable, ...),
        ...
    ]

Die Aktion, die diese Datei generiert, muss das ausführbare Bit für die Datei festlegen. Bei einer ctx.actions.run- oder ctx.actions.run_shell-Aktion sollte dies über das zugrunde liegende Tool erfolgen, das von der Aktion aufgerufen wird. Für eine ctx.actions.write-Aktion übergeben Sie is_executable=True.

Als Legacy-Verhalten haben ausführbare Regeln eine spezielle ctx.outputs.executable deklarierte Ausgabe. Diese Datei dient als standardmäßige ausführbare Datei, wenn Sie mit DefaultInfo keine Datei angeben. Andernfalls darf er nicht verwendet werden. Dieser Ausgabemechanismus ist veraltet, da er den Namen der ausführbaren Datei zum Zeitpunkt der Analyse nicht unterstützt.

Beispiele für ausführbare Regeln und Testregeln finden Sie hier.

Ausführbare Regeln und Testregeln haben zusätzlich zu den für alle Regeln hinzugefügten zusätzlichen Attributen implizit definiert. Die Standardeinstellungen der implizit hinzugefügten Attribute können nicht geändert werden. Sie können das Problem aber umgehen, indem Sie eine private Regel in ein Starlark-Makro einbinden, das den Standardwert ändert:

def example_test(size="small", **kwargs):
  _example_test(size=size, **kwargs)

_example_test = rule(
 ...
)

Speicherort von Runfiles

Wenn ein ausführbares Ziel mit bazel run (oder test) ausgeführt wird, befindet sich das Stammverzeichnis des Runfiles-Verzeichnisses neben der ausführbaren Datei. Die Pfade beziehen sich so:

# Given executable_file and runfile_file:
runfiles_root = executable_file.path + ".runfiles"
workspace_name = ctx.workspace_name
runfile_path = runfile_file.short_path
execution_root_relative_path = "%s/%s/%s" % (
    runfiles_root, workspace_name, runfile_path)

Der Pfad zu einer File im Runfiles-Verzeichnis entspricht File.short_path.

Die direkt von bazel ausgeführte Binärdatei befindet sich neben dem Stammverzeichnis des Verzeichnisses runfiles. Mit Binärdateien, die von den Runfiles aufgerufen werden, kann jedoch nicht dieselbe Annahme getroffen werden. Um dies zu vermeiden, sollte jede Binärdatei eine Möglichkeit bieten, den Runfiles-Stamm als Parameter mithilfe einer Umgebung oder eines Befehlszeilenarguments/Flags zu akzeptieren. Dadurch können Binärdateien den richtigen kanonischen Runfiles-Stamm an die aufgerufenen Binärdateien übergeben. Wenn dies nicht festgelegt ist, kann eine Binärdatei feststellen, dass es die erste Binärdatei war, und nach einem angrenzenden Runfiles-Verzeichnis suchen.

Weiterführende Informationen

Ausgabedateien anfordern

Ein einzelnes Ziel kann mehrere Ausgabedateien haben. Wenn ein bazel build-Befehl ausgeführt wird, werden einige Ausgaben der Ziele, die dem Befehl zugewiesen wurden, als angefordert betrachtet. Bazel erstellt nur diese angeforderten Dateien und die Dateien, von denen sie direkt oder indirekt abhängig sind. In Bezug auf das Aktionsdiagramm führt Bazel nur die Aktionen aus, die als transitive Abhängigkeiten der angeforderten Dateien erreichbar sind.

Zusätzlich zu den Standardausgaben kann jede angegebene Ausgabe explizit in der Befehlszeile angefordert werden. Regeln können deklarierte Ausgaben über Ausgabeattribute angeben. In diesem Fall wählt der Nutzer beim Instanziieren der Regel explizit Labels für Ausgaben aus. Verwenden Sie das entsprechende Attribut von ctx.outputs, um File-Objekte für Ausgabeattribute abzurufen. Mithilfe von Regeln können Sie vorgegebene Ausgaben auch implizit anhand des Zielnamens definieren. Dieses Feature wurde jedoch eingestellt.

Zusätzlich zu den Standardausgaben gibt es Ausgabegruppen, d. h. Sammlungen von Ausgabedateien, die zusammen angefordert werden können. Diese können mit --output_groups angefordert werden. Wenn ein Ziel-//pkg:mytarget beispielsweise einen Regeltyp mit einer Ausgabegruppe debug_files hat, können diese Dateien durch Ausführen von bazel build //pkg:mytarget --output_groups=debug_files erstellt werden. Da nicht angegebene Ausgaben keine Labels haben, können sie nur angefordert werden, indem sie in den Standardausgaben oder einer Ausgabegruppe erscheinen.

Ausgabegruppen können mit dem OutputGroupInfo-Anbieter angegeben werden. Beachten Sie, dass im Gegensatz zu vielen integrierten Anbietern OutputGroupInfo Parameter mit beliebigen Namen verwenden kann, um Ausgabegruppen mit diesem Namen zu definieren:

def _example_library_impl(ctx):
    ...
    debug_file = ctx.actions.declare_file(name + ".pdb")
    ...
    return [
        DefaultInfo(files = depset([output_file]), ...),
        OutputGroupInfo(
            debug_files = depset([debug_file]),
            all_files = depset([output_file, debug_file]),
        ),
        ...
    ]

Außerdem kann OutputGroupInfo im Gegensatz zu den meisten Anbietern sowohl von einem aspekt als auch von dem Regelziel, auf das dieser Aspekt angewendet wird, zurückgegeben werden, sofern sie nicht dieselbe Ausgabe definieren Gruppen. In diesem Fall werden die resultierenden Anbieter zusammengeführt.

Beachten Sie, dass OutputGroupInfo im Allgemeinen nicht verwendet werden sollte, um bestimmte Arten von Dateien von einem Ziel an die Aktionen seiner Nutzer zu senden. Definieren Sie stattdessen regelspezifische Anbieter.

Konfigurationen

Angenommen, Sie möchten eine C++-Binärdatei für eine andere Architektur erstellen. Der Build kann komplex sein und mehrere Schritte umfassen. Einige Zwischen-Binärprogramme wie Compiler und Code-Generatoren müssen auf der Ausführungsplattform ausgeführt werden, die Ihr Host oder ein Remote-Executor sein kann. Einige Binärdateien wie die endgültige Ausgabe müssen für die Zielarchitektur erstellt werden.

Aus diesem Grund hat Bazel das Konzept "Konfigurationen" und Übergänge. Die obersten Ziele (die in der Befehlszeile angefordert werden) werden in der "Ziel"-Konfiguration erstellt, während die auf der Ausführungsplattform auszuführenden Tools in einer "Executive"-Konfiguration erstellt werden. Regeln können verschiedene Aktionen basierend auf der Konfiguration generieren, z. B. um die CPU-Architektur zu ändern, die an den Compiler übergeben wird. In einigen Fällen kann für verschiedene Konfigurationen dieselbe Bibliothek erforderlich sein. In diesem Fall wird es analysiert und möglicherweise mehrmals erstellt.

Standardmäßig erstellt Bazel die Abhängigkeiten eines Ziels in derselben Konfiguration wie das Ziel selbst, mit anderen Worten, ohne Übergänge. Wenn eine Abhängigkeit ein Tool ist, das zum Erstellen des Ziels benötigt wird, sollte für das entsprechende Attribut ein Übergang zu einer Ausführungskonfiguration angegeben werden. Dadurch werden das Tool und alle zugehörigen Abhängigkeiten für die Ausführungsplattform erstellt.

Mit cfg können Sie für jedes Abhängigkeitsattribut entscheiden, ob Abhängigkeiten in derselben Konfiguration erstellt werden oder zu einer ausführbaren Konfiguration wechseln sollen. Wenn ein Abhängigkeitsattribut das Flag executable=True hat, muss cfg explizit festgelegt werden. Dies soll verhindern, dass versehentlich ein Tool für die falsche Konfiguration erstellt wird. Beispiel

Im Allgemeinen können Quellen, abhängige Bibliotheken und ausführbare Dateien, die zur Laufzeit benötigt werden, dieselbe Konfiguration verwenden.

Tools, die als Teil des Builds ausgeführt werden (z. B. Compiler oder Code-Generatoren), sollten für eine ausführbare Konfiguration erstellt werden. Geben Sie in diesem Fall cfg="exec" im Attribut an.

Andernfalls sollten ausführbare Dateien, die zur Laufzeit verwendet werden (z. B. als Teil eines Tests), für die Zielkonfiguration erstellt werden. Geben Sie in diesem Fall cfg="target" im Attribut an.

Mit cfg="target" wird gar nichts unternommen: Es handelt sich lediglich um einen praktischen Wert, um Regeldesignern die Absicht zu verdeutlichen. Wenn executable=False (was bedeutet, cfg ist optional) sollte nur festgelegt werden, wenn dies wirklich zur Lesbarkeit beiträgt.

Sie können auchcfg=my_transition verwendenbenutzerdefinierte Übergänge , die Regelautoren ein hohes Maß an Flexibilität beim Ändern von Konfigurationen bietet. Dabei werden allerdingswodurch die Build-Grafik größer und weniger verständlich wird aus.

Hinweis: In der Vergangenheit hatte Bazel kein Konzept von Ausführungsplattformen, daher wurden alle Build-Aktionen auf dem Hostcomputer ausgeführt. Aus diesem Grund gibt es eine einzelne "Host"-Konfiguration und einen "Host"-Übergang, der zum Erstellen einer Abhängigkeit in der Hostkonfiguration verwendet werden kann. Viele Regeln verwenden für ihre Tools weiterhin die Umstellung hostHost“, aber diese wird derzeit eingestellt und nach Möglichkeit auf execExec“-Übergänge umgestellt.

Zwischen den Konfigurationen hosthost“ und execexec“ gibt es zahlreiche Unterschiede:

  • "host" ist Terminal, "exec" ist nicht: Wenn sich eine Abhängigkeit in der "host"-Konfiguration befindet, sind keine weiteren Übergänge zulässig. Sie können weitere Konfigurationsübergänge vornehmen, sobald Sie sich in einer "Executive"-Konfiguration befinden.
  • "host" ist monolithisch, "exec" ist nicht: Es gibt nur eine "host"-Konfiguration, aber für jede Ausführungsplattform kann eine andere "exec"-Konfiguration vorhanden sein.
  • "host" setzt voraus, dass Sie Tools auf demselben Computer wie Bazel oder auf einem wesentlich ähnlichen Computer ausführen. Dies gilt nicht mehr: Sie können Build-Aktionen auf Ihrem lokalen Computer oder auf einem Remote-Executor ausführen und es kann nicht garantiert werden, dass der Remote-Executor die gleiche CPU und das gleiche Betriebssystem wie Ihr lokaler Computer hat.

Für die Konfigurationen execexec“ und hosthost“ werden die gleichen Optionsänderungen angewendet, z. B. --compilation_mode von --host_compilation_mode, --cpu von --host_cpu usw. aus. Der Unterschied besteht darin, dass die Konfiguration "Host" mit den Standardwerten aller anderen Flags beginnt, während die Konfiguration "Executive" mit den aktuellen Werten beginnt. von Flags, basierend auf der Zielkonfiguration.

Konfigurationsfragmente

Regeln können auf Konfigurationsfragmente wie cpp, java und jvm zugreifen. Alle erforderlichen Fragmente müssen jedoch deklariert werden, um Zugriffsfehler zu vermeiden:

def _impl(ctx):
    # Using ctx.fragments.cpp leads to an error since it was not declared.
    x = ctx.fragments.java
    ...

my_rule = rule(
    implementation = _impl,
    fragments = ["java"],      # Required fragments of the target configuration
    host_fragments = ["java"], # Required fragments of the host configuration
    ...
)

ctx.fragments stellt nur Konfigurationsfragmente für die Zielkonfiguration bereit. Wenn du auf Fragmente in der Hostkonfiguration zugreifen möchtest, verwende stattdessen ctx.host_fragments.

Normalerweise ist der relative Pfad einer Datei im Runfiles-Baum mit dem relativen Pfad dieser Datei in der Quellstruktur oder dem generierten Ausgabebaum identisch. Wenn diese aus irgendeinem Grund abweichen müssen, können Sie die Argumente root_symlinks oder symlinks angeben. root_symlinks ist ein Wörterbuchzuordnungspfad zu Dateien, wobei die Pfade sich auf das Stammverzeichnis des Runfiles-Verzeichnisses beziehen. Das Wörterbuch symlinks ist identisch, die Pfade sind jedoch implizit mit dem Namen des Arbeitsbereichs versehen.

    ...
    runfiles = ctx.runfiles(
        root_symlinks = {"some/path/here.foo": ctx.file.some_data_file2}
        symlinks = {"some/path/here.bar": ctx.file.some_data_file3}
    )
    # Creates something like:
    # sometarget.runfiles/
    #     some/
    #         path/
    #             here.foo -> some_data_file2
    #     <workspace_name>/
    #         some/
    #             path/
    #                 here.bar -> some_data_file3

Wenn symlinks oder root_symlinks verwendet wird, dürfen Sie nicht zwei verschiedene Dateien demselben Pfad im Runfiles-Baum zuordnen. Dies führt dazu, dass der Build mit einem Fehler fehlschlägt, der den Konflikt beschreibt. Um die Kollision zu entfernen, musst du deine ctx.runfiles-Argumente ändern. Diese Prüfung wird für alle Ziele durchgeführt, die Ihre Regel verwenden, sowie für alle Ziele jeglicher Art, die von diesen Zielen abhängen. Dies ist besonders riskant, wenn Ihr Tool wahrscheinlich transitiv von einem anderen Tool verwendet wird. Symlink-Namen dürfen in den Runfiles eines Tools und allen Abhängigkeiten nur einmal vorkommen.

Codeabdeckung

Wenn der Befehl coverage ausgeführt wird, muss der Build für bestimmte Ziele möglicherweise eine Instrumentierung der Abdeckung hinzufügen. Der Build erfasst außerdem die Liste der instrumentierten Quelldateien. Die Teilmenge der als Ziel angesehenen Ziele wird vom Flag --instrumentation_filter gesteuert. Testziele werden ausgeschlossen, sofern nicht --instrument_test_targets angegeben ist.

Wenn durch eine Regelimplementierung zum Zeitpunkt der Erstellung eine Instrumentierung für die Abdeckung hinzugefügt wird, muss dies in der Implementierungsfunktion berücksichtigt werden. ctx.coverage_instrumented gibt im Deckungsmodus "true" zurück, wenn die Quellen eines Ziels zulässig sind. muss instrumentiert werden:

# Are this rule's sources instrumented?
if ctx.coverage_instrumented():
  # Do something to turn on coverage for this compile action

Logik, die immer im Abdeckungsmodus aktiviert sein muss (ob die Quellen eines Ziels instrumentiert sind), kann mit ctx.configuration.coverage_enabled konditioniert werden.

Wenn die Regel vor der Kompilierung direkt Quellen aus den Abhängigkeiten enthält (z. B. Header-Dateien), muss möglicherweise auch die Instrumentierung zur Kompilierungszeit aktiviert werden, wenn die Quellen der Abhängigkeiten instrumentiert werden sollen:

# Are this rule's sources or any of the sources for its direct dependencies
# in deps instrumented?
if (ctx.configuration.coverage_enabled and
    (ctx.coverage_instrumented() or
     any([ctx.coverage_instrumented(dep) for dep in ctx.attr.deps]))):
    # Do something to turn on coverage for this compile action

Regeln sollten außerdem Informationen darüber liefern, welche Attribute für die Abdeckung mit dem InstrumentedFilesInfo-Anbieter relevant sind, die mit coverage_common.instrumented_files_info erstellt wurden. Der Parameter dependency_attributes von instrumented_files_info sollte alle Attribute für die Laufzeitabhängigkeit auflisten, einschließlich Codeabhängigkeiten wie deps und Datenabhängigkeiten wie data. Der Parameter source_attributes sollte die Quelldateiattribute der Regel auflisten, wenn eine Instrumentierung für die Abdeckung hinzugefügt werden kann:

def _example_library_impl(ctx):
    ...
    return [
        ...
        coverage_common.instrumented_files_info(
            ctx,
            dependency_attributes = ["deps", "data"],
            # Omitted if coverage is not supported for this rule:
            source_attributes = ["srcs", "hdrs"],
        )
        ...
    ]

Wenn InstrumentedFilesInfo nicht zurückgegeben wird, wird für jedes Abhängigkeitsattribut, das nicht festgelegt ist, ein Standardattribut erstellt, für das cfg nicht festgelegt ist. in "host" oder "exec" im Attributschema) in dependency_attributes. (Das ist kein ideales Verhalten, weil es Attribute wiesrcs independency_attributes stattsource_attributes, allerdings ist dann für alle Regeln in der Abhängigkeitskette keine Explicit-Abdeckungskonfiguration erforderlich.

Validierungsaktionen

Manchmal müssen Sie etwas über den Build validieren, und die für diese Validierung erforderlichen Informationen sind nur in Artefakten (Quelldateien oder generierten Dateien) verfügbar. Da sich diese Informationen in Artefakten befinden, können Regeln diese Validierung bei der Analyse nicht ausführen, da Regeln keine Dateien lesen können. Stattdessen müssen Aktionen diese Validierung zum Ausführungszeitpunkt durchführen. Wenn die Validierung fehlschlägt, schlägt die Aktion fehl und damit auch der Build.

Beispiele für Validierungen sind statische Analysen, Linting, Konsistenz- und Konsistenzprüfungen und Stilprüfungen.

Validierungsaktionen können auch zur Verbesserung der Build-Leistung beitragen, indem Teile von Aktionen, die nicht zum Erstellen von Artefakten erforderlich sind, in separate Aktionen verschoben werden. Wenn beispielsweise eine einzelne Aktion, die kompiliert und Linting durchführt, in eine Kompilierungs- und eine Linting-Aktion aufgeteilt werden kann, kann die Linting-Aktion als Validierungsaktion ausgeführt und parallel zu anderen Aktionen ausgeführt werden.

Diese "Validierungsaktionen" erzeugen häufig nichts, das an anderer Stelle im Build verwendet wird, da sie nur Informationen zu ihren Eingaben bestätigen müssen. Dies stellt jedoch ein Problem dar: Wie erzeugt eine Regel eine Aktion, die an anderer Stelle im Build verwendet wird, wenn nicht eine Validierungsaktion erzeugt wird? In der Vergangenheit sollte die Validierungsaktion eine leere Datei ausgeben und diese Ausgabe dann den Eingaben anderer wichtiger Aktionen im Build künstlich hinzufügen:

Dies funktioniert, da Bazel die Validierungsaktion immer ausführt, wenn die Kompilierungsaktion ausgeführt wird. Dies hat jedoch große Nachteile:

  1. Die Validierungsaktion befindet sich im kritischen Pfad des Builds. Da Bazel davon ausgeht, dass die leere Ausgabe zum Ausführen der Kompilierungsaktion erforderlich ist, wird die Validierungsaktion zuerst ausgeführt. Die Kompilierungsaktion wird dann ignoriert. Dies reduziert die Parallelität und verlangsamt Builds.

  2. Wenn anstelle der Kompilierungsaktion möglicherweise andere Aktionen im Build ausgeführt werden, müssen den Aktionen die leeren Ausgaben von Validierungsaktionen hinzugefügt werden (z. B. die JAR-Ausgabe der Quell-API von java_library). aus. Dies ist auch ein Problem, wenn später neue Aktionen hinzugefügt werden, die anstelle der Kompilierungsaktion ausgeführt werden könnten. Die leere Validierungsausgabe wird versehentlich ausgelassen.

Die Lösung für diese Probleme ist die Verwendung der Validierungsausgabegruppe.

Validierungsausgabegruppe

Die Validierungsausgabegruppe ist eine Ausgabegruppe, die für die ansonsten nicht verwendeten Ausgaben von Validierungsaktionen vorgesehen ist, sodass sie den Eingaben anderer Aktionen nicht künstlich hinzugefügt werden müssen.

Das Besondere an dieser Gruppe ist, dass ihre Ausgaben immer angefordert werden, unabhängig vom Wert des Flags --output_groups und unabhängig davon, wie das Ziel davon abhängt (z. B. in der Befehlszeile als Abhängigkeit oder durch implizite Ausgaben des Ziels. Das normale Caching und die inkrementelle Steigerung gelten weiterhin: Wenn sich die Eingaben für die Validierungsaktion nicht geändert haben und die Validierungsaktion zuvor erfolgreich war, wird die Validierungsaktion nicht ausgeführt.

Die Verwendung dieser Ausgabegruppe erfordert weiterhin, dass Validierungsaktionen eine Datei ausgeben, auch eine leere. Dies kann die Zusammenstellung von Tools erfordern, die normalerweise keine Ausgaben erstellen, damit eine Datei erstellt wird.

Die Validierungsaktionen eines Ziels werden in drei Fällen nicht ausgeführt:

  • Wann das Ziel als Tool erforderlich ist
  • Wenn das Ziel von einer impliziten Abhängigkeit abhängt (z. B. ein Attribut, das mit __“ beginnt)
  • Wenn das Ziel in der Host- oder Ausführungskonfiguration erstellt wurde.

Es wird davon ausgegangen, dass diese Ziele eigene separate Builds und Tests haben, die Validierungsfehler aufdecken würden.

Ausgabegruppe "Validierungen" verwenden

Die Ausgabegruppe der Validierungen heißt _validation und wird wie jede andere Ausgabegruppe verwendet:

def _rule_with_validation_impl(ctx):

  ctx.actions.write(ctx.outputs.main, "main output\n")

  ctx.actions.write(ctx.outputs.implicit, "implicit output\n")

  validation_output = ctx.actions.declare_file(ctx.attr.name + ".validation")
  ctx.actions.run(
      outputs = [validation_output],
      executable = ctx.executable._validation_tool,
      arguments = [validation_output.path])

  return [
    DefaultInfo(files = depset([ctx.outputs.main])),
    OutputGroupInfo(_validation = depset([validation_output])),
  ]


rule_with_validation = rule(
  implementation = _rule_with_validation_impl,
  outputs = {
    "main": "%{name}.main",
    "implicit": "%{name}.implicit",
  },
  attrs = {
    "_validation_tool": attr.label(
        default = Label("//validation_actions:validation_tool"),
        executable = True,
        cfg = "exec"),
  }
)

Beachten Sie, dass die Validierungsausgabedatei nicht der DefaultInfo hinzugefügt wird oder die Eingaben einer anderen Aktion. Die Validierungsaktion für ein Ziel dieser Regelart wird weiterhin ausgeführt, wenn das Ziel durch ein Label oder eine der impliziten Ausgaben des Ziels direkt oder indirekt davon abhängig ist.

Normalerweise ist es wichtig, dass die Ausgaben von Validierungsaktionen nur in die Validierungsausgabegruppe fallen und nicht den Eingaben anderer Aktionen hinzugefügt werden, da dies die Parallelverarbeitung beeinträchtigen kann. In Bazel gibt es derzeit keine spezielle Überprüfung, um dies zu erzwingen. Daher sollten Sie testen, ob die Ausgaben der Validierungsaktion den Eingaben jeglicher Aktionen in den Tests für Starlark-Regeln nicht hinzugefügt werden. Beispiel:

load("@bazel_skylib//lib:unittest.bzl", "analysistest")

def _validation_outputs_test_impl(ctx):
  env = analysistest.begin(ctx)

  actions = analysistest.target_actions(env)
  target = analysistest.target_under_test(env)
  validation_outputs = target.output_groups._validation.to_list()
  for action in actions:
    for validation_output in validation_outputs:
      if validation_output in action.inputs.to_list():
        analysistest.fail(env,
            "%s is a validation action output, but is an input to action %s" % (
                validation_output, action))

  return analysistest.end(env)

validation_outputs_test = analysistest.make(_validation_outputs_test_impl)

Flag für Validierungsaktionen

Das Ausführen von Validierungsaktionen wird durch das Befehlszeilen-Flag --run_validations gesteuert, das standardmäßig den Wert "true" hat.

Eingestellte Funktionen

Eingestellte bereits deklarierte Ausgabe

Es gibt zwei eingestellte Möglichkeiten, deklarierte Ausgaben zu verwenden:

  • Der Parameter outputs von rule gibt eine Zuordnung zwischen den Namen der Ausgabeattribute und Stringvorlagen zur Generierung von Ausgabelabels an. Verwenden Sie lieber nicht deklarierte Ausgaben und fügen Sie Ausgabeen explizit zu DefaultInfo.files hinzu. Verwenden Sie das Label des Regelziels als Eingabe für Regeln, die die Ausgabe anstelle eines Labels der vordefinierten Ausgabe verwenden.

  • Bei ausführbaren Regeln bezieht sich ctx.outputs.executable auf eine zuvor deklarierte ausführbare Ausgabe mit demselben Namen wie das Regelziel. Geben Sie die Ausgabe explizit an, z. B. mit ctx.actions.declare_file(ctx.label.name), und achten Sie darauf, dass der Befehl, der die ausführbare Datei generiert, die Berechtigungen zum Ausführen der Ausführung festlegt. Übergeben Sie explizit die ausführbare Ausgabe an den Parameter executable von DefaultInfo.

Zu vermeidende Runfiles-Features

ctx.runfiles und der Typ runfiles bieten einen komplexen Satz von Funktionen, von denen viele aus alten Gründen erhalten bleiben. Die folgenden Empfehlungen helfen, die Komplexität zu reduzieren:

  • Vermeiden Sie die Modi collect_data und collect_default von ctx.runfiles. Mit diesen Modi werden implizit Dateien über bestimmte hartcodierte Abhängigkeitskanten verwirrend erfasst. Fügen Sie stattdessen Dateien mit den Parametern files oder transitive_files von ctx.runfiles hinzu oder führen Sie Runfiles aus Abhängigkeiten mit runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles) zusammen.

  • Vermeiden Sie die Verwendung von data_runfiles und default_runfiles des Konstruktors DefaultInfo. Geben Sie stattdessen DefaultInfo(runfiles = ...) an. Die Unterscheidung zwischen "Standard"- und "Daten"-Runfiles wird aus alten Gründen beibehalten. Einige Regeln geben beispielsweise ihre Standardausgaben in data_runfiles an, aber nicht in default_runfiles. Stattdata_runfiles , Regeln solltenBeide Standardausgaben einschließen und zusammenführen default_runfiles von Attributen, die Runfiles bereitstellen (häufigdata ).

  • Wenn Sie runfiles aus DefaultInfo abrufen (im Allgemeinen nur zum Zusammenführen von Runfiles zwischen der aktuellen Regel und ihren Abhängigkeiten), verwenden Sie DefaultInfo.default_runfiles, nicht DefaultInfo.data_runfiles. aus.

Von alten Anbietern migrieren

In der Vergangenheit waren Bazel-Anbieter einfache Felder für das Objekt Target. Der Zugriff darauf erfolgte mit dem Punktoperator. Er wurde erstellt, indem das Feld in eine Struktur eingebettet wurde, die von der Implementierungsfunktion der Regel zurückgegeben wurde.

Dieser Stil wurde verworfen und sollte nicht in neuem Code verwendet werden. Weiter unten finden Sie Informationen, die Ihnen bei der Migration helfen können. Der neue Anbietermechanismus verhindert Namenskonflikte. Außerdem wird das Ausblenden von Daten unterstützt, da Code, der auf eine Anbieterinstanz zugreift, über das Anbietersymbol abgerufen wird.

Bisher werden alte Anbieter weiterhin unterstützt. Eine Regel kann so sowohl Legacy-Anbieter als auch moderne Anbieter zurückgeben:

def _old_rule_impl(ctx):
  ...
  legacy_data = struct(x="foo", ...)
  modern_data = MyInfo(y="bar", ...)
  # When any legacy providers are returned, the top-level returned value is a
  # struct.
  return struct(
      # One key = value entry for each legacy provider.
      legacy_info = legacy_data,
      ...
      # Additional modern providers:
      providers = [modern_data, ...])

Wenn dep das resultierende Target-Objekt für eine Instanz dieser Regel ist, können die Anbieter und deren Inhalte als dep.legacy_info.x und dep[MyInfo].y abgerufen werden.

Neben providers kann der zurückgegebene Struct auch mehrere andere Felder mit besonderer Bedeutung enthalten (und daher keinen entsprechenden Legacy-Anbieter erstellen):

  • Die Felder files, runfiles, data_runfiles, default_runfiles und executable entsprechen den Feldern mit denselben Namen: DefaultInfo. Es ist nicht zulässig, eines dieser Felder anzugeben und gleichzeitig einen DefaultInfo-Anbieter zurückzugeben.

  • Das Feld output_groups nimmt einen Strukturwert und entspricht einem OutputGroupInfo.

In den provides-Regeldeklarationen und in providers-Deklarationen der Abhängigkeitsattribute werden Legacy-Anbieter als Strings und moderne Anbieter übergeben. werden durch das Symbol *Info übergeben. Achten Sie darauf, bei der Migration von Strings in Symbole zu wechseln. Bei komplexen oder großen Regelsätzen, bei denen es schwierig ist, alle Regeln atomar zu aktualisieren, ist die Zeit möglicherweise einfacher, wenn Sie diese Abfolge von Schritten befolgen:

  1. Ändern Sie die Regeln, die den Legacy-Anbieter erzeugen, so, dass sowohl die Legacy-Anbieter als auch die modernen Anbieter erzeugt werden. Verwenden Sie dazu die obige Syntax. Aktualisieren Sie diese Deklaration bei Regeln, die den Legacy-Anbieter zurückgeben, so, dass sowohl der Legacy-Anbieter als auch der moderne Anbieter enthalten sind.

  2. Ändern Sie die Regeln, die den Legacy-Anbieter nutzen, so dass der moderne Anbieter verwendet wird. Wenn für eine Attributdeklaration der Legacy-Anbieter erforderlich ist, aktualisieren Sie diese so, dass stattdessen der moderne Anbieter erforderlich ist. Optional können Sie diese Arbeit mit Schritt 1 verschränken. Kunden müssen dafür entweder einen der Anbieter akzeptieren oder verlangen: Testen Sie mithilfe der hasattr(target, 'foo') oder den neuen Anbieter verwenden:FooInfo in target aus.

  3. Entfernen Sie vollständig den alten Anbieter aus allen Regeln.