Hướng dẫn XÂY DỰNG phong cách

Ưu tiên tệp BUILD DAMP hơn DRY

Nguyên tắc DRY ("Don't Repeat Yourself" – Đừng lặp lại chính mình) khuyến khích tính duy nhất bằng cách giới thiệu các khái niệm trừu tượng như biến và hàm để tránh sự dư thừa trong mã.

Ngược lại, nguyên tắc DAMP ("Descriptive and Meaningful Phrases" – Cụm từ mang tính mô tả và có ý nghĩa) khuyến khích tính dễ đọc hơn tính duy nhất để giúp tệp dễ hiểu và dễ duy trì hơn.

Tệp BUILD không phải là mã mà là cấu hình. Chúng không được kiểm thử như mã, nhưng cần được duy trì bởi con người và công cụ. Điều đó khiến DAMP phù hợp hơn với chúng so với DRY.

Định dạng tệp BUILD.bazel

Định dạng tệp BUILD tuân theo cùng một phương pháp như Go, trong đó một công cụ tiêu chuẩn hoá sẽ xử lý hầu hết các vấn đề về định dạng. Buildifier là một công cụ phân tích cú pháp và phát ra mã nguồn theo kiểu tiêu chuẩn. Do đó, mọi tệp BUILD đều được định dạng theo cùng một cách tự động, giúp việc định dạng không còn là vấn đề trong quá trình xem xét mã. Điều này cũng giúp các công cụ dễ dàng hiểu, chỉnh sửa và tạo tệp BUILD hơn.

Định dạng tệp BUILD phải khớp với đầu ra của buildifier.

Ví dụ về định dạng

# Test code implementing the Foo controller.
package(default_testonly = True)

py_test(
    name = "foo_test",
    srcs = glob(["*.py"]),
    data = [
        "//data/production/foo:startfoo",
        "//foo",
        "//third_party/java/jdk:jdk-k8",
    ],
    flaky = True,
    deps = [
        ":check_bar_lib",
        ":foo_data_check",
        ":pick_foo_port",
        "//pyglib",
        "//testing/pybase",
    ],
)

Cấu trúc tệp

Đề xuất: Sử dụng thứ tự sau (mọi phần tử đều không bắt buộc):

  • Nội dung mô tả gói (nhận xét)

  • Tất cả câu lệnh load()

  • Hàm package().

  • Lệnh gọi đến quy tắc và macro

Buildifier phân biệt giữa nhận xét độc lập và nhận xét được đính kèm vào một phần tử. Nếu một nhận xét không được đính kèm vào một phần tử cụ thể, hãy sử dụng dòng trống sau nhận xét đó. Sự phân biệt này rất quan trọng khi thực hiện các thay đổi tự động (ví dụ: để giữ lại hoặc xoá một nhận xét khi xoá một quy tắc).

# Standalone comment (such as to make a section in a file)

# Comment for the cc_library below
cc_library(name = "cc")

Tham chiếu đến các mục tiêu trong gói hiện tại

Các tệp phải được tham chiếu theo đường dẫn tương đối của chúng so với thư mục gói (không bao giờ sử dụng tham chiếu lên, chẳng hạn như ..). Các tệp được tạo phải có tiền tố là ":" để cho biết rằng chúng không phải là nguồn. Các tệp nguồn không được có tiền tố là :. Các quy tắc phải có tiền tố là :. Ví dụ: giả sử x.cc là một tệp nguồn:

cc_library(
    name = "lib",
    srcs = ["x.cc"],
    hdrs = [":gen_header"],
)

genrule(
    name = "gen_header",
    srcs = [],
    outs = ["x.h"],
    cmd = "echo 'int x();' > $@",
)

Đặt tên mục tiêu

Tên mục tiêu phải mang tính mô tả. Nếu một mục tiêu chứa một tệp nguồn, mục tiêu đó thường phải có tên bắt nguồn từ nguồn đó (ví dụ: một cc_library cho chat.cc có thể được đặt tên là chat, hoặc một java_library cho DirectMessage.java có thể được đặt tên là direct_message).

Mục tiêu cùng tên cho một gói (mục tiêu có cùng tên với thư mục chứa) phải cung cấp chức năng được mô tả theo tên thư mục. Nếu không có mục tiêu nào như vậy, đừng tạo mục tiêu cùng tên.

Bạn nên sử dụng tên ngắn khi tham chiếu đến mục tiêu cùng tên (//x thay vì //x:x). Nếu bạn đang ở cùng một gói, hãy ưu tiên tham chiếu cục bộ (:x thay vì //x).

Tránh sử dụng tên mục tiêu "dành riêng" có ý nghĩa đặc biệt. Điều này bao gồm all, __pkg____subpackages__. Các tên này có ngữ nghĩa đặc biệt và có thể gây nhầm lẫn cũng như hành vi không mong muốn khi được sử dụng.

Trong trường hợp không có quy ước nhóm hiện hành, đây là một số đề xuất không ràng buộc được sử dụng rộng rãi tại Google:

  • Nói chung, hãy sử dụng "snake_case"
    • Đối với java_library có một src, điều này có nghĩa là sử dụng tên không giống với tên tệp mà không có đuôi
    • Đối với các quy tắc Java *_binary*_test, hãy sử dụng "Upper CamelCase". Điều này cho phép tên mục tiêu khớp với một trong các src. Đối với java_test, điều này giúp thuộc tính test_class có thể được suy luận từ tên của mục tiêu.
  • Nếu có nhiều biến thể của một mục tiêu cụ thể, hãy thêm hậu tố để phân biệt (chẳng hạn như. :foo_dev, :foo_prod hoặc :bar_x86, :bar_x64)
  • Hậu tố _test nhắm mục tiêu bằng _test, _unittest, Test hoặc Tests
  • Tránh các hậu tố vô nghĩa như _lib hoặc _library (trừ phi cần thiết để tránh xung đột giữa mục tiêu _library_binary tương ứng)
  • Đối với các mục tiêu liên quan đến proto:
    • Các mục tiêu proto_library phải có tên kết thúc bằng _proto
    • Các quy tắc *_proto_library dành riêng cho ngôn ngữ phải khớp với proto cơ bản nhưng thay thế _proto bằng hậu tố dành riêng cho ngôn ngữ, chẳng hạn như:
      • cc_proto_library: _cc_proto
      • java_proto_library: _java_proto
      • java_lite_proto_library: _java_proto_lite

Khả năng hiển thị

Chế độ hiển thị phải được giới hạn chặt chẽ nhất có thể, đồng thời vẫn cho phép các bài kiểm thử và phần phụ thuộc đảo ngược truy cập. Sử dụng __pkg____subpackages__ khi thích hợp.

Tránh đặt default_visibility của gói thành //visibility:public. Bạn chỉ nên đặt //visibility:public riêng cho các mục tiêu trong API công khai của dự án. Đây có thể là các thư viện được thiết kế để các dự án bên ngoài hoặc tệp nhị phân phụ thuộc vào có thể được sử dụng bởi quy trình xây dựng của dự án bên ngoài.

Phần phụ thuộc

Các phần phụ thuộc phải được giới hạn ở các phần phụ thuộc trực tiếp (các phần phụ thuộc cần thiết cho các nguồn được liệt kê trong quy tắc). Không liệt kê các phần phụ thuộc bắc cầu.

Các phần phụ thuộc cục bộ của gói phải được liệt kê trước và được tham chiếu theo cách tương thích với phần Tham chiếu đến các mục tiêu trong gói hiện tại ở trên (không phải theo tên gói tuyệt đối của chúng).

Bạn nên liệt kê các phần phụ thuộc trực tiếp dưới dạng một danh sách duy nhất. Việc đưa các phần phụ thuộc "chung" của một số mục tiêu vào một biến sẽ làm giảm khả năng duy trì, khiến các công cụ không thể thay đổi các phần phụ thuộc của một mục tiêu và có thể dẫn đến các phần phụ thuộc không được sử dụng.

Globs

Cho biết "không có mục tiêu" bằng []. Không sử dụng glob không khớp với bất kỳ mục tiêu nào: glob này dễ bị lỗi hơn và ít rõ ràng hơn so với danh sách trống.

Đệ quy

Không sử dụng glob đệ quy để khớp với các tệp nguồn (ví dụ: glob(["**/*.java"])).

Glob đệ quy khiến tệp BUILD khó suy luận vì chúng bỏ qua các thư mục con chứa tệp BUILD.

Glob đệ quy thường kém hiệu quả hơn so với việc có một tệp BUILD cho mỗi thư mục có biểu đồ phần phụ thuộc được xác định giữa chúng vì điều này cho phép lưu vào bộ nhớ đệm từ xa và tính song song tốt hơn.

Bạn nên tạo tệp BUILD trong mỗi thư mục và xác định biểu đồ phần phụ thuộc giữa chúng.

Không đệ quy

Glob không đệ quy thường được chấp nhận.

Tránh hiểu danh sách

Tránh sử dụng hiểu danh sách ở cấp cao nhất của tệp BUILD.bazel. Tự động hoá các lệnh gọi lặp đi lặp lại bằng cách tạo từng mục tiêu được đặt tên bằng một quy tắc hoặc lệnh gọi macro cấp cao nhất riêng biệt. Cung cấp cho mỗi tham số name ngắn để cho rõ ràng.

Hiểu danh sách làm giảm những điều sau:

  • Khả năng bảo trì. Người duy trì và các thay đổi tự động quy mô lớn khó hoặc không thể cập nhật chính xác các hiểu danh sách.
  • Khả năng khám phá. Vì mẫu không có tham số name, nên khó tìm thấy quy tắc theo tên.

Một ứng dụng phổ biến của mẫu hiểu danh sách là tạo các bài kiểm thử. Ví dụ:

[[java_test(
    name = "test_%s_%s" % (backend, count),
    srcs = [ ... ],
    deps = [ ... ],
    ...
) for backend in [
    "fake",
    "mock",
]] for count in [
    1,
    10,
]]

Bạn nên sử dụng các lựa chọn thay thế đơn giản hơn. Ví dụ: xác định một macro tạo một bài kiểm thử và gọi macro đó cho mỗi name cấp cao nhất:

my_java_test(name = "test_fake_1",
    ...)
my_java_test(name = "test_fake_10",
    ...)
...

Không sử dụng biến deps

Không sử dụng biến danh sách để đóng gói các phần phụ thuộc chung:

COMMON_DEPS = [
  "//d:e",
  "//x/y:z",
]

cc_library(name = "a",
    srcs = ["a.cc"],
    deps = COMMON_DEPS + [ ... ],
)

cc_library(name = "b",
    srcs = ["b.cc"],
    deps = COMMON_DEPS + [ ... ],
)

Tương tự, không sử dụng mục tiêu thư viện có exports để nhóm các phần phụ thuộc.

Thay vào đó, hãy liệt kê các phần phụ thuộc riêng cho từng mục tiêu:

cc_library(name = "a",
    srcs = ["a.cc"],
    deps = [
      "//a:b",
      "//x/y:z",
      ...
    ],
)

cc_library(name = "b",
    srcs = ["b.cc"],
    deps = [
      "//a:b",
      "//x/y:z",
      ...
    ],
)

Hãy để Gazelle và các công cụ khác duy trì các phần phụ thuộc này. Sẽ có sự lặp lại, nhưng bạn sẽ không phải nghĩ về cách quản lý các phần phụ thuộc.

Ưu tiên chuỗi ký tự

Mặc dù Starlark cung cấp các toán tử chuỗi để nối (+) và định dạng (%), nhưng hãy sử dụng chúng một cách thận trọng. Bạn có thể muốn tách các phần chuỗi chung để làm cho biểu thức ngắn gọn hơn hoặc ngắt các dòng dài. Tuy nhiên,

Do đó, hãy ưu tiên các chuỗi ký tự rõ ràng, tường minh hơn các chuỗi được nối hoặc định dạng, đặc biệt là trong các thuộc tính thuộc loại nhãn như namedeps. Ví dụ: đoạn mã BUILD này:

NAME = "foo"
PACKAGE = "//a/b"

proto_library(
  name = "%s_proto" % NAME,
  deps = [PACKAGE + ":other_proto"],
  alt_dep = "//surprisingly/long/chain/of/package/names:" +
            "extravagantly_long_target_name",
)

sẽ được viết lại tốt hơn thành

proto_library(
  name = "foo_proto",
  deps = ["//a/b:other_proto"],
  alt_dep = "//surprisingly/long/chain/of/package/names:extravagantly_long_target_name",
)

Giới hạn các ký hiệu được xuất bởi mỗi tệp .bzl

Giảm thiểu số lượng ký hiệu (quy tắc, macro, hằng số, hàm) được xuất bởi mỗi tệp .bzl (Starlark) công khai. Bạn nên chỉ xuất nhiều ký hiệu nếu chắc chắn rằng chúng sẽ được sử dụng cùng nhau. Nếu không, hãy chia thành nhiều tệp .bzl, mỗi tệp có bzl_library riêng.

Quá nhiều ký hiệu có thể khiến các tệp .bzl phát triển thành các "thư viện" ký hiệu rộng lớn, khiến các thay đổi đối với một tệp buộc Bazel phải xây dựng lại nhiều mục tiêu.

Các quy ước khác

  • Sử dụng chữ in hoa và dấu gạch dưới để khai báo hằng số (chẳng hạn như GLOBAL_CONSTANT), sử dụng chữ in thường và dấu gạch dưới để khai báo biến (chẳng hạn như my_variable).

  • Không bao giờ được chia tách nhãn, ngay cả khi chúng dài hơn 79 ký tự. Nhãn phải là chuỗi ký tự bất cứ khi nào có thể. Lý do: Điều này giúp bạn dễ dàng tìm và thay thế. Điều này cũng cải thiện tính dễ đọc.

  • Giá trị của thuộc tính name phải là một chuỗi hằng số ký tự (trừ trong macro). Lý do: Các công cụ bên ngoài sử dụng thuộc tính name để tham chiếu đến một quy tắc. Chúng cần tìm quy tắc mà không cần phải diễn giải mã.

  • Khi đặt thuộc tính thuộc loại boolean, hãy sử dụng giá trị boolean, không phải giá trị số nguyên. Vì lý do cũ, các quy tắc vẫn chuyển đổi số nguyên thành boolean khi cần, nhưng bạn không nên làm như vậy. Lý do: flaky = 1 có thể bị đọc nhầm là "deflake mục tiêu này bằng cách chạy lại một lần". flaky = True nói rõ ràng "bài kiểm thử này không ổn định".

Điểm khác biệt với hướng dẫn về kiểu Python

Mặc dù mục tiêu là khả năng tương thích với hướng dẫn về kiểu Python nhưng có một số điểm khác biệt:

  • Không có giới hạn nghiêm ngặt về độ dài dòng. Các nhận xét dài và chuỗi dài thường được chia thành 79 cột, nhưng không bắt buộc. Không nên thực thi trong quá trình xem xét mã hoặc tập lệnh trước khi gửi. Lý do: Nhãn có thể dài và vượt quá giới hạn này. Các tệp BUILD thường được tạo hoặc chỉnh sửa bằng các công cụ, điều này không phù hợp với giới hạn về độ dài dòng.

  • Không hỗ trợ nối chuỗi ngầm ẩn. Sử dụng toán tử +. Lý do: Tệp BUILD chứa nhiều danh sách chuỗi. Bạn có thể dễ dàng quên dấu phẩy, dẫn đến kết quả hoàn toàn khác. Điều này đã tạo ra nhiều lỗi trong quá khứ. Xem thêm cuộc thảo luận này.

  • Sử dụng dấu cách xung quanh dấu = cho các đối số từ khoá trong quy tắc. Lý do: Các đối số được đặt tên thường xuyên hơn nhiều so với trong Python và luôn nằm trên một dòng riêng biệt. Dấu cách giúp cải thiện tính dễ đọc. Quy ước này đã tồn tại từ lâu và không đáng để sửa đổi tất cả các tệp BUILD hiện có.

  • Theo mặc định, hãy sử dụng dấu ngoặc kép cho chuỗi. Lý do: Điều này không được chỉ định trong hướng dẫn về kiểu Python, nhưng hướng dẫn này khuyến nghị tính nhất quán. Vì vậy, chúng tôi quyết định chỉ sử dụng chuỗi được đặt trong dấu ngoặc kép. Nhiều ngôn ngữ sử dụng dấu ngoặc kép cho chuỗi ký tự.

  • Sử dụng một dòng trống duy nhất giữa hai định nghĩa cấp cao nhất. Lý do: Cấu trúc của tệp BUILD không giống như tệp Python thông thường. Tệp này chỉ có câu lệnh cấp cao nhất. Việc sử dụng một dòng trống duy nhất giúp tệp BUILD ngắn hơn.