Quy tắc xác định một loạt hành động mà Bazel thực hiện trên các đầu vào để tạo ra một tập hợp đầu ra đượ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 tệp nhị phân C++ có thể:
- Lấy một tập hợp các tệp nguồn
.cpp
(dữ liệu đầu vào). - Chạy
g++
trên các tệp nguồn (hành động). - Trả về trình cung cấp
DefaultInfo
với đầu ra thực thi và các tệp khác để cung cấp trong thời gian chạy. - Trả về trình cung cấp
CcInfo
có thông tin cụ thể về 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ừ quan điểm của Bazel, g++
và thư viện C++ 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ỉ phải xem xét dữ liệu đầu vào do người dùng cung cấp cho một quy tắc, mà còn phải xem xét tất cả các công cụ và thư viện cần thiết để thực thi các hành động.
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. Bạn cần hiểu rõ 3 giai đoạn của một 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 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 sử dụng trang này làm tài liệu tham khảo.
Một vài quy tắc được tích hợp sẵn vào Bazel. Các quy tắc gốc này, chẳng hạn như cc_library
và java_binary
, cung cấp một số tính năng 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ợ gốc.
Bazel cung cấp một 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ừ tệp BUILD
.
Khi xác định quy tắc của riêng mình, bạn có thể quyết định những thuộc tính mà quy tắc đó hỗ trợ và cách quy tắc đó tạo ra đầu ra.
Hàm implementation
của quy tắc xác định hành vi chính xác của quy tắ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 đó, lớp này đăng ký các hành động sẽ được sử dụng sau trong giai đoạn thực thi để tạo đầu ra của quy tắc, nếu cần.
Tạo quy tắc
Trong tệp .bzl
, hãy sử dụng hàm rule (quy tắc) để xác định một quy tắc mới và lưu trữ kết quả trong một biến toàn cục. Lệnh gọi đến rule
chỉ định
các thuộc tính và
hà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 liệu quy tắc này tạo ra một đầu ra thực thi (bằng 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 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
Bạn có thể tải và gọi các quy tắc 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 bản dựng không trả về giá trị nào, nhưng có tác dụng phụ là xác định một mục tiêu. Quá trình này được gọi là tạo thực thể cho quy tắc. Thao tác này chỉ định tên cho mục tiêu mới và giá trị cho các thuộc tính của mục tiêu.
Bạn cũng có thể gọi các quy tắc từ các hàm Starlark và tải trong tệp .bzl
.
Các hàm Starlark gọi quy tắc được gọi là macro Starlark.
Cuối cùng, bạn phải gọi macro Starlark từ các tệp BUILD
và chỉ có thể gọi trong giai đoạn tải, khi các tệp BUILD
được đánh giá để tạo bản sao cho các 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 các giá trị cụ thể cho quá trình triển khai của một mục tiêu hoặc có thể tham chiếu đến các mục tiêu khác, tạo ra biểu đồ 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 chung, chẳng hạn như name
và visibility
, được ngầm thêm vào tất cả quy tắc. Các thuộc tính bổ sung được thêm ngầm vào các quy tắc có thể thực thi và kiểm thử. Các thuộc tính được ngầm thêm vào một quy tắc không thể được đưa vào từ điển chuyển cho 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 các tệp nguồn được xử lý bởi các 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ý. Các quy tắc cho các ngôn ngữ có tệp tiêu đề thường chỉ định một thuộc tínhhdrs
riêng cho 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ột mục tiêu. Giản đồ thuộc tính phải chỉ định nhà cung cấp nào phải cung cấp các phần phụ thuộc đó. (Ví dụ:cc_library
cung cấpCcInfo
.)data
chỉ định các tệp sẽ được cung cấp trong thời gian chạy cho mọi tệp thực thi phụ thuộc vào một mục tiêu. Điều đó 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ề thuộc tính phần phụ thuộc. Mọi thuộc tính chỉ định một nhãn đầ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 của một loại nhất định giữa một mục tiêu và các mục tiêu có nhãn (hoặ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à đườ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
và do đó other_target
sẽ được phân tích trước tiên. Sẽ là lỗi nếu có chu kỳ trong đồ thị phần phụ thuộc của các mục tiêu.
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 một phần phụ thuộc ngầm ẩn. Phương thức này là 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 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ụ (một phần phụ thuộc tại thời điểm tạo bản dựng, chẳng hạn như một trình biên dịch), vì hầu hết thời gian 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ị đó, bạn có thể đặt thuộc tính 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 thuộc loại example_library
đều có một phần phụ thuộc ngầm ẩn 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 truyền nhãn của hàm này làm 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 tất cả các 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 compiler
mà không cần dấu gạch dưới và giữ nguyên 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 phải nhậ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, thì quy tắc 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.output
và attr.output_list
, khai báo tệp đầu ra mà mục tiêu tạo ra. Các thuộc tính này khác với thuộc tính phần phụ thuộc ở hai điểm:
- Các thuộc tính 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.
- 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 sử dụng khi một quy tắc cần tạo đầu ra bằng tên do người dùng xác định và không được dựa trên tên mục tiêu. Nếu một quy tắc có một thuộc tính đầu ra, thì quy tắc đó thường được đặt tên là out
hoặc outs
.
Thuộc tính đầu ra là cách ưu tiên để tạo đầu ra được khai báo trước, có thể phụ thuộc cụ thể hoặc yêu cầu tại dòng lệnh.
Hàm triển khai
Mỗi quy tắc đều yêu cầu một 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à chuyển đổi biểu đồ các mục tiêu được tạo trong giai đoạn tải thành biểu đồ các 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 sự đọc hoặc ghi tệp.
Các hàm triển khai quy tắc thường là riêng tư (được đặt tên bằng dấu gạch dưới ở đầu). Theo thông lệ, các lớp này được đặt tên giống với quy tắc, nhưng có hậu tố là _impl
.
Các hàm triển khai nhận đúng một tham số: một ngữ cảnh quy tắc, thường được đặt tên là ctx
. Các hàm này trả về danh sách 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 trình cung cấp được tạo khi thực thi hàm triển khai của mục tiêu.
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
duy nhất.
Danh sách đối tượng nhà cung cấp được hàm triển khai của mục tiêu trả về:
return [ExampleInfo(headers = depset(...))]
Bạn có thể truy cập vào các nhà cung cấp đó bằng cách sử dụng ký hiệu chỉ mục ([]
), trong đó loại nhà cung cấp đóng vai trò là khoá. Đây có thể là nhà cung cấp tuỳ chỉnh được xác định trong Starlark hoặc nhà cung cấp cho quy tắc gốc có sẵn dưới dạng biến toàn cục Starlark.
Ví dụ: nếu một quy tắc lấy tệp tiêu đề thông qua thuộc tính hdrs
và cung cấp các tệp đó cho hành động biên dịch của mục tiêu và đối tượng tiêu dùng, thì quy tắc đó có thể thu thập các tệp như vậy:
def _example_library_impl(ctx):
...
transitive_headers = [hdr[ExampleInfo].headers for hdr in ctx.attr.hdrs]
Đối với kiểu cũ, trong đó struct
được trả về từ hàm triển khai của mục tiêu 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à nên di chuyển các quy tắc ra khỏi kiểu này.
Files
Các tệp được biểu thị bằng đối tượng File
. Vì Bazel không thực hiện I/O tệp trong giai đoạn phân tích, nên bạn không thể sử 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 đó, các hành động này được truyền đến các hàm phát hành 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à đầu ra của đúng một hành động. Tệp nguồn không được là đầu ra của bất kỳ thao tác 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 đầ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ố kỹ thuật đặ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ó thông số kỹ thuật đặt executable=True
.
Khai báo đầu ra
Trong giai đoạn phân tích, hàm triển khai của quy tắc có thể tạo ra đầu ra.
Vì phải biết tất cả các nhãn trong giai đoạn tải, nên những dữ liệu đầ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_file
và ctx.actions.declare_directory
. Thông thường, tên của đầu ra được đặt 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, chẳng hạn như những đầu ra được tạo cho thuộc tính đầu ra, bạn có thể truy xuất đối tượng File
từ các trường tương ứng của ctx.outputs
.
Thao tác
Một hành động mô tả cách tạo một tập hợp đầu ra từ một tập hợp đầu vào, ví dụ: "chạy gcc trên hello.c và nhận hello.o". Khi một hành động được tạo, Bazel sẽ không chạy lệnh ngay lập tức. Phương thức này đăng ký trong biểu đồ phần phụ thuộc, vì một hành động có thể phụ thuộc vào đầu ra của một hành động 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 đa năng tạo hành động được xác định trong ctx.actions
:
ctx.actions.run
, để chạy một tệp thực thi.ctx.actions.run_shell
, để chạy một lệnh shell.ctx.actions.write
để ghi một chuỗi vào tệp.ctx.actions.expand_template
, để tạo một tệp từ mẫu.
Bạn có thể dùng ctx.actions.args
để tích luỹ hiệu quả các đối số cho các thao tác. Cách này giúp tránh làm phẳng các nhóm phần phụ thuộc cho đến thời gian 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],
)
...
Hành động lấy một danh sách hoặc tập hợp các tệp đầu vào và tạo một danh sách (không trống) các tệp đầu ra. Bạn phải biết tập hợp tệp đầu vào và đầu ra trong giai đoạn phân tích. Giá trị 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 được phụ thuộc vào kết quả của quá trình thực thi. Ví dụ: nếu thao tác của bạn chạy lệnh giải nén, bạn phải chỉ định tệp nào sẽ được tăng cường (trước khi chạy lệnh giải nén). Các thao tác tạo số lượng tệp biến đổi trong nội bộ có thể gói các tệp đó trong một tệp duy nhất (chẳng hạn như tệp 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. Danh sách dữ liệu đầu vào không được sử dụng được cho phép nhưng không hiệu quả.
Thao tác phải tạo ra tất cả kết quả của thao tác đó. Các tệp này có thể ghi các tệp khác, nhưng người dùng sẽ không thấy bất kỳ nội dung nào không có trong đầu ra. Tất cả đầu ra đã khai báo phải được ghi bằng một số hành động.
Hành động có thể so sánh với các hàm thuần tuý: Chúng chỉ nên phụ thuộc vào dữ liệu đầu vào đã 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ừ việc đọc dữ liệu đầu vào và đầu ra ghi). Điều 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 được Bazel phân giải và sẽ quyết định những hành động nào được thực thi. Đây là lỗi nếu có chu kỳ trong biểu đồ phần phụ thuộc. Việc tạo một hành động không đảm bảo rằng hành động đó sẽ được thực thi, điều này phụ thuộc vào việc bản dựng có cần đầu ra của hành động đó hay không.
Nhà cung cấp
Nhà cung cấp là các phần thông tin mà một quy tắc hiển thị cho các quy tắc khác phụ thuộc vào quy tắc đó. Dữ liệu này có thể bao gồm các tệp đầu ra, thư viện, tham số để truyền trên dòng lệnh của một công cụ hoặc bất kỳ nội dung nào khác mà người tiêu dùng của mục tiêu nên biết.
Vì hàm triển khai của một 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 đã tạo thực thể, 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 tiêu dùng của mục tiêu cần biết, thường bằng cách tích luỹ thông tin đó vào depset
.
Nhà cung cấp của một mục tiêu được chỉ định bằng danh sách các đối tượng Provider
do hàm triển khai trả về.
Các hàm triển khai cũ cũng có thể được viết theo kiểu cũ, trong đó hàm triển khai trả về struct
thay vì danh sách đối tượng trình cung cấp. Bạn không nên sử dụng kiểu này và nên di chuyển các quy tắc ra khỏi kiểu này.
Đầu ra mặc định
Đầu ra mặc định của mục tiêu là các đầu ra được yêu cầu theo mặc định khi mục tiêu được yêu cầu xây dựng trong dòng lệnh. Ví dụ: mục tiêu java_library
//pkg:foo
có foo.jar
làm đầu ra mặc định, do đó, mục tiêu này sẽ được tạo bằng lệnh bazel build //pkg:foo
.
Đầ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 trả về bằng cách triển khai quy tắc 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 đầu ra do thuộc tính đầu ra tạo).
Các quy tắc thực hiện hành động phải cung cấp kết quả mặc định, ngay cả khi bạn không muốn sử dụng trực tiếp các kết quả đó. Các hành động không có trong biểu đồ của đầu ra được yêu cầu sẽ bị cắt bỏ. Nếu một đầu ra chỉ được người dùng của mục tiêu sử dụng, thì các hành động đó sẽ không được thực hiện khi mục tiêu được tạo riêng biệt. Điều này làm cho việc gỡ lỗi trở nên khó khăn hơn vì việc chỉ tạo lại mục tiêu không thành công sẽ không tái tạo lỗi.
Tệp chạy
Tệp chạy là một tập hợp các tệp mà 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 tệp chạy. Thao tác này sẽ triển khai 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 tệp chạy theo cách thủ công trong quá trình tạo quy tắc.
Phương thức runfiles
có thể tạo các đối tượng runfiles
trên 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 thêm ngầm vào 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
, có đầu ra được thêm vào tệp chạy của mục tiêu. Tệp chạy cũng phải được hợp nhất từ data
, cũng như từ mọi thuộc tính có thể cung cấp mã để thực thi cuối cùng, thường là srcs
(có thể chứa các mục tiêu filegroup
có liên kết data
) 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 nhà cung cấp bằng cách sử dụng hàm provider
để truyền tải thông tin cụ thể về 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 thực thể của trì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 của trình cung cấp
Bạn có thể bảo vệ việc tạo bản sao của một 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 tính năng 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ố hằng số nhất định hoặc để cung cấp cho người dùng một API rõ ràng hơn để lấy một thực thể.
Bạn có thể thực hiện việc này bằng cách truyền lệnh gọi lại init
vào hàm provider
. Nếu thực hiện lệnh gọi lại này, thì loại dữ liệu trả về của provider()
sẽ thay đổi thành một bộ dữ liệu gồm hai giá trị: biểu tượng trình cung cấp là giá trị trả về thông thường khi không sử 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, biểu tượng này sẽ chuyển tiếp các đối số đến lệnh gọi lại init
. Giá trị trả về của lệnh gọi lại phải là một dict ánh xạ tên trường (chuỗi) đến các giá trị; giá trị này được dùng để khởi tạo các trường của thực thể mới. Xin 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 sử dụng init
để xử lý trước và xác thực các đối số của hàm:
# //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ể cho 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
)
Bạn có thể sử dụng hàm khởi tạo thô để 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 ta 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) để mã của người dùng không thể tải biến đó và tạo các thực thể nhà cung cấp tuỳ ý.
Một cách sử dụng khác của init
là chỉ ngăn người dùng gọi biểu tượng 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 có thể thực thi và quy tắc kiểm thử
Quy tắc thực thi xác định các mục tiêu có thể được lệnh bazel run
gọi.
Quy tắc kiểm thử là một loại quy tắc đặc biệt có thể thực thi, trong đó các mục tiêu cũng có thể được lệnh bazel test
gọi. Bạn có thể tạo quy tắc kiểm thử và thực thi 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
. (Theo quy ước, tên mục tiêu kiểm thử cũng thường kết thúc bằng _test
, nhưng điều này là không bắt buộc.) Các quy tắc không phải quy tắc kiểm thử không được có hậu tố này.
Cả hai loại quy tắc này đều phải tạo ra một tệp đầu ra thực thi (có thể được khai báo trước hoặc không được khai báo trước) mà sẽ được gọi bằng lệnh run
hoặc test
. Để cho Bazel biết nên sử dụng kết quả đầu ra nào của quy tắc làm tệp thực thi này, hãy truyền kết quả đó dưới dạng đối số executable
của trình cung cấp DefaultInfo
được trả về. executable
đó được thêm vào đầu ra mặc định của quy tắc (vì vậy, bạn không cần truyền đầu ra đó cho cả executable
và files
). executable
cũng được thêm ngầm vào runfiles:
def _example_binary_impl(ctx):
executable = ctx.actions.declare_file(ctx.label.name)
...
return [
DefaultInfo(executable = executable, ...),
...
]
Thao tác tạo tệp này phải đặt bit thực thi trên tệp. Đối với hành động ctx.actions.run
hoặc ctx.actions.run_shell
, việc này sẽ do công cụ cơ bản được hành động gọi thực hiện. Đối với thao tác ctx.actions.write
, hãy truyền is_executable=True
.
Là hành vi cũ, các quy tắc thực thi có đầu ra đặc biệt được khai báo trước là 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 thực thi bằng DefaultInfo
; nếu không, bạn không được sử dụng tệp này. Cơ chế đầu ra này không còn được dùng nữa vì không hỗ trợ việc 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 thi và quy tắc kiểm thử.
Quy tắc có thể thực thi và quy 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ể giải quyết vấn đề này bằng cách gói một quy tắc riêng tư trong một macro Starlark để thay đổi giá trị mặc định:
def example_test(size="small", **kwargs):
_example_test(size=size, **kwargs)
_example_test = rule(
...
)
Vị trí tệp chạy
Khi một mục tiêu thực thi được chạy bằng bazel run
(hoặc test
), thư mục gốc của thư mục runfiles sẽ nằm cạnh tệp thực thi. Các đường dẫn có 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 File
trong thư mục runfile tương ứng với File.short_path
.
Tệp nhị phân do bazel
thực thi trực tiếp nằm cạnh thư mục gốc của thư mục runfiles
. Tuy nhiên, các tệp nhị phân được gọi từ tệp chạy không thể đưa ra giả định tương tự. Để giảm thiểu vấn đề này, mỗi tệp nhị phân phải cung cấp một cách để chấp nhận thư mục gốc của tệp chạy dưới dạng tham số bằng cách sử dụng môi trường hoặc đối số/cờ dòng lệnh. Điều này cho phép các tệp nhị phân truyền đúng gốc chạy tệp chuẩn hoá đến tệp nhị phân mà nó gọi. Nếu bạn không đặt thuộc tính này, thì 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 runfile liền kề.
Chủ đề nâng cao
Yêu cầu tệp đầu ra
Một mục tiêu có thể có nhiều tệp đầu ra. Khi một lệnh bazel build
được chạy, một số kết quả của các mục tiêu được cung cấp cho lệnh đó được coi là được yêu cầu. Bazel chỉ tạo những tệp được yêu cầu này và những tệp mà họ phụ thuộc trực tiếp hoặc gián tiếp. (Xét về biểu đồ hành động, Bazel chỉ thực thi các hành động có thể truy cập được 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 đầu ra mặc định, mọi đầu ra được khai báo trước đều có thể được yêu cầu rõ ràng trên dòng lệnh. Quy tắc có thể chỉ định đầu ra đã 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 rõ ràng nhãn cho đầu ra khi tạo bản sao của quy tắc. Để lấy đối tượng File
cho các 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 đầu ra được khai báo trước dựa trên tên mục tiêu, nhưng tính năng này không còn được dùng nữa.
Ngoài đầu ra mặc định, còn có nhóm đầu ra, là 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 thông tin 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ì các đầu ra không được khai báo trước không có nhãn, nên bạn chỉ có thể yêu cầu các đầu ra này bằng cách xuất hiện trong các đầu ra mặc định hoặc nhóm đầu ra.
Bạn có thể chỉ định các nhóm đầu ra bằng trình cung cấp OutputGroupInfo
. Xin lưu ý rằng không giống như nhiều nhà cung cấp tích hợp, OutputGroupInfo
có thể lấy 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 nhà cung cấp, OutputGroupInfo
có thể được trả về bằng cả một phương diện và mục tiêu quy tắc mà phương diện đó được áp dụng, miễn là chúng không xác định cùng một nhóm đầu ra. Trong trường hợp đó, các nhà cung cấp kết quả sẽ được hợp nhất.
Lưu ý rằng bạn không nên sử dụng OutputGroupInfo
để 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 tiêu dùng. Thay vào đó, hãy xác định nhà cung cấp dành riêng cho quy tắc.
Cấu hình
Hãy tưởng tượng bạn muốn tạo một tệp nhị phân C++ cho một cấu trúc khác. Bản dựng có thể phức tạp và liên quan đến nhiều bước. Một số tệp nhị phân trung gian, chẳng hạn 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ư đầ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à quá trình 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 ở 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 ở cấu hình "thực thi". Các quy tắc có thể tạo ra nhiều hành động khác nhau 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 sử dụng cùng một thư viện cho nhiều cấu hình. Nếu điều này xảy ra, lớp này 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 với chính mục tiêu đó, nói cách khác là không có quá trình chuyển đổi. Khi một phần phụ thuộc là một công cụ cần thiết để giúp tạo mục tiêu, thuộc tính tương ứng sẽ chỉ định quá trình chuyển đổi sang cấu hình thực thi. Điều này khiến công cụ và tất cả các phần phụ thuộc của công cụ đó được tạo 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 có nên tạo trong cùng một cấu hình hay chuyển 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 là để tránh 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.
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ã) phải được tạo 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, 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 quy trình kiểm thử) sẽ được tạo 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"
thực sự không làm gì cả: đó chỉ là một giá trị tiện lợi để giúp nhà thiết kế quy tắc thể hiện rõ ý định của họ. Khi executable=False
, nghĩa là cfg
là không bắt buộc, chỉ đặt thuộc tính này khi thuộc tính này thực sự giúp tăng khả năng đọc.
Bạn cũng có thể sử dụng cfg=my_transition
để sử dụng chuyển đổi do người dùng xác định. Điều này cho phép tác giả quy tắc linh hoạt hơn nhiều trong việc thay đổi cấu hình, nhưng 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, thay vào đó, tất cả các thao tác xây dựng đều được coi là chạy trên máy chủ lưu trữ. Do đó, có một cấu hình "máy chủ" và một quá trình chuyển đổi "máy chủ" có thể được dùng để tạo một phần phụ thuộc trong cấu hình máy chủ. Nhiều quy tắc vẫn sử dụng quá trình chuyển đổi "máy chủ" cho các công cụ của chúng, nhưng quy tắc này hiện không còn được dùng nữa và đang được di chuyển để sử dụng quá trình chuyển đổi "thực thi" khi có thể.
Có nhiều điểm khác biệt giữa cấu hình "máy chủ" và "thực thi":
- "host" là thiết bị đầu cuối, "exec" không phải: Sau khi một phần phụ thuộc nằm trong cấu hình "host" (máy chủ lưu trữ), bạn không được phép chuyển đổi thêm nữa. Bạn có thể tiếp tục chuyển đổi thêm cấu hình sau khi ở cấu hình "thực thi".
- "máy chủ" là nguyên khối, "thực thi" không phải: Chỉ có một cấu hình "máy chủ", nhưng có thể có một cấu hình "thực thi" khác nhau cho mỗi nền tảng thực thi.
- "máy chủ" giả định rằng bạn chạy các công cụ trên cùng một máy với Bazel hoặc trên một máy tương tự đáng kể. Điều này không còn đúng nữa: bạn có thể chạy các thao tác xây dựng trên máy cục bộ hoặc trên trình thực thi từ xa và không thể đảm bảo rằng trình thực thi từ xa cùng CPU và hệ điều hành với máy cục bộ.
Cả hai cấu hình "exec" và "host" (máy chủ lưu trữ) đều áp dụng cùng các thay đổi về 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 "máy chủ" bắt đầu bằng các giá trị mặc định của tất cả cờ khác, trong khi cấu hình "thực thi" bắt đầu bằng các giá trị hiện tại của cờ, dựa trên cấu hình mục tiêu.
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
, java
và jvm
. 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 vào các mảnh cho cấu hình máy chủ, hãy sử dụng ctx.host_fragments
.
Đường liên kết tượng trưng của tệp chạy
Thông thường, đường dẫn tương đối của một tệp trong cây runfile 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 đã tạo. Nếu các giá trị 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 từ điển ánh xạ các đường dẫn đến tệp, trong đó các đường dẫn tương ứng với thư mục gốc của thư mục runfiles. Từ điển symlinks
cũng giống như vậy, nhưng các đường dẫn được thêm tiền tố ngầm ẩn là 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 sử dụng symlinks
hoặc root_symlinks
, hãy cẩn thận để không ánh xạ 2 tệp khác nhau đến cùng một đường dẫn trong cây chạy tệp. Điều này sẽ khiến bản dựng không thành công và xuất hiện 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. Quy trình kiểm tra này sẽ được thực hiện cho mọi mục tiêu sử dụng quy tắc của bạn, cũng như mọi loại mục tiêu phụ thuộc vào các mục tiêu đó. Điều này đặc biệt nguy hiểm nếu công cụ của bạn có khả năng được một công cụ khác sử dụng chuyển tiếp; tên đường liên kết tượng trưng phải là duy nhất trên các tệp chạy của một 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 công cụ đ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ủa các mục tiêu được xem xét được kiểm soát bằng cờ --instrumentation_filter
.
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 việc triển khai quy tắc thêm khả năng đo lường mức độ sử dụng tại thời điểm tạo bản dựng, thì bạn cần tính đến điều đó trong hàm triển khai. ctx.coverage_instrumented trả về true ở chế độ mức độ sử dụng nếu nguồn của mục tiêu cần đượ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 phải bật ở chế độ sử dụng phạm vi (cho dù các nguồn của mục tiêu có được đo lường cụ thể hay không) có thể được điều chỉnh trên ctx.configuration.coverage_enabled.
Nếu quy tắc đó trực tiếp đưa các nguồn từ các phần phụ thuộc của nó vào 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 cần đo lường các nguồn của phần phụ thuộc:
# 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
Quy tắc cũng phải cung cấp thông tin về những thuộc tính có liên quan đến phạm vi sử dụng của nhà cung cấp InstrumentedFilesInfo
, được tạo bằng coverage_common.instrumented_files_info
.
Tham số dependency_attributes
của instrumented_files_info
phải 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 khả năng đo lường mức độ phù hợp:
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 không trả về InstrumentedFilesInfo
, thì một thuộc tính mặc định sẽ được tạo với từng thuộc tính phần phụ thuộc không phải công cụ mà 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ì nó đặt các thuộc tính như srcs
vào dependency_attributes
thay vì source_attributes
, nhưng nhờ đó, bạn không cần phải định cấu hình mức độ sử dụng 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 số thông tin về bản dựng và thông tin cần thiết để xác thực chỉ có trong 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ể xác thực việ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 thực hiện quy trình xác thực này tại thời điểm thực thi. Khi quá trình xác thực không thành công, thao tác sẽ không thành công và do đó, bản dựng cũng sẽ không thành công.
Ví dụ về các 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 bản dựng bằng cách di chuyển các phần của thao tác không bắt buộc để tạo cấu phần phần mềm thành các thao tác riêng biệt. Ví dụ: nếu một hành động thực hiện việc biên dịch và tìm lỗi mã nguồn có thể được tách 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 một hành động xác thực và chạy song song với các hành động khác.
Những "hành động xác thực" này thường không tạo ra bất kỳ nội dung nào được sử dụng ở nơi khác trong bản dựng, vì chúng chỉ cần xác nhận những nội dung về dữ liệu đầu vào. Tuy nhiên, điều này gây ra một vấn đề: Nếu một hành động xác thực không tạo ra bất kỳ nội dung nào được sử dụng ở nơi khác trong bản dựng, thì làm cách nào để quy tắc chạy hành động đó? Trước đây, phương pháp này là để hành động xác thực xuất ra một tệp trống và thêm dữ liệu đầu ra đó 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 một cách giả tạo:
Cách này hoạt động vì Bazel sẽ luôn chạy thao tác xác thực khi chạy thao tác biên dịch, nhưng cách này có những hạn chế đáng kể:
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 đầu ra trống là bắt buộc để chạy thao tác biên dịch, nên trước tiên, Bazel sẽ chạy thao tác xác thực, mặc dù thao tác biên dịch sẽ bỏ qua dữ liệu đầu vào. Điều này làm giảm tính song song và làm chậm các bản dựng.
Nếu các hành động khác trong bản dựng có thể chạy thay cho hành động biên dịch, thì bạn cũng cần thêm kết quả trống của các hành động xác thực vào các hành động đó (ví dụ: kết quả tệp nguồn của
java_library
). Đây cũng là một vấn đề nếu sau này các hành động mới có thể chạy thay vì hành động biên dịch được thêm vào và kết quả xác thực trống vô tình bị bỏ qua.
Giải pháp cho những vấn đề này là sử dụng Nhóm đầu ra xác thực.
Nhóm đầu ra của quy trình xác thực
Nhóm đầu ra của quy trình xác thực là một nhóm đầu ra được thiết kế để lưu giữ các đầu ra không được sử dụng của các hành động xác thực, để không cần thêm các đầu ra này vào đầu vào của các hành động khác.
Nhóm này đặc biệt ở chỗ đầu ra của nhóm này luôn được yêu cầu, bất kể giá trị của cờ --output_groups
và bất kể mục tiêu phụ thuộc vào cách 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 tính năng lưu vào bộ nhớ đệm và tăng dần thông thường vẫn á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 trước đó đã thành công, thì thao tác xác thực sẽ không 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 phải xuất ra một số tệp, ngay cả tệp trống. Điều này có thể yêu cầu gói một số công cụ thường không tạo đầu ra để tạo tệp.
Hành động xác thực của mục tiêu sẽ không chạy trong 3 trường hợp sau:
- Khi mục tiêu được dùng làm công cụ
- Khi mục tiêu được phụ thuộc vào dưới dạng 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 thực thi.
Giả sử các mục tiêu này có các bản dựng và kiểm thử riêng để phát hiện mọi lỗi xác thực.
Sử dụng Nhóm đầu ra của quy trình xác thực
Nhóm đầu ra xác thực có tên là _validation
và được sử 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 đầu vào cho bất kỳ hành động nào khác. Thao tác xác thực cho một mục tiêu của 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.
Điều quan trọng thường là đầu ra của hành động xác thực chỉ đi vào nhóm đầu ra xác thực và không được thêm vào đầu vào của các hành động khác, vì điều này có thể làm giảm mức tăng của tính 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 rằng dữ liệu đầu ra của hành động xác thực sẽ không được thêm vào dữ liệu đầu vào của bất kỳ hành động nào trong các lần kiểm thử 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 thao tác xác thực được kiểm soát bằng cờ dòng lệnh --run_validations
, 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 còn được dùng nữa
Có hai cách không dùng nữa để sử dụng đầu ra được khai báo trước:
Tham số
outputs
củarule
chỉ định mối liên kết giữa tên thuộc tính đầu ra và các mẫu chuỗi để tạo các nhãn đầu ra được khai báo trước. Ưu tiên sử dụng các đầu ra không được khai báo trước và thêm các đầu ra một cách rõ ràng vàoDefaultInfo.files
. 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 đầu ra thay vì nhãn của đầu ra được khai báo trước.Đối với quy tắc có thể thực thi,
ctx.outputs.executable
tham chiếu đến một đầu ra 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 rõ ràng đầu ra, ví dụ: vớictx.actions.declare_file(ctx.label.name)
và đảm bảo rằng lệnh tạo tệp thực thi sẽ đặt quyền cho phép thực thi. Truyền rõ ràng kết quả thực thi đến tham sốexecutable
củaDefaultInfo
.
Các tính năng 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ì các lý do cũ.
Các đề xuất sau đây giúp giảm độ phức tạp:
Tránh sử dụng chế độ
collect_data
vàcollect_default
củactx.runfiles
. Các chế độ này thu thập tệp chạy một cách ngầm ẩn trên một số cạnh phần phụ thuộc được mã hoá cứng theo cách gây nhầm lẫn. Thay vào đó, hãy thêm tệp bằng cách sử dụng tham sốfiles
hoặctransitive_files
củactx.runfiles
hoặc bằng cách hợp nhất các tệp chạy từ các phần phụ thuộc vớirunfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
.Tránh sử dụng
data_runfiles
vàdefault_runfiles
của hàm khởi tạoDefaultInfo
. Thay vào đó, hãy chỉ địnhDefaultInfo(runfiles = ...)
. Tuy nhiên, sự khác biệt giữa tệp chạy "mặc định" và "dữ liệu" vẫn được duy trì vì những lý do cũ. Ví dụ: một số quy tắc đặt đầu ra mặc định của chúng vàodata_runfiles
, nhưng không phảidefault_runfiles
. Thay vì sử dụngdata_runfiles
, các quy tắc phải cùng bao gồm cả đầu ra mặc định và hợp nhất trongdefault_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 runfile giữa quy tắc hiện tại và các phần phụ thuộc của quy tắc đó), hãy dùngDefaultInfo.default_runfiles
, khôngDefaultInfo.data_runfiles
.
Di chuyển từ các nhà cung cấp cũ
Trước đây, các nhà cung cấp Bazel là các trường đơn giản trên đối tượng Target
. Các giá trị này được truy cập bằng toán tử dấu chấm và được tạo bằng cách đặt trường vào một cấu trúc do 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; hãy xem phần 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. Lớp này cũng hỗ trợ tính năng ẩn dữ liệu bằng cách yêu cầu mọi mã truy cập vào một thực thể của trình cung cấp phải truy xuất dữ liệu đó bằng biểu tượng của trình cung cấp.
Hiện tại, các nhà cung cấp cũ vẫn được hỗ trợ. 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 các trình cung cấp và nội dung của chúng dưới dạng dep.legacy_info.x
và dep[MyInfo].y
.
Ngoài providers
, cấu trúc được trả về cũng có thể lấy một số trường khác có ý nghĩa đặc biệt (và do đó không tạo ra một trình cung cấp cũ tương ứng):
Các trường
files
,runfiles
,data_runfiles
,default_runfiles
vàexecutable
tương ứng với các trường có cùng tên củaDefaultInfo
. Không được phép chỉ định bất kỳ trường nào trong số này trong khi đồng thời trả về trình cung cấpDefaultInfo
.Trường
output_groups
nhận giá trị cấu trúc và tương ứng vớiOutputGroupInfo
.
Trong phần khai báo quy tắc provides
và trong phần khai báo thuộc tính phần phụ thuộc providers
, các trình cung cấp cũ được truyền vào dưới dạng chuỗi và các trình cung cấp hiện đại được truyền vào bằng biểu tượng *Info
. Hãy nhớ thay đổi từ chuỗi sang biểu tượng khi di chuyển. Đối với các bộ quy tắc phức tạp hoặc lớn, khó cập nhật tất cả các quy tắc một cách nguyên vẹn, bạn có thể dễ dàng hơn nếu làm theo trình tự các bước sau:
Sửa đổi các quy tắc tạo trình cung cấp cũ để tạo cả trình cung cấp cũ và trình cung cấp hiện đại bằng cách sử dụng cú pháp ở trên. Đối với các quy tắc khai báo rằng chúng trả về trình cung cấp cũ, hãy cập nhật nội dung khai báo đó để bao gồm cả trình cung cấp cũ và hiện đại.
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ũ, hãy cập nhật các nội dung đó để yêu cầu trình cung cấp hiện đại. Bạn có thể xen kẽ công việc này với 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 tra sự hiện diện của nhà cung cấp cũ bằng
hasattr(target, 'foo')
hoặc nhà cung cấp mới bằngFooInfo in target
.Xoá hoàn toàn nhà cung cấp cũ khỏi tất cả quy tắc.