Macro

Trang này trình bày những kiến thức 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.

Bạn có thể tìm thấy ví dụ về macro tượng trưng có thể thực thi trong kho lưu trữ ví dụ.

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 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

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, hàm triển khai macro chấp nhận một tham số cho mỗi đối số. Các hàm này nhận một đối số duy nhất (ctx) chứa tham chiếu đến các thuộc tính.

# 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 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ệ thống vị trí hiển thị giúp bảo vệ thông tin chi tiết về việc triển khai của cả macro (biểu tượng) và phương thức gọi của macro đó.

Theo mặc định, các mục tiêu được tạo trong một macro tượng trưng sẽ hiển thị trong chính macro đó, nhưng không nhất thiết phải hiển thị với phương thức gọi của 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 macro đó, như trong some_rule(..., visibility = visibility).

Các ý tưởng chính về chế độ hiển thị macro là:

  1. Khả năng 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 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 khiến 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 trở thành phần phụ thuộc của các macro khác hoặc mục tiêu cấp cao nhất trong gói.
  2. Tất 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 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 phải trong macro).

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, mục tiêu đó sẽ mặc định là nội bộ trong 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ỳ đối tượng gọi nào của macro được 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 của macro, cũng như mã của chính macro. Một cách khác để suy nghĩ về điều này là chế độ hiển thị của macro xác định người nào (ngoài chính macro) có thể xem các mục tiêu đã 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 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 đã xuất). Xin lưu ý rằng đây là một mẫu chống đối để một macro khai báo một mục tiêu có chế độ hiển thị công khai (visibility = ["//visibility:public"]). Lý do là chế độ này khiến mục tiêu hiển thị vô điều kiện cho mọi gói, ngay cả khi phương thức gọi chỉ định chế độ hiển thị hạn chế hơn.

Tất cả hoạt động kiểm tra chế độ hiển thị đều được thực hiện liên quan đến macro tượng trưng đang chạy ở trong cùng. Tuy nhiên, có một cơ chế uỷ quyền chế độ hiển thị: Nếu một macro truyền một nhãn dưới dạng giá trị thuộc tính đến một macro bên trong, thì mọi hoạt động 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 chế độ hiển thị để biết thêm thông tin.

Hãy nhớ rằng các 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 từ đó.

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 bất kỳ thuộc tính không bắt buộc nào được kế thừa.

Phương thức hoàn tất

Trình kết thúc 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 kết thúc đã đượ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.