Quản lý các phần phụ thuộc từ bên ngoài bằng Bzlmod

Bzlmod là tên mã của hệ thống phụ thuộc bên ngoài mới được giới thiệu trong Bazel 5.0. Chúng tôi đưa ra giải pháp này để giải quyết một số vấn đề của hệ thống cũ, vì điều này không thể thực sự khắc phục được. hãy xem phần Tuyên bố về vấn đề trong tài liệu thiết kế gốc để biết thêm chi tiết.

Trong Bazel 5.0, Bzlmod không được bật theo mặc định; cờ --experimental_enable_bzlmod cần được chỉ định để tính năng sau có hiệu lực. Như tên cờ đã đề xuất, tính năng này hiện đang thử nghiệm; các API và hành vi có thể thay đổi cho đến khi tính năng này chính thức ra mắt.

Mô-đun bazel

Hệ thống phụ thuộc bên ngoài dựa trên WORKSPACE cũ được đặt ở giữa kho lưu trữ (hoặc kho lưu trữ), được tạo thông qua quy tắc lưu trữ (hoặc quy tắc thu hồi). Mặc dù các mô phỏng vẫn là khái niệm quan trọng trong hệ thống mới, nhưng các mô-đun là đơn vị cốt lõi của phần phụ thuộc.

Một mô-đun về cơ bản là một dự án Bazel có thể có nhiều phiên bản, trong đó mỗi phiên bản phát hành siêu dữ liệu về những mô-đun khác mà dự án này phụ thuộc. Đây là các khái niệm tương tự như các khái niệm quen thuộc trong các hệ thống quản lý phần phụ thuộc khác: Maven artifact, một gói npm, bảng Hàng hóa, mô-đun Go, v.v.

Một mô-đun chỉ xác định các phần phụ thuộc bằng cách sử dụng các cặp nameversion, thay vì các URL cụ thể trong WORKSPACE. Sau đó, bạn có thể tra cứu các phần phụ thuộc trong Tổ chức quản lý tên miền Brazil; Theo mặc định của Cơ quan quản lý tên miền Brazil. Trong không gian làm việc của bạn, mỗi mô-đun sẽ được chuyển thành một bản repo.

Hiển thị các tiêu đề bổ sung

Mọi phiên bản của mỗi mô-đun đều có tệp MODULE.bazel khai báo các phần phụ thuộc và siêu dữ liệu khác. Dưới đây là một ví dụ cơ bản:

module(
    name = "my-module",
    version = "1.0",
)

bazel_dep(name = "rules_cc", version = "0.0.1")
bazel_dep(name = "protobuf", version = "3.19.0")

Tệp MODULE.bazel phải nằm ở thư mục gốc của thư mục không gian làm việc (bên cạnh tệp WORKSPACE). Không giống như với tệp WORKSPACE, bạn không cần chỉ định phần phụ thuộc vận động; thay vào đó, bạn chỉ nên chỉ định phần phụ thuộc trực tiếp và các tệp MODULE.bazel của phần phụ thuộc sẽ được xử lý để tự động phát hiện các phần phụ thuộc

Tệp MODULE.bazel tương tự như BUILD tệp vì tệp này không hỗ trợ bất kỳ hình thức nào của quy trình kiểm soát; bổ sung cũng cấm load câu lệnh. Các lệnh MODULE.bazel hỗ trợ tệp như sau:

Định dạng phiên bản

Bazel có một hệ sinh thái đa dạng và các dự án sử dụng các kế hoạch tạo phiên bản khác nhau. phổ biến nhất từ trước đến nay là SemVer, nhưng cũng có các dự án nổi bật sử dụng các lược đồ khác nhau như Abseil và các phiên bản dựa trên ngày, ví dụ: 20210324.2).

Vì lý do này, Bzlmod sử dụng một phiên bản thoải mái hơn của đặc tả SemVer, đặc biệt là cho phép số lượng chuỗi chữ số bất kỳ trong phần "phát hành" của phiên bản (thay vì chính xác 3 chuỗi như SemVer quy định: MAJOR.MINOR.PATCH). Ngoài ra, ngữ nghĩa của các phiên bản lớn, nhỏ và bản vá không bắt buộc phải thực thi. (Tuy nhiên, hãy xem cấp độ tương thích để biết chi tiết về cách chúng tôi biểu thị khả năng tương thích ngược.) Các phần khác của thông số SemVer, chẳng hạn như dấu gạch ngang biểu thị phiên bản bản phát hành trước, sẽ không được sửa đổi.

Độ phân giải của phiên bản

Vấn đề về phụ thuộc kim cương là yếu tố chính trong không gian quản lý phần phụ thuộc được tạo phiên bản. Giả sử bạn có biểu đồ phần phụ thuộc sau:

       A 1.0
      /     \
   B 1.0    C 1.1
     |        |
   D 1.0    D 1.1

Bạn nên dùng phiên bản D nào? Để giải quyết câu hỏi này, Bzlmod sẽ sử dụng thuật toán Lựa chọn phiên bản tối thiểu (MVS) được giới thiệu trong hệ thống mô-đun Go. MVS giả định rằng tất cả các phiên bản mới của một mô-đun đều tương thích ngược, do đó, chỉ đơn giản là chọn phiên bản cao nhất do bất kỳ phần phụ thuộc nào xác định (D 1.1 trong ví dụ của chúng tôi). Nó được gọi là "tối thiểu" vì D 1.1 ở đây là phiên bản tối thiểu có thể đáp ứng các yêu cầu của chúng tôi; ngay cả khi D 1.2 hoặc mới hơn tồn tại, chúng tôi cũng không chọn chúng. Điều này sẽ mang lại thêm một lợi ích là lựa chọn phiên bản có độ trung thực caotái sản xuất.

Độ phân giải phiên bản được thực hiện trên máy của bạn, chứ không phải bằng tổ chức quản lý tên miền.

Mức độ tương thích

Lưu ý rằng giả định về khả năng tương thích ngược của MVS là khả thi vì điều này đơn giản chỉ là coi các phiên bản mô-đun không tương thích là một mô-đun riêng biệt. Về SemVer, điều đó có nghĩa là A 1.x và A 2.x được coi là các mô-đun riêng biệt và có thể cùng tồn tại trong biểu đồ phần phụ thuộc đã phân giải. Điều này là nhờ vào việc thực hiện phiên bản chính được mã hóa trong đường dẫn gói trong Go, vì vậy không có bất kỳ xung đột thời gian biên dịch hay thời gian liên kết nào.

Tại Bazel, chúng tôi không đảm bảo như vậy. Do đó, chúng tôi cần có cách để biểu thị số "phiên bản chính" để phát hiện các phiên bản không tương thích ngược. Số này được gọi là cấp độ tương thích và được xác định cho từng phiên bản mô-đun trong lệnh module(). Khi có thông tin này, chúng tôi có thể phát hiện lỗi khi phát hiện thấy các phiên bản của cùng một mô-đun có các mức độ tương thích khác nhau tồn tại trong biểu đồ phần phụ thuộc đã phân giải.

Tên lưu trữ

Trong Bazel, mỗi phần phụ thuộc bên ngoài đều có tên kho lưu trữ. Đôi khi, bạn có thể dùng cùng một phần phụ thuộc thông qua tên kho lưu trữ khác nhau (ví dụ: cả @io_bazel_skylib@bazel_skylib có nghĩa là Bazel skylib) hoặc giống nhau tên kho lưu trữ có thể được dùng cho nhiều phần phụ thuộc trong các dự án khác nhau.

Trong Bzlmod, có thể tạo kho lưu trữ bằng các mô-đun Bazel và phần mở rộng của mô-đun. Để giải quyết các xung đột về tên kho lưu trữ, chúng tôi sẽ sử dụng cơ chế lập bản đồ kho lưu trữ trong hệ thống mới. Sau đây là 2 khái niệm quan trọng:

  • Tên kho lưu trữ chính tắc: Tên kho lưu trữ độc đáo trên toàn cầu cho mỗi kho lưu trữ. Đây sẽ là tên thư mục chứa kho lưu trữ.
    Định dạng này được tạo như sau (Cảnh báo: định dạng tên chính tắc không phải là một API mà bạn nên phụ thuộc và có thể thay đổi bất cứ lúc nào):

    • Đối với đề xuất mô-đun Bazel: module_name.version
      (Ví dụ). @bazel_skylib.1.0.3)
    • Đối với vị trí đặt lại phần mở rộng mô-đun: module_name.version.extension_name.repo_name
      (Ví dụ. @rules_cc.0.0.1.cc_configure.local_config_cc)
  • Tên kho lưu trữ cục bộ: Tên kho lưu trữ được dùng trong tệp BUILD.bzl trong repo. Cùng một phần phụ thuộc có thể có các tên cục bộ khác nhau cho các kho lưu trữ khác nhau.
    Giải pháp này được xác định như sau:

    • Đối với vị trí mô-đun Bazel: module_name theo mặc định, hoặc tên được chỉ định bởi thuộc tính repo_name trong bazel_dep.
    • Đối với tùy chọn cài đặt lại phần mở rộng của mô-đun: tên kho lưu trữ được giới thiệu qua use_repo.

Mọi kho lưu trữ đều có từ điển ánh xạ các kho phụ thuộc trực tiếp của kho lưu trữ. Đây là một bản đồ từ tên kho lưu trữ cục bộ sang tên kho lưu trữ chính tắc. Chúng tôi sử dụng các liên kết kho lưu trữ để phân giải tên kho lưu trữ khi tạo nhãn. Lưu ý rằng sẽ không có xung đột với tên kho lưu trữ chính tắc và việc sử dụng tên kho lưu trữ cục bộ có thể được phát hiện bằng cách phân tích cú pháp tệp MODULE.bazel, do đó, xung đột có thể dễ dàng bị phát hiện và giải quyết mà không ảnh hưởng đến các phần phụ thuộc khác.

Nghiêm ngặt nghiêm trọng

Định dạng thông số phụ thuộc mới cho phép chúng tôi thực hiện các quy trình kiểm tra nghiêm ngặt hơn. Đặc biệt, chúng tôi hiện thực thi rằng một mô-đun chỉ có thể sử dụng các vị trí đặt lại được tạo từ các phần phụ thuộc trực tiếp của mô-đun đó. Điều này giúp ngăn chặn các lần ngắt ngẫu nhiên và khó gỡ lỗi khi có sự thay đổi trong biểu đồ phần phụ thuộc bắc cầu.

Các yêu cầu xóa nghiêm ngặt được triển khai dựa trên mối liên kết kho lưu trữ. Về cơ bản, việc liên kết kho lưu trữ cho mỗi kho lưu trữ chứa tất cả các phần phụ thuộc trực tiếp của kho lưu trữ đó, mọi kho lưu trữ khác đều không hiển thị. Các phần phụ thuộc hiển thị cho từng kho lưu trữ được xác định như sau:

  • Bạn có thể xem bản tóm tắt của mô-đun Bazel trong tệp MODULE.bazel thông qua bazel_depuse_repo.
  • Tiện ích mô-đun của tiện ích có thể thấy tất cả các phần phụ thuộc hiển thị của mô-đun cung cấp tiện ích đó, cùng với tất cả các tệp tải lại khác của cùng tiện ích đó.

Tổ chức quản lý tên miền

Bzlmod phát hiện các phần phụ thuộc bằng cách yêu cầu thông tin của họ từ tổ chức quản lý tên miền Bazel. Tổ chức quản lý tên miền Bazel chỉ đơn giản là cơ sở dữ liệu của các mô-đun Bazel. Mẫu đăng ký duy nhất được hỗ trợ là cơ quan quản lý tên miền index mục tiêu, là một thư mục cục bộ hoặc máy chủ HTTP tĩnh theo một định dạng cụ thể. Trong tương lai, chúng tôi dự định bổ sung dịch vụ hỗ trợ cho tổ chức quản lý tên miền gồm một mô-đun (đơn giản là cơ chế đăng ký lại có nguồn gốc và lịch sử dự án).

Đăng ký chỉ mục

sổ đăng ký chỉ mục là một thư mục cục bộ hoặc một máy chủ HTTP tĩnh chứa thông tin về danh sách các mô-đun, bao gồm trang chủ, trình bảo trì, tệp MODULE.bazel của mỗi phiên bản và cách tìm nạp nguồn của mỗi mô-đun phiên bản. Đáng chú ý là nguồn dữ liệu này không cần phân phát chính các bản lưu trữ nguồn.

sổ đăng ký chỉ mục phải tuân theo định dạng dưới đây:

  • /bazel_registry.json: Tệp JSON chứa siêu dữ liệu cho tổ chức quản lý tên miền. Hiện tại, khóa này chỉ có một khóa, mirrors, chỉ định danh sách các bản sao có thể sử dụng để lưu trữ nguồn.
  • /modules: Một thư mục chứa một thư mục con cho mỗi mô-đun trong cơ quan quản lý này.
  • /modules/$MODULE: Thư mục chứa một thư mục con cho mỗi phiên bản của mô-đun này cũng như tệp sau đây:
    • metadata.json: Tệp JSON chứa thông tin về mô-đun, với các trường sau:
      • homepage: URL trang chủ của dự án.
      • maintainers: Danh sách các đối tượng JSON, mỗi đối tượng tương ứng với thông tin của trình duy trì mô-đun trong sổ đăng ký. Xin lưu ý rằng tên này không nhất thiết giống với tác giả của dự án.
      • versions: Danh sách tất cả các phiên bản của mô-đun này có trong sổ đăng ký này.
      • yanked_versions: Danh sách các phiên bản bạn đã tải lên mô-đun này. Hiện tại, đây là một phiên bản không hoạt động, nhưng trong tương lai, các phiên bản chèn sẽ bị bỏ qua hoặc gây ra lỗi.
  • /modules/$MODULE/$VERSION: Thư mục chứa các tệp sau:
    • MODULE.bazel: Tệp MODULE.bazel của phiên bản mô-đun này.
    • source.json: Tệp JSON chứa thông tin về cách tìm nạp nguồn của phiên bản mô-đun này, với các trường sau:
      • url: URL của tệp lưu trữ nguồn.
      • integrity: Kiểm tra tính toàn vẹn của tài nguyên phụ của bản lưu trữ.
      • strip_prefix: Một tiền tố thư mục cần xóa khi trích xuất kho lưu trữ nguồn.
      • patches: Danh sách các chuỗi, mỗi chuỗi chứa tên một tệp bản vá để áp dụng cho bản lưu trữ đã trích xuất. Các tệp bản vá nằm trong thư mục /modules/$MODULE/$VERSION/patches.
      • patch_strip: Giống như đối số --strip của bản vá Unix.
    • patches/: Thư mục tùy chọn chứa tệp bản vá.

Cơ quan đăng ký của Trung tâm Bazel

Tổ chức quản lý tên miền Bazel (BCR) là một hệ thống tên miền chỉ mục nằm tại registry.bazel.build. Nội dung của tệp này được khôi phục qua bản repo GitHub bazelbuild/bazel-central-registry.

BCR do cộng đồng Bazel duy trì; cộng tác viên có thể gửi yêu cầu kéo. Hãy xem Quy trình và chính sách của Cơ quan đăng ký Brazil.

Ngoài việc tuân thủ định dạng của sổ đăng ký chỉ mục thông thường, BCR còn yêu cầu tệp presubmit.yml cho mỗi phiên bản mô-đun (/modules/$MODULE/$VERSION/presubmit.yml). Tệp này chỉ định một số mục tiêu thiết lập và bản dựng thử nghiệm cần thiết có thể dùng để kiểm tra tính hợp lệ của phiên bản mô-đun này và dùng các quy trình CI của BCR để đảm bảo khả năng tương tác giữa các mô-đun trong BCR ,

Chọn tổ chức quản lý tên miền

Cờ Bazel --registry có thể lặp lại có thể được dùng để chỉ định danh sách các tổ chức quản lý tên miền mà bạn muốn yêu cầu mô-đun, để bạn có thể thiết lập dự án của mình để tìm nạp các phần phụ thuộc từ sổ đăng ký nội bộ hoặc bên thứ ba. Các tổ chức quản lý tên miền trước đây cung cấp mức độ ưu tiên. Để thuận tiện, bạn có thể đặt danh sách --registry cờ trong tệp .bazelrc của dự án.

Phần mở rộng của mô-đun

Phần mở rộng mô-đun cho phép bạn mở rộng hệ thống mô-đun bằng cách đọc dữ liệu đầu vào từ các mô-đun trên biểu đồ phần phụ thuộc, thực hiện logic cần thiết để giải quyết các phần phụ thuộc và cuối cùng là tạo lại vị trí bằng cách gọi các quy tắc repo. Các macro này có chức năng tương tự như các macro WORKSPACE hiện nay, nhưng phù hợp hơn trong thế giới của các mô-đun và phần phụ thuộc động từ.

Phần mở rộng về mô-đun được xác định trong .bzl tệp, tương tự như các quy tắc repo hoặc macro WORKSPACE. Họ không được gọi trực tiếp; thay vào đó, mỗi mô-đun có thể chỉ định các phần dữ liệu có tên là thẻ để tiện ích đọc. Sau đó, sau khi giải quyết xong phiên bản mô-đun, các tiện ích mô-đun sẽ chạy. Mỗi tiện ích được chạy một lần sau khi phân giải mô-đun (vẫn trước khi xảy ra bất kỳ bản dựng nào) và đọc tất cả các thẻ thuộc về tiện ích đó trên toàn bộ biểu đồ phần phụ thuộc.

          [ A 1.1                ]
          [   * maven.dep(X 2.1) ]
          [   * maven.pom(...)   ]
              /              \
   bazel_dep /                \ bazel_dep
            /                  \
[ B 1.2                ]     [ C 1.0                ]
[   * maven.dep(X 1.2) ]     [   * maven.dep(X 2.1) ]
[   * maven.dep(Y 1.3) ]     [   * cargo.dep(P 1.1) ]
            \                  /
   bazel_dep \                / bazel_dep
              \              /
          [ D 1.4                ]
          [   * maven.dep(Z 1.4) ]
          [   * cargo.dep(Q 1.1) ]

Trong biểu đồ phần phụ thuộc mẫu ở trên, A 1.1B 1.2 là các mô-đun Bazel; bạn có thể coi mỗi mô-đun là một tệp MODULE.bazel. Mỗi mô-đun có thể chỉ định một số thẻ cho phần mở rộng về mô-đun; ở đây một số được chỉ định cho tiện ích "maven", và một số khác được chỉ định cho "hàng hóa". Khi biểu đồ phần phụ thuộc này được hoàn tất (ví dụ: có thể B 1.2 thực sự có bazel_dep vào D 1.3 nhưng đã được nâng cấp lên D 1.4 do C), tiện ích "maven" được chạy và trình đọc này đọc tất cả các thẻ maven.*, sử dụng thông tin trong đó để quyết định việc tạo tệp lại. Tương tự cho phần mở rộng "hàng hóa".

Sử dụng phần mở rộng

Các tiện ích được lưu trữ trong chính các mô-đun Bazel, vì vậy, để sử dụng tiện ích trong mô-đun của bạn, trước tiên, bạn cần thêm bazel_dep trên mô-đun đó rồi gọi use_extension chức năng tích hợp để đưa chức năng đó vào phạm vi cụ thể. Hãy xem xét ví dụ sau, một đoạn mã từ tệp MODULE.bazel để sử dụng phần mở rộng giả định về "maven" đã xác định trong mô-đun rules_jvm_external:

bazel_dep(name = "rules_jvm_external", version = "1.0")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

Sau khi đưa tiện ích vào phạm vi, bạn có thể sử dụng dấu chấm cú pháp để chỉ định thẻ cho tiện ích. Hãy lưu ý rằng các thẻ cần tuân theo giản đồ do các lớp thẻ tương ứng xác định (hãy xem định nghĩa phần mở rộng bên dưới). Dưới đây là ví dụ về cách xác định một số thẻ maven.depmaven.pom.

maven.dep(coord="org.junit:junit:3.0")
maven.dep(coord="com.google.guava:guava:1.2")
maven.pom(pom_xml="//:pom.xml")

Nếu tiện ích tạo ra các vị trí đặt lại mà bạn muốn sử dụng trong mô-đun, hãy sử dụng lệnh use_repo để khai báo các tiện ích đó. Điều này là để đáp ứng điều kiện nghiêm ngặt về deps và tránh xung đột tên phương thức cập nhật cục bộ.

use_repo(
    maven,
    "org_junit_junit",
    guava="com_google_guava_guava",
)

Các kho lưu trữ do một tiện ích tạo ra là một phần của API. Do đó, từ các thẻ mà bạn đã chỉ định, bạn nên biết rằng tiện ích "maven" sẽ tạo một repo có tên là "org_junit_junit" và một tiện ích có tên là "com_google_guava_guava". Với use_repo, bạn có thể tùy ý đổi tên cho các tệp đó trong phạm vi của mô-đun, như "guava" tại đây.

Định nghĩa phần mở rộng

Các phần mở rộng về mô-đun được định nghĩa tương tự như các quy tắc repo bằng cách sử dụng hàm module_extension. Cả hai đều có chức năng triển khai; nhưng quy tắc repo có một số thuộc tính, còn phần mở rộng về mô-đun có số lượng tag_classes, mỗi thuộc tính có một số thuộc tính. Các lớp thẻ xác định giản đồ cho các thẻ mà tiện ích này sử dụng. Tiếp tục ví dụ về phần mở rộng giả định về "maven" ở trên:

# @rules_jvm_external//:extensions.bzl
maven_dep = tag_class(attrs = {"coord": attr.string()})
maven_pom = tag_class(attrs = {"pom_xml": attr.label()})
maven = module_extension(
    implementation=_maven_impl,
    tag_classes={"dep": maven_dep, "pom": maven_pom},
)

Các thông tin khai báo này cho thấy rõ rằng bạn có thể chỉ định các thẻ maven.depmaven.pom bằng cách sử dụng giản đồ thuộc tính được xác định ở trên.

Hàm triển khai tương tự như macro WORKSPACE, ngoại trừ việc nhận được đối tượng module_ctx Sau đó, hàm triển khai sẽ gọi các quy tắc repo để tạo các kho lưu trữ:

# @rules_jvm_external//:extensions.bzl
load("//:repo_rules.bzl", "maven_single_jar")
def _maven_impl(ctx):
  coords = []
  for mod in ctx.modules:
    coords += [dep.coord for dep in mod.tags.dep]
  output = ctx.execute(["coursier", "resolve", coords])  # hypothetical call
  repo_attrs = process_coursier(output)
  [maven_single_jar(**attrs) for attrs in repo_attrs]

Trong ví dụ trên, chúng ta sẽ tìm hiểu tất cả các mô-đun trong biểu đồ phần phụ thuộc (ctx.modules), mỗi mô-đun là một đối tượng bazel_moduletags trường hiển thị tất cả các thẻ maven.* trên mô-đun. Sau đó, chúng tôi gọi ra tiện ích CLI mà bạn muốn liên hệ với Maven và thực hiện giải pháp. Cuối cùng, chúng tôi sử dụng kết quả độ phân giải để tạo một số kho lưu trữ lại, bằng cách sử dụng quy tắc maven_single_jar giả định.