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. Tập hợp này được tham chiếu trong trì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ể:
- Lấy một nhóm tệp nguồn
.cpp
(đầ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
cùng với đầu ra có thể 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ùng với thông tin cụ thể về C++ được thu thập từ đích đến và các phần phụ thuộc của đích đến.
Theo Bazel, g++
và các thư viện C++ chuẩn cũng là đầ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 các thông tin đầ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 đã nắm rõ 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 rõ 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 tham khảo trang này.
Một số quy tắc được tích hợp sẵn trong Bazel. Những quy tắc gốc này (chẳng hạn như genrule
và filegroup
) cung cấp một số hỗ trợ cốt lõi.
Bằng cách xác định các quy tắc của riêng mình, bạn có thể thêm chế độ hỗ trợ cho các ngôn ngữ và công cụ mà Bazel không hỗ trợ một cách tự nhiên.
Bazel cung cấp một mô hình có khả năng mở rộng để viết các quy tắc bằng ngôn ngữ Starlark. Các quy tắc này được ghi trong tệp .bzl
và có thể được 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 sẽ quyết định những thuộc tính mà quy tắc đó hỗ trợ và cách quy tắc đó tạo ra các đầu ra.
Hàm implementation
của quy tắc xác định chính xác hành vi 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 đó, nó sẽ đăng ký các thao tác sẽ được dùng sau này 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 dùng hàm rule để xác định một quy tắc mới và lưu trữ kết quả trong một biến chung. Lệnh gọi đến rule
chỉ định các thuộc tính và một 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 xem quy tắc có 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 là tệp thực thi kiểm thử, thì quy tắc này là một quy tắc kiểm thử và tên của quy tắc phải kết thúc bằng _test
.
Hoạt động khởi tạo 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 xây dựng đều 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. Hành động này được gọi là khởi tạo quy tắc. Thao tác 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.
Bạn cũng có thể gọi các quy tắc từ các hàm Starlark và tải trong các tệp .bzl
.
Các hàm Starlark gọi các quy tắc được gọi là macro Starlark.
Cuối cùng, các macro Starlark phải được gọi từ các 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á để khởi tạo các mục tiêu.
Thuộc tính
Thuộc tính là một đối số quy tắc. Các thuộc tính có thể cung cấp các giá trị cụ thể cho việc 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 một 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 các 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ư name
và visibility
, được thêm ngầm vào tất cả các quy tắc. Các thuộc tính bổ sung được thêm ngầm vào các quy tắc thực thi và kiểm thử một cách cụ thể. Bạn không thể thêm các thuộc tính được thêm ngầm vào một 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 các tệp nguồn do các thao tác của mục tiêu xử lý. Thông thường, giản đồ thuộc tính chỉ định những đuôi tệp dự kiến cho loại tệp nguồn mà quy tắc xử lý. Các quy tắc cho những ngôn ngữ có tệp tiêu đề thường chỉ định một thuộc tínhhdrs
riêng cho các tiêu đề do một mục tiêu và người dùng của mục tiêu đó xử lý.deps
chỉ định các phần phụ thuộc mã cho một mục tiêu. Lược đồ thuộc tính phải chỉ định nhà cung cấp nào phải cung cấp những 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 tuỳ thuộc vào một mục tiêu. Thao tác đó 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à các ví dụ về thuộc tính phần phụ thuộc. Mọi thuộc tính chỉ định nhãn đầu vào (những nhãn được xác định bằng attr.label_list
, attr.label
hoặc attr.label_keyed_string_dict
) đều chỉ định các phần phụ thuộc thuộc một loại nhất định giữa một đích và các đích có nhãn (hoặc các đối tượng Label
tương ứng) được liệt kê trong thuộc tính đó khi đích đượ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à một phần phụ thuộc của my_target
, do đó, other_target
được phân tích trước. Đây là lỗi nếu có một chu kỳ trong biểu đồ phần phụ thuộc của các mục tiêu.
Thuộc tính riêng tư và các phần phụ thuộc ngầm
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. Đây là một phần ngầm định vì nó là một phần của biểu đồ đích mà người dùng không chỉ định trong tệp BUILD
. Các phần phụ thuộc ngầm định rất hữu ích khi 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 trong thời gian xây dựng, chẳng hạn như 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. Trong hàm triển khai của quy tắc, hàm này được coi 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 mà không cho phép người dùng ghi đè giá trị đó, bạn có thể đặt thuộc tính là riêng tư bằng cách đặt cho thuộc tính đó một tên bắt đầu bằng dấu gạch dưới (_
). Thuộc tính riêng tư phải có giá trị mặc định. Thông thường, 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.
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 trên 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 ra 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 đó 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 yêu cầu họ phải biết nhãn của trình biên dịch.
Các phần phụ thuộc ngầm thường được dùng cho những 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ày đế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
Thuộc tính đầu ra, chẳng hạn như attr.output
và attr.output_list
, khai báo một 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ụ thuộc ở hai điểm:
- Chúng 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 của tệp đầu ra phụ thuộc vào mục tiêu quy tắc được khởi tạo, 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 đầu ra 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 một 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 đầu ra được khai báo trước, 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 đề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 đồ mục tiêu được tạo trong giai đoạn tải thành biểu đồ thao tác 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 là hàm riêng tư (được đặt tên bằng dấu gạch dưới ở đầu). Theo quy ước, chúng được đặt tên giống như quy tắc của chúng, nhưng có hậu tố là _impl
.
Các hàm triển khai chỉ nhận đúng một tham số: ngữ cảnh quy tắc, theo quy ước có tên là ctx
. Chúng 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 các đối tượng Target
. Các đối tượng này chứa các trì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 bằng cách sử dụng 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
.
Một danh sách các đối tượng nhà cung cấp được trả về theo hàm triển khai của mục tiêu:
return [ExampleInfo(headers = depset(...))]
Bạn có thể truy cập vào những thông tin này bằng ký hiệu chỉ mục ([]
), với loại nhà cung cấp là một 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 các quy tắc gốc có sẵn dưới dạng các biến chung Starlark.
Ví dụ: nếu một quy tắc lấy các tệp tiêu đề bằng cách sử dụng 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à người 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]
Có một kiểu cấu trúc cũ, bạn không nên dùng kiểu này và nên di chuyển các quy tắc ra khỏi kiểu này.
Tệp
Các tệp được biểu thị bằng các đối tượng File
. Vì Bazel không thực hiện thao tác I/O tệp trong giai đoạn phân tích, nên bạn không thể dùng các đối tượng này để đọc hoặc ghi trực tiếp nội dung tệp. Thay vào đó, các đối tượ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 thao tác. Tệp nguồn không thể là đầu ra của bất kỳ thao tác nào.
Đối với mỗi thuộc tính phụ thuộc, trường tương ứng của ctx.files
chứa danh sách các đầu ra mặc định của tất cả các phần phụ thuộc bằng 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
cho các thuộc tính phụ thuộc mà quy cách đặ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 những thuộc tính phụ thuộc mà 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 một quy tắc có thể tạo ra các kết quả đầu ra.
Vì tất cả nhãn phải được biết trong giai đoạn tải, nên các đầu ra bổ sung này không có nhãn. Bạn có thể tạo các đố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 các đầ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, 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 các đố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 thao tác được tạo, Bazel sẽ không chạy lệnh ngay lập tức. Test Orchestrator sẽ đăng ký hoạt động này trong biểu đồ phần phụ thuộc, vì một hoạt động có thể phụ thuộc vào đầu ra của một hoạt độ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 ra các thao tác đượ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ỹ các đối số cho các thao tác một cách hiệu quả. Thao tác này giúp tránh làm phẳng depsets 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],
)
...
Các thao tác lấy danh sách hoặc depset của tệp đầu vào và tạo danh sách (không trống) các tệp đầu ra. Bạn phải biết tập hợp các tệp đầu vào và đầu ra trong giai đoạn phân tích. Thao tác này có thể phụ thuộc vào giá trị của các thuộc tính, bao gồm cả các 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ả 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 những tệp mà bạn dự kiến sẽ được giải nén (trước khi chạy lệnh giải nén). Các thao tác tạo một số lượng biến đổi của tệp nội bộ có thể gói các 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).
Các thao tác phải liệt kê tất cả các đầu vào. Bạn được phép liệt kê những đầu vào không được dùng, nhưng điều này không hiệu quả.
Các thao tác 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 người dùng sẽ không truy cập được vào bất kỳ nội dung nào không có trong đầu ra. Tất cả các đầu ra đã khai báo phải được ghi bằng một số thao tác.
Các thao tác có thể so sánh với các hàm thuần tuý: Chúng chỉ nên phụ thuộc vào các đầu vào được cung cấp và 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ị đầu vào/đầu ra (ngoại trừ việc đọc đầu vào và ghi đầu ra). Điều này rất quan trọng vì đầu ra sẽ được lưu vào bộ nhớ đệm và dùng lại.
Các phần phụ thuộc được Bazel phân giải, Bazel sẽ quyết định những hành động cần thực thi. Đây là lỗi nếu có một chu kỳ trong biểu đồ 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, điều này phụ thuộc vào việc các đầ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à 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ỳ thông tin nào khác mà người dùng của mục tiêu cần biết.
Vì hàm triển khai của một quy tắc chỉ có thể đọc các nhà cung cấp từ các phần phụ thuộc tức thì của mục tiêu được 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, thường là bằng cách tích luỹ thông tin đó vào một depset
.
Các nhà cung cấp của mục tiêu được chỉ định bằng danh sách các đối tượng nhà cung cấp 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 trả về struct
thay vì danh sách các đối tượng 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à đầ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 tại dòng lệnh. Ví dụ: đích java_library
//pkg:foo
có foo.jar
làm đầu ra mặc định, vì vậy, đích 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 một quy tắc triển khai 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 đầu ra mặc định, ngay cả khi những đầu ra đó không được dùng trực tiếp. Các thao tác không có trong biểu đồ của đầu ra được yêu cầu sẽ bị cắt bớt. Nếu một đầu ra chỉ được người dùng của mục tiêu sử dụng, thì những 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 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 hiện được lỗi.
Runfiles
Runfile 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 (khác với thời gian tạo bản dựng). Trong giai đoạn thực thi, Bazel sẽ tạo một cây thư mục chứa các symlink trỏ đến runfile. Thao tác này dàn dựng môi trường cho tệp nhị phân để tệp nhị phân có thể truy cập vào các tệp thời gian chạy trong thời gian chạy.
Bạn có thể thêm runfile theo cách thủ công trong quá trình tạo quy tắc.
Bạn có thể tạo các đối tượng runfiles
bằng phương thức runfiles
trên ngữ cảnh quy tắc, ctx.runfiles
và truyền đến tham số runfiles
trên DefaultInfo
. Đầu ra thực thi của các quy tắc có thể thực thi được thêm ngầm vào runfiles.
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 runfiles của các mục tiêu. Runfile 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 sau cùng, thường là srcs
(có thể chứa các mục tiêu filegroup
có 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 các nhà cung cấp bằ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ác trình cung cấp
Bạn có thể bảo vệ quá trình khởi tạo một trình cung cấp bằng logic xác thực và tiền xử lý tuỳ chỉnh. Bạn có thể dùng cách này để đảm bảo rằng tất cả các thực thể nhà cung cấp đều đáp ứng 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 để 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 một lệnh gọi lại init
vào hàm provider
. Nếu bạn cung cấp lệnh gọi lại này, loại 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 init
không được dùng và một "trình 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ố 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à một tên trường ánh xạ từ điển (chuỗi) đến các giá trị; điều này được dùng để khởi tạo các trường của phiên bản 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ý, thì 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 dựng thô sẽ bỏ qua lệnh gọi lại init
.
Ví dụ sau đây sử dụng init
để tiền xử lý và xác thực các đối số của nó:
# //pkg:exampleinfo.bzl
_core_headers = [...] # private constant representing standard library files
# 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(
fields = ["files_to_link", "headers"],
init = _exampleinfo_init,
)
Sau đó, quá trình triển khai quy tắc có thể khởi tạo 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ể dùng hàm khởi tạo thô để xác định các hàm tạo công khai thay thế không thông qua logic init
. Ví dụ: exampleinfo.bzl 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ã người dùng không thể tải hàm khởi tạo đó và tạo các thực thể trình cung cấp tuỳ ý.
Một cách sử dụng khác cho init
là ngăn người dùng gọi hoàn toàn biểu tượng nhà cung cấp và buộc họ sử dụng một hàm gốc thay thế:
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ử
Các quy tắc có thể thực thi xác định những 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 thực thi đặc biệt mà các mục tiêu của quy tắc này cũng có thể được gọi bằng lệnh bazel test
. Các quy tắc có thể 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,
...
)
Các 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 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 đều phải tạo ra một tệp đầu ra có thể thực thi (có thể được khai báo trước hoặc không) sẽ được gọi bằng lệnh run
hoặc test
. Để cho Bazel biết đầu ra nào của một quy tắc sẽ dùng làm tệp thực thi này, hãy truyền đầu ra đó dưới dạng đối số executable
của trình cung cấp DefaultInfo
được trả về. executable
đó được thêm vào các đầu ra mặc định của quy tắc (vì vậy, bạn không cần truyền quy tắc đó cho cả executable
và files
). Quy tắc này 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 thao tác ctx.actions.run
hoặc ctx.actions.run_shell
, thao tác này phải được thực hiện bằng công cụ cơ bản do thao tác gọi. Đối với thao tác ctx.actions.write
, hãy truyền is_executable = True
.
Theo hành vi cũ, các quy tắc có thể thực thi có một đầu ra ctx.outputs.executable
đặc biệt được khai báo trước. 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 dùng tệp này. 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ề một quy tắc có thể thực thi và một quy tắc kiểm thử.
Quy tắc có thể thực thi và quy tắc kiểm thử có thêm các thuộc tính được xác định ngầm, ngoài những thuộc tính được thêm cho tất cả quy tắc. Bạn không thể thay đổi các giá trị mặc định của những 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 bao bọc 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í của runfile
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 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 cạnh thư mục gốc của runfiles
. Tuy nhiên, các tệp nhị phân được gọi từ runfiles 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 gốc runfiles làm tham số bằng cách sử dụng một môi trường, hoặc đối số hoặc cờ dòng lệnh. Điều này cho phép các tệp nhị phân truyền gốc runfiles chuẩn chính xác đến các tệp nhị phân mà nó gọi. Nếu không được đặt, 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 kiếm một thư mục runfiles liền kề.
Chủ đề nâng cao
Yêu cầu tệp đầu ra
Một đích đến có thể có nhiều tệp đầu ra. Khi lệnh bazel build
được chạy, một số đầu ra của các mục tiêu được đưa ra cho lệnh này đượ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à chúng phụ thuộc trực tiếp hoặc gián tiếp. (Về biểu đồ hành động, Bazel chỉ thực thi những hành động có thể truy cập được dưới dạng các phần phụ thuộc bắc cầu của tệp được yêu cầu.)
Ngoài đầu ra 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 bằng cách sử dụng thuộc tính đầu ra. Trong trường hợp đó, người dùng sẽ chọn nhãn một cách rõ ràng cho các đầu ra khi họ tạo thực thể cho quy tắc. Để lấy các đố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 dùng nữa.
Ngoài các đầu ra mặc định, còn có nhóm đầu ra. Đây là các tập hợp gồm 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 một mục tiêu //pkg:mytarget
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 một nhóm đầu ra.
Bạn có thể chỉ định các nhóm đầu ra bằng 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ởi cả khía cạnh và mục tiêu quy tắc mà khía cạnh đó đượ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.
Xin lưu ý rằng OutputGroupInfo
thường không được dù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 người 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 rằng bạn muốn tạo một tệp nhị phân C++ cho một cấu trúc khác. Quá trình tạo có thể phức tạp và bao gồm 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ữ của bạn hoặc một 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 đích.
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 (những mục tiêu được yêu cầu trên dòng lệnh) được tích hợp trong cấu hình "target", trong khi các công cụ sẽ chạy trên nền tảng thực thi được tích hợp trong cấu hình "exec". Các quy tắc có thể tạo ra các 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 cùng một thư viện cho nhiều cấu hình. Nếu điều này xảy ra, thì tệp sẽ được phân tích và có thể được tạo nhiều lần.
Theo mặc định, Bazel sẽ 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à 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 một quá trình chuyển đổi sang cấu hình exec. Đ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 exec.
Nếu 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 nhằm ngăn chặn việc vô tình tạo ra 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 tại 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 quá trình tạo (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 exec. 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 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ả: đây hoàn toàn là một giá trị tiện lợi giúp nhà thiết kế quy tắc nêu rõ ý định của họ. Khi executable = False
, tức là cfg
là không bắt buộc, bạn chỉ nên đặt tham số này khi tham số 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
để dùng các hiệu ứ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 có nhiều lựa chọn linh hoạt trong việc thay đổi cấu hình, nhưng có nhược điểm là khiến 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 và 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ủ. Các phiên bản Bazel trước 6.0 đã tạo một cấu hình "máy chủ lưu trữ" riêng biệt để biểu thị điều này. Nếu bạn thấy các thông tin tham khảo về "máy chủ lưu trữ" trong mã hoặc tài liệu cũ, thì đó là nội dung mà thông tin này đề cập đến. Bạn nên sử dụng Bazel 6.0 trở lên để tránh chi phí phát sinh thêm này.
Mảnh cấu hình
Các quy tắc có thể truy cập vào các mảnh cấu hình, chẳng hạn như cpp
và java
. 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
...
)
Đường liên kết tượng trưng Runfiles
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 cần phải thay đổi các đối số này vì một 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
là như nhau, nhưng các đường dẫn được thêm tiền tố một cách ngầm định bằng tên của không gian làm việc chính (không phải tên của kho lưu trữ chứa mục tiêu hiện tại).
...
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 liên kết hai tệp khác nhau với cùng một đường dẫn trong cây runfiles. Việc này sẽ khiến bản dựng không thành công kèm theo một 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. Thao tác 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ục tiêu thuộc mọi loại phụ thuộc vào những mục tiêu đó. Điều này đặc biệt rủi ro nếu công cụ của bạn có khả năng được một công cụ khác sử dụng một cách gián tiếp; tên symlink phải là duy nhất trong các tệp thời gian 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 lệnh coverage
được chạy, bản dựng có thể cần thêm công cụ đo lường mức độ phù hợp 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 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 một quy tắc triển khai thêm tính năng đo lường mức độ phù hợp tại thời điểm tạo bản dựng, thì quy tắc đó cần tính đến điều này trong hàm triển khai. ctx.coverage_instrumented trả về True
ở chế độ đo lường mức độ phù hợp nếu các 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 cần được bật ở chế độ 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 kiện hoá trên ctx.configuration.coverage_enabled.
Nếu quy tắc trực tiếp bao gồm các nguồn từ các phần phụ thuộc của quy tắ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 bật tính năng đo lường thời gian biên dịch nếu các 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.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 phải cung cấp thông tin về những thuộc tính có liên quan đến phạm vi cung cấp 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 có thể thêm tính 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 InstrumentedFilesInfo
không được trả về, một giá trị mặc định sẽ được tạo với mỗi thuộc tính phần phụ thuộc không phải công cụ không đặt cfg
thành "exec"
trong giản đồ thuộc tính. trong dependency_attributes
. (Đây không phải là hành vi lý tưởng, vì hành vi này đặt các thuộc tính như srcs
vào dependency_attributes
thay vì source_attributes
, nhưng hành vi này giúp bạn không cần phải định cấu hình phạm vi một cách rõ ràng cho tất cả các quy tắc trong chuỗi phần phụ thuộc.)
Kiểm thử quy tắc
Các quy tắc kiểm thử cần có chế độ thiết lập bổ sung để tạo báo cáo mức độ phù hợp. Bản thân quy tắc phải thêm các thuộc tính ngầm sau:
my_test = rule(
...,
attrs = {
...,
# Implicit dependencies used by Bazel to generate coverage reports.
"_lcov_merger": attr.label(
default = configuration_field(fragment = "coverage", name = "output_generator"),
executable = True,
cfg = config.exec(exec_group = "test"),
),
"_collect_cc_coverage": attr.label(
default = "@bazel_tools//tools/test:collect_cc_coverage",
executable = True,
cfg = config.exec(exec_group = "test"),
)
},
test = True,
)
Bằng cách sử dụng configuration_field
, bạn có thể tránh được sự phụ thuộc vào công cụ hợp nhất LCOV của Java miễn là bạn không yêu cầu phạm vi.
Khi chạy, kiểm thử sẽ phát ra thông tin về mức độ phù hợp dưới dạng một hoặc nhiều tệp LCOV có tên riêng biệt vào thư mục do biến môi trường COVERAGE_DIR
chỉ định. Sau đó, Bazel sẽ hợp nhất các tệp này thành một tệp LCOV duy nhất bằng công cụ _lcov_merger
. Nếu có, công cụ này cũng sẽ thu thập mức độ phù hợp của C/C++ bằng công cụ _collect_cc_coverage
.
Mức độ bao phủ cơ sở
Vì mức độ sử dụng chỉ được thu thập cho mã nằm trong cây phần phụ thuộc của một kiểm thử, nên báo cáo về mức độ sử dụng có thể gây hiểu lầm vì không nhất thiết phải bao gồm tất cả mã khớp với cờ --instrumentation_filter
.
Vì lý do này, Bazel cho phép các quy tắc chỉ định các tệp mức độ phù hợp cơ sở bằng cách sử dụng thuộc tính baseline_coverage_files
của ctx.instrumented_files_info
. Người dùng phải tạo các tệp này ở định dạng LCOV bằng một thao tác do người dùng xác định và các tệp này phải liệt kê tất cả các dòng, nhánh, hàm và/hoặc khối trong tệp nguồn của mục tiêu (theo các tham số sources_attributes
và extensions
). Đối với các tệp nguồn trong những mục tiêu được đo lường mức độ phù hợp, Bazel sẽ hợp nhất mức độ phù hợp cơ sở của các tệp đó vào báo cáo mức độ phù hợp kết hợp được tạo bằng --combined_report
và do đó đảm bảo rằng các tệp chưa được kiểm thử vẫn xuất hiện dưới dạng chưa được bao phủ.
Nếu một quy tắc không cung cấp bất kỳ tệp phạm vi cơ sở nào, Bazel sẽ tạo thông tin phạm vi tổng hợp chỉ đề cập đến đường dẫn tệp nguồn, nhưng không chứa bất kỳ thông tin nào về nội dung của các tệp đó.
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 để thực hiện quy 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ác cấu phần phần mềm, nên các quy tắc không thể thực hiện quy trình 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 thao tác 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ể được chạy là phân tích tĩnh, linting, kiểm tra tính nhất quán và phần phụ thuộc, 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 thao tác duy nhất thực hiện việc biên dịch và kiểm tra có thể được tách thành một thao tác biên dịch và một thao tác kiểm tra, thì thao tác kiểm tra có thể chạy dưới dạng một thao tác xác thực và chạy song song với các thao tác khác.
Những "hành động xác thực" này thường không tạo ra bất kỳ thứ gì được dùng ở nơi khác trong bản dựng, vì chúng chỉ cần khẳng định mọi thứ về dữ liệu đầu vào của chúng. Tuy nhiên, điều này lại gây ra một vấn đề: Nếu một thao tác xác thực không tạo ra bất kỳ thứ gì được dùng ở nơi khác trong bản dựng, thì làm cách nào để một quy tắc có thể chạy thao tác đó? Trước đây, phương pháp này là để thao tác 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 đầu vào của một số thao tác quan trọng khác trong bản dựng:
Điều này có 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 được chạy, nhưng điều này có những hạn chế đáng kể:
Thao tác 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 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, ngay cả khi thao tác biên dịch sẽ bỏ qua đầu vào. Điều này làm giảm tính song song và làm chậm quá trình tạo bản dựng.
Nếu các thao tác khác trong bản dựng có thể chạy thay vì thao tác biên dịch, thì bạn cũng cần thêm các đầu ra trống của thao tác xác thực vào các thao tác đó (ví dụ: đầu ra jar nguồn 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 được thêm vào sau và đầu ra 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 xác thực
Nhóm đầu ra xác thực là một nhóm đầu ra được thiết kế để lưu giữ các đầu ra không dùng đến của các thao tác xác thực, để không cần phải thêm các đầu ra này một cách giả tạo vào đầu vào của các thao tác khác.
Nhóm này đặc biệt ở chỗ các đầu ra của nhóm 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 một phần phụ thuộc hoặc thông qua đầu ra ngầm định của mục tiêu). Xin lưu ý rằng hoạt động lưu vào bộ nhớ đệm và tính gia tăng thông thường vẫn được áp dụng: nếu các 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 thao tác xác thực xuất ra một số tệp, ngay cả khi đó là tệp trống. Điều này có thể yêu cầu bao bọc một số công cụ thường không tạo ra đầu ra để tạo một tệp.
Các thao tác xác thực của mục tiêu không được chạy trong 3 trường hợp:
- Khi mục tiêu được coi là một công cụ
- Khi mục tiêu được coi là một phần phụ thuộc ngầm (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 exec.
Giả sử rằng các mục tiêu này có các bản dựng và kiểm thử riêng biệt để phát hiện mọi lỗi xác thực.
Sử dụng Nhóm đầu ra 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 đầu vào của bất kỳ thao tác nào khác. Thao tác xác thực cho đích của loại quy tắc này vẫn sẽ chạy nếu đích phụ thuộc vào nhãn hoặc bất kỳ đầu ra ngầm định nào của đích phụ thuộc trực tiếp hoặc gián tiếp.
Thông thường, điều quan trọng là đầu ra của các thao tác xác thực chỉ được đưa 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 thao tác khác, vì điều này có thể làm giảm hiệu quả của tính song song. Tuy nhiên, lưu ý rằng Bazel không có bất kỳ quy trình kiểm tra đặc biệt nào để thực thi điều này. Do đó, bạn nên kiểm thử để đảm bảo rằng các đầu ra của thao tác xác thực không được thêm vào đầu vào của bất kỳ thao tác nào trong các kiểm thử cho 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ờ thao tác 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
, 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 được dùng nữa
Có 2 cách không dùng nữa để sử dụng các đầ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à mẫu chuỗi để tạo 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 rõ ràng các đầu ra vàoDefaultInfo.files
. Sử dụng nhãn của mục tiêu quy tắc làm đầ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 các quy tắc có thể thực thi,
ctx.outputs.executable
đề cập đến 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 rõ ràng đầu ra, chẳng hạn như bằngctx.actions.declare_file(ctx.label.name)
, đồng thời đả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 đầu ra có thể thực thi đến tham sốexecutable
củaDefaultInfo
.
Các tính năng runfile 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ì lý do kế thừa.
Các đề xuất sau đây giúp giảm độ phức tạp:
Tránh sử dụng các chế độ
collect_data
vàcollect_default
củactx.runfiles
. Các chế độ này ngầm thu thập runfile trên một số cạnh phụ thuộc được mã hoá cứng theo những cách gây nhầm lẫn. Thay vào đó, hãy thêm tệp bằng cách sử dụng các tham sốfiles
hoặctransitive_files
củactx.runfiles
, hoặc bằng cách hợp nhất trong runfile từ các phần phụ thuộc bằngrunfiles = 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 = ...)
. Sự khác biệt giữa các tệp thực thi "mặc định" và "dữ liệu" được duy trì vì lý do cũ. Ví dụ: một số quy tắc đặt đầu ra mặc định trongdata_runfiles
, nhưng không đặt trongdefault_runfiles
. Thay vì sử dụngdata_runfiles
, các quy tắc phải cả bao gồm cá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ỉ dùng để hợp nhất các tệp thực thi 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
. Chúng được truy cập bằng toán tử dấu chấm và được tạo bằng cách đặt trường trong struct
do hàm triển khai của quy tắc trả về thay vì danh sách các đối tượng nhà cung cấp:
return struct(example_info = struct(headers = depset(...)))
Bạn có thể truy xuất những nhà cung cấp như vậy từ trường tương ứng của đối tượng Target
:
transitive_headers = [hdr.example_info.headers for hdr in ctx.attr.hdrs]
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 thông tin sau đây để 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. Nó cũng hỗ trợ việc ẩ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ể nhà cung cấp đều phải truy xuất thực thể đó bằng biểu tượng 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ả nhà cung cấp cũ và nhà cung cấp 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 phiên bản của quy tắc này, thì bạn có thể truy xuất các nhà cung cấp và nội dung của họ 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 nhà cung cấp tương ứng cũ):
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
. Bạn không được phép chỉ định bất kỳ trường nào trong số này khi đồng thời trả về nhà cung cấpDefaultInfo
.Trường
output_groups
nhận một giá trị cấu trúc và tương ứng vớiOutputGroupInfo
.
Trong các khai báo provides
về quy tắc và trong các khai báo providers
về thuộc tính phần phụ thuộc, các trình cung cấp cũ được truyền dưới dạng chuỗi và các trình cung cấp hiện đại được truyền bằng biểu tượng Info
. Hãy nhớ thay đổi từ chuỗi thành biểu tượng khi di chuyển. Đối với các quy tắc phức tạp hoặc quy tắc lớn mà bạn khó cập nhật tất cả các quy tắc một cách riêng lẻ, bạn có thể thực hiện 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 ra 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ước đó. Đố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 khai báo đó để bao gồm cả trình cung cấp cũ và trình cung cấp hiện đại.
Sửa đổi các quy tắc sử dụng trình cung cấp cũ để thay vào đó sử dụng trình cung cấp hiện đại. Nếu bất kỳ khai báo thuộc tính nào yêu cầu nhà cung cấp cũ, hãy cập nhật các khai báo đó để thay vào đó yêu cầu nhà cung cấp hiện đại. Bạn có thể tuỳ ý 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 hoặc 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 cách sử dụng
hasattr(target, 'foo')
hoặc nhà cung cấp mới bằng cách sử dụngFooInfo in target
.Xoá hoàn toàn nhà cung cấp cũ khỏi tất cả các quy tắc.