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 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ó hai 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 khả năng 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 trong một bản phát hành Bazel trong tương lai). Theo mặc định, các macro tượng trưng có trong Bazel 8. Khi đề cập đến macros
, tài liệu này đang đề cập đến các macro mang tính biểu tượ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
Các macro được xác định trong 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 gồm tên thuộc tính đến 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 selects.
Kế thừa thuộc tính
Macro thường được dùng để bao bọc 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 bao bọc 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 các thuộc tính từ một quy tắc hoặc macro khác bằng cách truyền quy tắc hoặc biểu tượng macro đến đối số inherit_attrs
của macro()
. (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ển attrs
:
# 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 không bắt buộc được kế thừa 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 không bắt buộc được kế thừa (ví dụ: nếu muốn thêm thẻ vào một 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
def _my_macro_impl(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 ra các mục tiêu bằng cách gọi một hoặc nhiều quy tắc và chúng thường là riêng tư (được đặt tên bằng dấu gạch dưới ở đầu). Theo quy ước, các biến này có cùng tên với macro tương ứng, 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 nhận một đối số (ctx
) chứa thông tin 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ó một tham số từ khoá còn lại **kwargs
, có thể được chuyển tiếp đến lệnh gọi gọi 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 macro trong tệp BUILD
.
# 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ư thể người gọi macro đã bỏ qua. Ví dụ: hai 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 sẽ hữu ích khi bạn lập trình để bao bọc một macro bên trong một macro khác.
Thông tin chi tiết
Quy ước đặt tên cho các mục tiêu đã tạo
Tên của mọi mục tiêu hoặc macro con do một macro tượng trưng tạo ra phải khớp với tham số name
của macro hoặc phải có tiền tố là name
, theo sau là _
(nên dùng), .
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 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ể tạo và không thể 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 một thực thể macro không được 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 triển khai đánh giá trì hoãn để cải thiện hiệu suất cho các macro Ký hiệu. Các macro này sẽ bị suy giảm trong những gói vi phạm giản đồ đặt tên.
Quy định hạn chế
Macro tượng trưng có một số hạn chế bổ sung so với macro cũ.
Macro tượng trưng
- phải lấy một đối số
name
và một đối sốvisibility
- phải có hàm
implementation
- có thể không trả về giá trị
- không được phép thay đổi đối số của chúng
- không được gọi
native.existing_rules()
trừ phi chúng là các macrofinalizer
đặc biệt - có thể không gọi được cho
native.package()
- có thể không gọi được cho
glob()
- có thể không gọi được cho
native.environment_group()
- phải tạo các mục tiêu có tên tuân thủ giản đồ đặ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 vào 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 phần khả năng hiển thị và macro để biết thêm chi tiết).
Chế độ hiển thị và macro
Hệ thống khả năng hiển thị giúp bảo vệ các chi tiết triển khai của cả macro (tượng trưng) và các lệnh gọi của chúng.
Theo mặc định, các mục tiêu được tạo trong một macro tượng trưng sẽ xuất hiện trong chính macro đó, nhưng không nhất thiết phải xuất hiện với 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
của chính mục tiêu đó, như trong some_rule(..., visibility = visibility)
.
Sau đây là những ý tưởng chính về chế độ hiển thị macro:
Khả năng hiển thị được kiểm tra dựa trên macro nào đã khai báo mục tiêu, chứ không phải gói nào đã 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ị cho 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 trở thành các phần phụ thuộc của các macro hoặc mục tiêu cấp cao nhất khác 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í mà quy tắc hoặc macro được gọi.- Do đó, mục tiêu luôn hiển thị cho 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ục tiêu luôn hiển thị cho các mục tiêu khác được khai báo trong cùng một macro (hoặc tệp
Trên 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
, mục tiêu đó sẽ mặc định là mục tiêu nội bộ của macro. (Chế độ hiển thị mặc định của gói không áp dụng trong macro.) Xuất đích có nghĩa là đích có thể nhìn thấy đối với bất kỳ đối tượng gọi nào mà macro chỉ định trong thuộc tính visibility
của macro, cộng với gói của chính đối tượng gọi macro, cũng như mã của chính macro.
Một cách khác để hiểu là chế độ hiển thị của macro sẽ xác định những người (ngoài chính macro đó) có thể xem 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 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ị cho 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 đây là một mẫu chống lại việc một macro khai báo mục tiêu có khả năng hiển thị công khai (visibility = ["//visibility:public"]
). Điều này là do macro này khiến mục tiêu luôn hiển thị cho mọi gói, ngay cả khi phương thức gọi chỉ định khả năng hiển thị hạn chế hơn.
Tất cả hoạt động kiểm tra khả năng hiển thị đều được thực hiện đối với macro tượng trưng đang chạy trong cùng. Tuy nhiên, có một cơ chế uỷ quyền khả năng hiển thị: Nếu một macro truyền nhãn làm giá trị thuộc tính cho một macro bên trong, mọi cách sử dụng nhãn trong macro bên trong sẽ được kiểm tra liên quan đến macro bên ngoài. Hãy xem trang về chế độ hiển thị để biết thêm thông tin chi tiết.
Hãy nhớ rằng các macro cũ hoàn toàn minh bạch đối với hệ thống 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 kết thúc và chế độ hiển thị
Các mục tiêu được khai báo trong một trình kết thúc quy tắc, ngoài việc xem các mục tiêu theo các quy tắc hiển thị macro tượng trưng thông thường, cũng có thể xem tất cả các mục tiêu mà gói của mục tiêu trình kết thúc có thể nhìn thấy.
Điều này có nghĩa là nếu bạn di chuyển một macro cũ dựa trên native.existing_rules()
sang một trình kết thúc, thì các mục tiêu do trình kết thúc khai báo vẫn có thể thấy các phần phụ thuộc cũ của chúng.
Tuy nhiên, lưu ý rằng bạn có thể khai báo một mục tiêu trong một macro tượng trưng sao cho các mục tiêu của trình kết thúc không thể nhìn thấy mục tiêu đó trong hệ thống hiển thị – ngay cả khi trình kết thúc 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ẽ coi giá trị thuộc tính được bao bọc 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 này 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 (chẳng hạn như 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 select
), thì tác giả macro có thể đưa ra lựa chọn: họ có thể viết lại macro của mình để 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
.
Mục tiêu của quy tắc đảo ngược quá trình chuyển đổi này và lưu trữ các select
không đáng kể dưới dạng giá trị vô điều kiện; trong ví dụ trên, nếu _my_macro_impl
khai báo một mục tiêu của quy tắc my_rule(..., deps = deps)
, thì deps
của mục tiêu đó sẽ được lưu trữ dưới dạng ["//a"]
. Điều này đảm bảo rằng việc bao bọc 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 khởi tạo.
Nếu giá trị của một thuộc tính có thể định cấu hình là None
, thì giá trị đó sẽ không được bao bọc trong một 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, quy tắc sẽ hoạt động đúng cách (tức là như thể thuộc tính hoàn toàn không được truyền vào). Không phải lúc nào một 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 mọi thuộc tính không bắt buộc được kế thừa.
Finalizer
Trình hoàn tất quy tắc là một macro tượng trưng đặc biệt. Bất kể vị trí từ vựng của macro này trong tệp BUILD, macro này sẽ đượ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 tất đã được xác định. Không giống như các macro tượng trưng thông thường, trình kết thúc có thể gọi native.existing_rules()
, trong đó trình kết thúc hoạt động hơi khác so với trong các macro cũ: trình kết thúc chỉ trả về tập hợp các mục tiêu quy tắc không phải trình kết thúc. Trình kết thúc 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 một phương thức kết thúc, 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,
)
Lười biếng
QUAN TRỌNG: Chúng tôi đang trong quá trình triển khai tính năng mở rộng và đánh giá macro theo yêu cầu. Tính năng này 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 những 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 là macro kết thúc sẽ chỉ được đánh giá nếu cần thiết 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.
- Cuộc 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 cách sử dụng thuộc tính label-list:
# BUILD file
my_macro(
...,
deps = glob(...),
)
- Macro cũ có một tham số không phải là loại
attr
starlark hợ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 ra mục tiêu vi phạm lược đồ đặt tên
Không sao cả, bạn chỉ cần đừng phụ thuộc vào mục tiêu "gây khó chịu". Quy tắc kiểm tra tên sẽ bị bỏ qua một cách âm thầm.