Khía cạnh

Báo cáo sự cố Xem nguồn

Trang này giải thích các khái niệm cơ bản và lợi ích của việc sử dụng các khía cạnh, đồng thời cung cấp các ví dụ đơn giản và nâng cao.

Các khía cạnh cho phép bổ sung các biểu đồ phần phụ thuộc của bản dựng bằng thông tin và hành động bổ sung. Sau đây là một số trường hợp điển hình mà các khía cạnh có thể hữu ích:

  • Các IDE tích hợp Bazel có thể sử dụng các khía cạnh để thu thập thông tin về dự án.
  • Các công cụ tạo mã có thể tận dụng các khía cạnh để thực thi trên dữ liệu đầu vào của chúng theo cách không phân biệt mục tiêu. Ví dụ: tệp BUILD có thể chỉ định hệ phân cấp định nghĩa thư viện protobuf, còn các quy tắc theo ngôn ngữ cụ thể có thể sử dụng các khía cạnh để đính kèm thao tác tạo mã hỗ trợ protobuf cho một ngôn ngữ cụ thể.

Thông tin cơ bản về tỷ lệ

Các tệp BUILD cung cấp nội dung mô tả về mã nguồn của dự án: tệp nguồn nào thuộc dự án, cấu phần phần mềm (mục tiêu) nào nên được tạo từ các tệp đó, phần phụ thuộc giữa các tệp đó, v.v. Bazel sử dụng thông tin này để tạo bản dựng, tức là chỉ ra tập hợp hành động cần thiết để tạo ra các cấu phần phần mềm (chẳng hạn như chạy trình biên dịch hoặc trình liên kết) và thực thi các hành động đó. Bazel làm được điều này bằng cách xây dựng một biểu đồ phần phụ thuộc giữa các mục tiêu và truy cập vào biểu đồ này để thu thập các hành động đó.

Hãy xem xét tệp BUILD sau:

java_library(name = 'W', ...)
java_library(name = 'Y', deps = [':W'], ...)
java_library(name = 'Z', deps = [':W'], ...)
java_library(name = 'Q', ...)
java_library(name = 'T', deps = [':Q'], ...)
java_library(name = 'X', deps = [':Y',':Z'], runtime_deps = [':T'], ...)

Tệp BUILD này xác định một biểu đồ phần phụ thuộc được minh hoạ trong hình sau:

Tạo biểu đồ

Hình 1. Biểu đồ phần phụ thuộc của tệp BUILD.

Bazel phân tích biểu đồ phần phụ thuộc này bằng cách gọi một hàm triển khai của quy tắc tương ứng (trong trường hợp này là "java_library") cho mọi mục tiêu trong ví dụ trên. Các hàm triển khai quy tắc tạo ra những thao tác tạo cấu phần phần mềm, chẳng hạn như tệp .jar, và truyền thông tin (chẳng hạn như vị trí và tên của các cấu phần phần mềm đó) sang các phần phụ thuộc đảo ngược của các mục tiêu đó trong nhà cung cấp.

Các khía cạnh tương tự như quy tắc ở chỗ chúng có hàm triển khai tạo hành động và trả về trình cung cấp. Tuy nhiên, sức mạnh của chúng đến từ cách tạo biểu đồ phần phụ thuộc. Một khía cạnh có cách triển khai và danh sách tất cả các thuộc tính được truyền cùng. Hãy xem xét một khía cạnh A lan truyền dọc theo các thuộc tính có tên là "phần phụ thuộc". Khung hình này có thể được áp dụng cho mục tiêu X, tạo ra nút ứng dụng khung hình A(X). Trong quá trình áp dụng, khung hình A được áp dụng đệ quy cho tất cả các mục tiêu mà X tham chiếu đến trong thuộc tính "phần phụ thuộc" (tất cả các thuộc tính trong danh sách truyền của A).

Do đó, một hành động áp dụng khung hình A cho mục tiêu X sẽ tạo ra "biểu đồ bóng" của biểu đồ phần phụ thuộc ban đầu của các mục tiêu như trong hình sau:

Tạo biểu đồ theo khung hình

Hình 2. Xây dựng biểu đồ với các khía cạnh.

Các cạnh duy nhất bị làm mờ là các cạnh dọc theo các thuộc tính trong tập hợp truyền tải, do đó cạnh runtime_deps không bị ẩn trong ví dụ này. Sau đó, hàm triển khai khung hình được gọi trên tất cả các nút trong biểu đồ bóng, tương tự như cách triển khai quy tắc được gọi trên các nút của biểu đồ ban đầu.

Ví dụ đơn giản

Ví dụ này minh hoạ cách in đệ quy các tệp nguồn cho một quy tắc và tất cả các phần phụ thuộc của quy tắc có thuộc tính deps. Hình này cho thấy cách triển khai khung hình, định nghĩa khung hình và cách gọi thành phần đó từ dòng lệnh Bazel.

def _print_aspect_impl(target, ctx):
    # Make sure the rule has a srcs attribute.
    if hasattr(ctx.rule.attr, 'srcs'):
        # Iterate through the files that make up the sources and
        # print their paths.
        for src in ctx.rule.attr.srcs:
            for f in src.files.to_list():
                print(f.path)
    return []

print_aspect = aspect(
    implementation = _print_aspect_impl,
    attr_aspects = ['deps'],
)

Hãy chia nhỏ ví dụ thành các phần và tìm hiểu riêng từng phần.

Định nghĩa khung hình

print_aspect = aspect(
    implementation = _print_aspect_impl,
    attr_aspects = ['deps'],
)

Việc định nghĩa khung hình cũng tương tự như định nghĩa quy tắc và được xác định bằng hàm aspect.

Tương tự như quy tắc, một thành phần có hàm triển khai mà trong trường hợp này là _print_aspect_impl.

attr_aspects là danh sách các thuộc tính quy tắc mà theo đó thành phần sẽ được truyền tải. Trong trường hợp này, thành phần sẽ được truyền dọc theo thuộc tính deps của các quy tắc mà thành phần đó được áp dụng.

Một đối số phổ biến khác cho attr_aspects['*']. Đối số này sẽ truyền phiên bản này cho tất cả thuộc tính của một quy tắc.

Triển khai khung hình

def _print_aspect_impl(target, ctx):
    # Make sure the rule has a srcs attribute.
    if hasattr(ctx.rule.attr, 'srcs'):
        # Iterate through the files that make up the sources and
        # print their paths.
        for src in ctx.rule.attr.srcs:
            for f in src.files.to_list():
                print(f.path)
    return []

Các hàm triển khai khung hình tương tự như hàm triển khai quy tắc. Các phương thức này trả về provider, có thể tạo thao tác và nhận 2 đối số:

  • target: mục tiêu mà khung đang được áp dụng.
  • ctx: Đối tượng ctx có thể dùng để truy cập vào các thuộc tính cũng như tạo dữ liệu đầu ra và hành động.

Hàm triển khai có thể truy cập vào các thuộc tính của quy tắc mục tiêu thông qua ctx.rule.attr. Phương thức này có thể kiểm tra các trình cung cấp do mục tiêu cung cấp mà được áp dụng (thông qua đối số target).

Các thuộc tính là bắt buộc để trả về danh sách nhà cung cấp. Trong ví dụ này, khung hiển thị không cung cấp bất kỳ thông tin nào nên sẽ trả về một danh sách trống.

Gọi thành phần hiển thị bằng dòng lệnh

Cách đơn giản nhất để áp dụng một thành phần là dùng dòng lệnh thông qua đối số --aspects. Giả sử các khía cạnh trên đã được xác định trong tệp có tên print.bzl như sau:

bazel build //MyExample:example --aspects print.bzl%print_aspect

sẽ áp dụng print_aspect cho example mục tiêu và tất cả các quy tắc mục tiêu có thể truy cập đệ quy qua thuộc tính deps.

Cờ --aspects lấy một đối số, là thông số kỹ thuật của khung hình ở định dạng <extension file label>%<aspect top-level name>.

Ví dụ nâng cao

Ví dụ sau minh hoạ việc sử dụng một khía cạnh trong quy tắc mục tiêu có tính số tệp trong mục tiêu và có thể lọc theo tiện ích. Tài liệu này trình bày cách sử dụng một nhà cung cấp để trả về các giá trị, cách sử dụng các tham số để truyền một đối số vào phương thức triển khai khung hình và cách gọi một thành phần qua quy tắc.

Tệp file_count.bzl:

FileCountInfo = provider(
    fields = {
        'count' : 'number of files'
    }
)

def _file_count_aspect_impl(target, ctx):
    count = 0
    # Make sure the rule has a srcs attribute.
    if hasattr(ctx.rule.attr, 'srcs'):
        # Iterate through the sources counting files
        for src in ctx.rule.attr.srcs:
            for f in src.files.to_list():
                if ctx.attr.extension == '*' or ctx.attr.extension == f.extension:
                    count = count + 1
    # Get the counts from our dependencies.
    for dep in ctx.rule.attr.deps:
        count = count + dep[FileCountInfo].count
    return [FileCountInfo(count = count)]

file_count_aspect = aspect(
    implementation = _file_count_aspect_impl,
    attr_aspects = ['deps'],
    attrs = {
        'extension' : attr.string(values = ['*', 'h', 'cc']),
    }
)

def _file_count_rule_impl(ctx):
    for dep in ctx.attr.deps:
        print(dep[FileCountInfo].count)

file_count_rule = rule(
    implementation = _file_count_rule_impl,
    attrs = {
        'deps' : attr.label_list(aspects = [file_count_aspect]),
        'extension' : attr.string(default = '*'),
    },
)

Tệp BUILD.bazel:

load('//:file_count.bzl', 'file_count_rule')

cc_library(
    name = 'lib',
    srcs = [
        'lib.h',
        'lib.cc',
    ],
)

cc_binary(
    name = 'app',
    srcs = [
        'app.h',
        'app.cc',
        'main.cc',
    ],
    deps = ['lib'],
)

file_count_rule(
    name = 'file_count',
    deps = ['app'],
    extension = 'h',
)

Định nghĩa khung hình

file_count_aspect = aspect(
    implementation = _file_count_aspect_impl,
    attr_aspects = ['deps'],
    attrs = {
        'extension' : attr.string(values = ['*', 'h', 'cc']),
    }
)

Ví dụ này cho thấy cách một thành phần được truyền tải thông qua thuộc tính deps.

attrs xác định một tập hợp thuộc tính của một khung hiển thị. Các thuộc tính khung công khai xác định các tham số và chỉ có thể thuộc loại bool, int hoặc string. Đối với các thành phần được truyền tải theo quy tắc, các tham số intstring phải được chỉ định values. Ví dụ này có một tham số tên là extension. Tham số này được phép có một giá trị là "*", "h" hoặc "cc".

Đối với các thành phần được truyền tải theo quy tắc, giá trị tham số được lấy từ quy tắc yêu cầu khía cạnh đó, bằng cách sử dụng thuộc tính của quy tắc có cùng tên và loại. (xem định nghĩa của file_count_rule).

Đối với các khía cạnh dòng lệnh, các giá trị tham số có thể được chuyển bằng cách sử dụng cờ --aspects_parameters. Bạn có thể bỏ qua giới hạn values của các tham số intstring.

Các khung hình cũng được phép có các thuộc tính riêng tư thuộc loại label hoặc label_list. Bạn có thể dùng thuộc tính nhãn riêng tư để chỉ định các phần phụ thuộc trên các công cụ hoặc thư viện cần thiết cho những thao tác được tạo theo các chương trình thành phần. Không có thuộc tính riêng tư nào được xác định trong ví dụ này, nhưng đoạn mã sau đây minh hoạ cách bạn có thể truyền một công cụ đến một khía cạnh:

...
    attrs = {
        '_protoc' : attr.label(
            default = Label('//tools:protoc'),
            executable = True,
            cfg = "exec"
        )
    }
...

Triển khai khung hình

FileCountInfo = provider(
    fields = {
        'count' : 'number of files'
    }
)

def _file_count_aspect_impl(target, ctx):
    count = 0
    # Make sure the rule has a srcs attribute.
    if hasattr(ctx.rule.attr, 'srcs'):
        # Iterate through the sources counting files
        for src in ctx.rule.attr.srcs:
            for f in src.files.to_list():
                if ctx.attr.extension == '*' or ctx.attr.extension == f.extension:
                    count = count + 1
    # Get the counts from our dependencies.
    for dep in ctx.rule.attr.deps:
        count = count + dep[FileCountInfo].count
    return [FileCountInfo(count = count)]

Giống như hàm triển khai quy tắc, hàm triển khai khung hình trả về một cấu trúc của các trình cung cấp mà các phần phụ thuộc của hàm đó có thể truy cập được.

Trong ví dụ này, FileCountInfo được định nghĩa là một nhà cung cấp có một trường count. Phương pháp hay nhất là xác định rõ ràng các trường của trình cung cấp bằng cách sử dụng thuộc tính fields.

Tập hợp các trình cung cấp cho ứng dụng khung hình A(X) là sự hợp nhất các trình cung cấp đến từ việc triển khai quy tắc cho mục tiêu X và từ việc triển khai khía cạnh A. Các nhà cung cấp mà phương thức triển khai quy tắc truyền tải sẽ được tạo và cố định trước khi áp dụng các khía cạnh, đồng thời không thể sửa đổi từ một khía cạnh. Sẽ là lỗi nếu mục tiêu và khía cạnh được áp dụng cho nó cung cấp cùng một loại, với các ngoại lệ là OutputGroupInfo (được hợp nhất, miễn là quy tắc và khía cạnh chỉ định các nhóm đầu ra khác nhau) và InstrumentedFilesInfo (được lấy từ khía cạnh). Điều này có nghĩa là các quá trình triển khai khung hình có thể không bao giờ trả về DefaultInfo.

Các tham số và thuộc tính riêng tư được truyền vào các thuộc tính của ctx. Ví dụ này tham chiếu đến tham số extension và xác định tệp nào cần đếm.

Đối với các trình cung cấp trả về, giá trị của các thuộc tính theo đó thành phần được truyền (từ danh sách attr_aspects) sẽ được thay thế bằng kết quả của việc áp dụng một thành phần hiển thị cho các thành phần đó. Ví dụ: nếu mục tiêu X có Y và Z trong các phần phụ thuộc, thì ctx.rule.attr.deps cho A(X) sẽ là [A(Y), A(Z)]. Trong ví dụ này, ctx.rule.attr.deps là các đối tượng Mục tiêu. Đây là kết quả của việc áp dụng khung hình cho "phần phụ thuộc" của mục tiêu ban đầu mà khung đó đã được áp dụng.

Trong ví dụ này, thành phần hiển thị truy cập vào nhà cung cấp FileCountInfo từ các phần phụ thuộc của mục tiêu để tích luỹ tổng số tệp chuyển tiếp.

Gọi khía cạnh từ quy tắc

def _file_count_rule_impl(ctx):
    for dep in ctx.attr.deps:
        print(dep[FileCountInfo].count)

file_count_rule = rule(
    implementation = _file_count_rule_impl,
    attrs = {
        'deps' : attr.label_list(aspects = [file_count_aspect]),
        'extension' : attr.string(default = '*'),
    },
)

Quá trình triển khai quy tắc minh hoạ cách truy cập vào FileCountInfo thông qua ctx.attr.deps.

Định nghĩa quy tắc minh hoạ cách xác định một tham số (extension) và cung cấp cho tham số đó một giá trị mặc định (*). Lưu ý rằng việc có giá trị mặc định không phải là một trong các "cc", "h" hoặc "*" sẽ là lỗi do các quy định hạn chế áp dụng cho tham số trong định nghĩa khung hình.

Gọi một khía cạnh thông qua quy tắc mục tiêu

load('//:file_count.bzl', 'file_count_rule')

cc_binary(
    name = 'app',
...
)

file_count_rule(
    name = 'file_count',
    deps = ['app'],
    extension = 'h',
)

Hình này minh hoạ cách truyền tham số extension vào khung hiển thị thông qua quy tắc. Vì tham số extension có giá trị mặc định trong quá trình triển khai quy tắc, nên extension sẽ được coi là một tham số không bắt buộc.

Khi mục tiêu file_count được tạo, khía cạnh của chúng ta sẽ được đánh giá chính nó và tất cả các mục tiêu có thể truy cập đệ quy qua deps.

Tài liệu tham khảo