Starlark là một ngôn ngữ cấu hình tương tự như Python, ban đầu được phát triển để sử dụng trong Bazel và sau đó được các công cụ khác áp dụng. Các tệp BUILD
và .bzl
của Bazel được viết bằng một phương ngữ của Starlark, được gọi đúng cách là "Ngôn ngữ xây dựng", mặc dù thường được gọi đơn giản là "Starlark", đặc biệt là khi nhấn mạnh rằng một tính năng được thể hiện bằng Ngôn ngữ xây dựng thay vì là một phần tích hợp hoặc "gốc" của Bazel. Bazel bổ sung cho ngôn ngữ cốt lõi nhiều hàm liên quan đến bản dựng, chẳng hạn như glob
, genrule
, java_binary
, v.v.
Hãy xem tài liệu về Bazel và Starlark để biết thêm thông tin chi tiết, cũng như mẫu SIG quy tắc làm điểm bắt đầu cho các nhóm quy tắc mới.
Quy tắc trống
Để tạo quy tắc đầu tiên, hãy tạo tệp foo.bzl
:
def _foo_binary_impl(ctx):
pass
foo_binary = rule(
implementation = _foo_binary_impl,
)
Khi gọi hàm rule
, bạn phải xác định một hàm gọi lại. Logic sẽ nằm ở đó, nhưng hiện tại bạn có thể để trống hàm. Đối số ctx
cung cấp thông tin về mục tiêu.
Bạn có thể tải quy tắc và sử dụng quy tắc đó từ tệp BUILD
.
Tạo một tệp BUILD
trong cùng thư mục:
load(":foo.bzl", "foo_binary")
foo_binary(name = "bin")
Giờ đây, bạn có thể tạo mục tiêu:
$ bazel build bin
INFO: Analyzed target //:bin (2 packages loaded, 17 targets configured).
INFO: Found 1 target...
Target //:bin up-to-date (nothing to build)
Mặc dù quy tắc này không làm gì cả, nhưng nó vẫn hoạt động như các quy tắc khác: có tên bắt buộc, hỗ trợ các thuộc tính chung như visibility
, testonly
và tags
.
Mô hình đánh giá
Trước khi tiếp tục, bạn cần hiểu cách đánh giá mã.
Cập nhật foo.bzl
bằng một số câu lệnh in:
def _foo_binary_impl(ctx):
print("analyzing", ctx.label)
foo_binary = rule(
implementation = _foo_binary_impl,
)
print("bzl file evaluation")
và BUILD:
load(":foo.bzl", "foo_binary")
print("BUILD file")
foo_binary(name = "bin1")
foo_binary(name = "bin2")
ctx.label
tương ứng với nhãn của mục tiêu đang được phân tích. Đối tượng ctx
có nhiều trường và phương thức hữu ích; bạn có thể xem danh sách đầy đủ trong tài liệu tham khảo về API.
Truy vấn mã:
$ bazel query :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:8:1: bzl file evaluation
DEBUG: /usr/home/bazel-codelab/BUILD:2:1: BUILD file
//:bin2
//:bin1
Hãy quan sát một vài điều:
- "bzl file evaluation" (đánh giá tệp bzl) sẽ được in trước. Trước khi đánh giá tệp
BUILD
, Bazel sẽ đánh giá tất cả các tệp mà nó tải. Nếu nhiều tệpBUILD
đang tải foo.bzl, bạn sẽ chỉ thấy một lần xuất hiện của "đánh giá tệp bzl" vì Bazel lưu kết quả đánh giá vào bộ nhớ đệm. - Hàm gọi lại
_foo_binary_impl
không được gọi. Truy vấn Bazel tải các tệpBUILD
nhưng không phân tích các mục tiêu.
Để phân tích các mục tiêu, hãy sử dụng cquery
("configured query") hoặc lệnh build
:
$ bazel build :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin1
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin2
INFO: Analyzed 2 targets (0 packages loaded, 0 targets configured).
INFO: Found 2 targets...
Như bạn có thể thấy, _foo_binary_impl
hiện được gọi hai lần – một lần cho mỗi mục tiêu.
Xin lưu ý rằng cả "bzl file evaluation" (đánh giá tệp bzl) và "BUILD file" (tệp BUILD) đều không được in lại, vì quá trình đánh giá foo.bzl
được lưu vào bộ nhớ đệm sau khi gọi bazel query
.
Bazel chỉ phát ra các câu lệnh print
khi chúng thực sự được thực thi.
Tạo tệp
Để quy tắc của bạn hữu ích hơn, hãy cập nhật quy tắc đó để tạo một tệp. Trước tiên, hãy khai báo tệp và đặt tên cho tệp đó. Trong ví dụ này, hãy tạo một tệp có cùng tên với mục tiêu:
ctx.actions.declare_file(ctx.label.name)
Nếu chạy bazel build :all
ngay bây giờ, bạn sẽ gặp lỗi:
The following files have no generating action:
bin2
Bất cứ khi nào khai báo một tệp, bạn phải cho Bazel biết cách tạo tệp đó bằng cách tạo một thao tác. Dùng ctx.actions.write
để tạo một tệp có nội dung đã cho.
def _foo_binary_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(
output = out,
content = "Hello\n",
)
Mã này hợp lệ nhưng sẽ không làm gì cả:
$ bazel build bin1
Target //:bin1 up-to-date (nothing to build)
Hàm ctx.actions.write
đã đăng ký một thao tác, hướng dẫn Bazel cách tạo tệp. Nhưng Bazel sẽ không tạo tệp cho đến khi tệp đó thực sự được yêu cầu. Vì vậy, việc cuối cùng cần làm là cho Bazel biết rằng tệp này là đầu ra của quy tắc, chứ không phải là tệp tạm thời được dùng trong quá trình triển khai quy tắc.
def _foo_binary_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(
output = out,
content = "Hello!\n",
)
return [DefaultInfo(files = depset([out]))]
Hãy xem các hàm DefaultInfo
và depset
sau. Hiện tại, hãy giả sử rằng dòng cuối cùng là cách chọn đầu ra của một quy tắc.
Bây giờ, hãy chạy Bazel:
$ bazel build bin1
INFO: Found 1 target...
Target //:bin1 up-to-date:
bazel-bin/bin1
$ cat bazel-bin/bin1
Hello!
Bạn đã tạo thành công một tệp!
Thuộc tính
Để quy tắc hữu ích hơn, hãy thêm các thuộc tính mới bằng cách sử dụng mô-đun attr
và cập nhật định nghĩa quy tắc.
Thêm một thuộc tính chuỗi có tên là username
:
foo_binary = rule(
implementation = _foo_binary_impl,
attrs = {
"username": attr.string(),
},
)
Tiếp theo, hãy đặt nó trong tệp BUILD
:
foo_binary(
name = "bin",
username = "Alice",
)
Để truy cập vào giá trị trong hàm gọi lại, hãy sử dụng ctx.attr.username
. Ví dụ:
def _foo_binary_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(
output = out,
content = "Hello {}!\n".format(ctx.attr.username),
)
return [DefaultInfo(files = depset([out]))]
Xin lưu ý rằng bạn có thể đặt thuộc tính là bắt buộc hoặc đặt giá trị mặc định. Xem tài liệu của attr.string
.
Bạn cũng có thể sử dụng các loại thuộc tính khác, chẳng hạn như boolean hoặc danh sách số nguyên.
Phần phụ thuộc
Các thuộc tính phần phụ thuộc, chẳng hạn như attr.label
và attr.label_list
, khai báo một phần phụ thuộc từ mục tiêu sở hữu thuộc tính đến mục tiêu có nhãn xuất hiện trong giá trị của thuộc tính. Loại thuộc tính này tạo thành cơ sở của biểu đồ mục tiêu.
Trong tệp BUILD
, nhãn đích xuất hiện dưới dạng một đối tượng chuỗi, chẳng hạn như //pkg:name
. Trong hàm triển khai, đích đến sẽ có thể truy cập được dưới dạng một đối tượng Target
. Ví dụ: xem các tệp do đích đến trả về bằng cách sử dụng Target.files
.
Nhiều tệp
Theo mặc định, chỉ những mục tiêu do quy tắc tạo mới có thể xuất hiện dưới dạng các phần phụ thuộc (chẳng hạn như mục tiêu foo_library()
). Nếu muốn thuộc tính chấp nhận các mục tiêu là tệp đầu vào (chẳng hạn như tệp nguồn trong kho lưu trữ), bạn có thể thực hiện bằng allow_files
và chỉ định danh sách các phần mở rộng tệp được chấp nhận (hoặc True
để cho phép mọi phần mở rộng tệp):
"srcs": attr.label_list(allow_files = [".java"]),
Bạn có thể truy cập vào danh sách tệp bằng ctx.files.<attribute name>
. Ví dụ: bạn có thể truy cập vào danh sách tệp trong thuộc tính srcs
thông qua
ctx.files.srcs
Một tệp
Nếu bạn chỉ cần một tệp, hãy sử dụng allow_single_file
:
"src": attr.label(allow_single_file = [".java"])
Sau đó, bạn có thể truy cập vào tệp này trong ctx.file.<attribute name>
:
ctx.file.src
Tạo tệp bằng mẫu
Bạn có thể tạo một quy tắc để tạo tệp .cc dựa trên mẫu. Ngoài ra, bạn có thể dùng ctx.actions.write
để xuất một chuỗi được tạo trong hàm triển khai quy tắc, nhưng cách này có 2 vấn đề. Trước tiên, khi mẫu lớn hơn, việc đặt mẫu vào một tệp riêng biệt sẽ giúp tiết kiệm bộ nhớ hơn và tránh tạo các chuỗi lớn trong giai đoạn phân tích. Thứ hai, việc sử dụng một tệp riêng biệt sẽ thuận tiện hơn cho người dùng. Thay vào đó, hãy dùng ctx.actions.expand_template
, thao tác này sẽ thực hiện việc thay thế trên một tệp mẫu.
Tạo một thuộc tính template
để khai báo một phần phụ thuộc trên tệp mẫu:
def _hello_world_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name + ".cc")
ctx.actions.expand_template(
output = out,
template = ctx.file.template,
substitutions = {"{NAME}": ctx.attr.username},
)
return [DefaultInfo(files = depset([out]))]
hello_world = rule(
implementation = _hello_world_impl,
attrs = {
"username": attr.string(default = "unknown person"),
"template": attr.label(
allow_single_file = [".cc.tpl"],
mandatory = True,
),
},
)
Người dùng có thể sử dụng quy tắc như sau:
hello_world(
name = "hello",
username = "Alice",
template = "file.cc.tpl",
)
cc_binary(
name = "hello_bin",
srcs = [":hello"],
)
Nếu không muốn cho người dùng cuối thấy mẫu và luôn sử dụng cùng một mẫu, bạn có thể đặt giá trị mặc định và đặt thuộc tính ở chế độ riêng tư:
"_template": attr.label(
allow_single_file = True,
default = "file.cc.tpl",
),
Các thuộc tính bắt đầu bằng dấu gạch dưới là thuộc tính riêng tư và không thể đặt trong tệp BUILD
. Giờ đây, mẫu này là một phần phụ thuộc ngầm: Mọi mục tiêu hello_world
đều có một phần phụ thuộc vào tệp này. Đừng quên cho phép các gói khác nhìn thấy tệp này bằng cách cập nhật tệp BUILD
và sử dụng exports_files
:
exports_files(["file.cc.tpl"])
Vươn xa hơn
- Hãy xem tài liệu tham khảo về các quy tắc.
- Làm quen với depset.
- Hãy xem kho lưu trữ ví dụ, trong đó có thêm các ví dụ về quy tắc.