Trang này trình bày những thông tin cơ bản về cách sử dụng macro, bao gồm các trường hợp sử dụng điển hình, gỡ lỗi và quy ước.
Macro là một hàm được gọi từ tệp BUILD có thể tạo thực thể cho các quy tắc.
Macro chủ yếu được dùng để đóng gói và sử dụng lại mã của các quy tắc hiện có và
các macro khác.
Macro có 2 loại: macro tượng trưng (được mô tả trên trang này), và macro cũ. Nếu có thể, bạn nên sử dụng macro tượng trưng để mã rõ ràng hơn.
Macro tượng trưng cung cấp các đối số được nhập (chuyển đổi chuỗi thành nhãn, tương ứng với
vị trí gọi macro) và khả năng hạn chế cũng như chỉ định chế độ hiển thị của các mục tiêu được tạo. Chúng được thiết kế để phù hợp với việc đánh giá trì hoãn (sẽ được thêm vào bản phát hành Bazel trong tương lai). Theo mặc định, macro tượng trưng có
trong Bazel 8. Khi tài liệu này đề cập đến macros, tức là
đang đề cập đến macro tượng trưng.
Bạn có thể tìm thấy một ví dụ có thể thực thi về macro tượng trưng trong kho lưu trữ ví dụ.
Cách sử dụng
Macro được xác định trong các tệp .bzl bằng cách gọi hàm
macro() với
2 tham số bắt buộc: attrs và implementation.
Thuộc tính
attrs chấp nhận một từ điển tên thuộc tính cho các loại thuộc tính, đại diện cho các đối số của macro. Hai thuộc tính phổ biến – name và visibility –
được thêm ngầm vào tất cả các macro và không có trong từ điển được truyền
đến attrs.
# macro/macro.bzl
my_macro = macro(
attrs = {
"deps": attr.label_list(mandatory = True, doc = "The dependencies passed to the inner cc_binary and cc_test targets"),
"create_test": attr.bool(default = False, configurable = False, doc = "If true, creates a test target"),
},
implementation = _my_macro_impl,
)
Khai báo loại thuộc tính chấp nhận các
tham số,
mandatory, default, và doc. Hầu hết các loại thuộc tính cũng chấp nhận tham số
configurable, tham số này xác định xem thuộc tính có chấp nhận
select hay không. Nếu một thuộc tính là configurable, thì thuộc tính đó sẽ phân tích cú pháp các giá trị không phải select
dưới dạng select không thể định cấu hình – "foo" sẽ trở thành
select({"//conditions:default": "foo"}). Tìm hiểu thêm trong phần chọn.
Kế thừa thuộc tính
Macro thường được dùng để gói một quy tắc (hoặc một macro khác) và tác giả của macro thường muốn chuyển tiếp phần lớn các thuộc tính của biểu tượng được gói mà không thay đổi, bằng cách sử dụng **kwargs, đến mục tiêu chính của macro (hoặc macro bên trong chính).
Để hỗ trợ mẫu này, một macro có thể kế thừa thuộc tính từ một quy tắc hoặc một
macro khác bằng cách truyền biểu tượng quy tắc hoặc
macro đến macro()'s
inherit_attrs đối số. (Bạn cũng có thể sử dụng chuỗi đặc biệt "common"
thay vì biểu tượng quy tắc hoặc macro để kế thừa các thuộc tính chung được xác định cho
tất cả các quy tắc xây dựng Starlark.)
Chỉ các thuộc tính công khai mới được kế thừa và các thuộc tính trong từ điển
attrs của macro sẽ ghi đè các thuộc tính được kế thừa có cùng tên. Bạn cũng có thể
xoá các thuộc tính được kế thừa bằng cách sử dụng None làm giá trị trong từ điểnattrs:
# macro/macro.bzl
my_macro = macro(
inherit_attrs = native.cc_library,
attrs = {
# override native.cc_library's `local_defines` attribute
"local_defines": attr.string_list(default = ["FOO"]),
# do not inherit native.cc_library's `defines` attribute
"defines": None,
},
...
)
Giá trị mặc định của các thuộc tính được kế thừa không bắt buộc luôn bị ghi đè thành
None, bất kể giá trị mặc định của định nghĩa thuộc tính ban đầu. Nếu
cần kiểm tra hoặc sửa đổi một thuộc tính được kế thừa không bắt buộc (ví
dụ: nếu bạn muốn thêm thẻ vào thuộc tính tags được kế thừa), bạn phải
đảm bảo xử lý trường hợp None trong hàm triển khai của macro:
# macro/macro.bzl
_my_macro_implementation(name, visibility, tags, **kwargs):
# Append a tag; tags attr is an inherited non-mandatory attribute, and
# therefore is None unless explicitly set by the caller of our macro.
my_tags = (tags or []) + ["another_tag"]
native.cc_library(
...
tags = my_tags,
**kwargs,
)
...
Triển khai
implementation chấp nhận một hàm chứa logic của macro.
Các hàm triển khai thường tạo mục tiêu bằng cách gọi một hoặc nhiều quy tắc và
thường là 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ư macro, nhưng có tiền tố là _ và hậu tố là
_impl.
Không giống như các hàm triển khai quy tắc (chỉ nhận một đối số (ctx) chứa tham chiếu đến các thuộc tính), các hàm triển khai macro chấp nhận một
tham số cho mỗi đối số.
# macro/macro.bzl
def _my_macro_impl(name, visibility, deps, create_test):
cc_library(
name = name + "_cc_lib",
deps = deps,
)
if create_test:
cc_test(
name = name + "_test",
srcs = ["my_test.cc"],
deps = deps,
)
Nếu một macro kế thừa các thuộc tính, thì hàm triển khai của macro đó phải có tham số từ khoá còn lại
**kwargs. Tham số này có thể được chuyển tiếp đến lệnh gọi kích hoạt quy tắc hoặc macro con được kế thừa. (Điều này giúp đảm bảo rằng macro của bạn sẽ không bị hỏng nếu quy tắc hoặc macro mà bạn đang kế thừa thêm một thuộc tính mới.)
Khai báo
Macro được khai báo bằng cách tải và gọi định nghĩa của chúng trong tệp BUILD.
```starlark
pkg/BUILD
my_macro( name = "macro_instance", deps = ["src.cc"] + select( { "//config_setting:special": ["special_source.cc"], "//conditions:default": [], }, ), create_tests = True, ) ```
Thao tác này sẽ tạo các mục tiêu
//pkg:macro_instance_cc_lib và//pkg:macro_instance_test.
Giống như trong các lệnh gọi quy tắc, nếu giá trị thuộc tính trong lệnh gọi macro được đặt thành None,
thì thuộc tính đó sẽ được coi như bị người gọi macro bỏ qua. Ví dụ: 2 lệnh gọi macro sau đây là tương đương:
# pkg/BUILD
my_macro(name = "abc", srcs = ["src.cc"], deps = None)
my_macro(name = "abc", srcs = ["src.cc"])
Điều này thường không hữu ích trong các tệp BUILD, nhưng hữu ích khi
gói một macro bên trong một macro khác theo phương thức lập trình.
Thông tin chi tiết
Quy ước đặt tên cho các mục tiêu được tạo
Tên của mọi mục tiêu hoặc macro con do macro tượng trưng tạo phải
khớp với tham số name của macro hoặc phải có tiền tố là name theo sau
bởi _ (ưu tiên), . hoặc -. Ví dụ: my_macro(name = "foo") chỉ có thể
tạo các tệp hoặc mục tiêu có tên là foo hoặc có tiền tố là foo_, foo- hoặc foo.,
ví dụ: foo_bar.
Bạn có thể khai báo các mục tiêu hoặc tệp vi phạm quy ước đặt tên macro, nhưng không thể xây dựng và không thể sử dụng làm phần phụ thuộc.
Các tệp và mục tiêu không phải macro trong cùng một gói với thực thể macro nên không có tên xung đột với tên mục tiêu macro tiềm năng, mặc dù tính độc quyền này không được thực thi. Chúng tôi đang trong quá trình triển khai tính năng đánh giá trì hoãn để cải thiện hiệu suất cho macro tượng trưng. Tính năng này sẽ bị suy giảm trong các gói vi phạm sơ đồ đặt tên.
Quy định hạn chế
Macro tượng trưng có một số quy định hạn chế bổ sung so với macro cũ.
Macro tượng trưng
- phải nhận đối số
namevà đối sốvisibility - phải có hàm
implementation - không được trả về giá trị
- không được thay đổi đối số
- không được gọi
native.existing_rules()trừ phi đó là macrofinalizerđặc biệt - không được gọi
native.package() - không được gọi
glob() - không được gọi
native.environment_group() - phải tạo các mục tiêu có tên tuân thủ sơ đồ đặt tên
- không thể tham chiếu đến các tệp đầu vào chưa được khai báo hoặc truyền dưới dạng đối số
- không thể tham chiếu đến các mục tiêu riêng tư của người gọi (xem chế độ hiển thị và macro để biết thêm thông tin chi tiết).
Chế độ hiển thị và macro
Hệ thống chế độ hiển thị giúp bảo vệ thông tin chi tiết về việc triển khai cả macro (tượng trưng) và người gọi.
Theo mặc định, các mục tiêu được tạo trong macro tượng trưng có thể nhìn thấy trong chính macro
đó, nhưng không nhất thiết là người gọi macro. Macro có thể "xuất" một
mục tiêu dưới dạng API công khai bằng cách chuyển tiếp giá trị của thuộc tính visibility, như trong some_rule(..., visibility = visibility).
Các ý tưởng chính về chế độ hiển thị macro là:
Chế độ hiển thị được kiểm tra dựa trên macro đã khai báo mục tiêu, chứ không phải gói đã gọi macro.
- Nói cách khác, việc nằm trong cùng một gói không tự động giúp một mục tiêu hiển thị với mục tiêu khác. Điều này giúp bảo vệ các mục tiêu nội bộ của macro khỏi việc trở thành phần phụ thuộc của các macro khác hoặc các mục tiêu cấp cao nhất trong gói.
Tất cả các thuộc tính
visibility, trên cả quy tắc và macro, đều tự động bao gồm vị trí gọi quy tắc hoặc macro.- Do đó, một mục tiêu sẽ hiển thị vô điều kiện với các mục tiêu khác được khai báo trong cùng một macro (hoặc tệp
BUILD, nếu không có trong macro).
- Do đó, một mục tiêu sẽ hiển thị vô điều kiện với các mục tiêu khác được khai báo trong cùng một macro (hoặc tệp
Trong thực tế, điều này có nghĩa là khi một macro khai báo một mục tiêu mà không đặt
visibility, thì mục tiêu đó sẽ mặc định là nội bộ đối với macro. (Chế độ hiển thị mặc định của gói
không áp dụng trong macro.) Việc xuất mục tiêu có nghĩa là mục tiêu đó hiển thị
với bất kỳ mục tiêu nào mà người gọi macro đã chỉ định trong thuộc tính visibility của macro,
cộng với gói của chính người gọi macro, cũng như mã của chính macro.
Một cách khác để nghĩ về điều này là chế độ hiển thị của macro xác định người
(ngoài chính macro) có thể nhìn thấy các mục tiêu được xuất của macro.
# tool/BUILD
...
some_rule(
name = "some_tool",
visibility = ["//macro:__pkg__"],
)
# macro/macro.bzl
def _impl(name, visibility):
cc_library(
name = name + "_helper",
...
# No visibility passed in. Same as passing `visibility = None` or
# `visibility = ["//visibility:private"]`. Visible to the //macro
# package only.
)
cc_binary(
name = name + "_exported",
deps = [
# Allowed because we're also in //macro. (Targets in any other
# instance of this macro, or any other macro in //macro, can see it
# too.)
name + "_helper",
# Allowed by some_tool's visibility, regardless of what BUILD file
# we're called from.
"//tool:some_tool",
],
...
visibility = visibility,
)
my_macro = macro(implementation = _impl, ...)
# pkg/BUILD
load("//macro:macro.bzl", "my_macro")
...
my_macro(
name = "foo",
...
)
some_rule(
...
deps = [
# Allowed, its visibility is ["//pkg:__pkg__", "//macro:__pkg__"].
":foo_exported",
# Disallowed, its visibility is ["//macro:__pkg__"] and
# we are not in //macro.
":foo_helper",
]
)
Nếu my_macro được gọi bằng visibility = ["//other_pkg:__pkg__"], hoặc nếu
gói //pkg đã đặt default_visibility thành giá trị đó, thì
//pkg:foo_exported cũng có thể được sử dụng trong //other_pkg/BUILD hoặc trong một
macro được xác định trong //other_pkg:defs.bzl, nhưng //pkg:foo_helper sẽ vẫn được
bảo vệ.
Một macro có thể khai báo rằng một mục tiêu hiển thị với một gói bạn bè bằng cách truyền
visibility = ["//some_friend:__pkg__"] (đối với mục tiêu nội bộ) hoặc
visibility = visibility + ["//some_friend:__pkg__"] (đối với mục tiêu được xuất).
Xin lưu ý rằng việc macro khai báo một mục tiêu có chế độ hiển thị công khai (visibility = ["//visibility:public"]) là một mẫu chống đối. Điều này là do nó khiến mục tiêu hiển thị vô điều kiện với mọi gói, ngay cả khi người gọi chỉ định chế độ hiển thị hạn chế hơn.
Tất cả quá trình kiểm tra chế độ hiển thị đều được thực hiện đối với macro tượng trưng đang chạy trong cùng một thời điểm. Tuy nhiên, có một cơ chế uỷ quyền chế độ hiển thị: Nếu một macro truyền nhãn dưới dạng giá trị thuộc tính đến một macro bên trong, thì mọi cách sử dụng nhãn trong macro bên trong đều được kiểm tra đối với macro bên ngoài. Xem trang chế độ hiển thị để biết thêm thông tin chi tiết.
Hãy nhớ rằng macro cũ hoàn toàn minh bạch đối với hệ thống chế độ hiển thị, và hoạt động như thể vị trí của chúng là bất kỳ tệp BUILD hoặc macro tượng trưng nào mà chúng được gọi.
Trình hoàn thiện và chế độ hiển thị
Ngoài việc xem các mục tiêu tuân theo các quy tắc thông thường về chế độ hiển thị macro tượng trưng, các mục tiêu được khai báo trong trình hoàn thiện quy tắc có thể cũng xem tất cả các mục tiêu hiển thị với gói của mục tiêu trình hoàn thiện.
Điều này có nghĩa là nếu bạn di chuyển một native.existing_rules()-based macro cũ sang
trình hoàn thiện, thì các mục tiêu do trình hoàn thiện khai báo vẫn có thể nhìn thấy
các phần phụ thuộc cũ của chúng.
Tuy nhiên, xin lưu ý rằng bạn có thể khai báo một mục tiêu trong macro tượng trưng sao cho các mục tiêu của trình hoàn thiện không thể nhìn thấy mục tiêu đó trong hệ thống chế độ hiển thị – mặc dù trình hoàn thiện có thể kiểm tra các thuộc tính của mục tiêu đó bằng native.existing_rules().
Chọn
Nếu một thuộc tính là configurable (mặc định) và giá trị của thuộc tính đó không phải là None,
thì hàm triển khai macro sẽ thấy giá trị thuộc tính được gói
trong một select không đáng kể. Điều này giúp tác giả macro dễ dàng phát hiện lỗi
khi họ không dự đoán được rằng giá trị thuộc tính có thể là select.
Ví dụ: hãy xem xét macro sau:
my_macro = macro(
attrs = {"deps": attr.label_list()}, # configurable unless specified otherwise
implementation = _my_macro_impl,
)
Nếu my_macro được gọi bằng deps = ["//a"], thì thao tác đó sẽ khiến _my_macro_impl
được gọi với tham số deps được đặt thành select({"//conditions:default":
["//a"]}). Nếu điều này khiến hàm triển khai không thành công (ví dụ: vì mã đã cố gắng lập chỉ mục vào giá trị như trong deps[0], điều này không được phép đối với selects), thì tác giả macro có thể lựa chọn: hoặc họ có thể viết lại macro để chỉ sử dụng các thao tác tương thích với select, hoặc họ có thể đánh dấu thuộc tính là không thể định cấu hình (attr.label_list(configurable = False)). Lựa chọn thứ hai đảm bảo rằng người dùng không được phép truyền giá trị select.
Các mục tiêu quy tắc đảo ngược quá trình chuyển đổi này và lưu trữ select không đáng kể dưới dạng các giá trị vô điều kiện; trong ví dụ trên, nếu _my_macro_impl khai báo mục tiêu quy tắc my_rule(..., deps = deps), thì deps của mục tiêu quy tắc đó sẽ được lưu trữ dưới dạng ["//a"]. Điều này đảm bảo rằng việc gói select không khiến các giá trị select
không đáng kể được lưu trữ trong tất cả các mục tiêu do macro tạo thực thể.
Nếu giá trị của thuộc tính có thể định cấu hình là None, thì giá trị đó sẽ không được gói trong a
select. Điều này đảm bảo rằng các kiểm thử như my_attr == None vẫn hoạt động và
khi thuộc tính được chuyển tiếp đến một quy tắc có giá trị mặc định được tính toán, thì quy tắc đó
sẽ hoạt động đúng cách (tức là như thể thuộc tính không được truyền vào). Không phải lúc nào thuộc tính cũng có thể nhận giá trị None nhưng điều này có thể xảy ra đối với loại attr.label() và đối với bất kỳ thuộc tính được kế thừa không bắt buộc nào.
Trình hoàn thiện
Trình hoàn thiện quy tắc là một macro tượng trưng đặc biệt. Macro này (bất kể vị trí từ vựng của nó trong tệp BUILD) được đánh giá ở giai đoạn cuối cùng của việc tải một gói,
sau khi tất cả các mục tiêu không phải trình hoàn thiện đã được xác định. Không giống như các macro tượng trưng thông thường
, trình hoàn thiện có thể gọi native.existing_rules(), trong đó, trình hoàn thiện hoạt động hơi khác so với trong macro cũ: trình hoàn thiện chỉ trả về tập hợp các mục tiêu quy tắc không phải trình hoàn thiện. Trình hoàn thiện có thể xác nhận trạng thái của tập hợp đó hoặc
xác định các mục tiêu mới.
Để khai báo trình hoàn thiện, hãy gọi macro() bằng finalizer = True:
def _my_finalizer_impl(name, visibility, tags_filter):
for r in native.existing_rules().values():
for tag in r.get("tags", []):
if tag in tags_filter:
my_test(
name = name + "_" + r["name"] + "_finalizer_test",
deps = [r["name"]],
data = r["srcs"],
...
)
continue
my_finalizer = macro(
attrs = {"tags_filter": attr.string_list(configurable = False)},
implementation = _impl,
finalizer = True,
)
Tính trì hoãn
QUAN TRỌNG: Chúng tôi đang trong quá trình triển khai tính năng đánh giá và mở rộng macro trì hoãn. Tính năng này hiện chưa hoạt động.
Hiện tại, tất cả các macro đều được đánh giá ngay khi tệp BUILD được tải. Điều này có thể ảnh hưởng tiêu cực đến hiệu suất của các mục tiêu trong các gói cũng có các macro không liên quan tốn kém. Trong tương lai, các macro tượng trưng không phải trình hoàn thiện sẽ chỉ được đánh giá nếu cần cho bản dựng. Sơ đồ đặt tên tiền tố giúp Bazel xác định macro nào cần mở rộng cho một mục tiêu được yêu cầu.
Khắc phục sự cố di chuyển
Sau đây là một số vấn đề thường gặp khi di chuyển và cách khắc phục.
- Lệnh gọi macro cũ
glob()
Di chuyển lệnh gọi glob() đến tệp BUILD (hoặc đến một macro cũ được gọi từ tệp
BUILD) và truyền giá trị glob() đến macro tượng trưng bằng thuộc tính danh sách
nhãn:
# BUILD file
my_macro(
...,
deps = glob(...),
)
- Macro cũ có một tham số không phải là loại starlark
attrhợp lệ.
Kéo càng nhiều logic càng tốt vào một macro tượng trưng lồng nhau, nhưng vẫn giữ macro cấp cao nhất là macro cũ.
- Macro cũ gọi một quy tắc tạo mục tiêu phá vỡ sơ đồ đặt tên
Không sao cả, bạn chỉ cần không phụ thuộc vào mục tiêu "vi phạm". Quá trình kiểm tra tên sẽ bị bỏ qua một cách âm thầm.