Makro

Laporkan masalah Lihat sumber Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

Halaman ini membahas dasar-dasar penggunaan makro dan mencakup kasus penggunaan umum, proses debug, dan konvensi.

Makro adalah fungsi yang dipanggil dari file BUILD yang dapat membuat instance aturan. Makro terutama digunakan untuk enkapsulasi dan penggunaan kembali kode aturan yang ada dan makro lainnya.

Makro terdiri dari dua jenis: makro simbolik, yang dijelaskan di halaman ini, dan makro lama. Jika memungkinkan, sebaiknya gunakan makro simbolik untuk kejelasan kode.

Makro simbolis menawarkan argumen yang diketik (konversi string ke label, relatif terhadap tempat makro dipanggil) dan kemampuan untuk membatasi serta menentukan visibilitas target yang dibuat. Aturan ini dirancang agar sesuai dengan evaluasi lambat (yang akan ditambahkan dalam rilis Bazel mendatang). Makro simbolis tersedia secara default di Bazel 8. Jika dokumen ini menyebutkan macros, berarti dokumen ini merujuk pada makro simbolis.

Contoh yang dapat dieksekusi dari makro simbolis dapat ditemukan di repositori contoh.

Penggunaan

Makro ditentukan dalam file .bzl dengan memanggil fungsi macro() dengan dua parameter yang diperlukan: attrs dan implementation.

Atribut

attrs menerima kamus nama atribut ke jenis atribut, yang merepresentasikan argumen ke makro. Dua atribut umum – name dan visibility – ditambahkan secara implisit ke semua makro dan tidak disertakan dalam kamus yang diteruskan ke attrs.

# macro/macro.bzl
my_macro = macro(
    attrs = {
        "deps": attr.label_list(mandatory = True, doc = "The dependencies passed to the inner cc_binary and cc_test targets"),
        "create_test": attr.bool(default = False, configurable = False, doc = "If true, creates a test target"),
    },
    implementation = _my_macro_impl,
)

Deklarasi jenis atribut menerima parameter, mandatory, default, dan doc. Sebagian besar jenis atribut juga menerima parameter configurable, yang menentukan apakah atribut menerima select. Jika atribut adalah configurable, atribut akan mem-parsing nilai non-select sebagai select yang tidak dapat dikonfigurasi – "foo" akan menjadi select({"//conditions:default": "foo"}). Pelajari lebih lanjut di pilihan.

Pewarisan atribut

Makro sering kali dimaksudkan untuk membungkus aturan (atau makro lain), dan penulis makro sering kali ingin meneruskan sebagian besar atribut simbol yang dibungkus tanpa diubah, menggunakan **kwargs, ke target utama makro (atau makro dalam utama).

Untuk mendukung pola ini, makro dapat mewarisi atribut dari aturan atau makro lain dengan meneruskan aturan atau simbol makro ke argumen inherit_attrs macro(). (Anda juga dapat menggunakan string khusus "common" alih-alih simbol aturan atau makro untuk mewarisi atribut umum yang ditentukan untuk semua aturan build Starlark.) Hanya atribut publik yang diwarisi, dan atribut dalam kamus attrs makro sendiri menggantikan atribut yang diwarisi dengan nama yang sama. Anda juga dapat menghapus atribut yang diwariskan dengan menggunakan None sebagai nilai dalam kamus attrs:

# macro/macro.bzl
my_macro = macro(
    inherit_attrs = native.cc_library,
    attrs = {
        # override native.cc_library's `local_defines` attribute
        "local_defines": attr.string_list(default = ["FOO"]),
        # do not inherit native.cc_library's `defines` attribute
        "defines": None,
    },
    ...
)

Nilai default atribut yang diwarisi dan tidak wajib selalu diganti menjadi None, terlepas dari nilai default definisi atribut aslinya. Jika Anda perlu memeriksa atau mengubah atribut non-wajib yang diwariskan – misalnya, jika Anda ingin menambahkan tag ke atribut tags yang diwariskan – Anda harus memastikan untuk menangani kasus None dalam fungsi penerapan makro Anda:

# macro/macro.bzl
def _my_macro_impl(name, visibility, tags, **kwargs):
    # Append a tag; tags attr is an inherited non-mandatory attribute, and
    # therefore is None unless explicitly set by the caller of our macro.
    my_tags = (tags or []) + ["another_tag"]
    native.cc_library(
        ...
        tags = my_tags,
        **kwargs,
    )
    ...

Penerapan

implementation menerima fungsi yang berisi logika makro. Fungsi penerapan sering membuat target dengan memanggil satu atau beberapa aturan, dan biasanya bersifat pribadi (diberi nama dengan garis bawah di depannya). Secara konvensional, nama mereka sama dengan makronya, tetapi diawali dengan _ dan diakhiri dengan _impl.

Tidak seperti fungsi penerapan aturan, yang mengambil satu argumen (ctx) yang berisi referensi ke atribut, fungsi penerapan makro menerima parameter untuk setiap argumen.

# macro/macro.bzl
def _my_macro_impl(name, visibility, deps, create_test):
    cc_library(
        name = name + "_cc_lib",
        deps = deps,
    )

    if create_test:
        cc_test(
            name = name + "_test",
            srcs = ["my_test.cc"],
            deps = deps,
        )

Jika makro mewarisi atribut, fungsi implementasinya harus memiliki parameter kata kunci residual **kwargs, yang dapat diteruskan ke panggilan yang memanggil aturan atau submakro yang diwarisi. (Hal ini membantu memastikan bahwa makro Anda tidak akan rusak jika aturan atau makro yang Anda warisi menambahkan atribut baru.)

Pernyataan

Makro dideklarasikan dengan memuat dan memanggil definisinya dalam file BUILD.


# pkg/BUILD

my_macro(
    name = "macro_instance",
    deps = ["src.cc"] + select(
        {
            "//config_setting:special": ["special_source.cc"],
            "//conditions:default": [],
        },
    ),
    create_tests = True,
)

Tindakan ini akan membuat target //pkg:macro_instance_cc_lib dan//pkg:macro_instance_test.

Sama seperti pada panggilan aturan, jika nilai atribut dalam panggilan makro ditetapkan ke None, atribut tersebut diperlakukan seolah-olah dihilangkan oleh pemanggil makro. Misalnya, dua panggilan makro berikut setara:

# pkg/BUILD
my_macro(name = "abc", srcs = ["src.cc"], deps = None)
my_macro(name = "abc", srcs = ["src.cc"])

Ini umumnya tidak berguna dalam file BUILD, tetapi berguna saat membungkus makro secara terprogram di dalam makro lain.

Detail

Konvensi penamaan untuk target yang dibuat

Nama target atau submakro yang dibuat oleh makro simbolis harus cocok dengan parameter name makro atau harus diawali dengan name, diikuti dengan _ (lebih disarankan), ., atau -. Misalnya, my_macro(name = "foo") hanya dapat membuat file atau target yang bernama foo, atau diawali dengan foo_, foo-, atau foo., misalnya, foo_bar.

Target atau file yang melanggar konvensi penamaan makro dapat dideklarasikan, tetapi tidak dapat dibangun dan tidak dapat digunakan sebagai dependensi.

File dan target non-makro dalam paket yang sama dengan instance makro tidak boleh memiliki nama yang berkonflik dengan nama target makro potensial, meskipun eksklusivitas ini tidak diterapkan. Kami sedang dalam proses menerapkan evaluasi lambat sebagai peningkatan performa untuk makro Simbolik, yang akan terganggu dalam paket yang melanggar skema penamaan.

Pembatasan

Makro simbolik memiliki beberapa batasan tambahan dibandingkan dengan makro lama.

Makro simbolis

  • harus menggunakan argumen name dan argumen visibility
  • harus memiliki fungsi implementation
  • mungkin tidak menampilkan nilai
  • mungkin tidak mengubah argumennya
  • tidak boleh memanggil native.existing_rules() kecuali jika merupakan makro finalizer khusus
  • mungkin tidak dapat menelepon native.package()
  • mungkin tidak dapat menelepon glob()
  • mungkin tidak dapat menelepon native.environment_group()
  • harus membuat target yang namanya mematuhi skema penamaan
  • tidak dapat merujuk ke file input yang tidak dideklarasikan atau diteruskan sebagai argumen
  • tidak dapat merujuk ke target pribadi pemanggilnya (lihat visibilitas dan makro untuk mengetahui detail selengkapnya).

Visibilitas dan makro

Sistem visibilitas membantu melindungi detail penerapan makro (simbolis) dan pemanggilnya.

Secara default, target yang dibuat dalam makro simbolis terlihat dalam makro itu sendiri, tetapi tidak selalu terlihat oleh pemanggil makro. Makro dapat "mengekspor" target sebagai API publik dengan meneruskan nilai atribut visibility-nya sendiri, seperti pada some_rule(..., visibility = visibility).

Ide utama visibilitas makro adalah:

  1. Visibilitas diperiksa berdasarkan makro yang mendeklarasikan target, bukan paket yang memanggil makro.

    • Dengan kata lain, berada dalam paket yang sama tidak dengan sendirinya membuat satu target terlihat oleh target lain. Hal ini melindungi target internal makro agar tidak menjadi dependensi makro lain atau target tingkat teratas dalam paket.
  2. Semua atribut visibility, baik pada aturan maupun makro, otomatis mencakup tempat aturan atau makro dipanggil.

    • Dengan demikian, target terlihat tanpa syarat oleh target lain yang dideklarasikan dalam makro yang sama (atau file BUILD, jika tidak dalam makro).

Dalam praktiknya, ini berarti bahwa saat makro mendeklarasikan target tanpa menetapkan visibility, target secara default akan menjadi internal untuk makro. (Visibilitas default paket tidak berlaku dalam makro.) Mengekspor target berarti target terlihat oleh apa pun yang ditentukan pemanggil makro dalam atribut visibility makro, ditambah paket pemanggil makro itu sendiri, serta kode makro itu sendiri. Cara lain untuk memikirkannya adalah bahwa visibilitas makro menentukan siapa (selain makro itu sendiri) yang dapat melihat target yang diekspor makro.

# tool/BUILD
...
some_rule(
    name = "some_tool",
    visibility = ["//macro:__pkg__"],
)
# macro/macro.bzl

def _impl(name, visibility):
    cc_library(
        name = name + "_helper",
        ...
        # No visibility passed in. Same as passing `visibility = None` or
        # `visibility = ["//visibility:private"]`. Visible to the //macro
        # package only.
    )
    cc_binary(
        name = name + "_exported",
        deps = [
            # Allowed because we're also in //macro. (Targets in any other
            # instance of this macro, or any other macro in //macro, can see it
            # too.)
            name + "_helper",
            # Allowed by some_tool's visibility, regardless of what BUILD file
            # we're called from.
            "//tool:some_tool",
        ],
        ...
        visibility = visibility,
    )

my_macro = macro(implementation = _impl, ...)
# pkg/BUILD
load("//macro:macro.bzl", "my_macro")
...

my_macro(
    name = "foo",
    ...
)

some_rule(
    ...
    deps = [
        # Allowed, its visibility is ["//pkg:__pkg__", "//macro:__pkg__"].
        ":foo_exported",
        # Disallowed, its visibility is ["//macro:__pkg__"] and
        # we are not in //macro.
        ":foo_helper",
    ]
)

Jika my_macro dipanggil dengan visibility = ["//other_pkg:__pkg__"], atau jika paket //pkg telah menyetel default_visibility ke nilai tersebut, maka //pkg:foo_exported juga dapat digunakan dalam //other_pkg/BUILD atau dalam makro yang ditentukan dalam //other_pkg:defs.bzl, tetapi //pkg:foo_helper akan tetap dilindungi.

Makro dapat menyatakan bahwa target terlihat oleh paket teman dengan meneruskan visibility = ["//some_friend:__pkg__"] (untuk target internal) atau visibility = visibility + ["//some_friend:__pkg__"] (untuk target yang diekspor). Perhatikan bahwa mendeklarasikan target dengan visibilitas publik (visibility = ["//visibility:public"]) adalah antipola untuk makro. Hal ini karena membuat target terlihat tanpa syarat oleh setiap paket, meskipun pemanggil menentukan visibilitas yang lebih terbatas.

Semua pemeriksaan visibilitas dilakukan sehubungan dengan makro simbolis yang saat ini berjalan paling dalam. Namun, ada mekanisme pendelegasian visibilitas: Jika makro meneruskan label sebagai nilai atribut ke makro dalam, setiap penggunaan label dalam makro dalam diperiksa sehubungan dengan makro luar. Lihat halaman visibilitas untuk mengetahui detail selengkapnya.

Ingatlah bahwa makro lama sepenuhnya transparan terhadap sistem visibilitas, dan berperilaku seolah-olah lokasinya adalah file BUILD atau makro simbolis tempat makro tersebut dipanggil.

Finalizer dan visibilitas

Target yang dideklarasikan di finalizer aturan, selain melihat target yang mengikuti aturan visibilitas makro simbolik biasa, juga dapat melihat semua target yang terlihat oleh paket target finalizer.

Artinya, jika Anda memigrasikan makro lama berbasis native.existing_rules() ke finalizer, target yang dideklarasikan oleh finalizer akan tetap dapat melihat dependensi lamanya.

Namun, perhatikan bahwa target dapat dideklarasikan dalam makro simbolik sehingga target finalizer tidak dapat melihatnya dalam sistem visibilitas – meskipun finalizer dapat mengintrospeksi atributnya menggunakan native.existing_rules().

Memilih

Jika atribut adalah configurable (default) dan nilainya bukan None, maka fungsi implementasi makro akan melihat nilai atribut sebagai yang di-wrap dalam select yang tidak penting. Hal ini memudahkan penulis makro untuk menemukan bug yang tidak mengantisipasi bahwa nilai atribut dapat berupa select.

Misalnya, pertimbangkan makro berikut:

my_macro = macro(
    attrs = {"deps": attr.label_list()},  # configurable unless specified otherwise
    implementation = _my_macro_impl,
)

Jika my_macro dipanggil dengan deps = ["//a"], _my_macro_impl akan dipanggil dengan parameter deps yang ditetapkan ke select({"//conditions:default": ["//a"]}). Jika hal ini menyebabkan fungsi penerapan gagal (misalnya, karena kode mencoba mengindeks nilai seperti pada deps[0], yang tidak diizinkan untuk select), penulis makro kemudian dapat membuat pilihan: mereka dapat menulis ulang makro agar hanya menggunakan operasi yang kompatibel dengan select, atau mereka dapat menandai atribut sebagai tidak dapat dikonfigurasi (attr.label_list(configurable = False)). Opsi terakhir memastikan bahwa pengguna tidak diizinkan untuk meneruskan nilai select.

Target aturan membalikkan transformasi ini, dan menyimpan select yang tidak penting sebagai nilai tanpa syaratnya; dalam contoh di atas, jika _my_macro_impl mendeklarasikan target aturan my_rule(..., deps = deps), deps target aturan tersebut akan disimpan sebagai ["//a"]. Hal ini memastikan bahwa pembungkusan select tidak menyebabkan nilai select sepele disimpan di semua target yang di-instansiasi oleh makro.

Jika nilai atribut yang dapat dikonfigurasi adalah None, atribut tersebut tidak akan dibungkus dalam select. Hal ini memastikan bahwa pengujian seperti my_attr == None tetap berfungsi, dan bahwa saat atribut diteruskan ke aturan dengan default terkomputasi, aturan berperilaku dengan benar (yaitu, seolah-olah atribut tidak diteruskan sama sekali). Atribut tidak selalu dapat mengambil nilai None, tetapi hal ini dapat terjadi untuk jenis attr.label(), dan untuk atribut non-wajib yang diwariskan.

Finalisasi

Finalizer aturan adalah makro simbolik khusus yang – terlepas dari posisi leksikalnya dalam file BUILD – dievaluasi pada tahap akhir pemuatan paket, setelah semua target non-finalizer ditentukan. Tidak seperti makro simbolik biasa, finalizer dapat memanggil native.existing_rules(), yang perilakunya sedikit berbeda dengan makro lama: hanya menampilkan kumpulan target aturan non-finalizer. Finalizer dapat menegaskan status set tersebut atau menentukan target baru.

Untuk mendeklarasikan finalizer, panggil macro() dengan finalizer = True:

def _my_finalizer_impl(name, visibility, tags_filter):
    for r in native.existing_rules().values():
        for tag in r.get("tags", []):
            if tag in tags_filter:
                my_test(
                    name = name + "_" + r["name"] + "_finalizer_test",
                    deps = [r["name"]],
                    data = r["srcs"],
                    ...
                )
                continue

my_finalizer = macro(
    attrs = {"tags_filter": attr.string_list(configurable = False)},
    implementation = _impl,
    finalizer = True,
)

Kemalasan

PENTING: Kami sedang dalam proses menerapkan ekspansi dan evaluasi makro lambat. Fitur ini belum tersedia.

Saat ini, semua makro dievaluasi segera setelah file BUILD dimuat, yang dapat berdampak negatif pada performa untuk target dalam paket yang juga memiliki makro tidak terkait yang mahal. Pada masa mendatang, makro simbolis non-finalizer hanya akan dievaluasi jika diperlukan untuk build. Skema penamaan awalan membantu Bazel menentukan makro mana yang akan diperluas berdasarkan target yang diminta.

Pemecahan masalah migrasi

Berikut adalah beberapa masalah umum saat migrasi dan cara mengatasinya.

  • Panggilan makro lama glob()

Pindahkan panggilan glob() ke file BUILD Anda (atau ke makro lama yang dipanggil dari file BUILD), dan teruskan nilai glob() ke makro simbolik menggunakan atribut label-list:

# BUILD file
my_macro(
    ...,
    deps = glob(...),
)
  • Makro lama memiliki parameter yang bukan jenis attr starlark yang valid.

Tarik sebanyak mungkin logika ke dalam makro simbolis bertingkat, tetapi pertahankan makro tingkat teratas sebagai makro lama.

  • Makro lama memanggil aturan yang membuat target yang melanggar skema penamaan

Tidak apa-apa, cukup jangan bergantung pada target yang "menyinggung". Pemeriksaan penamaan akan diabaikan tanpa pemberitahuan.