Hướng dẫn về quy tắc

Báo cáo sự cố Xem nguồn

Starlark là một ngôn ngữ cấu hình giống Python, ban đầu được phát triển để sử dụng ở Bazel và sau đó được các công cụ khác sử dụng. Các tệp BUILD.bzl của Bazel được viết bằng một phương ngữ của Starlark, hay còn được gọi là "Ngôn ngữ xây dựng", mặc dù người ta thường chỉ gọi một tính năng là "Starlark", đặc biệt là khi nhấn mạnh rằng một tính năng được thể hiện bằng Ngôn ngữ bản dựng thay vì một tính năng được tích hợp sẵn hoặc "gốc" của Bazel. Bazel tăng cường ngôn ngữ cốt lõi bằng nhiều hàm liên quan đến bản dựng, chẳng hạn như glob, genrule, java_binary, v.v.

Hãy xem tài liệu về BazelStarlark để biết thêm thông tin chi tiết, cũng như tham khảo mẫu Quy tắc SIG để bắt đầu cho các tập quy tắc mới.

Quy tắc trống

Để tạo quy tắc đầu tiên, hãy tạo tệp foo.bzl:

def _foo_binary_impl(ctx):
    pass

foo_binary = rule(
    implementation = _foo_binary_impl,
)

Khi gọi hàm rule, bạn phải xác định một hàm callback. Logic sẽ chuyển đến đó, nhưng bạn có thể để trống hàm này ngay bây giờ. Đối số ctx cung cấp thông tin về mục tiêu.

Bạn có thể tải quy tắc này và sử dụng từ tệp BUILD.

Tạo tệp BUILD trong cùng thư mục:

load(":foo.bzl", "foo_binary")

foo_binary(name = "bin")

Bây giờ, bạn có thể xây dựng mục tiêu:

$ bazel build bin
INFO: Analyzed target //:bin (2 packages loaded, 17 targets configured).
INFO: Found 1 target...
Target //:bin up-to-date (nothing to build)

Mặc dù quy tắc không thực hiện gì, nhưng quy tắc này vốn đã hoạt động như các quy tắc khác: quy tắc này có tên bắt buộc, hỗ trợ các thuộc tính phổ biến như visibility, testonlytags.

Mô hình đánh giá

Trước khi tìm hiểu kỹ hơn, bạn cần hiểu cách đánh giá mã.

Cập nhật foo.bzl với một số câu lệnh in:

def _foo_binary_impl(ctx):
    print("analyzing", ctx.label)

foo_binary = rule(
    implementation = _foo_binary_impl,
)

print("bzl file evaluation")

và XÂY DỰNG:

load(":foo.bzl", "foo_binary")

print("BUILD file")
foo_binary(name = "bin1")
foo_binary(name = "bin2")

ctx.label tương ứng với nhãn của mục tiêu đang được phân tích. Đối tượng ctx có nhiều trường và phương thức hữu ích; bạn có thể tìm thấy danh sách đầy đủ trong tài liệu tham khảo API.

Truy vấn mã:

$ bazel query :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:8:1: bzl file evaluation
DEBUG: /usr/home/bazel-codelab/BUILD:2:1: BUILD file
//:bin2
//:bin1

Hãy quan sát một số điểm sau đây:

  • "đánh giá tệp bzl" sẽ được in trước. Trước khi đánh giá tệp BUILD, Bazel đánh giá tất cả các tệp mà tệp sẽ tải. Nếu nhiều tệp BUILD đang tải foo.bzl, bạn sẽ chỉ thấy một lần "đánh giá tệp bzl" vì Bazel sẽ lưu kết quả đánh giá vào bộ nhớ đệm.
  • Hàm callback _foo_binary_impl không được gọi. Truy vấn Bazel tải các tệp BUILD nhưng không phân tích các mục tiêu.

Để phân tích các mục tiêu, hãy sử dụng cquery ("truy vấn đã định cấu hình") hoặc lệnh build:

$ bazel build :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin1
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin2
INFO: Analyzed 2 targets (0 packages loaded, 0 targets configured).
INFO: Found 2 targets...

Như bạn có thể thấy, _foo_binary_impl hiện được gọi hai lần – một lần cho mỗi mục tiêu.

Lưu ý rằng cả các trường "đánh giá tệp bzl" và "tệp BUILD" sẽ không được in lại vì hoạt động đánh giá foo.bzl được lưu vào bộ nhớ đệm sau lệnh gọi đến bazel query. Bazel chỉ phát ra các câu lệnh print khi các câu lệnh đó thực sự được thực thi.

Tạo tệp

Để quy tắc của bạn hữu ích hơn, hãy cập nhật quy tắc để tạo tệp. Trước tiên, hãy khai báo và đặt tên cho tệp. Trong ví dụ này, hãy tạo một tệp có cùng tên với mục tiêu:

ctx.actions.declare_file(ctx.label.name)

Nếu chạy bazel build :all ngay bây giờ, bạn sẽ gặp lỗi:

The following files have no generating action:
bin2

Mỗi khi khai báo một tệp, bạn phải cho Bazel biết cách tạo tệp đó bằng cách tạo một thao tác. Sử dụng ctx.actions.write để tạo tệp có nội dung đã cho.

def _foo_binary_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name)
    ctx.actions.write(
        output = out,
        content = "Hello\n",
    )

Mã này hợp lệ, nhưng sẽ không có tác dụng nào:

$ bazel build bin1
Target //:bin1 up-to-date (nothing to build)

Hàm ctx.actions.write đã đăng ký một thao tác để hướng dẫn Bazel cách tạo tệp. Nhưng Bazel sẽ không tạo tệp cho đến khi thực sự yêu cầu. Vì vậy, điều cuối cùng cần làm là cho Bazel biết rằng tệp này là kết quả của quy tắc chứ không phải là tệp tạm thời được dùng trong quá trình triển khai quy tắc.

def _foo_binary_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name)
    ctx.actions.write(
        output = out,
        content = "Hello!\n",
    )
    return [DefaultInfo(files = depset([out]))]

Xem hàm DefaultInfodepset sau. Bây giờ, giả sử rằng dòng cuối cùng là cách chọn kết quả của một quy tắc.

Bây giờ, hãy chạy Bazel:

$ bazel build bin1
INFO: Found 1 target...
Target //:bin1 up-to-date:
  bazel-bin/bin1

$ cat bazel-bin/bin1
Hello!

Bạn đã tạo tệp thành công!

Thuộc tính

Để quy tắc trở nên hữu ích hơn, hãy thêm các thuộc tính mới bằng mô-đun attr và cập nhật định nghĩa quy tắc.

Thêm thuộc tính chuỗi có tên username:

foo_binary = rule(
    implementation = _foo_binary_impl,
    attrs = {
        "username": attr.string(),
    },
)

Tiếp theo, hãy thiết lập trong tệp BUILD:

foo_binary(
    name = "bin",
    username = "Alice",
)

Để truy cập vào giá trị trong hàm callback, sử dụng ctx.attr.username. Ví dụ:

def _foo_binary_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name)
    ctx.actions.write(
        output = out,
        content = "Hello {}!\n".format(ctx.attr.username),
    )
    return [DefaultInfo(files = depset([out]))]

Xin lưu ý rằng bạn có thể làm cho thuộc tính này trở thành thuộc tính bắt buộc hoặc đặt giá trị mặc định. Hãy xem tài liệu về attr.string. Bạn cũng có thể sử dụng các loại thuộc tính khác, chẳng hạn như boolean hoặc danh sách số nguyên.

Phần phụ thuộc

Các thuộc tính phần phụ thuộc, chẳng hạn như attr.labelattr.label_list, khai báo phần phụ thuộc từ mục tiêu sở hữu thuộc tính này cho mục tiêu có nhãn xuất hiện trong giá trị của thuộc tính. Loại thuộc tính này tạo thành cơ sở của biểu đồ mục tiêu.

Trong tệp BUILD, nhãn mục tiêu sẽ xuất hiện dưới dạng một đối tượng chuỗi, chẳng hạn như //pkg:name. Trong hàm triển khai, mục tiêu sẽ có thể truy cập được dưới dạng đối tượng Target. Ví dụ: xem các tệp mà mục tiêu trả về bằng cách sử dụng Target.files.

Nhiều tệp

Theo mặc định, chỉ những mục tiêu do quy tắc tạo ra mới có thể xuất hiện dưới dạng phần phụ thuộc (chẳng hạn như mục tiêu foo_library()). Nếu muốn thuộc tính chấp nhận các mục tiêu là tệp đầu vào (chẳng hạn như tệp nguồn trong kho lưu trữ), bạn có thể thực hiện việc này với allow_files và chỉ định danh sách đuôi tệp được chấp nhận (hoặc True để cho phép mọi đuôi tệp):

"srcs": attr.label_list(allow_files = [".java"]),

Bạn có thể truy cập danh sách tệp bằng ctx.files.<attribute name>. Ví dụ: danh sách các tệp trong thuộc tính srcs có thể được truy cập thông qua

ctx.files.srcs

Tệp đơn

Nếu bạn chỉ cần một tệp, hãy sử dụng allow_single_file:

"src": attr.label(allow_single_file = [".java"])

Sau đó, bạn có thể truy cập vào tệp này trong ctx.file.<attribute name>:

ctx.file.src

Tạo tệp bằng mẫu

Bạn có thể tạo một quy tắc để tạo tệp .cc dựa trên mẫu. Ngoài ra, bạn có thể sử dụng ctx.actions.write để xuất một chuỗi được tạo trong hàm triển khai quy tắc, nhưng việc này có hai vấn đề. Trước tiên, khi mẫu trở nên lớn hơn, bộ nhớ sẽ hiệu quả hơn nếu bạn đưa mẫu đó vào một tệp riêng và tránh tạo các chuỗi lớn trong giai đoạn phân tích. Thứ hai, việc sử dụng một tệp riêng sẽ thuận tiện hơn cho người dùng. Thay vào đó, hãy sử dụng ctx.actions.expand_template để thay thế tệp mẫu.

Tạo thuộc tính template để khai báo phần phụ thuộc trên tệp mẫu:

def _hello_world_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name + ".cc")
    ctx.actions.expand_template(
        output = out,
        template = ctx.file.template,
        substitutions = {"{NAME}": ctx.attr.username},
    )
    return [DefaultInfo(files = depset([out]))]

hello_world = rule(
    implementation = _hello_world_impl,
    attrs = {
        "username": attr.string(default = "unknown person"),
        "template": attr.label(
            allow_single_file = [".cc.tpl"],
            mandatory = True,
        ),
    },
)

Người dùng có thể sử dụng quy tắc như sau:

hello_world(
    name = "hello",
    username = "Alice",
    template = "file.cc.tpl",
)

cc_binary(
    name = "hello_bin",
    srcs = [":hello"],
)

Nếu không muốn hiển thị mẫu cho người dùng cuối và luôn sử dụng cùng một mẫu, bạn có thể đặt giá trị mặc định và đặt thuộc tính ở chế độ riêng tư:

    "_template": attr.label(
        allow_single_file = True,
        default = "file.cc.tpl",
    ),

Các thuộc tính bắt đầu bằng dấu gạch dưới là riêng tư và không thể đặt trong tệp BUILD. Mẫu hiện là một phần phụ thuộc ngầm ẩn: Mỗi mục tiêu hello_world đều có một phần phụ thuộc trên tệp này. Đừng quên hiển thị tệp này với các gói khác bằng cách cập nhật tệp BUILD và sử dụng exports_files:

exports_files(["file.cc.tpl"])

Tìm hiểu thêm