Quy tắc

Quy tắc xác định một loạt hành động mà Bazel thực hiện trên dữ liệu đầu vào để tạo ra một tập hợp đầu ra. Tập hợp này được tham chiếu trong nhà cung cấp do hàm triển khai của quy tắc trả về. Ví dụ: quy tắc nhị phân C++ có thể:

  1. Lấy một nhóm các tệp nguồn .cpp (dữ liệu đầu vào).
  2. Chạy g++ trên các tệp nguồn (hành động).
  3. Trả về trình cung cấp DefaultInfo cùng với dữ liệu đầu ra có thể thực thi và các tệp khác để cung cấp trong thời gian chạy.
  4. Trả về trình cung cấp CcInfo có thông tin dành riêng cho C++ được thu thập từ mục tiêu và các phần phụ thuộc của mục tiêu.

Từ góc độ của Bazel, g++ và các thư viện C++ tiêu chuẩn cũng là dữ liệu đầu vào cho quy tắc này. Là người viết quy tắc, bạn không chỉ xem xét dữ liệu đầu vào do người dùng cung cấp cho quy tắc, mà còn phải xem xét tất cả công cụ và thư viện cần thiết để thực thi các thao tác.

Trước khi tạo hoặc sửa đổi bất kỳ quy tắc nào, hãy đảm bảo bạn quen thuộc với các giai đoạn xây dựng của Bazel. Điều quan trọng là bạn phải hiểu 3 giai đoạn của quá trình tạo bản dựng (tải, phân tích và thực thi). Bạn cũng nên tìm hiểu về macro để hiểu được sự khác biệt giữa quy tắc và macro. Để bắt đầu, trước tiên, hãy xem Hướng dẫn về quy tắc. Sau đó, hãy dùng trang này làm tài liệu tham khảo.

Một số quy tắc được áp dụng cho chính Bazel. Các quy tắc gốc này, chẳng hạn như cc_libraryjava_binary, cung cấp một số hỗ trợ cốt lõi cho một số ngôn ngữ nhất định. Bằng cách xác định các quy tắc của riêng mình, bạn có thể thêm tính năng hỗ trợ tương tự cho các ngôn ngữ và công cụ mà Bazel không hỗ trợ sẵn.

Bazel cung cấp mô hình mở rộng để viết quy tắc bằng ngôn ngữ Starlark. Các quy tắc này được viết trong tệp .bzl và có thể tải trực tiếp từ các tệp BUILD.

Khi xác định quy tắc của riêng mình, bạn có thể quyết định quy tắc nào hỗ trợ thuộc tính và cách quy tắc tạo kết quả đầu ra.

Hàm implementation của quy tắc xác định hành vi chính xác trong giai đoạn phân tích. Hàm này không chạy bất kỳ lệnh bên ngoài nào. Thay vào đó, phương thức này đăng ký các hành động sẽ được dùng sau này trong giai đoạn thực thi để tạo kết quả đầu ra của quy tắc (nếu cần).

Tạo quy tắc

Trong tệp .bzl, hãy dùng hàm quy tắc để xác định quy tắc mới và lưu trữ kết quả trong biến toàn cục. Lệnh gọi đến rule chỉ định thuộc tínhhàm triển khai:

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

Thao tác này xác định một loại quy tắc có tên là example_library.

Lệnh gọi đến rule cũng phải chỉ định xem quy tắc sẽ tạo đầu ra có thể thực thi (với executable=True) hay cụ thể là một tệp thực thi kiểm thử (với test=True). Nếu quy tắc thứ hai là quy tắc kiểm thử và tên của quy tắc phải kết thúc bằng _test.

Tạo bản sao mục tiêu

Các quy tắc có thể được tải và gọi trong tệp BUILD:

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

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

Mỗi lệnh gọi đến một quy tắc tạo bản dựng sẽ không trả về giá trị nào, nhưng có tác dụng phụ là việc xác định mục tiêu. Đây được gọi là tạo thực thể cho quy tắc. Thuộc tính này chỉ định tên cho mục tiêu mới và các giá trị cho thuộc tính của mục tiêu.

Các quy tắc cũng có thể được gọi qua các hàm Starlark và tải trong tệp .bzl. Các hàm Starlark gọi quy tắc gọi là macro Starlark. Cuối cùng, macro Starlark phải được gọi từ tệp BUILD và chỉ có thể được gọi trong giai đoạn tải, khi các tệp BUILD được đánh giá để tạo thực thể mục tiêu.

Thuộc tính

Thuộc tính là đối số quy tắc. Thuộc tính có thể cung cấp giá trị cụ thể cho quá trình triển khai của một mục tiêu hoặc các thuộc tính đó có thể tham chiếu đến các mục tiêu khác, tạo ra biểu đồ về các phần phụ thuộc.

Các thuộc tính dành riêng cho quy tắc, chẳng hạn như srcs hoặc deps, được xác định bằng cách truyền một bản đồ từ tên thuộc tính đến giản đồ (được tạo bằng mô-đun attr) đến tham số attrs của rule. Các thuộc tính phổ biến, chẳng hạn như namevisibility, được ngầm thêm vào tất cả quy tắc. Các thuộc tính bổ sung được ngầm thêm vào các quy tắc thực thi và kiểm thử. Bạn không thể đưa các thuộc tính ngầm được thêm vào quy tắc vào từ điển được truyền đến attrs.

Thuộc tính phần phụ thuộc

Các quy tắc xử lý mã nguồn thường xác định các thuộc tính sau để xử lý nhiều loại phần phụ thuộc:

  • srcs chỉ định tệp nguồn được xử lý bởi hành động của mục tiêu. Thông thường, giản đồ thuộc tính chỉ định đuôi tệp nào được dự kiến cho loại tệp nguồn mà quy tắc xử lý. Quy tắc cho các ngôn ngữ có tệp tiêu đề thường chỉ định một thuộc tính hdrs riêng cho các tiêu đề do mục tiêu và đối tượng sử dụng xử lý.
  • deps chỉ định các phần phụ thuộc mã cho mục tiêu. Giản đồ thuộc tính phải chỉ định nhà cung cấp phần phụ thuộc đó phải cung cấp. (Ví dụ: cc_library cung cấp CcInfo.)
  • data chỉ định các tệp được cung cấp trong thời gian chạy cho bất kỳ tệp thực thi nào phụ thuộc vào mục tiêu. Việc này sẽ cho phép chỉ định các tệp tuỳ ý.
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),
        ...
    },
)

Đây là ví dụ về các thuộc tính phần phụ thuộc. Mọi thuộc tính chỉ định nhãn dữ liệu đầu vào (những thuộc tính được xác định bằng attr.label_list, attr.label hoặc attr.label_keyed_string_dict) sẽ chỉ định các phần phụ thuộc thuộc một loại nhất định giữa một mục tiêu và mục tiêu có nhãn (hoặc các đối tượng Label tương ứng) được liệt kê trong thuộc tính đó khi mục tiêu được xác định. Kho lưu trữ và có thể là cả đường dẫn cho các nhãn này được phân giải tương ứng với mục tiêu đã xác định.

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

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

Trong ví dụ này, other_target là phần phụ thuộc của my_target, do đó, other_target sẽ được phân tích trước tiên. Nếu có chu kỳ trong biểu đồ phần phụ thuộc của các mục tiêu thì đó là lỗi.

Thuộc tính riêng tư và phần phụ thuộc ngầm ẩn

Thuộc tính phần phụ thuộc có giá trị mặc định sẽ tạo ra một phần phụ thuộc ngầm ẩn. Thuộc tính này ngầm ẩn vì là một phần của biểu đồ mục tiêu mà người dùng không chỉ định trong tệp BUILD. Các phần phụ thuộc ngầm ẩn rất hữu ích cho việc mã hoá cứng mối quan hệ giữa một quy tắc và một công cụ (phần phụ thuộc thời gian xây dựng, chẳng hạn như trình biên dịch), vì hầu hết người dùng không quan tâm đến việc chỉ định công cụ mà quy tắc sử dụng. Bên trong hàm triển khai của quy tắc, hàm này được xử lý giống như các phần phụ thuộc khác.

Nếu muốn cung cấp một phần phụ thuộc ngầm ẩn mà không cho phép người dùng ghi đè giá trị đó, thì bạn có thể đặt thuộc tính này ở chế độ riêng tư bằng cách đặt tên bắt đầu bằng dấu gạch dưới (_). Các thuộc tính riêng tư phải có giá trị mặc định. Nhìn chung, bạn chỉ nên sử dụng các thuộc tính riêng tư cho các phần phụ thuộc ngầm ẩn.

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

Trong ví dụ này, mọi mục tiêu của loại example_library đều có một phần phụ thuộc ngầm định vào trình biên dịch //tools:example_compiler. Điều này cho phép hàm triển khai của example_library tạo các thao tác gọi trình biên dịch, ngay cả khi người dùng không chuyển nhãn dưới dạng dữ liệu đầu vào. Vì _compiler là một thuộc tính riêng tư, nên ctx.attr._compiler sẽ luôn trỏ đến //tools:example_compiler trong mọi mục tiêu của loại quy tắc này. Ngoài ra, bạn có thể đặt tên cho thuộc tính là compiler mà không dùng dấu gạch dưới và giữ giá trị mặc định. Điều này cho phép người dùng thay thế một trình biên dịch khác nếu cần, nhưng không cần biết nhãn của trình biên dịch.

Các phần phụ thuộc ngầm ẩn thường được dùng cho các công cụ nằm trong cùng một kho lưu trữ với quá trình triển khai quy tắc. Nếu công cụ đến từ nền tảng thực thi hoặc một kho lưu trữ khác, quy tắc này sẽ lấy công cụ đó từ một chuỗi công cụ.

Thuộc tính đầu ra

Các thuộc tính đầu ra, chẳng hạn như attr.outputattr.output_list, khai báo tệp đầu ra mà mục tiêu tạo ra. Những thuộc tính này khác với các thuộc tính phần phụ thuộc theo 2 cách:

  • Các mục tiêu này xác định các mục tiêu tệp đầu ra thay vì tham chiếu đến các mục tiêu được xác định ở nơi khác.
  • Các mục tiêu tệp đầu ra phụ thuộc vào mục tiêu quy tắc được tạo bản sao, thay vì ngược lại.

Thông thường, các thuộc tính đầu ra chỉ được dùng khi một quy tắc cần tạo kết quả có tên do người dùng xác định mà không thể dựa trên tên mục tiêu. Nếu quy tắc có một thuộc tính đầu ra, thì thuộc tính đó thường có tên là out hoặc outs.

Thuộc tính đầu ra là cách ưu tiên để tạo kết quả được khai báo trước. Các thuộc tính này có thể phụ thuộc cụ thể vào hoặc được yêu cầu tại dòng lệnh.

Hàm triển khai

Mỗi quy tắc yêu cầu hàm implementation. Các hàm này được thực thi nghiêm ngặt trong giai đoạn phân tích và biến đổi biểu đồ của các mục tiêu được tạo trong giai đoạn tải thành biểu đồ hành động sẽ được thực hiện trong giai đoạn thực thi. Do đó, các hàm triển khai không thể đọc hoặc ghi tệp.

Các hàm triển khai quy tắc thường ở chế độ riêng tư (được đặt tên bằng dấu gạch dưới ở đầu). Thông thường, các quy tắc này có tên giống với quy tắc tương ứng, nhưng có hậu tố là _impl.

Các hàm triển khai nhận chính xác một tham số: ngữ cảnh quy tắc, thường được đặt tên là ctx. Phương thức này trả về danh sách trình cung cấp.

Mục tiêu

Các phần phụ thuộc được biểu thị tại thời điểm phân tích dưới dạng đối tượng Target. Các đối tượng này chứa nhà cung cấp được tạo khi hàm triển khai của mục tiêu được thực thi.

ctx.attr có các trường tương ứng với tên của từng thuộc tính phần phụ thuộc, chứa các đối tượng Target đại diện cho từng phần phụ thuộc trực tiếp thông qua thuộc tính đó. Đối với các thuộc tính label_list, đây là danh sách Targets. Đối với các thuộc tính label, đây là một Target hoặc None.

Hàm triển khai của mục tiêu sẽ trả về danh sách đối tượng nhà cung cấp:

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

Bạn có thể truy cập các tệp đó bằng cách sử dụng ký hiệu chỉ mục ([]), với loại trình cung cấp là khoá. Đây có thể là nhà cung cấp tuỳ chỉnh được xác định trong Starlark hoặc những nhà cung cấp cho các quy tắc gốc có sẵn dưới dạng biến chung Starlark.

Ví dụ: nếu một quy tắc lấy các tệp tiêu đề thông qua thuộc tính hdrs và cung cấp các tệp đó cho các thao tác biên dịch của mục tiêu và đối tượng sử dụng, thì quy tắc đó có thể thu thập các tệp đó như sau:

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

Đối với kiểu cũ mà trong đó struct được hàm triển khai của mục tiêu trả về thay vì danh sách đối tượng nhà cung cấp:

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

Bạn có thể truy xuất trình cung cấp từ trường tương ứng của đối tượng Target:

transitive_headers = [hdr.example_info.headers for hdr in ctx.attr.hdrs]

Bạn không nên sử dụng kiểu này và bạn nên di chuyển các quy tắc khỏi kiểu này.

Files

Tệp được biểu thị bằng đối tượng File. Vì Bazel không thực hiện tệp I/O trong giai đoạn phân tích, nên không thể dùng các đối tượng này để trực tiếp đọc hoặc ghi nội dung tệp. Thay vào đó, chúng được truyền đến các hàm phát hành động (xem ctx.actions) để tạo các phần của biểu đồ hành động.

File có thể là tệp nguồn hoặc tệp được tạo. Mỗi tệp được tạo phải là dữ liệu đầu ra của đúng một hành động. Tệp nguồn không được là kết quả của bất kỳ hành động nào.

Đối với mỗi thuộc tính phần phụ thuộc, trường tương ứng của ctx.files chứa danh sách kết quả đầu ra mặc định của tất cả các phần phụ thuộc thông qua thuộc tính đó:

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

ctx.file chứa một File hoặc None duy nhất cho các thuộc tính phần phụ thuộc có thông số được đặt allow_single_file=True. ctx.executable hoạt động giống như ctx.file, nhưng chỉ chứa các trường cho các thuộc tính phần phụ thuộc có bộ thông số kỹ thuật executable=True.

Khai báo kết quả đầu ra

Trong giai đoạn phân tích, chức năng triển khai của quy tắc có thể tạo ra kết quả. Vì tất cả các nhãn phải được xác định trong giai đoạn tải, nên các đầu ra bổ sung này sẽ không có nhãn. Bạn có thể tạo đối tượng File cho đầu ra bằng cách sử dụng ctx.actions.declare_filectx.actions.declare_directory. Thông thường, tên của dữ liệu đầu ra dựa trên tên của mục tiêu, ctx.label.name:

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

Đối với đầu ra được khai báo trước, như các đầu ra được tạo cho thuộc tính đầu ra, bạn có thể truy xuất đối tượng File qua các trường tương ứng của ctx.outputs.

Thao tác

Một thao tác mô tả cách tạo một tập hợp đầu ra từ một tập hợp dữ liệu đầu vào, ví dụ: "chạy gcc trên hello.c và nhận hello.o". Khi một thao tác được tạo, Bazel không chạy lệnh ngay lập tức. Phương thức này đăng ký nó trong biểu đồ của các phần phụ thuộc, vì một thao tác có thể phụ thuộc vào kết quả của một thao tác khác. Ví dụ: trong C, trình liên kết phải được gọi sau trình biên dịch.

Các hàm mục đích chung tạo thao tác được xác định trong ctx.actions:

Bạn có thể sử dụng ctx.actions.args để tích luỹ một cách hiệu quả các đối số cho hành động. Điều này giúp tránh làm phẳng các phần phụ thuộc cho đến thời điểm thực thi:

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],
    )
    ...

Các thao tác sẽ lấy một danh sách hoặc tập phụ thuộc các tệp đầu vào rồi tạo một danh sách tệp đầu ra (không trống). Bạn phải xác định tập hợp các tệp đầu vào và đầu ra trong giai đoạn phân tích. Điều này có thể phụ thuộc vào giá trị của các thuộc tính, bao gồm cả trình cung cấp từ các phần phụ thuộc, nhưng không thể phụ thuộc vào kết quả thực thi. Ví dụ: nếu hành động của bạn chạy lệnh giải nén, thì bạn phải chỉ định những tệp mà bạn muốn sẽ được tăng cường (trước khi chạy giải nén). Các thao tác tạo ra số lượng tệp thay đổi nội bộ có thể gói những tệp đó trong một tệp duy nhất (chẳng hạn như zip, tar hoặc định dạng lưu trữ khác).

Hành động phải liệt kê tất cả dữ liệu đầu vào. Việc nhập danh sách không được sử dụng là được phép, nhưng không hiệu quả.

Hành động phải tạo tất cả đầu ra của chúng. Họ có thể ghi các tệp khác, nhưng bất kỳ tệp nào không có trong dữ liệu đầu ra sẽ không được cung cấp cho người dùng. Tất cả dữ liệu đầu ra đã khai báo phải được ghi bằng một số thao tác.

Các thao tác tương đương với hàm thuần tuý: Các thao tác này chỉ nên phụ thuộc vào dữ liệu đầu vào được cung cấp, đồng thời tránh truy cập vào thông tin máy tính, tên người dùng, đồng hồ, mạng hoặc thiết bị I/O (ngoại trừ hoạt động đọc dữ liệu đầu vào và dữ liệu đầu ra). Việc này rất quan trọng vì kết quả sẽ được lưu vào bộ nhớ đệm và sử dụng lại.

Các phần phụ thuộc sẽ do Bazel phân giải. Điều này sẽ quyết định thao tác nào được thực thi. Sẽ xảy ra lỗi nếu có chu kỳ trong biểu đồ phần phụ thuộc. Việc tạo một thao tác không đảm bảo rằng thao tác đó sẽ được thực thi, mà phụ thuộc vào việc liệu đầu ra của thao tác đó có cần thiết cho bản dựng hay không.

Nhà cung cấp

Trình cung cấp là những thông tin mà quy tắc hiển thị cho các quy tắc khác phụ thuộc vào nó. Dữ liệu này có thể bao gồm các tệp đầu ra, thư viện, tham số cần chuyển vào dòng lệnh của công cụ hoặc bất cứ điều gì khác mà người tiêu dùng mục tiêu nên biết.

Vì hàm triển khai quy tắc chỉ có thể đọc trình cung cấp từ các phần phụ thuộc tức thì của mục tiêu đã khởi tạo, nên các quy tắc cần chuyển tiếp mọi thông tin từ các phần phụ thuộc của mục tiêu mà người dùng của mục tiêu cần biết, nói chung bằng cách tích luỹ thông tin đó vào depset.

Các nhà cung cấp của mục tiêu được chỉ định bởi danh sách đối tượng Provider do hàm triển khai trả về.

Bạn cũng có thể viết các hàm triển khai cũ theo kiểu cũ, trong đó hàm triển khai sẽ trả về struct thay vì danh sách đối tượng nhà cung cấp. Bạn không nên sử dụng kiểu này và bạn nên di chuyển các quy tắc khỏi kiểu này.

Đầu ra mặc định

Đầu ra mặc định của mục tiêu là đầu ra được yêu cầu theo mặc định khi mục tiêu được yêu cầu tạo bản dựng ở dòng lệnh. Ví dụ: //pkg:foo của mục tiêu java_libraryfoo.jar làm đầu ra mặc định nên sẽ được tạo bằng lệnh bazel build //pkg:foo.

Dữ liệu đầu ra mặc định được chỉ định bằng tham số files của DefaultInfo:

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

Nếu DefaultInfo không được quá trình triển khai quy tắc trả về hoặc tham số files không được chỉ định, thì DefaultInfo.files sẽ mặc định là tất cả đầu ra được khai báo trước (thường là những kết quả được tạo bởi thuộc tính đầu ra).

Các quy tắc thực hiện thao tác sẽ cung cấp kết quả đầu ra mặc định, ngay cả khi các kết quả đó không dự kiến sẽ được sử dụng trực tiếp. Các thao tác không có trong biểu đồ của dữ liệu đầu ra được yêu cầu sẽ bị cắt bớt. Nếu chỉ có người dùng của một mục tiêu sử dụng đầu ra, thì các thao tác đó sẽ không được thực hiện khi mục tiêu được tạo riêng biệt. Điều này khiến việc gỡ lỗi khó khăn hơn vì chỉ tạo lại mục tiêu không thành công sẽ không tái hiện lỗi.

Tệp Run

Runfile là một tập hợp các tệp mà một mục tiêu sử dụng trong thời gian chạy (thay vì thời gian tạo bản dựng). Trong giai đoạn thực thi, Bazel tạo một cây thư mục chứa các đường liên kết tượng trưng trỏ đến các tệp runfile. Thao tác này phân chia môi trường cho tệp nhị phân để tệp có thể truy cập vào các tệp chạy trong thời gian chạy.

Bạn có thể thêm các tệp Runfile theo cách thủ công trong quá trình tạo quy tắc. Bạn có thể tạo đối tượng runfiles bằng phương thức runfiles trong ngữ cảnh quy tắc ctx.runfiles và truyền vào tham số runfiles trên DefaultInfo. Đầu ra có thể thực thi của các quy tắc có thể thực thi được ngầm thêm vào các tệp chạy.

Một số quy tắc chỉ định các thuộc tính, thường được đặt tên là data. Kết quả này được thêm vào các tệp chạy của mục tiêu. Bạn cũng nên hợp nhất Runfile từ data, cũng như từ bất kỳ thuộc tính nào có thể cung cấp mã để thực thi sau cùng, thường là srcs (có thể chứa các mục tiêu filegroup với data được liên kết) và deps.

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),
        ...
    ]

Nhà cung cấp tuỳ chỉnh

Bạn có thể xác định ứng dụng cung cấp bằng cách sử dụng hàm provider để truyền đạt thông tin cụ thể theo quy tắc:

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.",
    })

Sau đó, các hàm triển khai quy tắc có thể tạo và trả về các phiên bản của nhà cung cấp:

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
              ],
          ),
      )
  ]
Khởi chạy tuỳ chỉnh trình cung cấp

Bạn có thể bảo vệ quá trình tạo bản sao của trình cung cấp bằng logic xác thực và xử lý trước tuỳ chỉnh. Bạn có thể sử dụng thuộc tính này để đảm bảo rằng tất cả các thực thể của nhà cung cấp đều tuân theo một số bất biến nhất định, hoặc cung cấp cho người dùng một API rõ ràng hơn để thu được một thực thể.

Bạn có thể thực hiện việc này bằng cách truyền một lệnh gọi lại init đến hàm provider. Nếu bạn cung cấp lệnh gọi lại này, kiểu dữ liệu trả về của provider() sẽ thay đổi thành một bộ gồm hai giá trị: biểu tượng nhà cung cấp là giá trị trả về thông thường khi không dùng init và "hàm khởi tạo thô".

Trong trường hợp này, khi biểu tượng nhà cung cấp được gọi, thay vì trực tiếp trả về một thực thể mới, hệ thống sẽ chuyển tiếp các đối số cùng với lệnh gọi lại init. Giá trị trả về của lệnh gọi lại phải là tên trường ánh xạ chính tả (chuỗi) đến các giá trị; giá trị này dùng để khởi tạo các trường của thực thể mới. Lưu ý rằng lệnh gọi lại có thể có bất kỳ chữ ký nào và nếu các đối số không khớp với chữ ký, lỗi sẽ được báo cáo như thể lệnh gọi lại được gọi trực tiếp.

Ngược lại, hàm khởi tạo thô sẽ bỏ qua lệnh gọi lại init.

Ví dụ sau đây sử dụng init để xử lý trước và xác thực các đối số:

# //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

Sau đó, việc triển khai quy tắc có thể tạo thực thể của trình cung cấp như sau:

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

Hàm khởi tạo thô có thể được dùng để xác định các hàm nhà máy công khai thay thế không đi qua logic init. Ví dụ: trong exampleinfo.bzl, chúng tôi có thể xác định:

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)

Thông thường, hàm khởi tạo thô được liên kết với một biến có tên bắt đầu bằng dấu gạch dưới (_new_exampleinfo ở trên). Do đó, mã người dùng không thể tải biến đó và tạo các thực thể tuỳ ý của nhà cung cấp.

Một mục đích sử dụng khác của init là ngăn người dùng gọi toàn bộ biểu tượng trình cung cấp và buộc họ sử dụng hàm nhà máy:

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(...)

Quy tắc thực thi và quy tắc kiểm thử

Các quy tắc có thể thực thi xác định các mục tiêu có thể gọi bằng lệnh bazel run. Quy tắc kiểm thử là một loại quy tắc thực thi đặc biệt mà các mục tiêu cũng có thể được gọi bằng lệnh bazel test. Các quy tắc thực thi và quy tắc kiểm thử được tạo bằng cách đặt đối số executable hoặc test tương ứng thành True trong lệnh gọi đến rule:

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

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

Quy tắc kiểm thử phải có tên kết thúc bằng _test. (Tên mục tiêu kiểm thử cũng thường kết thúc bằng _test theo quy ước, nhưng điều này là không bắt buộc.) Quy tắc không kiểm thử không được có hậu tố này.

Cả hai loại quy tắc đều phải tạo một tệp đầu ra có thể thực thi (có thể được hoặc không được khai báo trước) và sẽ được gọi bằng các lệnh run hoặc test. Để cho Bazel biết kết quả nào của quy tắc sẽ sử dụng khi thực thi này, hãy truyền dữ liệu đó dưới dạng đối số executable của nhà cung cấp DefaultInfo được trả về. executable đó được thêm vào dữ liệu đầu ra mặc định của quy tắc (vì vậy, bạn không cần truyền dữ liệu đó vào cả executablefiles). Mã này cũng được ngầm thêm vào runfiles:

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

Tác vụ tạo tệp này phải đặt bit có thể thực thi trên tệp. Đối với một thao tác ctx.actions.run hoặc ctx.actions.run_shell, bạn nên thực hiện việc này bằng công cụ cơ bản đã được thao tác đó gọi. Đối với thao tác ctx.actions.write, hãy truyền is_executable=True.

Do hành vi cũ, các quy tắc thực thi có đầu ra đặc biệt được khai báo trước ctx.outputs.executable. Tệp này đóng vai trò là tệp thực thi mặc định nếu bạn không chỉ định tệp bằng DefaultInfo; bạn không được sử dụng tệp này nếu không có tệp thực thi. Cơ chế đầu ra này không được dùng nữa vì không hỗ trợ tuỳ chỉnh tên của tệp thực thi tại thời điểm phân tích.

Xem ví dụ về quy tắc có thể thực thiquy tắc kiểm thử.

Quy tắc thực thiquy tắc kiểm thử có các thuộc tính bổ sung được xác định ngầm, ngoài các thuộc tính được thêm cho tất cả quy tắc. Bạn không thể thay đổi giá trị mặc định của các thuộc tính được thêm ngầm, mặc dù bạn có thể khắc phục bằng cách gói một quy tắc riêng tư trong macro Starlark để thay đổi mặc định:

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

_example_test = rule(
 ...
)

Vị trí Runfiles

Khi một mục tiêu thực thi được chạy bằng bazel run (hoặc test), gốc của thư mục runfiles sẽ nằm cạnh tệp thực thi. Các đường dẫn này liên quan như sau:

# Given launcher_path and runfile_file:
runfiles_root = launcher_path.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)

Đường dẫn đến một File trong thư mục runfiles tương ứng với File.short_path.

Tệp nhị phân do bazel thực thi trực tiếp nằm gần gốc của thư mục runfiles. Tuy nhiên, các tệp nhị phân được gọi từ các tệp chạy không thể đưa ra giả định tương tự. Để giảm thiểu điều này, mỗi tệp nhị phân phải đưa ra một cách để chấp nhận gốc runfile của nó làm tham số bằng cách sử dụng môi trường hoặc đối số/cờ của dòng lệnh. Điều này cho phép các tệp nhị phân chuyển đúng thư mục gốc runfile chuẩn vào các tệp nhị phân mà nó gọi. Nếu bạn không đặt giá trị này, một tệp nhị phân có thể đoán rằng đó là tệp nhị phân đầu tiên được gọi và tìm một thư mục runfiles liền kề.

Chủ đề nâng cao

Đang yêu cầu tệp đầu ra

Một mục tiêu có thể có nhiều tệp đầu ra. Khi chạy lệnh bazel build, một số kết quả đầu ra của các mục tiêu được cung cấp cho lệnh đó sẽ được coi là được yêu cầu. Bazel chỉ tạo các tệp được yêu cầu này và những tệp mà chúng phụ thuộc trực tiếp hoặc gián tiếp. (Xét về biểu đồ thao tác, Bazel chỉ thực thi các thao tác có thể truy cập dưới dạng phần phụ thuộc bắc cầu của các tệp được yêu cầu.)

Ngoài các kết quả mặc định, bạn có thể yêu cầu rõ ràng mọi đầu ra được khai báo trước trên dòng lệnh. Các quy tắc có thể chỉ định đầu ra được khai báo trước thông qua thuộc tính đầu ra. Trong trường hợp đó, người dùng chọn nhãn cho kết quả một cách rõ ràng khi tạo thực thể cho quy tắc. Để lấy các đối tượng File cho thuộc tính đầu ra, hãy sử dụng thuộc tính tương ứng của ctx.outputs. Các quy tắc cũng có thể xác định ngầm các kết quả được khai báo trước dựa trên tên mục tiêu, nhưng tính năng này sẽ không được dùng nữa.

Ngoài các kết quả đầu ra mặc định, còn có các nhóm đầu ra, là các tập hợp các tệp đầu ra có thể được yêu cầu cùng nhau. Bạn có thể yêu cầu các dữ liệu này bằng --output_groups. Ví dụ: nếu //pkg:mytarget mục tiêu thuộc loại quy tắc có nhóm đầu ra debug_files, thì bạn có thể tạo các tệp này bằng cách chạy bazel build //pkg:mytarget --output_groups=debug_files. Vì dữ liệu đầu ra không được khai báo trước sẽ không có nhãn nên bạn chỉ có thể yêu cầu chúng bằng cách xuất hiện trong dữ liệu đầu ra mặc định hoặc nhóm đầu ra.

Bạn có thể chỉ định nhóm đầu ra bằng trình cung cấp OutputGroupInfo. Lưu ý rằng không giống như nhiều trình cung cấp tích hợp sẵn, OutputGroupInfo có thể nhận các tham số có tên tuỳ ý để xác định các nhóm đầu ra có tên đó:

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]),
        ),
        ...
    ]

Ngoài ra, không giống như hầu hết các trình cung cấp, OutputGroupInfo có thể được trả về theo cả một khía cạnh và mục tiêu quy tắc được áp dụng cho khía cạnh đó, miễn là các thành phần này không xác định cùng một nhóm đầu ra. Trong trường hợp đó, các trình cung cấp thu được sẽ được hợp nhất.

Lưu ý rằng bạn không nên dùng OutputGroupInfo thường để truyền tải các loại tệp cụ thể từ một mục tiêu đến hành động của đối tượng sử dụng. Thay vào đó, hãy xác định trình cung cấp theo quy tắc cụ thể cho trường hợp đó.

Cấu hình

Giả sử bạn muốn tạo tệp nhị phân C++ cho một kiến trúc khác. Quá trình tạo bản dựng có thể phức tạp và bao gồm nhiều bước. Một số tệp nhị phân trung gian, như trình biên dịch và trình tạo mã, phải chạy trên nền tảng thực thi (có thể là máy chủ lưu trữ hoặc trình thực thi từ xa). Một số tệp nhị phân (như dữ liệu đầu ra cuối cùng) phải được tạo cho cấu trúc mục tiêu.

Vì lý do này, Bazel có khái niệm về "cấu hình" và chuyển đổi. Các mục tiêu trên cùng (các mục tiêu được yêu cầu trên dòng lệnh) được tạo trong cấu hình "mục tiêu", trong khi các công cụ chạy trên nền tảng thực thi được tạo trong cấu hình "executor". Các quy tắc có thể tạo nhiều thao tác dựa trên cấu hình, chẳng hạn như thay đổi cấu trúc cpu được truyền đến trình biên dịch. Trong một số trường hợp, bạn có thể cần cùng một thư viện cho các cấu hình khác nhau. Nếu điều này xảy ra, ứng dụng sẽ được phân tích và có thể được tạo nhiều lần.

Theo mặc định, Bazel tạo các phần phụ thuộc của mục tiêu trong cùng một cấu hình như chính mục tiêu đó, nói cách khác là không cần chuyển đổi. Khi phần phụ thuộc là một công cụ cần thiết để giúp xây dựng mục tiêu, thuộc tính tương ứng sẽ chỉ định việc chuyển đổi sang cấu hình executor. Điều này giúp công cụ và tất cả các phần phụ thuộc của công cụ đó được xây dựng cho nền tảng thực thi.

Đối với mỗi thuộc tính phần phụ thuộc, bạn có thể sử dụng cfg để quyết định xem các phần phụ thuộc nên tạo trong cùng một cấu hình hay chuyển đổi sang cấu hình thực thi. Nếu một thuộc tính phần phụ thuộc có cờ executable=True, thì bạn phải đặt cfg một cách rõ ràng. Điều này giúp tránh việc vô tình tạo một công cụ cho cấu hình không chính xác. Xem ví dụ

Nhìn chung, các nguồn, thư viện phụ thuộc và tệp thực thi cần thiết trong thời gian chạy có thể sử dụng cùng một cấu hình.

Bạn phải xây dựng các công cụ được thực thi trong bản dựng (chẳng hạn như trình biên dịch hoặc trình tạo mã) cho cấu hình thực thi. Trong trường hợp này, hãy chỉ định cfg="exec" trong thuộc tính.

Nếu không, bạn phải tạo các tệp thực thi được sử dụng trong thời gian chạy (chẳng hạn như một phần của bài kiểm thử) cho cấu hình mục tiêu. Trong trường hợp này, hãy chỉ định cfg="target" trong thuộc tính.

cfg="target" không thực sự làm gì cả: đây đơn thuần là giá trị tiện lợi để giúp nhà thiết kế quy tắc thể hiện rõ ý định của mình. Khi executable=False, tức là cfg là không bắt buộc, hãy chỉ đặt giá trị này khi thuộc tính này thực sự giúp dễ đọc.

Bạn cũng có thể dùng cfg=my_transition để sử dụng các lượt chuyển đổi do người dùng xác định. Điều này cho phép tác giả quy tắc rất linh hoạt trong việc thay đổi cấu hình, với hạn chế là làm cho biểu đồ bản dựng lớn hơn và khó hiểu hơn.

Lưu ý: Trước đây, Bazel không có khái niệm về nền tảng thực thi mà thay vào đó, mọi hành động tạo bản dựng đều được xem là chạy trên máy chủ. Do đó, chỉ có một cấu hình "host" (máy chủ) và một lượt chuyển đổi "host" (máy chủ) có thể dùng để tạo một phần phụ thuộc trong cấu hình máy chủ lưu trữ. Nhiều quy tắc vẫn sử dụng quá trình chuyển đổi "host" (máy chủ) cho các công cụ của họ. Tuy nhiên, tính năng này hiện không được dùng nữa và được di chuyển để sử dụng quá trình chuyển đổi "exec" khi có thể.

Có nhiều điểm khác biệt giữa cấu hình "host" và "exec":

  • "host" là thiết bị đầu cuối, còn "exec" thì không: Sau khi một phần phụ thuộc nằm trong cấu hình "host" (máy chủ), bạn không được phép chuyển đổi nữa. Bạn có thể tiếp tục chuyển đổi cấu hình thêm sau khi đang ở cấu hình "exec".
  • "host" là một khối, "exec" thì không: Chỉ có một cấu hình "host" nhưng có thể có một cấu hình "exec" khác nhau cho mỗi nền tảng thực thi.
  • "host" giả định bạn chạy các công cụ trên cùng một máy như Bazel hoặc trên một máy tương tự đáng kể. Điều này không còn đúng: bạn có thể chạy các hành động tạo bản dựng trên máy cục bộ hoặc trên trình thực thi từ xa và không có gì đảm bảo rằng trình thực thi từ xa có cùng CPU và hệ điều hành với máy cục bộ của bạn.

Cả cấu hình "exec" và "host" đều áp dụng các thay đổi giống nhau cho tuỳ chọn (ví dụ: đặt --compilation_mode từ --host_compilation_mode, đặt --cpu từ --host_cpu, v.v.). Điểm khác biệt là cấu hình "host" bắt đầu bằng giá trị default của tất cả các cờ khác, trong khi cấu hình "exec" bắt đầu bằng giá trị hiện tại của cờ, dựa trên cấu hình mục tiêu.

Các mảnh cấu hình

Quy tắc có thể truy cập vào các mảnh cấu hình, chẳng hạn như cpp, javajvm. Tuy nhiên, bạn phải khai báo tất cả các mảnh bắt buộc để tránh lỗi truy cập:

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 chỉ cung cấp các mảnh cấu hình cho cấu hình mục tiêu. Nếu bạn muốn truy cập các mảnh cho cấu hình máy chủ lưu trữ, hãy sử dụng ctx.host_fragments.

Thông thường, đường dẫn tương đối của một tệp trong cây runfiles giống với đường dẫn tương đối của tệp đó trong cây nguồn hoặc cây đầu ra được tạo. Nếu những đối số này cần khác nhau vì lý do nào đó, bạn có thể chỉ định đối số root_symlinks hoặc symlinks. root_symlinks là một đường dẫn ánh xạ từ điển đến các tệp, trong đó các đường dẫn này tương ứng với gốc của thư mục runfiles. Từ điển symlinks cũng giống như trên, nhưng các đường dẫn được ngầm định sẵn bằng tên của không gian làm việc.

    ...
    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

Nếu bạn sử dụng symlinks hoặc root_symlinks, hãy cẩn thận để không ánh xạ hai tệp khác nhau đến cùng một đường dẫn trong cây runfiles. Điều này sẽ khiến bản dựng không hoạt động kèm theo lỗi mô tả xung đột. Để khắc phục, bạn cần sửa đổi các đối số ctx.runfiles để loại bỏ xung đột. Quá trình kiểm tra này sẽ được thực hiện đối với mọi mục tiêu đang sử dụng quy tắc của bạn, cũng như các mục tiêu thuộc bất kỳ loại nào phụ thuộc vào các mục tiêu đó. Điều này đặc biệt rủi ro nếu một công cụ khác có thể sẽ sử dụng công cụ này theo cách bắc cầu; tên của đường liên kết tượng trưng phải là duy nhất trong các tệp tin của công cụ và tất cả các phần phụ thuộc của công cụ đó.

Mức độ sử dụng mã

Khi chạy lệnh coverage, bản dựng có thể cần thêm khả năng đo lường mức độ sử dụng cho một số mục tiêu nhất định. Bản dựng cũng thu thập danh sách các tệp nguồn được đo lường. Tập hợp con các mục tiêu được xem là do cờ --instrumentation_filter kiểm soát. Các mục tiêu kiểm thử sẽ bị loại trừ, trừ phi bạn chỉ định --instrument_test_targets.

Nếu quá trình triển khai quy tắc thêm khả năng đo lường mức độ phù hợp tại thời điểm xây dựng, thì quá trình đó cần tính đến điều đó trong hàm triển khai. ctx.coverage_instrumented sẽ trả về giá trị true trong chế độ bao phủ nếu các nguồn của mục tiêu được đo lường:

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

Logic luôn cần bật ở chế độ mức độ sử dụng (cho dù các nguồn của mục tiêu cụ thể có được đo lường hay không) đều có thể được điều chỉnh trên ctx.configuration.coverage_enabled.

Nếu quy tắc trực tiếp sử dụng các nguồn từ các phần phụ thuộc trước khi biên dịch (chẳng hạn như tệp tiêu đề), thì quy tắc đó cũng có thể cần phải bật tính năng đo lường thời gian biên dịch nếu nguồn của phần phụ thuộc cần được đo lường:

# 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

Các quy tắc cũng cần cung cấp thông tin về những thuộc tính liên quan đến mức độ phù hợp với trình cung cấp InstrumentedFilesInfo, được tạo bằng coverage_common.instrumented_files_info. Tham số dependency_attributes của instrumented_files_info sẽ liệt kê tất cả các thuộc tính phần phụ thuộc thời gian chạy, bao gồm cả các phần phụ thuộc mã như deps và các phần phụ thuộc dữ liệu như data. Tham số source_attributes phải liệt kê các thuộc tính tệp nguồn của quy tắc nếu bạn có thể thêm đo lường mức độ sử dụng:

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"],
        )
        ...
    ]

Nếu InstrumentedFilesInfo không được trả về, hệ thống sẽ tạo một thuộc tính mặc định bằng mỗi thuộc tính phần phụ thuộc không phải công cụ và không đặt cfg thành "host" hoặc "exec" trong giản đồ thuộc tính) trong dependency_attributes. (Đây không phải là hành vi lý tưởng vì phương thức này sẽ đặt các thuộc tính như srcs trong dependency_attributes thay vì source_attributes, nhưng sẽ tránh được nhu cầu định cấu hình mức độ bao phủ rõ ràng cho tất cả quy tắc trong chuỗi phần phụ thuộc.)

Hành động xác thực

Đôi khi, bạn cần xác thực một nội dung nào đó về bản dựng, và thông tin cần thiết để thực hiện quá trình xác thực đó chỉ có trong các cấu phần phần mềm (tệp nguồn hoặc tệp được tạo). Vì thông tin này nằm trong cấu phần phần mềm, nên các quy tắc không thể thực hiện việc xác thực này tại thời điểm phân tích vì các quy tắc không thể đọc tệp. Thay vào đó, các hành động phải xác thực việc này tại thời điểm thực thi. Khi xác thực không thành công, hành động sẽ không thành công và do đó, bản dựng cũng vậy.

Ví dụ về quy trình xác thực có thể chạy là phân tích tĩnh, tìm lỗi mã nguồn, kiểm tra phần phụ thuộc và tính nhất quán, cũng như kiểm tra kiểu.

Các thao tác xác thực cũng có thể giúp cải thiện hiệu suất của bản dựng bằng cách chuyển các phần của hành động không cần thiết để tạo cấu phần phần mềm thành các hành động riêng biệt. Ví dụ: nếu có thể tách một hành động biên dịch và tìm lỗi mã nguồn thành một hành động biên dịch và một hành động tìm lỗi mã nguồn, thì hành động tìm lỗi mã nguồn đó có thể được chạy dưới dạng hành động xác thực và chạy song song với các hành động khác.

Các "thao tác xác thực" này thường không tạo ra bất kỳ dữ liệu nào được dùng ở nơi khác trong bản dựng, vì chúng chỉ cần xác nhận mọi thông tin về dữ liệu đầu vào. Tuy nhiên, điều này sẽ gây ra vấn đề: Nếu một thao tác xác thực không tạo ra bất kỳ nội dung nào dùng ở nơi khác trong bản dựng, thì quy tắc sẽ thực hiện thao tác đó bằng cách nào? Trước đây, phương pháp tiếp cận là để hành động xác thực xuất ra một tệp trống và thêm đầu ra đó một cách giả tạo vào dữ liệu đầu vào của một số hành động quan trọng khác trong bản dựng:

Cách này hiệu quả vì Bazel sẽ luôn chạy thao tác xác thực khi thao tác biên dịch đang chạy, nhưng điều này có một số hạn chế đáng kể:

  1. Hành động xác thực nằm trong đường dẫn quan trọng của bản dựng. Vì Bazel cho rằng cần phải có đầu ra trống để chạy thao tác biên dịch, nên Bazel sẽ chạy thao tác xác thực trước tiên, mặc dù thao tác biên dịch sẽ bỏ qua đầu vào. Điều này làm giảm hiện tượng tải song song và làm chậm quá trình tạo bản dựng.

  2. Nếu các hành động khác trong bản dựng có thể chạy thay vì hành động biên dịch, thì bạn cũng cần thêm các kết quả trống của hành động xác thực vào những hành động đó (ví dụ: dữ liệu đầu ra nguồn jar của java_library). Đây cũng là một vấn đề nếu các thao tác mới có thể chạy thay vì thao tác biên dịch sau đó được thêm vào và kết quả xác thực trống vô tình bị tắt.

Giải pháp cho những vấn đề này là sử dụng Nhóm kết quả xác thực.

Nhóm kết quả xác thực

Nhóm kết quả xác thực là một nhóm đầu ra được thiết kế để lưu giữ kết quả đầu ra không sử dụng của các hành động xác thực. Nhờ đó, chúng không cần được thêm một cách giả tạo vào dữ liệu đầu vào của các hành động khác.

Nhóm này đặc biệt ở chỗ dữ liệu đầu ra luôn được yêu cầu, bất kể giá trị của cờ --output_groups là gì, và bất kể mục tiêu được phụ thuộc như thế nào (ví dụ: trên dòng lệnh, dưới dạng phần phụ thuộc hoặc thông qua đầu ra ngầm ẩn của mục tiêu). Xin lưu ý rằng khả năng lưu vào bộ nhớ đệm và mức độ gia tăng thông thường vẫn được áp dụng: nếu dữ liệu đầu vào cho thao tác xác thực không thay đổi và thao tác xác thực thành công trước đó, thì thao tác xác thực sẽ không được chạy.

Việc sử dụng nhóm đầu ra này vẫn yêu cầu các hành động xác thực xuất ra một số tệp, ngay cả một tệp trống. Bạn có thể phải gói một số công cụ thường không tạo đầu ra để tạo tệp.

Các hành động xác thực của mục tiêu không được thực hiện trong 3 trường hợp sau:

  • Khi mục tiêu được coi là một công cụ
  • Khi mục tiêu được phụ thuộc vào dưới dạng một phần phụ thuộc ngầm ẩn (ví dụ: một thuộc tính bắt đầu bằng "_")
  • Khi mục tiêu được tạo trong cấu hình máy chủ lưu trữ hoặc cấu hình thực thi.

Giả định rằng các mục tiêu này có các bản dựng và thử nghiệm riêng biệt để phát hiện mọi lỗi xác thực.

Sử dụng nhóm kết quả xác thực

Nhóm đầu ra xác thực có tên là _validation và được dùng như mọi nhóm đầu ra khác:

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"),
  }
)

Lưu ý rằng tệp đầu ra xác thực không được thêm vào DefaultInfo hoặc dữ liệu đầu vào vào bất kỳ hành động nào khác. Thao tác xác thực cho một mục tiêu thuộc loại quy tắc này sẽ vẫn chạy nếu mục tiêu đó phụ thuộc vào nhãn hoặc bất kỳ đầu ra ngầm ẩn nào của mục tiêu phụ thuộc trực tiếp hoặc gián tiếp.

Thông thường, điều quan trọng là kết quả của các hành động xác thực chỉ chuyển vào nhóm đầu ra xác thực và không được thêm vào dữ liệu đầu vào của các hành động khác, vì điều này có thể đánh bại mức tăng của tính năng song song. Tuy nhiên, hãy lưu ý rằng Bazel hiện không có bất kỳ quy trình kiểm tra đặc biệt nào để thực thi việc này. Do đó, bạn nên kiểm thử để đảm bảo đầu ra của hành động xác thực không được thêm vào dữ liệu đầu vào của bất kỳ hành động nào trong quá trình kiểm thử cho các quy tắc Starlark. Ví dụ:

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)

Cờ hành động xác thực

Việc chạy các hành động xác thực được kiểm soát bởi cờ dòng lệnh --run_validations, theo mặc định là true.

Những tính năng đã ngừng hoạt động

Đầu ra được khai báo trước không dùng nữa

Có 2 cách không dùng nữa để sử dụng kết quả được khai báo trước:

  • Tham số outputs của rule chỉ định liên kết giữa tên thuộc tính đầu ra và mẫu chuỗi để tạo nhãn đầu ra được khai báo trước. Ưu tiên sử dụng kết quả không được khai báo trước và thêm rõ ràng kết quả vào DefaultInfo.files. Hãy sử dụng nhãn của mục tiêu quy tắc làm dữ liệu đầu vào cho các quy tắc sử dụng dữ liệu đầu ra thay vì nhãn của đầu ra được khai báo trước.

  • Đối với các quy tắc thực thi, ctx.outputs.executable là một đầu ra có thể thực thi được khai báo trước có cùng tên với mục tiêu quy tắc. Ưu tiên khai báo đầu ra một cách rõ ràng, ví dụ như với ctx.actions.declare_file(ctx.label.name) và đảm bảo rằng lệnh tạo tệp thực thi sẽ đặt các quyền để cho phép thực thi. Truyền rõ ràng đầu ra có thể thực thi đến tham số executable của DefaultInfo.

Những tính năng của Runfiles cần tránh

ctx.runfiles và loại runfiles có một bộ tính năng phức tạp, nhiều tính năng trong số đó được giữ lại vì những lý do cũ. Những đề xuất sau đây giúp giảm độ phức tạp:

  • Tránh sử dụng chế độ collect_datacollect_default của ctx.runfiles. Các chế độ này ngầm thu thập các tệp runfile trên một số cạnh phần phụ thuộc được mã hoá cứng theo những cách khó hiểu. Thay vào đó, hãy thêm các tệp bằng cách sử dụng tham số files hoặc transitive_files của ctx.runfiles, hoặc bằng cách hợp nhất các tệp runfile từ các phần phụ thuộc với runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles).

  • Tránh sử dụng data_runfilesdefault_runfiles của hàm khởi tạo DefaultInfo. Thay vào đó, hãy chỉ định DefaultInfo(runfiles = ...). Sự khác biệt giữa các tệp chạy "mặc định" và "dữ liệu" được duy trì vì các lý do cũ. Ví dụ: một số quy tắc đặt kết quả mặc định trong data_runfiles, nhưng không đặt kết quả đầu ra mặc định trong default_runfiles. Thay vì sử dụng data_runfiles, các quy tắc phải cả hai bao gồm kết quả đầu ra mặc định và hợp nhất vào default_runfiles từ các thuộc tính cung cấp tệp chạy (thường là data).

  • Khi truy xuất runfiles từ DefaultInfo (thường chỉ để hợp nhất các tệp chạy giữa quy tắc hiện tại và phần phụ thuộc), hãy sử dụng DefaultInfo.default_runfiles, không phải DefaultInfo.data_runfiles.

Di chuyển từ nhà cung cấp cũ

Trước đây, trình cung cấp Bazel là các trường đơn giản trên đối tượng Target. Chúng được truy cập bằng toán tử dấu chấm và các ký tự này được tạo bằng cách đặt trường trong một cấu trúc được hàm triển khai của quy tắc trả về.

Kiểu này không còn được dùng nữa và bạn không nên sử dụng trong mã mới; xem bên dưới để biết thông tin có thể giúp bạn di chuyển. Cơ chế nhà cung cấp mới giúp tránh xung đột tên. API này cũng hỗ trợ tính năng ẩn dữ liệu, bằng cách yêu cầu bất kỳ mã nào truy cập vào một thực thể nhà cung cấp để truy xuất dữ liệu đó bằng biểu tượng nhà cung cấp.

Hiện tại, chúng tôi vẫn hỗ trợ các nhà cung cấp cũ. Một quy tắc có thể trả về cả trình cung cấp cũ và hiện đại như sau:

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, ...])

Nếu dep là đối tượng Target thu được cho một thực thể của quy tắc này, thì bạn có thể truy xuất trình cung cấp và nội dung của trình cung cấp dưới dạng dep.legacy_info.xdep[MyInfo].y.

Ngoài providers, cấu trúc được trả về cũng có thể nhận một số trường khác có ý nghĩa đặc biệt (và do đó không tạo ra một nhà cung cấp cũ tương ứng):

  • Các trường files, runfiles, data_runfiles, default_runfilesexecutable tương ứng với các trường cùng tên của DefaultInfo. Không được phép chỉ định bất kỳ trường nào trong số các trường này khi đồng thời trả về một nhà cung cấp DefaultInfo.

  • Trường output_groups nhận một giá trị cấu trúc và tương ứng với một OutputGroupInfo.

Trong nội dung khai báo provides của quy tắc và trong mục khai báo providers của các thuộc tính phần phụ thuộc, trình cung cấp cũ được truyền vào dưới dạng chuỗi và trình cung cấp hiện đại được truyền bằng ký hiệu *Info. Hãy nhớ thay đổi từ chuỗi sang ký hiệu khi di chuyển. Đối với các bộ quy tắc phức tạp hoặc có quy tắc lớn khó có thể cập nhật tỉ mỉ tất cả các quy tắc, bạn có thể dễ dàng cập nhật hơn nếu làm theo trình tự các bước sau:

  1. Sửa đổi các quy tắc tạo ra trình cung cấp cũ để tạo cả trình cung cấp cũ và hiện đại, sử dụng cú pháp trên. Đối với các quy tắc khai báo rằng họ trả về nhà cung cấp cũ, hãy cập nhật nội dung khai báo đó để bao gồm cả nhà cung cấp cũ và nhà cung cấp hiện đại.

  2. Sửa đổi các quy tắc sử dụng trình cung cấp cũ để sử dụng trình cung cấp hiện đại. Nếu bất kỳ nội dung khai báo thuộc tính nào yêu cầu trình cung cấp cũ, bạn cũng cần cập nhật các nội dung đó để yêu cầu trình cung cấp hiện đại. Nếu muốn, bạn có thể xen kẽ công việc này ở bước 1 bằng cách yêu cầu người dùng chấp nhận/yêu cầu một trong hai nhà cung cấp: Kiểm thử sự hiện diện của trình cung cấp cũ bằng cách sử dụng hasattr(target, 'foo') hoặc nhà cung cấp mới bằng FooInfo in target.

  3. Xoá hoàn toàn nhà cung cấp cũ khỏi tất cả các quy tắc.