มาโคร

หน้านี้อธิบายพื้นฐานของการใช้มาโคร รวมถึงกรณีการใช้งานทั่วไป การแก้ไขข้อบกพร่อง และแบบแผน

มาโครคือฟังก์ชันที่เรียกจากไฟล์ BUILD ซึ่งสามารถสร้างอินสแตนซ์ของกฎได้ มาโครมีไว้สําหรับการรวมและนําโค้ดของกฎที่มีอยู่และมาโครอื่นๆ มาใช้ซ้ำเป็นหลัก

มาโครมี 2 ประเภท ได้แก่ มาโครสัญลักษณ์ที่อธิบายไว้ในหน้านี้ และมาโครเดิม เราขอแนะนำให้ใช้มาโครสัญลักษณ์เพื่อให้โค้ดมีความชัดเจน หากเป็นไปได้

มาโครเชิงสัญลักษณ์จะมีอาร์กิวเมนต์ที่กําหนดประเภท (การเปลี่ยนสตริงเป็นป้ายกำกับ โดยสัมพันธ์กับตําแหน่งที่มีการเรียกใช้มาโคร) และความสามารถในการจํากัดและระบุระดับการเข้าถึงของเป้าหมายที่สร้างขึ้น ไฟล์เหล่านี้ออกแบบมาให้ใช้การประเมินแบบเลื่อนเวลา (ซึ่งจะเพิ่มลงใน Bazel เวอร์ชันในอนาคต) มาโครเชิงสัญลักษณ์จะพร้อมใช้งานโดยค่าเริ่มต้นใน Bazel 8 ในกรณีที่เอกสารนี้พูดถึง macros แสดงว่าเอกสารดังกล่าวหมายถึงมาโครเชิงสัญลักษณ์

การใช้งาน

มาโครจะกำหนดไว้ในไฟล์ .bzl โดยการเรียกใช้ฟังก์ชัน macro() ที่มีพารามิเตอร์ที่จำเป็น 2 รายการ ได้แก่ attrs และ implementation

Attributes

attrs ยอมรับพจนานุกรมชื่อแอตทริบิวต์เป็นประเภทแอตทริบิวต์ ซึ่งแสดงอาร์กิวเมนต์ของมาโคร ระบบจะเพิ่มแอตทริบิวต์ทั่วไป 2 รายการ ได้แก่ name และ visibility ลงในมาโครทั้งหมดโดยปริยาย และไม่รวมไว้ในพจนานุกรมที่ส่งไปยัง 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,
)

การประกาศประเภทแอตทริบิวต์ยอมรับพารามิเตอร์ mandatory, default และ doc แอตทริบิวต์ประเภทส่วนใหญ่ยังยอมรับพารามิเตอร์ configurable ด้วย ซึ่งจะกำหนดว่าแอตทริบิวต์ยอมรับ select หรือไม่ หากแอตทริบิวต์เป็น configurable ระบบจะแยกวิเคราะห์ค่าที่ไม่ใช่ select เป็น select ที่ไม่สามารถกําหนดค่าได้ "foo" จะกลายเป็น select({"//conditions:default": "foo"}) ดูข้อมูลเพิ่มเติมในselects

การสืบทอดแอตทริบิวต์

มาโครมักมีไว้เพื่อตัดสตริงของกฎ (หรือมาโครอื่น) และนักเขียนมาโครมักต้องการส่งต่อแอตทริบิวต์ส่วนใหญ่ของสัญลักษณ์ที่ตัดโดยใช้ **kwargs ไปยังเป้าหมายหลัก (หรือมาโครภายในหลัก) ของมาโคร

เพื่อรองรับรูปแบบนี้ มาโครสามารถรับค่าแอตทริบิวต์จากกฎหรือมาโครอื่นได้โดยส่งกฎหรือสัญลักษณ์มาโครไปยังอาร์กิวเมนต์ inherit_attrs ของ macro() (คุณยังใช้สตริงพิเศษ "common"แทนสัญลักษณ์กฎหรือมาโครเพื่อรับค่าแอตทริบิวต์ทั่วไปที่กําหนดไว้สําหรับกฎการสร้าง Starlark ทั้งหมดได้ด้วย) ระบบจะรับค่าแอตทริบิวต์แบบสาธารณะเท่านั้น และระบบจะลบล้างแอตทริบิวต์ที่รับค่ามาซึ่งมีชื่อเดียวกันกับแอตทริบิวต์ในพจนานุกรมattrsของมาโคร นอกจากนี้ คุณยังนําแอตทริบิวต์ที่รับช่วงมาออกได้โดยใช้ None เป็นค่าในพจนานุกรม 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,
    },
    ...
)

ระบบจะลบล้างค่าเริ่มต้นของแอตทริบิวต์ที่รับค่ามาซึ่งไม่บังคับให้เป็น None เสมอ โดยไม่คำนึงถึงค่าเริ่มต้นของคำจำกัดความแอตทริบิวต์เดิม หากต้องการตรวจสอบหรือแก้ไขแอตทริบิวต์ที่ไม่บังคับซึ่งรับค่ามา เช่น หากต้องการเพิ่มแท็กลงในแอตทริบิวต์ tags ที่รับค่ามา คุณต้องจัดการเคส None ในฟังก์ชันการติดตั้งใช้งานของมาโคร ดังนี้

# 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,
    )
    ...

การใช้งาน

implementation ยอมรับฟังก์ชันที่มีตรรกะของมาโคร ฟังก์ชันการใช้งานมักจะสร้างเป้าหมายโดยการเรียกใช้กฎอย่างน้อย 1 กฎ และมักจะเป็นแบบส่วนตัว (ตั้งชื่อโดยนำขีดล่างขึ้นต้น) ตามธรรมเนียมแล้ว ไฟล์เหล่านี้จะมีชื่อเหมือนกับมาโคร แต่จะมี_อยู่หน้าและ_implอยู่ท้าย

ซึ่งแตกต่างจากฟังก์ชันการติดตั้งใช้งานกฎที่ใช้อาร์กิวเมนต์เดียว (ctx) ที่มีข้อมูลอ้างอิงถึงแอตทริบิวต์ แต่ฟังก์ชันการติดตั้งใช้งานมาโครจะยอมรับพารามิเตอร์สําหรับอาร์กิวเมนต์แต่ละรายการ

# 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,
        )

หากมาโครรับค่าแอตทริบิวต์มา ฟังก์ชันการใช้งานของมาโครต้องมีพารามิเตอร์คีย์เวิร์ดที่เหลืออยู่**kwargs ซึ่งสามารถส่งต่อไปยังการเรียกที่เรียกใช้กฎหรือมาโครย่อยที่รับค่ามา (การดำเนินการนี้ช่วยให้มั่นใจว่ามาโครจะไม่ใช้งานไม่ได้หากกฎหรือมาโครที่คุณรับค่ามาเพิ่มแอตทริบิวต์ใหม่)

คำประกาศ

มาโครจะประกาศโดยการโหลดและเรียกคําจํากัดความในไฟล์ BUILD


# pkg/BUILD

my_macro(
    name = "macro_instance",
    deps = ["src.cc"] + select(
        {
            "//config_setting:special": ["special_source.cc"],
            "//conditions:default": [],
        },
    ),
    create_tests = True,
)

ซึ่งจะสร้างเป้าหมาย //pkg:macro_instance_cc_lib และ//pkg:macro_instance_test

เช่นเดียวกับการเรียกใช้กฎ หากตั้งค่าค่าแอตทริบิวต์ในการเรียกใช้มาโครเป็น None ระบบจะถือว่าผู้เรียกใช้มาโครละเว้นแอตทริบิวต์นั้น ตัวอย่างเช่น การเรียกใช้มาโคร 2 รายการต่อไปนี้มีความหมายเหมือนกัน

# pkg/BUILD
my_macro(name = "abc", srcs = ["src.cc"], deps = None)
my_macro(name = "abc", srcs = ["src.cc"])

โดยทั่วไปแล้ว คำสั่งนี้ไม่มีประโยชน์ในไฟล์ BUILD แต่มีประโยชน์เมื่อต้องตัดมาโครภายในมาโครอื่นโดยใช้โปรแกรม

รายละเอียด

รูปแบบการตั้งชื่อสำหรับเป้าหมายที่สร้าง

ชื่อของเป้าหมายหรือมาโครย่อยที่สร้างโดยมาโครสัญลักษณ์ต้องตรงกับพารามิเตอร์ name ของมาโคร หรือต้องขึ้นต้นด้วย name ตามด้วย _ (แนะนำ) . หรือ - เช่น my_macro(name = "foo") อาจสร้างไฟล์หรือเป้าหมายที่มีชื่อ foo หรือมี foo_, foo- หรือ foo. นำหน้าเท่านั้น เช่น foo_bar

คุณประกาศเป้าหมายหรือไฟล์ที่ละเมิดรูปแบบการตั้งชื่อมาโครได้ แต่จะสร้างไม่ได้ และใช้เป็นแบบอย่างไม่ได้

ไฟล์และเป้าหมายที่ไม่ใช่มาโครภายในแพ็กเกจเดียวกับอินสแตนซ์มาโครควรไม่มีชื่อที่ขัดแย้งกับชื่อเป้าหมายมาโครที่เป็นไปได้ แม้ว่าเราจะไม่ได้บังคับใช้การผูกขาดนี้ เรากําลังอยู่ระหว่างการใช้การประเมินแบบเลื่อนเพื่อปรับปรุงประสิทธิภาพของมาโครเชิงสัญลักษณ์ ซึ่งจะทํางานได้ไม่ดีในแพ็กเกจที่ละเมิดสคีมาการตั้งชื่อ

ข้อจำกัด

มาโครเชิงสัญลักษณ์มีข้อจํากัดเพิ่มเติมบางอย่างเมื่อเทียบกับมาโครเดิม

มาโครสัญลักษณ์

  • ต้องรับอาร์กิวเมนต์ name และอาร์กิวเมนต์ visibility
  • ต้องมีฟังก์ชัน implementation
  • อาจไม่แสดงผลลัพธ์
  • ต้องไม่เปลี่ยนรูปแบบอาร์กิวเมนต์
  • ต้องไม่เรียกใช้ native.existing_rules() เว้นแต่จะเป็นมาโคร finalizer แบบพิเศษ
  • อาจโทรหา native.package() ไม่ได้
  • อาจโทรหา glob() ไม่ได้
  • อาจโทรหา native.environment_group() ไม่ได้
  • ต้องสร้างเป้าหมายที่มีชื่อเป็นไปตามสคีมาการตั้งชื่อ
  • ไม่สามารถอ้างอิงไฟล์อินพุตที่ไม่ได้ประกาศหรือส่งเป็นอาร์กิวเมนต์ (ดูรายละเอียดเพิ่มเติมที่ระดับการเข้าถึงและมาโคร)

ระดับการเข้าถึงและมาโคร

ดูระดับการมองเห็นเพื่อดูการพูดคุยเชิงลึกเกี่ยวกับระดับการมองเห็นใน Bazel

ระดับการเข้าถึงเป้าหมาย

โดยค่าเริ่มต้น เป้าหมายที่สร้างโดยมาโครเชิงสัญลักษณ์จะปรากฏเฉพาะในแพ็กเกจที่มีไฟล์ .bzl ที่กําหนดมาโคร โดยเฉพาะอย่างยิ่ง ตัวแปรเหล่านี้จะมองไม่เห็นผู้เรียกใช้มาโครเชิงสัญลักษณ์ เว้นแต่ผู้เรียกใช้จะอยู่ในแพ็กเกจเดียวกับไฟล์ .bzl ของมาโคร

หากต้องการให้ผู้เรียกใช้มาโครสัญลักษณ์เห็นเป้าหมาย ให้ส่ง visibility = visibility ไปยังกฎหรือมาโครภายใน นอกจากนี้ คุณยังทำให้เป้าหมายปรากฏในแพ็กเกจเพิ่มเติมได้ด้วยการเพิ่มระดับการแชร์ให้กว้างขึ้น (หรือแม้แต่เป็นแบบสาธารณะ)

ระบบจะส่งผ่านระดับการเข้าถึงเริ่มต้นของแพ็กเกจ (ตามที่ประกาศใน package()) ไปยังพารามิเตอร์ visibility ของมาโครด้านนอกสุดโดยค่าเริ่มต้น แต่มาโครจะส่งผ่าน (หรือไม่ส่งผ่าน) visibility ไปยังเป้าหมายที่สร้างอินสแตนซ์หรือไม่นั้นขึ้นอยู่กับมาโคร

ระดับการเข้าถึงทรัพยากร

เป้าหมายที่อ้างอิงในการใช้งานมาโครต้องปรากฏในคําจํากัดความของมาโครนั้น คุณสามารถกำหนดระดับการแชร์ได้ 1 ใน 2 วิธีต่อไปนี้

  • มาโครจะมองเห็นเป้าหมายหากมีการส่งผ่านเป้าหมายไปยังมาโครผ่านแอตทริบิวต์ป้ายกำกับ รายการป้ายกำกับ หรือพจนานุกรมที่มีคีย์หรือค่าเป็นป้ายกำกับ โดยต้องระบุอย่างชัดแจ้งดังนี้

# pkg/BUILD
my_macro(... deps = ["//other_package:my_tool"] )
  • ... หรือใช้เป็นค่าเริ่มต้นของแอตทริบิวต์
# my_macro:macro.bzl
my_macro = macro(
  attrs = {"deps" : attr.label_list(default = ["//other_package:my_tool"])},
  ...
)
  • มาโครจะมองเห็นเป้าหมายได้เช่นกันหากมีการประกาศให้มองเห็นเป้าหมายในแพ็กเกจที่มีไฟล์ .bzl ซึ่งกำหนดมาโคร ดังนี้
# 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__",
)

เลือก

หากแอตทริบิวต์เป็น configurable (ค่าเริ่มต้น) และค่าของแอตทริบิวต์ไม่ใช่ None ฟังก์ชันการใช้งานมาโครจะเห็นค่าแอตทริบิวต์เป็น select เล็กน้อย วิธีนี้ช่วยให้ผู้เขียนมาโครพบข้อบกพร่องได้ง่ายขึ้นในกรณีที่ไม่ได้คาดคิดว่าค่าแอตทริบิวต์อาจเป็น select

ตัวอย่างเช่น ลองดูมาโครต่อไปนี้

my_macro = macro(
    attrs = {"deps": attr.label_list()},  # configurable unless specified otherwise
    implementation = _my_macro_impl,
)

หากเรียกใช้ my_macro ด้วย deps = ["//a"] ระบบจะเรียกใช้ _my_macro_impl โดยตั้งค่าพารามิเตอร์ deps เป็น select({"//conditions:default": ["//a"]}) หากกรณีนี้ทําให้ฟังก์ชันการใช้งานไม่สําเร็จ (เช่น เนื่องจากโค้ดพยายามเข้าถึงค่าใน deps[0] ซึ่งไม่อนุญาตสําหรับ select) ผู้เขียนมาโครจะเลือกได้ว่าจะเขียนมาโครใหม่ให้ใช้เฉพาะการดำเนินการที่เข้ากันได้กับ select หรือจะทําเครื่องหมายแอตทริบิวต์ว่าไม่สามารถกําหนดค่าได้ (attr.label_list(configurable = False)) ซึ่งตัวเลือกหลังจะทำให้ผู้ใช้ไม่สามารถส่งค่า select ได้

เป้าหมายของกฎจะเปลี่ยนรูปแบบนี้กลับ และจัดเก็บ select ที่ไม่มีค่าเป็นค่าที่ไม่มีเงื่อนไข ในตัวอย่างข้างต้น หาก _my_macro_impl ประกาศเป้าหมายของกฎเป็น my_rule(..., deps = deps) ระบบจะจัดเก็บ deps ของเป้าหมายของกฎนั้นเป็น ["//a"] วิธีนี้ช่วยให้มั่นใจได้ว่าการรวม select จะไม่ทําให้ระบบจัดเก็บค่า select ที่ซ้ำกันในเป้าหมายทั้งหมดที่สร้างขึ้นโดยมาโคร

หากค่าของแอตทริบิวต์ที่กำหนดค่าได้คือ None ระบบจะไม่ตัดขึ้นบรรทัดใหม่ใน select วิธีนี้ช่วยให้การทดสอบอย่าง my_attr == None ยังคงทํางานได้ และเมื่อส่งต่อแอตทริบิวต์ไปยังกฎที่มีค่าเริ่มต้นที่คำนวณแล้ว กฎจะทํางานอย่างถูกต้อง (กล่าวคือ ราวกับว่าไม่มีการส่งแอตทริบิวต์เลย) แอตทริบิวต์อาจใช้ค่า None ไม่ได้เสมอไป แต่อาจใช้กับประเภท attr.label() และสำหรับแอตทริบิวต์ที่รับค่ามาซึ่งไม่บังคับ

Finalizers

ตัวสิ้นสุดกฎคือมาโครสัญลักษณ์พิเศษซึ่งจะได้รับการประเมินในระยะสุดท้ายของการโหลดแพ็กเกจ ไม่ว่าจะอยู่ในตําแหน่งใดในไฟล์ BUILD ก็ตาม หลังจากที่กําหนดเป้าหมายที่ไม่ใช่ตัวสิ้นสุดทั้งหมดแล้ว ต่างจากมาโครสัญลักษณ์ทั่วไปตรงที่ตัวสิ้นสุดสามารถเรียก native.existing_rules() ซึ่งจะทำงานแตกต่างจากในมาโครเดิมเล็กน้อย โดยจะแสดงเฉพาะชุดเป้าหมายกฎที่ไม่ใช่ตัวสิ้นสุด ตัวสรุปอาจยืนยันสถานะของชุดนั้นหรือกําหนดเป้าหมายใหม่

หากต้องการประกาศตัวดำเนินการสิ้นสุด ให้เรียก macro() ด้วย 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,
)

ความขี้เกียจ

สำคัญ: เรากําลังอยู่ระหว่างการขยายและประเมินมาโครแบบ Lazy ฟีเจอร์นี้ยังไม่พร้อมใช้งาน

ปัจจุบันระบบจะประเมินมาโครทั้งหมดทันทีที่โหลดไฟล์ BUILD ซึ่งอาจส่งผลเสียต่อประสิทธิภาพของเป้าหมายในแพ็กเกจที่มีมาโครที่ไม่เกี่ยวข้องและมีค่าใช้จ่ายสูง ในอนาคต มาโครสัญลักษณ์ที่ไม่ใช่ตัวสิ้นสุดจะได้รับการประเมินก็ต่อเมื่อจำเป็นต่อการสร้างเท่านั้น สคีมาการตั้งชื่อคำนำหน้าจะช่วย Bazel ระบุมาโครที่จะขยายตามเป้าหมายที่ขอ

การแก้ปัญหาการย้ายข้อมูล

ต่อไปนี้คือปัญหาที่พบบ่อยในการย้ายข้อมูลและวิธีแก้ไข

  • การเรียกมาโครเดิม glob()

ย้ายการเรียก glob() ไปยังไฟล์ BUILD (หรือไปยังมาโครเดิมที่เรียกจากไฟล์ BUILD) และส่งค่า glob() ไปยังมาโครสัญลักษณ์โดยใช้แอตทริบิวต์รายการป้ายกำกับ ดังนี้

# BUILD file
my_macro(
    ...,
    deps = glob(...),
)
  • มาโครเดิมมีพารามิเตอร์ที่ไม่ใช่ประเภท attr ของ Starlark ที่ถูกต้อง

ดึงตรรกะให้ได้มากที่สุดลงในมาโครสัญลักษณ์ที่ฝังอยู่ แต่ให้มาโครระดับบนสุดเป็นมาโครเดิม

  • มาโครเดิมเรียกใช้กฎที่สร้างเป้าหมายที่ละเมิดสคีมาการตั้งชื่อ

ไม่เป็นไร เพียงแค่อย่าใช้เป้าหมายที่ "ไม่เหมาะสม" ระบบจะละเว้นการตรวจสอบการตั้งชื่อ