Một quy tắc xác định một loạt hành động mà Bazel thực hiện đầu vào để tạo một tập hợp đầu ra, được tham chiếu trong nhà cung cấp do quy tắc trả về chức năng triển khai. Ví dụ: C++ quy tắc nhị phân có thể:
- Lấy một tập hợp gồm
.cpp
tệp nguồn (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
cùng với đầu ra có thể thực thi và các tệp khác để khả dụng trong thời gian chạy. - Trả về trình cung cấp
CcInfo
với 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 nó.
Theo góc nhìn của Bazel, g++
và các thư viện C++ tiêu chuẩn cũng là dữ liệu đầu vào
đối với quy tắc này. Là người viết quy tắc, bạn không chỉ cân nhắc quy tắc do người dùng cung cấp
đầu vào cho một quy tắc, mà còn tất 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 rằng bạn làm quen với quy tắc của Bazel các giai đoạn xây dựng. Điều quan trọng là bạn cần nắm được các giai đoạn của một bản dựng (tải, phân tích và thực thi). Điều này cũng hữu ích khi 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ố dịch vụ hỗ trợ chính cho một số ngôn ngữ.
Bằng cách xác định các quy tắc của riêng mình, bạn có thể thêm khả năng hỗ trợ tương tự cho các ngôn ngữ và công cụ
mà Bazel không hỗ trợ sẵn.
Bazel cung cấp một mô hình mở rộng để viết các quy tắc bằng cách sử dụng
Ngôn ngữ Starlark. Các quy tắc này được viết trong .bzl
tệp
có thể được tải trực tiếp từ BUILD
tệp.
Khi xác định quy tắc của riêng mình, bạn có thể quyết định thuộc tính nào mà quy tắc này hỗ trợ và cách nó tạo ra kết quả.
Hàm implementation
của quy tắc xác định hành vi chính xác của quy tắc đó trong thời gian
giai đoạn phân tích. Hàm này không chạy bất kỳ
các lệnh bên ngoài. Thay vào đó, thao tác này đăng ký các hành động mà sẽ được dùng
sau này trong giai đoạn thực thi để tạo kết quả của quy tắc, nếu chúng
cần thiết.
Tạo quy tắc
Trong tệp .bzl
, sử dụng hàm quy tắc để xác định một quy tắc mới
và lưu kết quả trong biến toàn cục. Lệnh gọi đến rule
sẽ chỉ định
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 có tạo một
đầu ra thực thi (với executable=True
) hoặc cụ thể
tệp thực thi kiểm thử (với test=True
). Nếu là quy tắc sau, thì quy tắc này là quy tắc kiểm thử,
và tên quy tắc phải kết thúc bằng _test
.
Tạo thực thể 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 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ể quy tắc. Thao tác này chỉ định tên cho mục tiêu và giá trị mới cho thuộc tính của mục tiêu.
Các quy tắc cũng có thể được gọi từ 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, macro Starlark phải được gọi từ các tệp BUILD
và chỉ có thể là
được gọi trong giai đoạn tải, khi BUILD
các tệp được đánh giá để tạo thực thể mục tiêu.
Thuộc tính
Thuộc tính là đối số quy tắc. Thuộc tính có thể cung cấp các giá trị cụ thể cho một cách triển khai mục tiêu hoặc có thể tham chiếu đến đích, tạo một biểu đồ về các phần phụ thuộc.
Các thuộc tính dành riêng cho quy tắc, chẳng hạn như srcs
hoặc deps
, được xác định bằng cách truyền một tệp ánh xạ
từ tên thuộc tính đến giản đồ (được tạo bằng attr
mô-đun) thành tham số attrs
của rule
.
Các thuộc tính phổ biến, chẳng hạn như
name
và visibility
sẽ ngầm thêm vào tất cả các quy tắc. Thông tin khác
các thuộc tính được ngầm thêm vào
quy tắc thực thi và quy tắc kiểm thử cụ thể. Những thuộc tính
được ngầm thêm vào quy tắc không thể được bao gồm trong từ điển được chuyển đến
attrs
.
Thuộc tính phần phụ thuộc
Quy tắc xử lý mã nguồn thường xác định các thuộc tính sau để xử lý các loại phần phụ thuộc khác nhau:
srcs
chỉ định các tệp nguồn được xử lý bằng 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 cách sắp xếp của tệp nguồn mà quy tắc sẽ xử lý. Quy tắc cho ngôn ngữ có tệp tiêu đề thường sẽ chỉ định một thuộc tínhhdrs
riêng cho tiêu đề được xử lý bởi mục tiêu và người tiêu dùng của mình.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 mà các phần phụ thuộc đó phải cung cấp. (Đối với 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ục tiêu. Thao tác này sẽ cho phép các tệp tuỳ ý đã chỉ định.
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. Bất kỳ thuộc tính nào 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
)
chỉ định các phần phụ thuộc thuộc một loại nhất định
giữa một mục tiêu và các mục tiêu có nhãn (hoặc nhãn
Label
đối tượng) được liệt kê trong thuộc tính đó khi đích
đã được xác định. Kho lưu trữ và có thể là cả đường dẫn cho các nhãn này đã được phân giải
so 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
biểu đồ phần phụ thuộc của các mục tiêu.
Các thuộc tính riêng tư và phần phụ thuộc ngầm ẩn
Thuộc tính phần phụ thuộc có giá trị mặc định sẽ tạo ra phần phụ thuộc ngầm ẩn. Nó
ngầm ẩn vì đó là một phần của biểu đồ mục tiêu mà người dùng không
chỉ định trong tệp BUILD
. Các phần phụ thuộc ngầm ẩn rất hữu ích trong việc mã hoá cứng
mối quan hệ giữa quy tắc và công cụ (phần phụ thuộc tại thời gian xây dựng, chẳng hạn như
trình biên dịch mã), vì hầu hết thời gian người dùng không muốn chỉ định
công cụ mà quy tắc sử dụng. Bên trong chức năng triển khai của quy tắc, tham số này được xử lý
giống như các phần phụ thuộc khác.
Nếu bạn muốn cung cấp 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 ở chế độ riêng tư bằng cách đặt tên cho thuộc tính
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
giá trị. Thông thường, bạn chỉ nên sử dụng các thuộc tính riêng tư để ngầm ẩn
phần phụ thuộc.
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 miền ngầm ẩn
phần phụ thuộc 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 các thao tác gọi lệnh
trình biên dịch, mặc dù người dùng không truyền nhãn của tệp dưới dạng dữ liệu đầu vào. Từ
_compiler
là một thuộc tính riêng tư, tuân theo sau ctx.attr._compiler
sẽ luôn trỏ đến //tools:example_compiler
trong tất cả các mục tiêu của quy tắc này
loại. 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ữ 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 nhau nếu cần, nhưng không cần nhận biết
.
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 nơi kho lưu trữ làm phương thức 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ì nên 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
, hãy khai báo tệp đầu ra mà
tạo ra mục tiêu. Các thuộc tính này khác với các thuộc tính phần phụ thuộc theo hai cách:
- 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 đã xác định nơi khác.
- Các mục tiêu tệp đầu ra phụ thuộc vào mục tiêu quy tắc được tạo thực thể, thay vì ngược lại.
Thông thường, thuộc tính đầu ra chỉ được dùng khi một quy tắc cần tạo dữ liệu đầu ra
với tên do người dùng xác định không thể dựa vào tên đích. Nếu một quy tắc có
một thuộc tính đầu ra, thuộc tính này 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 các đầ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.
Chức năng 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à biến đổi
biểu đồ các mục tiêu được tạo trong giai đoạn tải thành biểu đồ
hành động cần thực hiện trong giai đoạn thực thi. Do vậy,
các hàm triển khai thực sự không thể đọc hoặc ghi tệp.
Các hàm triển khai quy tắc thường ở chế độ riêng tư (được đặt tên bằng dấu đầu dòng
dấu gạch dưới). Thông thường, chúng được đặt tên giống như quy tắc của chúng, nhưng có hậu tố
cùng với _impl
.
Hàm triển khai nhận đúng một tham số: a
ngữ cảnh quy tắc, theo thông thường được đặt tên là ctx
. Các hàm này trả về một 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 Target
. Các đối tượng này chứa 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 mỗi trường
thuộc tính phần phụ thuộc, chứa các đối tượng Target
đại diện cho từng thuộc tính trực tiếp
phần phụ thuộc 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 các trang đó bằng cách sử dụng ký hiệu chỉ mục ([]
), với loại trì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 quy tắc gốc có sẵn dưới dạng Starlark
biến toàn cục.
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
chúng vào hành động biên dịch của mục tiêu và người tiêu dùng, thì nó có thể
thu thập chúng như sau:
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ừ một
chức năng triển khai của target 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 tuyệt đối không nên sử dụng kiểu này, nên các quy tắc nên di chuyển khỏi nó.
Files
Tệp được biểu thị bằng đố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 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 đó, chúng được chuyển đến phần phát ra 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 đã tạo. Mỗi tệp được tạo
phải là kết quả của đúng một hành động. Tệp nguồn không được là kết quả của
bất kỳ hành động nào.
Đối với mỗi thuộc tính phần phụ thuộc, trường tương ứng của
ctx.files
chứa danh sách đầu ra mặc định của tất 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
cho
các thuộc tính phần phụ thuộc có thông số kỹ thuật được đặt allow_single_file=True
.
ctx.executable
có hành vi giống như ctx.file
, nhưng chỉ
chứa các trường cho thuộc tính phần phụ thuộc có thông số kỹ thuật đặt executable=True
.
Khai báo dữ liệu đầu ra
Trong giai đoạn phân tích, chức năng triển khai của quy tắc có thể tạo kết quả.
Vì tất cả các nhãn đều phải được biết trong giai đoạn tải, nên các URL bổ sung
dữ liệu đầu ra 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ì
tên của các đầu ra đều 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ư các dữ liệu đầu ra được tạo cho
thuộc tính đầu ra, 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
Thao tác mô tả cách tạo một tập hợp đầu ra từ một tập hợp các đầ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 không chạy lệnh ngay lập tức. Phương thức này đăng ký dữ liệu này trong một biểu đồ của các phần phụ thuộc, vì một hành động có thể phụ thuộc vào kết quả 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 dùng chung để tạo thao tác được định nghĩa trong
ctx.actions
:
ctx.actions.run
để chạy một tệp thực thi.ctx.actions.run_shell
để chạy shell .ctx.actions.write
để ghi một chuỗi vào tệp.ctx.actions.expand_template
, thành tạo tệp từ mẫu.
Có thể sử dụng ctx.actions.args
để một cách hiệu quả
tích luỹ đối số cho hành động. Giúp làm phẳng phần phân tách cho đến khi
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 sẽ 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) tệp đầu ra. Tập hợp các tệp đầu vào và đầu ra phải được biết trong quá trình giai đoạn phân tích. Điều này có thể phụ thuộc vào giá trị của các thuộc tính phụ thuộc, 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ả 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 bạn muốn được tăng cường (trước khi chạy giải nén). Những thao tác tạo ra số lượng tệp thay đổi trong nội bộ có thể gói các tệp đó trong một một tệp (chẳng hạn như zip, tar hoặc định dạng lưu trữ khác).
Thao tác phải liệt kê tất cả thông tin đầu vào. Mục nhập danh sách không được sử dụng được 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 đó. Họ có thể ghi các tệp khác, nhưng mọi giá trị không có trong dữ liệu đầu ra sẽ không được cung cấp cho người tiêu dùng. Tất cả đầu ra đã khai báo phải được viết bằng hành động nào đó.
Các hành động tương tự như hàm thuần tuý: Chúng chỉ nên phụ thuộc vào đầu vào 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ị I/O (ngoại trừ việc đọc dữ liệu đầu vào và ghi đầu ra). Đây là quan trọng vì đầu ra 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 thao tác nào thực thi. Nếu có chu kỳ trong biểu đồ phần phụ thuộc thì đó là lỗi. Đang tạo một hành động không đảm bảo rằng hành động đó sẽ được thực thi, tuỳ thuộc vào việc dữ liệu đầu ra của nó cho bản dựng.
Nhà cung cấp
Nhà cung cấp là các thông tin mà quy tắc hiển thị cho các quy tắc khác phụ thuộc vào mã đó. Dữ liệu này có thể bao gồm các tệp đầu ra, thư viện, các tham số để truyền trên dòng lệnh của một công cụ hoặc bất cứ điều gì khác mà người tiêu dùng của mục tiêu nên biết về.
Do chức năng triển khai của quy tắc chỉ có thể đọc nhà cung cấp từ
các phần phụ thuộc trực tiếp của mục tiêu đã tạo thực thể, 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à bạn cần được
người tiêu dùng thông thường bằng cách tích luỹ dữ liệu đó vào một depset
.
Trình cung cấp của mục tiêu được chỉ định bằng danh sách đối tượng Provider
được trả về bởi
hàm triển khai.
Các hàm triển khai cũ cũng có thể được viết theo kiểu cũ, trong đó hàm
hàm triển khai trả về struct
thay vì danh sách
các đối tượng nhà cung cấp. Bạn tuyệt đối không nên sử dụng kiểu này, nên các quy tắc nên
di chuyển khỏi nó.
Dữ liệu đầ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ần tạo ở dòng lệnh. Ví dụ: một
Mục tiêu java_library
//pkg:foo
có foo.jar
làm đầu ra mặc định, do đó
sẽ được tạo bằng lệnh bazel build //pkg:foo
.
Đầu ra mặc định được chỉ định bởi tham số files
của
DefaultInfo
:
def _example_library_impl(ctx):
...
return [
DefaultInfo(files = depset([output_file]), ...),
...
]
Nếu quá trình triển khai quy tắc hoặc files
không trả về DefaultInfo
tham số không được chỉ định, DefaultInfo.files
mặc định là tất cả
đầu ra được khai báo trước (thường là những kết quả do đầu ra tạo ra
).
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 những kết quả đó không được sử dụng trực tiếp. Những hành động không có trong biểu đồ của dữ liệu đầu ra được yêu cầu bị cắt bớt. Nếu đầu ra chỉ được người tiêu dùng của mục tiêu sử dụng, 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. Chiến dịch này khiến việc gỡ lỗi khó khăn hơn vì việc 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 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 (trái ngược với bản dựng thời gian). Trong giai đoạn thực thi, Bazel tạo một cây thư mục chứa các liên kết tượng trưng trỏ đến các tệp runfile. Việc này giúp giai đoạn 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.
Bạn có thể tạo các đối tượng runfiles
bằng phương thức runfiles
vào ngữ cảnh quy tắc, ctx.runfiles
và được chuyển đến
Tham số runfiles
trên DefaultInfo
. Kết quả thực thi của
quy tắc có thể thực thi được ngầm thêm vào các tệp chạy.
Một số quy tắc chỉ định các thuộc tính, thường được đặt tên là
data
có dữ liệu đầu ra được thêm vào
mục tiêu tệp chạy bộ. Bạn cũng cần hợp nhất các tệp Runfile từ data
, cũng như
từ bất kỳ thuộc tính nào có thể cung cấp mã để thực thi cuối cùng, thường là
srcs
(có thể chứa các mục tiêu filegroup
với data
được liên kết) và
deps
.
def _example_library_impl(ctx):
...
runfiles = ctx.runfiles(files = ctx.files.data)
transitive_runfiles = []
for runfiles_attr in (
ctx.attr.srcs,
ctx.attr.hdrs,
ctx.attr.deps,
ctx.attr.data,
):
for target in runfiles_attr:
transitive_runfiles.append(target[DefaultInfo].default_runfiles)
runfiles = runfiles.merge_all(transitive_runfiles)
return [
DefaultInfo(..., runfiles = runfiles),
...
]
Nhà cung cấp tuỳ chỉnh
Bạn có thể xác định trình cung cấp bằng provider
để truyền tải thông tin cụ thể theo quy tắc:
ExampleInfo = provider(
"Info needed to compile/link Example code.",
fields={
"headers": "depset of header Files from transitive dependencies.",
"files_to_link": "depset of Files from compilation.",
})
Sau đó, các hàm triển khai quy tắc có thể tạo và trả về các thực thể 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 trình cung cấp
Bạn có thể bảo vệ quá trình tạo thực thể của trình cung cấp bằng logic tiền xử lý và xác thực. Điều này có thể được sử dụng để đảm bảo rằng tất cả thực thể của trình cung cấp tuân theo một số bất biến nhất định hoặc để cung cấp cho người dùng một API rõ ràng hơn cho lấy phiên bản.
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
đến phương thức
Hàm provider
. Nếu thực hiện lệnh gọi lại này,
loại dữ liệu trả về của provider()
thay đổi thành một bộ dữ liệu gồm hai giá trị: phần tử cung cấp
là giá trị trả về thông thường khi init
không được sử dụng và "thô"
hàm khởi tạo".
Trong trường hợp này, khi biểu tượng nhà cung cấp được gọi, thay vì trả về trực tiếp
một thực thể mới, phương thức này sẽ chuyển tiếp các đối số cùng với lệnh gọi lại init
. Chiến lược phát hành đĩa đơn
giá trị trả về của lệnh gọi lại phải là tên trường (chuỗi) ánh xạ chính tả đến giá trị;
giá trị này dùng để khởi tạo các trường của thực thể mới. Lưu ý rằng
lệnh gọi lại có thể có bất kỳ chữ ký nào và nếu các đối số không khớp với chữ ký
một lỗi được báo cáo như thể lệnh gọi lại được thực hiện trực tiếp.
Ngược lại, hàm khởi tạo thô sẽ bỏ qua lệnh gọi lại init
.
Ví dụ sau đây sử dụng init
để xử lý trước và xác thực các đối số của nó:
# //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
)
Hàm khởi tạo thô có thể dùng để xác định các hàm nhà máy công khai thay thế
không đi qua logic init
. Ví dụ: trong exampleinfo.bzl, chúng tôi
có thể xác định:
def make_barebones_exampleinfo(headers):
"""Returns an ExampleInfo with no files_to_link and only the specified headers."""
return _new_exampleinfo(files_to_link = depset(), headers = all_headers)
Thông thường, hàm khởi tạo thô được liên kết với một biến có tên bắt đầu bằng một
dấu gạch dưới (_new_exampleinfo
ở trên), để mã người dùng không thể tải mã này và
tạo các bản sao trình cung cấp tuỳ ý.
Một cách sử dụng khác của init
chỉ đơn giản là ngăn người dùng gọi cho ứng dụng nhà cung cấp
và buộc chúng sử dụng hàm factory:
def _exampleinfo_init_banned(*args, **kwargs):
fail("Do not call ExampleInfo(). Use make_exampleinfo() instead.")
ExampleInfo, _new_exampleinfo = provider(
...
init = _exampleinfo_init_banned)
def make_exampleinfo(...):
...
return _new_exampleinfo(...)
Quy tắc thực thi và quy tắc kiểm tra
Quy tắc thực thi xác định các mục tiêu có thể được gọi bằng lệnh bazel run
.
Quy tắc kiểm thử là loại quy tắc thực thi đặc biệt mà mục tiêu cũng có thể là
được gọi bằng lệnh bazel test
. Các quy tắc kiểm tra và thực thi được tạo bởi
đặt executable
hoặc
Đối số test
cho 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
. (Thường xuyên kiểm tra tên mục tiêu
kết thúc bằng _test
theo quy ước, nhưng điều này là không bắt buộc.) Quy tắc không kiểm thử không được
có hậu tố này.
Cả hai loại quy tắc đều phải tạo ra tệp đầu ra có thể thực thi (có thể hoặc không thể
được khai báo trước) sẽ được gọi bằng lệnh run
hoặc test
. Để cho biết
Bazel, trong số các dữ liệu đầu ra của một quy tắc để sử dụng dưới dạng tệp thực thi này, hãy truyền nó dưới dạng
Đối số executable
của DefaultInfo
được trả về
Google Cloud. executable
đó được thêm vào dữ liệu đầu ra mặc định của quy tắc (vì vậy bạn
không cần truyền giá trị đó cho cả executable
và files
). Điều này cũng được ngầm ẩn
đã thê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. Để
ctx.actions.run
hoặc
Hãy thực hiện thao tác ctx.actions.run_shell
này
theo công cụ cơ bản được gọi bằng hành động. Đối với
Thao tác ctx.actions.write
, truyền is_executable=True
.
Là hành vi cũ, các quy tắc thực thi có
đầu ra được khai báo trước ctx.outputs.executable
đặc biệt. 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
; không được
được sử dụng trong trường hợp khác. Cơ chế đầu ra này không được dùng nữa vì không hỗ trợ
tuỳ chỉnh tên của tệp thực thi tại thời điểm phân tích.
Xem ví dụ về quy tắc thực thi và một quy tắc kiểm thử.
Quy tắc thực thi và quy tắc kiểm tra có các quy tắc bổ sung được xác định rõ ràng ngoài các thuộc tính được thêm cho tất cả quy tắc. Giá trị mặc định của bạn không thể thay đổi các thuộc tính được thêm ngầm, mặc dù bạn có thể khắc phục vấn đề này bằng cách gói quy tắc riêng tư trong macro Starlark, thay đổi mặc định:
def example_test(size="small", **kwargs):
_example_test(size=size, **kwargs)
_example_test = rule(
...
)
Vị trí tệp Runfile
Khi một mục tiêu thực thi được chạy với bazel run
(hoặc test
), gốc của
Thư mục runfiles 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 liền kề với gốc của tệp
Thư mục runfiles
. Tuy nhiên, các tệp nhị phân có tên từ các tệp runfile đó không thể tạo
giả định tương tự. Để giảm thiểu điều 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 runfile của tệp đó làm tham số bằng cách dùng một môi trường hoặc dòng lệnh
đối số/gắn cờ. Điều này cho phép tệp nhị phân truyền đúng gốc của tệp chạy chuẩn chuẩn
vào các tệp nhị phân mà nó gọi. Nếu bạn không đặt thuộc tính này, 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 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
thì một số đầu ra của các mục tiêu được cung cấp cho lệnh sẽ được coi là
được yêu cầu. Bazel chỉ tạo 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 hay gián tiếp. (Về biểu đồ hành động, chỉ Bazel
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
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. Các quy tắc có thể chỉ định việc khai báo trước
đầu ra thông qua các thuộc tính đầu ra. Trong trường hợp đó, người dùng
chọn nhãn cho kết quả một cách rõ ràng khi chúng tạo thực thể quy tắc. Để có được
Đối tượng File
cho thuộc tính đầu ra, hãy sử dụng thuộc tính tương ứng
của ctx.outputs
. Quy tắc có thể
xác định ngầm các dữ liệu đầu ra được khai báo trước
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ó các nhóm đầu ra, là các tập hợp
tệp đầu ra có thể được yêu cầu cùng nhau. Bạn có thể yêu cầu những 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ó debug_files
nhóm đầu ra, 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ì đầu ra không được khai báo trước không có nhãn,
chúng chỉ có thể được yêu cầu bằng cách xuất hiện trong các dữ liệu đầu ra mặc định hoặc một đầu ra
nhóm.
Bạn có thể chỉ định nhóm đầu ra bằng
OutputGroupInfo
. Xin lưu ý rằng không giống như nhiều
trình cung cấp tích hợp sẵn, OutputGroupInfo
có thể 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 trình cung cấp, cả hai trình cung cấp đều có thể trả về OutputGroupInfo
khía cạnh và mục tiêu quy tắc mà khía cạnh đó được áp dụng, như
miễn là chúng không xác định cùng một nhóm đầu ra. Trong trường hợp đó, kết quả
các nhà cung cấp đã được hợp nhất.
Lưu ý rằng bạn không nên dùng OutputGroupInfo
để truyền đạt các cách sắp xếp cụ thể
tệp từ một mục tiêu đến hành động của người tiêu dùng. Định nghĩa
nhà cung cấp theo quy tắc cụ thể cho quy tắc đó.
Cấu hình
Hãy tưởng tượng rằng bạn muốn xây dựng một tệp nhị phân C++ cho một cấu trúc khác. Chiến lược phát hành đĩa đơn có thể phức tạp và gồm nhiều bước. Một số thuộc tính trung gian các tệp nhị phâ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 người thực thi từ xa). Bạn phải xây dựng một số tệp nhị phân (chẳng hạn như tệp đầu ra hoàn chỉnh) cấu trúc mục tiêu.
Vì lý do này, Bazel có khái niệm "cấu hình" và hiệu ứng chuyển cảnh. Chiến lược phát hành đĩa đơn các mục tiêu trên cùng (các mục tiêu được yêu cầu trên dòng lệnh) được tạo trong "mục tiêu" cấu hình, còn các công cụ chạy trên nền tảng thực thi được xây dựng dưới dạng "bộ phận thực thi" . Các quy tắc có thể tạo ra các hành động khác nhau dựa trên trên cấu hình, chẳng hạn như để thay đổi cấu trúc CPU được truyền cho trình biên dịch. Trong một số trường hợp, có thể bạn cần sử dụng cùng một thư viện cho các . Nếu điều này xảy ra, công cụ 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 theo cùng 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 phần phụ thuộc là một phần công cụ cần thiết để giúp xây dựng mục tiêu, thuộc tính tương ứng sẽ chỉ định hiệu ứng chuyển đổi sang cấu hình exec. Điều này khiến công cụ này và tất cả tạo các phần phụ thuộc dành 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ể dùng cfg
để quyết định xem có phần phụ thuộc hay không
phải tạo trong cùng một cấu hình hoặc chuyển đổi sang cấu hình thực thi.
Nếu thuộc tính phần phụ thuộc có cờ executable=True
, bạn phải đặt cfg
một cách rõ ràng. Việc này nhằm tránh việc vô tình tạo công cụ sai trái
.
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 xây dựng (chẳng hạn như trình biên dịch hoặc trình tạo mã)
phải được xây dựng 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 kiểm thử) phải
cho cấu hình mục tiêu. Trong trường hợp này, hãy chỉ định cfg="target"
trong
thuộc tính đó.
cfg="target"
không thực sự làm gì cả: nó chỉ đơn thuần 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
,
có nghĩa là cfg
là không bắt buộc, bạn chỉ nên đặt thuộc tính này khi nó thực sự giúp dễ đọc.
Bạn cũng có thể sử dụng cfg=my_transition
chuyển đổi do người dùng xác định, cho phép
tác giả quy tắc rất linh hoạt trong việc thay đổi cấu hình, với
nhược điểm của
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. và thay vào đó, tất cả các hành động của bản dựng đều được coi là chạy trên máy chủ lưu trữ. Do đó, chỉ có một "máy chủ" và một "máy chủ" chuyển đổi có thể dùng để tạo phần phụ thuộc trong cấu hình máy chủ lưu trữ. Nhiều quy tắc vẫn sử dụng "máy chủ lưu trữ" cho các công cụ của mình, nhưng công việc này hiện đang không dùng nữa và đang được chuyển sang sử dụng "exec" chuyển đổi khi có thể.
Có nhiều điểm khác biệt giữa "máy chủ lưu trữ" và "quản lý" cấu hình:
- "máy chủ lưu trữ" là thiết bị đầu cuối, "exec" not: Khi một phần phụ thuộc nằm trong "máy chủ lưu trữ" cấu hình, không cho phép chuyển đổi thêm. Bạn có thể tiếp tục cải thiện chuyển đổi cấu hình khi bạn đang ở trong quá trình "thực thi" .
- "máy chủ lưu trữ" là hình khối, "exec" not: Chỉ có một "máy chủ lưu trữ" cấu hình, nhưng có thể có một "người điều hành" khác cấu hình cho mỗi lần thực thi chủ.
- "máy chủ lưu trữ" giả sử 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áy tương tự đáng kể. Điều này không còn đúng nữa: bạn có thể chạy bản dựng trên máy cục bộ hoặc trên bộ thực thi từ xa và không có đảm bảo rằng trình thực thi từ xa có cùng CPU và hệ điều hành máy.
Cả "quản lý viên" và "host" sẽ áp dụng cùng các thay đổi về lựa chọn (ví dụ:
đặt --compilation_mode
từ --host_compilation_mode
, đặt --cpu
từ
--host_cpu
, v.v.). Sự khác biệt là "máy chủ lưu trữ" cấu hình bắt đầu bằng
các giá trị mặc định của tất cả cờ khác, trong khi lệnh "exec" cấu hình
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
Các quy tắc có thể truy cập
các mảnh cấu hình 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 trong
để 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 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ủ lưu trữ, hãy sử dụng
Hãy ctx.host_fragments
.
Đường liên kết tượng trưng trong Runfile
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 yêu cầu này
cần phải khác nhau vì một số lý do, bạn có thể chỉ định root_symlinks
hoặc
Đối số symlinks
. root_symlinks
là một đường dẫn ánh xạ từ điển đến
các tệp, trong đó đường dẫn tương ứng với thư mục gốc của thư mục runfile. Chiến lược phát hành đĩa đơn
Từ điển symlinks
giống nhau, nhưng các đường dẫn được ngầm định với tiền tố
tên của không gian làm việc.
...
runfiles = ctx.runfiles(
root_symlinks = {"some/path/here.foo": ctx.file.some_data_file2}
symlinks = {"some/path/here.bar": ctx.file.some_data_file3}
)
# Creates something like:
# sometarget.runfiles/
# some/
# path/
# here.foo -> some_data_file2
# <workspace_name>/
# some/
# path/
# here.bar -> some_data_file3
Nếu bạn đang sử dụng symlinks
hoặc root_symlinks
, hãy cẩn thận để không liên kết 2 thuộc tính khác nhau
đến cùng một đường dẫn trong cây runfile. Việc này sẽ khiến bản dựng bị lỗi
kèm theo lỗi mô tả xung đột. Để khắc phục vấn đề này, bạn cần phải sửa đổi
ctx.runfiles
đối số để loại bỏ xung đột. Việc kiểm tra này sẽ được thực hiện đối với
bất kỳ mục tiêu nào đang sử dụng quy tắc của bạn, cũng như mục tiêu thuộc bất kỳ loại nào phụ thuộc vào
mục tiêu. Điều này đặc biệt rủi ro nếu công cụ của bạn có thể được sử dụng theo cách bắc cầu
của một công cụ khác; tên liên kết tượng trưng phải là duy nhất trên các runfile của công cụ và
tất cả phần phụ thuộc của nó.
Mức độ sử dụng mã
Khi chạy lệnh coverage
,
bản dựng có thể cần thêm khả năng đo lường mức độ sử dụng cho một số mục tiêu nhất định. Chiến lược phát hành đĩa đơn
tạo cũng thu thập danh sách các tệp nguồn được đo lường. Tập con của
các mục tiêu được cho là do cờ kiểm soát
--instrumentation_filter
.
Các mục tiêu thử nghiệm sẽ bị loại trừ, trừ phi
--instrument_test_targets
được chỉ định.
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 xây dựng, thì quy tắc đó cần để tính đến điều đó trong chức năng triển khai. ctx.coverage_instrumented trả về giá trị true trong chế độ mức độ phù hợp nếu các nguồn của mục tiêu được đo lường:
# Are this rule's sources instrumented?
if ctx.coverage_instrumented():
# Do something to turn on coverage for this compile action
Logic luôn phải bật ở chế độ sử dụng (cho dù các nguồn của mục tiêu đượ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 bao gồm các nguồn từ các phần phụ thuộc của nó trước khi biên dịch (chẳng hạn như tệp tiêu đề), thì cũng có thể bạn phải bật tính năng đo lường thời gian biên dịch nếu phần phụ thuộc nguồn cần được đo lường:
# Are this rule's sources or any of the sources for its direct dependencies
# in deps instrumented?
if (ctx.configuration.coverage_enabled and
(ctx.coverage_instrumented() or
any([ctx.coverage_instrumented(dep) for dep in ctx.attr.deps]))):
# Do something to turn on coverage for this compile action
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
mức độ phù hợp với trình cung cấp InstrumentedFilesInfo
, được xây dựng bằng
coverage_common.instrumented_files_info
.
Tham số dependency_attributes
của instrumented_files_info
sẽ liệt kê
tất cả thuộc tính phần phụ thuộc trong thời gian chạy, bao gồm cả phần phụ thuộc mã như deps
và
phần phụ thuộc dữ liệu như data
. Tham số source_attributes
sẽ liệt kê
các thuộc tính tệp nguồn của quy tắc nếu có thể thêm khả năng đo lường mức độ sử dụng:
def _example_library_impl(ctx):
...
return [
...
coverage_common.instrumented_files_info(
ctx,
dependency_attributes = ["deps", "data"],
# Omitted if coverage is not supported for this rule:
source_attributes = ["srcs", "hdrs"],
)
...
]
Nếu không trả về InstrumentedFilesInfo
, thì hệ thống sẽ tạo một giá trị mặc định trong đó mỗi thuộc tính
thuộc tính phần phụ thuộc không phải của công cụ chưa được đặt
cfg
đến "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
trong dependency_attributes
thay vì source_attributes
, nhưng
tránh nhu cầu định cấu hình mức độ phù hợp rõ ràng cho tất cả cá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 điều gì đó 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 đã tạo). Vì thông tin này nằm trong cấu phần phần mềm, không thể thực hiện việc xác thực này tại thời điểm phân tích vì các quy tắc không thể đọc tệp. Thay vào đó, các hành động phải thực hiện quy trình xác thực này tại thời điểm thực thi. Thời gian không xác thực được, hành động sẽ không thành công và do đó bản dựng cũng vậy.
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.
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ác 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 duy nhất thực hiện việc biên dịch và tìm lỗi mã nguồn có thể được tách riêng thành thao tác biên dịch và thao tác tìm lỗi mã nguồn, sau đó là tìm lỗi mã nguồn có thể chạy dưới dạng hành động xác thực và chạy song song với các hành động khác.
Các "thao tác xác thực" này thường không tạo ra bất kỳ thứ gì được sử dụng ở nơi khác trong bản dựng, vì các công cụ này chỉ cần xác nhận thông tin về dữ liệu đầu vào. Chiến dịch này lại gặp phải một vấn đề: Nếu hành động xác thực không tạo ra bất kỳ nội dung nào được dùng ở nơi khác trong bản dựng, quy tắc làm cách nào để chạy hành động đó? Trước đây, phương pháp này là để hành động xác thực đầu ra trống và thêm một cách giả tạo đầu ra đó vào đầu vào của một số dữ liệu hành động trong bản dựng:
Cách này hiệu quả vì Bazel sẽ luôn chạy thao tác xác thực khi biên dịch là hành động được chạy nhưng điều này có 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 cần có đầu ra trống để chạy hành động biên dịch, thì tác vụ này sẽ chạy lệnh hành động xác thực trước, mặc dù hành động 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 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 vì biên dịch, thì bạn cần thêm các kết quả trống của hành động xác thực vào cả những thao tác đó (ví dụ: kết quả jar nguồn của
java_library
). Đây là Đây cũng là vấn đề nếu các hành động mới có thể chạy thay vì hành động biên dịch thêm sau này, nên kết quả xác thực trống vô tình bị bỏ dở.
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ế để chứa đầu ra không được sử dụng của các hành động xác thực, để chúng không cần phải là những hành động giả tạo vào thông tin đầu vào của các hành động khác.
Nhóm này đặc biệt ở chỗ dữ liệu đầu ra 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 (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). Lưu ý rằng việc lưu vào bộ nhớ đệm và mức độ gia tăng bình thường
vẫn áp dụng: nếu dữ liệu đầu vào cho hành động xác thực không thay đổi và
hành động xác thực đã thành công trước đó, thì hành động 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, thậm chí là một tệp trống. Bạn có thể phải gói một số công cụ thường không tạo dữ liệu đầu ra để tạo tệp.
Hành động xác thực của một mục tiêu không chạy trong 3 trường hợp sau:
- Khi mục tiêu được coi là một công cụ
- Khi mục tiêu phụ thuộc vào như một phần phụ thuộc ngầm ẩn (ví dụ: 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ả định rằng các mục tiêu này có riêng biệt các bản dựng và thử nghiệm để 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 sử dụng như bất kỳ nhóm nào khác
nhóm đầu ra:
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. Hành động xác thực cho 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 phụ thuộc trực tiếp hoặc gián tiếp.
Điều quan trọng là kết quả 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 dữ liệu đầu vào của các hành động khác, như điều này có thể đánh bại lợi ích của tính song song. Tuy nhiên, xin lưu ý rằng Bazel hiện không có bất kỳ yêu cầu kiểm tra đặc biệt nào để thực thi việc này. Do đó, bạn nên kiểm tra đầu ra của hành động xác thực sẽ không được thêm vào đầu vào của bất kỳ hành động nào trong cho các quy tắc Starlark. Ví dụ:
load("@bazel_skylib//lib:unittest.bzl", "analysistest")
def _validation_outputs_test_impl(ctx):
env = analysistest.begin(ctx)
actions = analysistest.target_actions(env)
target = analysistest.target_under_test(env)
validation_outputs = target.output_groups._validation.to_list()
for action in actions:
for validation_output in validation_outputs:
if validation_output in action.inputs.to_list():
analysistest.fail(env,
"%s is a validation action output, but is an input to action %s" % (
validation_output, action))
return analysistest.end(env)
validation_outputs_test = analysistest.make(_validation_outputs_test_impl)
Cờ hành động xác thực
Việc chạy các thao tác xác thực do dòng lệnh --run_validations
kiểm soát
cờ mặc định là true.
Những tính năng đã ngừng hoạt động
Kết quả được khai báo trước không dùng nữa
Có 2 cách không dùng nữa để sử dụng dữ liệu đầu ra được khai báo trước:
Tham số
outputs
củarule
chỉ định ánh xạ 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 dữ liệu đầu ra không được khai báo trước và thêm dữ liệu đầu ra một cách rõ ràng vàoDefaultInfo.files
. Sử dụng nhãn là dữ liệu đầu vào cho các quy tắc sử dụng đầu ra thay vì giá trị được khai báo trước của đầu ra.Đối với các quy tắc có thể thực thi,
ctx.outputs.executable
sẽ tham chiếu thành đầu ra tệp 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 dữ liệu đầu ra một cách rõ ràng, ví dụ như 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 các quyền để cho phép thực thi. Rõ ràng truyền đầu ra thực thi đến tham sốexecutable
củaDefaultInfo
.
Các tính năng cần tránh của Runfile
ctx.runfiles
và 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 sự phức tạp:
Tránh sử dụng các chế độ
collect_data
vàcollect_default
ctx.runfiles
. Các chế độ này ngầm thu thập các tệp runfile trên một số cạnh phụ 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 ngầm 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
. Hãy chỉ địnhDefaultInfo(runfiles = ...)
. Sự khác biệt giữa "mặc định" và "dữ liệu" các tệp runfile được duy trì cho những lý do cũ. Ví dụ: một số quy tắc đặt dữ liệu đầ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
thì các quy tắc phải cả hai đều phải bao gồm đầu ra mặc định và hợp nhất trongdefault_runfiles
từ các thuộc tính cung cấp các 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 chạy giữa quy tắc hiện tại và các phần phụ thuộc của quy tắc đó), hãy sử dụngDefaultInfo.default_runfiles
, không phảiDefaultInfo.data_runfiles
.
Di chuyển từ các nhà cung cấp cũ
Trước đây, trình cung cấp Bazel là các trường đơn giản trên đối tượng Target
. Chúng
được truy cập bằng toán tử dấu chấm và chúng được tạo bằng cách đặt trường
trong cấu trúc được hàm triển khai của quy tắc trả về.
Kiểu này không còn được dùng nữa và không nên được sử dụng trong mã mới; hãy xem bên dưới để biết có thể giúp bạn di chuyển. Cơ chế mới của nhà cung cấp giúp tránh việc đặt tên xung đột. Nền tảng này cũng hỗ trợ ẩn dữ liệu bằng cách yêu cầu bất kỳ mã nào truy cập vào phiên bản của nhà cung cấp để truy xuất dữ liệu đó bằng cách sử dụ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ả hai 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ì
và nội dung của các nhà cung cấp đó có thể được truy xuất 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ố lệnh khác
các trường có ý nghĩa đặc biệt (và do đó không tạo ra một kế thừa tương ứng
nhà cung cấp):
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ỳ các trường này, đồ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 provides
phần khai báo quy tắc và trong
Khai báo providers
về phần phụ thuộc
thì trình cung cấp cũ sẽ được chuyển vào dưới dạng chuỗi và 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 thành ký hiệu
khi di chuyển. Đối với các bộ quy tắc lớn hoặc phức tạp, khó cập nhật
tất cả các quy tắc một cách tỉ mỉ, bạn có thể dễ dàng hơn nếu tuân theo trình tự này
các bước:
Sửa đổi các quy tắc tạo nhà cung cấp cũ để tạo cả trình cung cấp cũ và các 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 trả về trình cung cấp cũ, cập nhật nội dung khai báo đó để bao gồm cả các nhà cung cấp cũ và hiện đại.
Sửa đổi các quy tắc sử dụng nhà cung cấp cũ để sử dụng Google Cloud. Nếu bất kỳ nội dung khai báo thuộc tính nào yêu cầu nhà cung cấp cũ, cũng hãy cập nhật chúng để yêu cầu nhà cung cấp hiện đại. Nếu muốn, 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 tiêu dùng chấp nhận/yêu cầu 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 trình cung cấp mới dùngFooInfo in target
.Xoá hoàn toàn nhà cung cấp cũ khỏi tất cả các quy tắc.