Toolchain

Halaman ini menjelaskan framework toolchain, yang merupakan cara bagi penulis aturan untuk memisahkan logika aturan dari pemilihan alat berbasis platform. Sebaiknya baca halaman aturan dan platform sebelum melanjutkan. Halaman ini membahas alasan toolchain diperlukan, cara menentukan dan menggunakannya, serta cara Bazel memilih toolchain yang sesuai berdasarkan batasan platform.

Motivasi

Pertama, mari kita lihat masalah yang dirancang untuk dipecahkan oleh toolchain. Misalnya, Anda menulis aturan untuk mendukung bahasa pemrograman "bar". Aturan bar_binary Anda akan mengompilasi file *.bar menggunakan compiler barc, alat yang dibuat sebagai target lain di ruang kerja Anda. Karena pengguna yang menulis target bar_binary tidak perlu menentukan dependensi pada compiler, Anda menjadikannya dependensi implisit dengan menambahkannya ke definisi aturan sebagai atribut pribadi.

bar_binary = rule(
    implementation = _bar_binary_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        ...
        "_compiler": attr.label(
            default = "//bar_tools:barc_linux",  # the compiler running on linux
            providers = [BarcInfo],
        ),
    },
)

//bar_tools:barc_linux kini menjadi dependensi setiap target bar_binary, sehingga akan dibuat sebelum target bar_binary apa pun. Fungsi ini dapat diakses oleh fungsi implementasi aturan seperti atribut lainnya:

BarcInfo = provider(
    doc = "Information about how to invoke the barc compiler.",
    # In the real world, compiler_path and system_lib might hold File objects,
    # but for simplicity they are strings for this example. arch_flags is a list
    # of strings.
    fields = ["compiler_path", "system_lib", "arch_flags"],
)

def _bar_binary_impl(ctx):
    ...
    info = ctx.attr._compiler[BarcInfo]
    command = "%s -l %s %s" % (
        info.compiler_path,
        info.system_lib,
        " ".join(info.arch_flags),
    )
    ...

Masalahnya di sini adalah label compiler dikodekan secara permanen ke dalam bar_binary, tetapi target yang berbeda mungkin memerlukan compiler yang berbeda, bergantung pada platform yang digunakan untuk membangunnya dan platform yang digunakan untuk membangunnya -- yang disebut platform target dan platform eksekusi, masing-masing. Selain itu, penulis aturan tidak selalu mengetahui semua alat dan platform yang tersedia, sehingga tidak mungkin untuk mengodekannya secara permanen dalam definisi aturan.

Solusi yang kurang ideal adalah mengalihkan beban ke pengguna, dengan membuat atribut _compiler tidak bersifat pribadi. Kemudian, setiap target dapat dikodekan secara permanen untuk membangun satu platform atau platform lainnya.

bar_binary(
    name = "myprog_on_linux",
    srcs = ["mysrc.bar"],
    compiler = "//bar_tools:barc_linux",
)

bar_binary(
    name = "myprog_on_windows",
    srcs = ["mysrc.bar"],
    compiler = "//bar_tools:barc_windows",
)

Anda dapat meningkatkan solusi ini dengan menggunakan select untuk memilih compiler berdasarkan platform:

config_setting(
    name = "on_linux",
    constraint_values = [
        "@platforms//os:linux",
    ],
)

config_setting(
    name = "on_windows",
    constraint_values = [
        "@platforms//os:windows",
    ],
)

bar_binary(
    name = "myprog",
    srcs = ["mysrc.bar"],
    compiler = select({
        ":on_linux": "//bar_tools:barc_linux",
        ":on_windows": "//bar_tools:barc_windows",
    }),
)

Namun, hal ini membosankan dan terlalu banyak untuk diminta dari setiap pengguna bar_binary. Jika gaya ini tidak digunakan secara konsisten di seluruh ruang kerja, gaya ini akan menyebabkan build yang berfungsi dengan baik di satu platform, tetapi gagal saat diperluas ke skenario multi-platform. Hal ini juga tidak mengatasi masalah penambahan dukungan untuk platform dan compiler baru tanpa mengubah aturan atau target yang ada.

Framework toolchain memecahkan masalah ini dengan menambahkan tingkat pengalihan tambahan. Pada dasarnya, Anda mendeklarasikan bahwa aturan Anda memiliki dependensi abstrak pada beberapa anggota keluarga target (jenis toolchain), dan Bazel secara otomatis me-resolve hal ini ke target tertentu (toolchain) berdasarkan batasan platform yang berlaku. Baik penulis aturan maupun penulis target tidak perlu mengetahui kumpulan lengkap platform dan toolchain yang tersedia.

Menulis aturan yang menggunakan toolchain

Dalam framework toolchain, aturan tidak bergantung langsung pada alat, tetapi bergantung pada jenis toolchain. Jenis toolchain adalah target sederhana yang mewakili class alat yang memiliki peran yang sama untuk platform yang berbeda. Misalnya, Anda dapat mendeklarasikan jenis yang mewakili compiler bar:

# By convention, toolchain_type targets are named "toolchain_type" and
# distinguished by their package path. So the full path for this would be
# //bar_tools:toolchain_type.
toolchain_type(name = "toolchain_type")

Definisi aturan di bagian sebelumnya diubah sehingga, alih-alih mengambil compiler sebagai atribut, definisi tersebut mendeklarasikan bahwa definisi tersebut menggunakan toolchain //bar_tools:toolchain_type.

bar_binary = rule(
    implementation = _bar_binary_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        ...
        # No `_compiler` attribute anymore.
    },
    toolchains = ["//bar_tools:toolchain_type"],
)

Fungsi implementasi kini mengakses dependensi ini di bagian ctx.toolchains, bukan ctx.attr, menggunakan jenis toolchain sebagai kunci.

def _bar_binary_impl(ctx):
    ...
    info = ctx.toolchains["//bar_tools:toolchain_type"].barcinfo
    # The rest is unchanged.
    command = "%s -l %s %s" % (
        info.compiler_path,
        info.system_lib,
        " ".join(info.arch_flags),
    )
    ...

ctx.toolchains["//bar_tools:toolchain_type"] menampilkan ToolchainInfo penyedia dari target apa pun yang di-resolve oleh Bazel ke dependensi toolchain. Kolom objek ToolchainInfo ditetapkan oleh aturan alat yang mendasarinya; di bagian berikutnya, aturan ini ditentukan sehingga ada kolom barcinfo yang menggabungkan objek BarcInfo.

Prosedur Bazel untuk me-resolve toolchain ke target dijelaskan di bawah. Hanya target toolchain yang di-resolve yang sebenarnya dijadikan dependensi target bar_binary, bukan seluruh ruang toolchain kandidat.

Toolchain Wajib dan Opsional

Secara default, saat aturan menyatakan dependensi jenis toolchain menggunakan label kosong (seperti yang ditunjukkan di atas), jenis toolchain dianggap wajib. Jika Bazel tidak dapat menemukan toolchain yang cocok (lihat Resolusi toolchain di bawah) untuk jenis toolchain wajib, ini adalah error dan analisis akan berhenti.

Anda dapat mendeklarasikan dependensi jenis toolchain opsional sebagai berikut:

bar_binary = rule(
    ...
    toolchains = [
        config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
    ],
)

Jika jenis toolchain opsional tidak dapat di-resolve, analisis akan dilanjutkan, dan hasilnya dari ctx.toolchains["//bar_tools:toolchain_type"] adalah None.

Fungsi secara default bersifat wajib.config_common.toolchain_type

Bentuk berikut dapat digunakan:

  • Jenis toolchain wajib:
    • toolchains = ["//bar_tools:toolchain_type"]
    • toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type")]
    • toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = True)]
  • Jenis toolchain opsional:
    • toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False)]
bar_binary = rule(
    ...
    toolchains = [
        "//foo_tools:toolchain_type",
        config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
    ],
)

Anda juga dapat memadupadankan bentuk dalam aturan yang sama. Namun, jika jenis toolchain yang sama tercantum beberapa kali, jenis toolchain tersebut akan menggunakan versi yang paling ketat, dengan wajib lebih ketat daripada opsional.

Menulis aspek yang menggunakan toolchain

Aspek memiliki akses ke API toolchain yang sama dengan aturan: Anda dapat menentukan jenis toolchain yang diperlukan, mengakses toolchain melalui konteks, dan menggunakannya untuk membuat tindakan baru menggunakan toolchain.

bar_aspect = aspect(
    implementation = _bar_aspect_impl,
    attrs = {},
    toolchains = ['//bar_tools:toolchain_type'],
)

def _bar_aspect_impl(target, ctx):
  toolchain = ctx.toolchains['//bar_tools:toolchain_type']
  # Use the toolchain provider like in a rule.
  return []

Menentukan toolchain

Untuk menentukan beberapa toolchain untuk jenis toolchain tertentu, Anda memerlukan tiga hal:

  1. Aturan khusus bahasa yang mewakili jenis alat atau rangkaian alat. Menurut konvensi, nama aturan ini diberi akhiran "_toolchain".

    1. Catatan: Aturan \_toolchain tidak dapat membuat tindakan build apa pun. Sebaliknya, aturan ini mengumpulkan artefak dari aturan lain dan meneruskannya ke aturan yang menggunakan toolchain. Aturan tersebut bertanggung jawab untuk membuat semua tindakan build.
  2. Beberapa target dari jenis aturan ini, yang mewakili versi alat atau rangkaian alat untuk platform yang berbeda.

  3. Untuk setiap target tersebut, target terkait dari aturan generik toolchain, untuk memberikan metadata yang digunakan oleh framework toolchain. Target toolchain ini juga merujuk ke toolchain_type yang terkait dengan toolchain ini. Artinya, aturan _toolchain tertentu dapat dikaitkan dengan toolchain_type apa pun, dan hanya dalam instance toolchain yang menggunakan aturan _toolchain ini, aturan tersebut dikaitkan dengan toolchain_type.

Untuk contoh yang sedang berjalan, berikut adalah definisi untuk aturan bar_toolchain. Contoh kita hanya memiliki compiler, tetapi alat lain seperti linker juga dapat dikelompokkan di bawahnya.

def _bar_toolchain_impl(ctx):
    toolchain_info = platform_common.ToolchainInfo(
        barcinfo = BarcInfo(
            compiler_path = ctx.attr.compiler_path,
            system_lib = ctx.attr.system_lib,
            arch_flags = ctx.attr.arch_flags,
        ),
    )
    return [toolchain_info]

bar_toolchain = rule(
    implementation = _bar_toolchain_impl,
    attrs = {
        "compiler_path": attr.string(),
        "system_lib": attr.string(),
        "arch_flags": attr.string_list(),
    },
)

Aturan ini harus menampilkan penyedia ToolchainInfo, yang menjadi objek yang diambil oleh aturan penggunaan menggunakan ctx.toolchains dan label jenis toolchain. ToolchainInfo, seperti struct, dapat menyimpan pasangan nilai kolom arbitrer. Spesifikasi kolom yang ditambahkan ke ToolchainInfo harus didokumentasikan dengan jelas di jenis toolchain. Dalam contoh ini, nilai yang ditampilkan digabungkan dalam objek BarcInfo untuk menggunakan kembali skema yang ditentukan di atas; gaya ini mungkin berguna untuk validasi dan penggunaan kembali kode.

Sekarang Anda dapat menentukan target untuk compiler barc tertentu.

bar_toolchain(
    name = "barc_linux",
    arch_flags = [
        "--arch=Linux",
        "--debug_everything",
    ],
    compiler_path = "/path/to/barc/on/linux",
    system_lib = "/usr/lib/libbarc.so",
)

bar_toolchain(
    name = "barc_windows",
    arch_flags = [
        "--arch=Windows",
        # Different flags, no debug support on windows.
    ],
    compiler_path = "C:\\path\\on\\windows\\barc.exe",
    system_lib = "C:\\path\\on\\windows\\barclib.dll",
)

Terakhir, buat toolchain definisi untuk dua bar_toolchain target. Definisi ini menautkan target khusus bahasa ke jenis toolchain dan memberikan informasi batasan yang memberi tahu Bazel kapan toolchain sesuai untuk platform tertentu.

toolchain(
    name = "barc_linux_toolchain",
    exec_compatible_with = [
        "@platforms//os:linux",
        "@platforms//cpu:x86_64",
    ],
    target_compatible_with = [
        "@platforms//os:linux",
        "@platforms//cpu:x86_64",
    ],
    toolchain = ":barc_linux",
    toolchain_type = ":toolchain_type",
)

toolchain(
    name = "barc_windows_toolchain",
    exec_compatible_with = [
        "@platforms//os:windows",
        "@platforms//cpu:x86_64",
    ],
    target_compatible_with = [
        "@platforms//os:windows",
        "@platforms//cpu:x86_64",
    ],
    toolchain = ":barc_windows",
    toolchain_type = ":toolchain_type",
)

Penggunaan sintaksis jalur relatif di atas menunjukkan bahwa semua definisi ini berada dalam paket yang sama, tetapi tidak ada alasan mengapa jenis toolchain, target toolchain khusus bahasa, dan target definisi toolchain tidak dapat berada dalam paket terpisah.

Lihat go_toolchain untuk contoh dunia nyata.

Toolchain dan konfigurasi

Pertanyaan penting bagi penulis aturan adalah, saat target bar_toolchain dianalisis, konfigurasi apa yang dilihatnya, dan transisi apa yang harus digunakan untuk dependensi? Contoh di atas menggunakan atribut string, tetapi apa yang akan terjadi untuk toolchain yang lebih rumit yang bergantung pada target lain di repositori Bazel?

Mari kita lihat versi bar_toolchain yang lebih kompleks:

def _bar_toolchain_impl(ctx):
    # The implementation is mostly the same as above, so skipping.
    pass

bar_toolchain = rule(
    implementation = _bar_toolchain_impl,
    attrs = {
        "compiler": attr.label(
            executable = True,
            mandatory = True,
            cfg = "exec",
        ),
        "system_lib": attr.label(
            mandatory = True,
            cfg = "target",
        ),
        "arch_flags": attr.string_list(),
    },
)

Penggunaan attr.label sama seperti untuk aturan standar, tetapi arti parameter cfg sedikit berbeda.

Dependensi dari target (disebut "induk") ke toolchain melalui resolusi toolchain menggunakan transisi konfigurasi khusus yang disebut "transisi toolchain". Transisi toolchain mempertahankan konfigurasi yang sama, kecuali bahwa transisi tersebut memaksa platform eksekusi menjadi sama untuk toolchain seperti untuk induk (jika tidak, resolusi toolchain untuk toolchain dapat memilih platform eksekusi apa pun, dan tidak harus sama dengan induk). Hal ini memungkinkan dependensi exec toolchain juga dapat dieksekusi untuk tindakan build induk. Dependensi toolchain yang menggunakan cfg = "target" (atau yang tidak menentukan cfg, karena "target" adalah default) dibuat untuk platform target yang sama dengan induk. Hal ini memungkinkan aturan toolchain untuk berkontribusi pada library (atribut system_lib di atas) dan alat (atribut compiler) ke aturan build yang memerlukannya. Library sistem ditautkan ke artefak akhir, sehingga perlu dibuat untuk platform yang sama, sedangkan compiler adalah alat yang dipanggil selama build, dan harus dapat berjalan di platform eksekusi.

Mendaftarkan dan membangun dengan toolchain

Pada tahap ini, semua blok penyusun telah dirakit, dan Anda hanya perlu menyediakan toolchain untuk prosedur resolusi Bazel. Hal ini dilakukan dengan mendaftarkan toolchain, baik dalam file MODULE.bazel menggunakan register_toolchains(), atau dengan meneruskan label toolchain di command line menggunakan flag --extra_toolchains.

register_toolchains(
    "//bar_tools:barc_linux_toolchain",
    "//bar_tools:barc_windows_toolchain",
    # Target patterns are also permitted, so you could have also written:
    # "//bar_tools:all",
    # or even
    # "//bar_tools/...",
)

Saat menggunakan pola target untuk mendaftarkan toolchain, urutan pendaftaran setiap toolchain ditentukan oleh aturan berikut:

  • Toolchain yang ditentukan dalam subpaket paket didaftarkan sebelum toolchain yang ditentukan dalam paket itu sendiri.
  • Dalam paket, toolchain didaftarkan dalam urutan leksikografis nama.

Sekarang, saat Anda membuat target yang bergantung pada jenis toolchain, toolchain yang sesuai akan dipilih berdasarkan platform target dan eksekusi.

# my_pkg/BUILD

platform(
    name = "my_target_platform",
    constraint_values = [
        "@platforms//os:linux",
    ],
)

bar_binary(
    name = "my_bar_binary",
    ...
)
bazel build //my_pkg:my_bar_binary --platforms=//my_pkg:my_target_platform

Bazel akan melihat bahwa //my_pkg:my_bar_binary sedang dibuat dengan platform yang memiliki @platforms//os:linux dan oleh karena itu, me-resolve referensi //bar_tools:toolchain_type ke //bar_tools:barc_linux_toolchain. Hal ini akan menghasilkan build //bar_tools:barc_linux, tetapi bukan //bar_tools:barc_windows.

Resolusi toolchain

Untuk setiap target yang menggunakan toolchain, prosedur resolusi toolchain Bazel menentukan dependensi toolchain konkret target. Prosedur ini mengambil sekumpulan jenis toolchain yang diperlukan, platform target, daftar platform eksekusi yang tersedia, dan daftar toolchain yang tersedia sebagai input. Outputnya adalah toolchain yang dipilih untuk setiap jenis toolchain serta platform eksekusi yang dipilih untuk target saat ini.

Platform dan toolchain eksekusi yang tersedia dikumpulkan dari grafik dependensi eksternal melalui register_execution_platforms dan register_toolchains panggilan dalam MODULE.bazel file. Platform dan toolchain eksekusi tambahan juga dapat ditentukan di command line melalui --extra_execution_platforms dan --extra_toolchains. Platform host otomatis disertakan sebagai platform eksekusi yang tersedia. Platform dan toolchain yang tersedia dilacak sebagai daftar yang diurutkan untuk determinisme, dengan preferensi diberikan kepada item sebelumnya dalam daftar.

Kumpulan toolchain yang tersedia, dalam urutan prioritas, dibuat dari --extra_toolchains dan register_toolchains:

  1. Toolchain yang didaftarkan menggunakan --extra_toolchains ditambahkan terlebih dahulu. (Di dalamnya, toolchain terakhir memiliki prioritas tertinggi.)
  2. Toolchain yang didaftarkan menggunakan register_toolchains dalam grafik dependensi eksternal transitif, dalam urutan berikut: (Di dalamnya, toolchain pertama yang disebutkan memiliki prioritas tertinggi.)
    1. Toolchain yang didaftarkan oleh modul root (seperti, MODULE.bazel di root ruang kerja);
    2. Toolchain yang didaftarkan dalam file WORKSPACE pengguna, termasuk dalam makro apa pun yang dipanggil dari sana;
    3. Toolchain yang didaftarkan oleh modul non-root (seperti, dependensi yang ditentukan oleh modul root, dan dependensinya, dan seterusnya);
    4. Toolchain yang didaftarkan dalam "sufiks WORKSPACE"; ini hanya digunakan oleh aturan native tertentu yang disertakan dengan penginstalan Bazel.

CATATAN: Target semu seperti :all, :*, dan /... diurutkan berdasarkan mekanisme pemuatan paket Bazel, yang menggunakan pengurutan leksikografis.

Langkah-langkah resolusinya adalah sebagai berikut.

  1. Klausa target_compatible_with atau exec_compatible_with cocok dengan platform jika, untuk setiap constraint_value dalam daftarnya, platform juga memiliki constraint_value tersebut (baik secara eksplisit maupun sebagai default).

    Jika platform memiliki constraint_value dari constraint_setting yang tidak direferensikan oleh klausa, hal ini tidak akan memengaruhi pencocokan.

  2. Jika target yang sedang dibuat menentukan atribut exec_compatible_with (atau definisi aturannya menentukan argumen exec_compatible_with), daftar platform eksekusi yang tersedia akan difilter untuk menghapus platform yang tidak cocok dengan batasan eksekusi.

  3. Daftar toolchain yang tersedia difilter untuk menghapus toolchain yang menentukan target_settings yang tidak cocok dengan konfigurasi saat ini.

  4. Untuk setiap platform eksekusi yang tersedia, Anda mengaitkan setiap jenis toolchain dengan toolchain pertama yang tersedia, jika ada, yang kompatibel dengan platform eksekusi dan platform target ini.

  5. Platform eksekusi yang gagal menemukan toolchain wajib yang kompatibel untuk salah satu jenis toolchainnya akan dikecualikan. Dari platform yang tersisa, platform pertama menjadi platform eksekusi target saat ini, dan toolchain terkait (jika ada) menjadi dependensi target.

Platform eksekusi yang dipilih digunakan untuk menjalankan semua tindakan yang dihasilkan oleh target.

Jika target yang sama dapat dibuat dalam beberapa konfigurasi (seperti untuk CPU yang berbeda) dalam build yang sama, prosedur resolusi akan diterapkan secara independen ke setiap versi target.

Jika aturan menggunakan grup eksekusi, setiap grup eksekusi akan melakukan resolusi toolchain secara terpisah, dan masing-masing memiliki platform dan toolchain eksekusinya sendiri.

Men-debug toolchain

Jika Anda menambahkan dukungan toolchain ke aturan yang ada, gunakan flag --toolchain_resolution_debug=regex. Selama resolusi toolchain, flag ini memberikan output verbose untuk jenis toolchain atau nama target yang cocok dengan variabel regex. Anda dapat menggunakan .* untuk menampilkan semua informasi. Bazel akan menampilkan nama toolchain yang diperiksa dan dilewati selama proses resolusi.

Misalnya, untuk men-debug pemilihan toolchain untuk semua tindakan yang dibuat langsung oleh //my:target:

$ bazel build //my:all --toolchain_resolution_debug=//my:target

Untuk men-debug pemilihan toolchain untuk semua tindakan di semua target build:

$ bazel build //my:all --toolchain_resolution_debug=.*

Jika Anda ingin melihat dependensi cquery mana yang berasal dari resolusi toolchain, gunakan cquery's flag --transitions:

# Find all direct dependencies of //cc:my_cc_lib. This includes explicitly
# declared dependencies, implicit dependencies, and toolchain dependencies.
$ bazel cquery 'deps(//cc:my_cc_lib, 1)'
//cc:my_cc_lib (96d6638)
@bazel_tools//tools/cpp:toolchain (96d6638)
@bazel_tools//tools/def_parser:def_parser (HOST)
//cc:my_cc_dep (96d6638)
@local_config_platform//:host (96d6638)
@bazel_tools//tools/cpp:toolchain_type (96d6638)
//:default_host_platform (96d6638)
@local_config_cc//:cc-compiler-k8 (HOST)
//cc:my_cc_lib.cc (null)
@bazel_tools//tools/cpp:grep-includes (HOST)

# Which of these are from toolchain resolution?
$ bazel cquery 'deps(//cc:my_cc_lib, 1)' --transitions=lite | grep "toolchain dependency"
  [toolchain dependency]#@local_config_cc//:cc-compiler-k8#HostTransition -> b6df211