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:
- Nehmen Sie eine Reihe von
.cpp
-Quelldateien (Eingaben). - Führen Sie
g++
für die Quelldateien (Aktion) aus. - Der
DefaultInfo
-Anbieter mit der ausführbaren Ausgabe und anderen Dateien zurückgeben, die zur Laufzeit verfügbar gemacht werden sollen - 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 separateshdrs
-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
stelltCcInfo
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:
ctx.actions.run
, um eine ausführbare Datei auszuführen.ctx.actions.run_shell
, um einen Shell-Befehl auszuführen.ctx.actions.write
, um einen String in eine Datei zu schreibenctx.actions.expand_template
, um eine Datei aus einer Vorlage zu generieren.
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
.
Symlinks für Runfiles
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:
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.
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
vonrule
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 zuDefaultInfo.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. mitctx.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 Parameterexecutable
vonDefaultInfo
.
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
undcollect_default
vonctx.runfiles
. Mit diesen Modi werden implizit Dateien über bestimmte hartcodierte Abhängigkeitskanten verwirrend erfasst. Fügen Sie stattdessen Dateien mit den Parameternfiles
odertransitive_files
vonctx.runfiles
hinzu oder führen Sie Runfiles aus Abhängigkeiten mitrunfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
zusammen.Vermeiden Sie die Verwendung von
data_runfiles
unddefault_runfiles
des KonstruktorsDefaultInfo
. Geben Sie stattdessenDefaultInfo(runfiles = ...)
an. Die Unterscheidung zwischen "Standard"- und "Daten"-Runfiles wird aus alten Gründen beibehalten. Einige Regeln geben beispielsweise ihre Standardausgaben indata_runfiles
an, aber nicht indefault_runfiles
. Stattdata_runfiles
, Regeln solltenBeide Standardausgaben einschließen und zusammenführendefault_runfiles
von Attributen, die Runfiles bereitstellen (häufigdata
).Wenn Sie
runfiles
ausDefaultInfo
abrufen (im Allgemeinen nur zum Zusammenführen von Runfiles zwischen der aktuellen Regel und ihren Abhängigkeiten), verwenden SieDefaultInfo.default_runfiles
, nichtDefaultInfo.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
undexecutable
entsprechen den Feldern mit denselben Namen:DefaultInfo
. Es ist nicht zulässig, eines dieser Felder anzugeben und gleichzeitig einenDefaultInfo
-Anbieter zurückzugeben.Das Feld
output_groups
nimmt einen Strukturwert und entspricht einemOutputGroupInfo
.
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:
Ä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.
Ä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.Entfernen Sie vollständig den alten Anbieter aus allen Regeln.