Macro

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, gỡ lỗi và quy ước thông thường.

Macro là một hàm được gọi từ tệp BUILD có thể tạo bản sao 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ó hai loại: macro biểu tượng (được mô tả trên trang này) và macro cũ. Nếu có thể, bạn nên sử dụng các macro tượng trưng để mã rõ ràng hơn.

Macro tượng trưng cung cấp các đối số đã 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 đã tạo. Các lớp này được thiết kế để có thể chấp nhận việc đánh giá lười biếng (sẽ được thêm vào bản phát hành Bazel trong tương lai). Theo mặc định, các macro tượng trưng có sẵn trong Bazel 8. Khi tài liệu này đề cập đến macros, tức là đề cập đến macro tượng trưng.

Cách sử dụng

Macro được xác định trong tệp .bzl bằng cách gọi hàm macro() với hai tham số bắt buộc: attrsimplementation.

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ố cho macro. Hai thuộc tính phổ biến – namevisibility – được thêm ngầm vào tất 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,
)

Nội dung khai báo loại thuộc tính chấp nhận các tham số, mandatory, defaultdoc. 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 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

QUAN TRỌNG: Tính năng kế thừa thuộc tính là một tính năng thử nghiệm được bật bằng cờ --experimental_enable_macro_inherit_attrs. Một số hành vi được mô tả trong phần này có thể thay đổi trước khi tính năng được bật theo mặc đị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 các thuộc tính từ một quy tắc hoặc một 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ì quy tắc hoặc ký hiệu macro để kế thừa các thuộc tính chung được xác định cho tất cả quy tắc bản 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 chính macro sẽ ghi đè các thuộc tính kế thừa có cùng tên. Bạn cũng có thể xoá các thuộc tính 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 kế thừa không bắt buộc luôn được ghi đè thành None, bất kể giá trị mặc định của định nghĩa thuộc tính ban đầu là gì. 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 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 thông lệ, các hàm này được đặt tên giống với 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 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ị lỗi 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

Bạn có thể khai báo macro 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//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 là bị phương thức gọi của macro bỏ qua. Ví dụ: hai lệnh gọi macro sau đây tương đương nhau:

# 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 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 đã tạo

Tên của mọi mục tiêu hoặc macro con do 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à _ (ư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 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 chất độ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á từng phần để cải thiện hiệu suất cho các macro biểu tượng. Tính năng này sẽ bị hạn chế trong các gói vi phạm giản đồ đặt tên.

Quy định hạn chế

Macro biểu tượng có một số hạn chế khác so với macro cũ.

Macro tượng trưng

  • phải nhận 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 thay đổi đối số
  • không được gọi native.existing_rules() trừ phi đó là các macro finalizer đặ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ủ 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ố (xem phần chế độ hiển thị và macro để biết thêm chi tiết).

Chế độ hiển thị và macro

Hãy xem phần Chế độ hiển thị để thảo luận sâu về chế độ hiển thị trong Bazel.

Mức độ hiển thị mục tiêu

Theo mặc định, các mục tiêu do macro tượng trưng tạo ra chỉ hiển thị trong gói chứa tệp .bzl xác định macro. Cụ thể, người gọi macro tượng trưng không thể thấy các macro này trừ phi người gọi nằm trong cùng một gói với tệp .bzl của macro.

Để cho phép phương thức gọi của macro tượng trưng nhìn thấy một mục tiêu, hãy truyền visibility = visibility đến quy tắc hoặc macro bên trong. Bạn cũng có thể hiển thị mục tiêu trong các gói bổ sung bằng cách cung cấp chế độ hiển thị rộng hơn (hoặc thậm chí là công khai) cho mục tiêu đó.

Theo mặc định, chế độ hiển thị mặc định của gói (như đã khai báo trong package()) được truyền đến tham số visibility của macro ngoài cùng, nhưng tuỳ thuộc vào macro để truyền (hoặc không truyền!) visibility đó đến các mục tiêu mà macro tạo bản sao.

Mức độ hiển thị phần phụ thuộc

Các mục tiêu được tham chiếu trong quá trình triển khai macro phải hiển thị với định nghĩa của macro đó. Bạn có thể cấp chế độ hiển thị theo một trong những cách sau:

  • Macro có thể nhìn thấy các mục tiêu nếu các mục tiêu đó được truyền đến macro thông qua thuộc tính nhãn, danh sách nhãn hoặc thuộc tính từ điển có khoá hoặc giá trị nhãn, một cách rõ ràng:

# pkg/BUILD
my_macro(... deps = ["//other_package:my_tool"] )
  • ... hoặc dưới dạng giá trị mặc định của thuộc tính:
# my_macro:macro.bzl
my_macro = macro(
  attrs = {"deps" : attr.label_list(default = ["//other_package:my_tool"])},
  ...
)
  • Các mục tiêu cũng hiển thị với một macro nếu các mục tiêu đó được khai báo hiển thị với gói chứa tệp .bzl xác định macro:
# other_package/BUILD
# Any macro defined in a .bzl file in //my_macro package can use this tool.
cc_binary(
    name = "my_tool",
    visibility = "//my_macro:\\__pkg__",
)

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 quan trọng. Điều này giúp tác giả macro dễ dàng phát hiện lỗi khi họ không lường trước đượ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ì _my_macro_impl sẽ đượ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ụ: do mã cố gắng lập chỉ mục vào giá trị như trong deps[0], 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 để 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)). Trường hợp sau đảm bảo rằng người dùng không được phép truyền giá trị select vào.

Mục tiêu quy tắc đảo ngược phép biến đổi này và lưu trữ các select không quan trọng 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 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 quan trọng được lưu trữ trong tất cả các mục tiêu do macro tạo bản sao.

Nếu giá trị của một thuộc tính có thể định cấu hình là None, thì thuộc tính đó sẽ không được gói trong select. Điều này đảm bảo rằng các chương trình 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 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 với loại attr.label() và với mọi thuộc tính không bắt buộc được kế thừa.

Trình kết thúc

Trình hoàn tất quy tắc là một macro biểu tượng đặc biệt – bất kể vị trí từ vựng của macro đó trong tệp BUILD – được đánh giá ở giai đoạn cuối cùng của quá trình tải gói, sau khi tất cả các mục tiêu không phải là 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 là trình kết thúc. Phương thức hoàn tất 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 trình hoàn tất, 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

LƯU Ý QUAN TRỌNG: Chúng tôi đang trong quá trình triển khai việc mở rộng và đánh giá macro tải lười. Tính năng này chưa được hỗ trợ.

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 và tốn kém. Trong tương lai, các macro biểu tượng không phải là trình hoàn thiện sẽ chỉ được đánh giá nếu cần thiết cho bản dựng. Giản đồ đặt tên tiền tố giúp Bazel xác định macro nào cần mở rộng theo mục tiêu được yêu cầu.

Khắc phục sự cố di chuyển

Dưới đâ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() vào tệp BUILD (hoặc vào một macro cũ được gọi từ tệp BUILD) và truyền giá trị glob() vào macro tượng trưng bằng cách sử dụ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 attr starlark hợp lệ.

Kéo nhiều logic nhất có thể vào một macro biểu tượng lồng nhau, nhưng hãy giữ macro cấp cao nhất là macro cũ.

  • Macro cũ gọi một quy tắc tạo một mục tiêu vi phạm giản đồ đặt tên

Không sao, chỉ cần không phụ thuộc vào mục tiêu "vi phạm". Quy trình kiểm tra tên sẽ bị bỏ qua.