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

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.
Báo cáo sự cố Xem nguồn

Bzlmod là tên mã của hệ thống phần phụ thuộc bên ngoài mới được giới thiệu trong Bazel 5.0. Bài viết này được giới thiệu để giải quyết một số điểm khó khăn của hệ thống cũ mà không thể khắc phục dần dần; xem phần Tuyên bố vấn đề trong tài liệu thiết kế gốc để biết thêm thông tin 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 để các nội dung sau đây có hiệu lực. Như tên cờ cho thấy, tính năng này hiện đang thử nghiệm; API và các hành vi có thể thay đổi cho đến khi tính năng này chính thức ra mắt.

Để di chuyển dự án của bạn sang Bzlmod, hãy làm theo Hướng dẫn di chuyển Bzlmod. Bạn cũng có thể tìm thấy các ví dụ về cách sử dụng Bzlmod trong kho lưu trữ examples.

Mô-đun Bazel

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

Mô-đun về cơ bản là một dự án Bazel có thể có nhiều phiên bản, mỗi phiên bản xuất bản siêu dữ liệu về các mô-đun khác mà mô-đun đó phụ thuộc. Điều này 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: cấu phần phần mềm Maven, gói npm, hộp Cargo, mô-đun Go, v.v.

Một mô-đun chỉ đơn giản là chỉ đị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 đó, các phần phụ thuộc được tra cứu trong một hệ thống tên miền Bazel; theo mặc định, Hệ thống đăng ký trung tâm 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 kho lưu trữ.

MODULE.bazel

Mỗi phiên bản của mỗi mô-đun đều có một tệp MODULE.bazel khai báo các phần phụ thuộc và siêu dữ liệu khác. Sau đâ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 không gian làm việc (bên cạnh tệp WORKSPACE). Không giống như tệp WORKSPACE, bạn không cần chỉ định các phần phụ thuộc bắc cầu; thay vào đó, bạn chỉ nên chỉ định các phần phụ thuộc trực tiếp và tệp MODULE.bazel của các phần phụ thuộc sẽ được tự động xử lý để khám phá các phần phụ thuộc bắc cầu.

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

Đị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 nhiều lược đồ tạo phiên bản khác nhau. Phổ biến nhất cho đế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 (phiên bản dựa trên ngày, ví dụ: 20210324.2).

Vì lý do này, Bzlmod sử dụng phiên bản đặc tả đơn giản hơn của SemVer. Sự khác biệt bao gồm:

  • SemVer chỉ định rằng phần "release" (phát hành) của phiên bản phải bao gồm 3 phân đoạn: MAJOR.MINOR.PATCH. Trong Bazel, yêu cầu này được nới lỏng để cho phép mọi phân đoạn được phép.
  • Trong SemVer, mỗi phân đoạn trong phần "release" (phát hành) chỉ được chứa chữ số. Trong Bazel, thao tác này cũng được nới lỏng để cho phép chữ cái, và ngữ nghĩa so sánh sẽ khớp với "giá trị nhận dạng" trong phần "phát hành".
  • Ngoài ra, ngữ nghĩa của việc tăng phiên bản chính, nhỏ và bản vá sẽ không được thực thi. (Tuy nhiên, hãy xem mức độ tương thích để biết thông tin chi tiết về cách chúng tôi biểu thị khả năng tương thích ngược.)

Mọi phiên bản SemVer hợp lệ đều là phiên bản mô-đun Bazel hợp lệ. Ngoài ra, hai phiên bản SemVer ab so sánh a < b có thuế suất giống nhau khi chúng được so sánh với các phiên bản mô-đun Bazel.

Độ phân giải phiên bản

Vấn đề phần phụ thuộc diamond là một vấn đề quan trọng 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

Công ty này nên sử dụng phiên bản D nào? Để giải quyết câu hỏi này, Bzlmod sử dụng thuật toán Lựa chọn phiên bản tối thiểu (MVS) 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ô-đun đều tương thích ngược, do đó chỉ cần chọn phiên bản cao nhất được chỉ định bởi phần phụ thuộc bất kỳ (trong ví dụ của chúng tôi là D.1.1). 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 trở lên, chúng tôi sẽ không chọn các phiên bản đó. Điều này mang lại thêm lợi ích là lựa chọn phiên bản có độ trung thực caocó thể tái tạo.

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

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

Xin lưu ý rằng giả định của MVS về khả năng tương thích ngược là khả thi vì nó đơn giản coi các phiên bản không tương thích ngược của một mô-đun như một mô-đun riêng biệt. Về mặt 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 lại, bạn có thể thực hiện việc này nhờ thực tế là phiên bản chính được mã hoá trong đường dẫn gói trong Go (nên sẽ không có bất kỳ xung đột nào tại thời điểm biên dịch hoặc thời gian liên kết).

Ở Bazel, chúng tôi không đảm bảo chắc chắn. Do đó, chúng tôi cần một 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à mức tương thích và được chỉ định bởi từng phiên bản mô-đun trong lệnh module(). Thông tin trên tay có thể gây ra 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 cấp 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 kho 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, một phần phụ thuộc có thể được sử dụng thông qua các 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 cùng một tên kho lưu trữ có thể được dùng cho các phần phụ thuộc khác nhau trong các dự án khác nhau.

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

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

    • Đối với kho lưu trữ mô-đun Bazel: module_name~version
      (Ví dụ). @bazel_skylib~1.0.3)
    • Đối với các kho lưu trữ phần mở rộng về 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ữ phụ: Tên kho lưu trữ sẽ dùng trong các tệp BUILD.bzl trong một kho lưu trữ. Phần phụ thuộc tương tự có thể có tên rõ ràng khác nhau trong các kho lưu trữ khác nhau.
    Được xác định như sau:

    • Đối với kho lưu 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 kho lưu trữ phần mở rộng về mô-đun: tên kho lưu trữ được giới thiệu thông qua use_repo.

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

Phần phụ thuộc nghiêm ngặt

Định dạng thông số kỹ thuật mới cho phần phụ thuộc mới cho phép chúng ta kiểm tra nghiêm ngặt hơn. Cụ thể, hiện chúng tôi bắt buộc một mô-đun chỉ có thể sử dụng các kho lưu trữ được tạo từ các phần phụ thuộc trực tiếp. Điều này giúp ngăn chặn các sự cố gián đoạn và khó gỡ lỗi khi có nội dung nào đó trong biểu đồ phần phụ thuộc bắc cầu thay đổi.

Phần phụ thuộc 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, bản đồ ánh xạ 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òn 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:

  • Kho lưu trữ mô-đun Bazel có thể thấy tất cả các kho lưu trữ được giới thiệu trong tệp MODULE.bazel thông qua bazel_depuse_repo.
  • Kho lưu trữ mô-đun mở rộng có thể xem tất 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 kho lưu trữ khác được tạo bằng cùng một tiện ích mô-đun.

Nhà đăng ký 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 từ các tổ chức quản lý Bazel. Sổ đăng ký Bazel chỉ đơn giản là cơ sở dữ liệu của các mô-đun Bazel. Hình thức đăng ký duy nhất được hỗ trợ là sổ đăng ký chỉ mục, là một thư mục cục bộ hoặc máy chủ HTTP tĩnh tuân theo một định dạng cụ thể. Trong tương lai, chúng tôi dự định thêm tính năng hỗ trợ cho các sổ đăng ký một mô-đun, đơn giản là các kho lưu trữ git chứa nguồn và lịch sử dự án.

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

Sổ đăng ký chỉ mục là một thư mục cục bộ hoặc 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 từng phiên bản và cách tìm nạp nguồn của từng phiên bản. Đáng chú ý là phương thức 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 theo định dạng dưới đây:

  • /bazel_registry.json: Tệp JSON chứa siêu dữ liệu của sổ đăng ký như:
    • mirrors, chỉ định danh sách phản chiếu để sử dụng cho kho lưu trữ nguồn.
    • module_base_path, xác định đường dẫn cơ sở cho các mô-đun có loại local_repository trong tệp source.json.
  • /modules: Một thư mục chứa thư mục con cho mỗi mô-đun trong quy trình đăng ký này.
  • /modules/$MODULE: Thư mục chứa thư mục con cho từng phiên bản của mô-đun này cũng như tệp sau:
    • 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 bảo trì mô-đun trong sổ đăng ký. Xin lưu ý rằng giá trị này không nhất thiết giống với các tác giả của dự án.
      • versions: Danh sách tất cả phiên bản của mô-đun này ở trong sổ đăng ký này.
      • yanked_versions: Danh sách các phiên bản phát lại của mô-đun này. Hiện tại, phương thức này không hoạt động. Tuy nhiên, trong tương lai, các phiên bả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.
      • Loại mặc định là "lưu trữ" với các trường sau:
        • url: URL của kho lưu trữ nguồn.
        • integrity: Kết quả kiểm thử Tính toàn vẹn của tài nguyên phụ.
        • strip_prefix: Một tiền tố thư mục cần xoá khi trích xuất tệp lưu trữ nguồn.
        • patches: Danh sách các chuỗi, mỗi chuỗi đặt tên một tệp bản vá để áp dụng cho tệp 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: Tương tự như đối số --strip của bản vá Unix.
      • Bạn có thể thay đổi loại để sử dụng đường dẫn cục bộ với các trường sau:
        • type: local_path
        • path: Đường dẫn cục bộ đến kho lưu trữ, được tính như sau:
          • Nếu đường dẫn là một đường dẫn tuyệt đối, sẽ được dùng nguyên trạng.
          • Nếu đường dẫn là một đường dẫn tương đối và module_base_path là một đường dẫn tuyệt đối, đường dẫn sẽ được phân giải thành <module_base_path>/<path>
          • Nếu đường dẫn và module_base_path đều là đường dẫn tương đối, thì đường dẫn sẽ được phân giải thành <registry_path>/<module_base_path>/<path>. Sổ đăng ký phải được lưu trữ trên máy và được sử dụng bởi --registry=file://<registry_path>. Nếu không, Bazel sẽ hiển thị lỗi.
    • patches/: Thư mục không bắt buộc chứa các tệp bản vá, chỉ được dùng khi source.json có loại "lưu trữ".

Hệ thống tên miền Trung tâm Bazel

Bazel Central Registry (BCR) là một chỉ mục đăng ký tại bcr.bazel.build. Nội dung của lớp này được hỗ trợ bởi kho lưu trữ GitHub bazelbuild/bazel-central-registry.

BCR do cộng đồng Bazel duy trì; các cộng tác viên có thể gửi yêu cầu lấy dữ liệu. Xem Quy trình và chính sách của Hệ thống tên miền Trung tâm Brazil.

Ngoài việc tuân theo đị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 bản dựng và kiểm thử thiết yếu có thể dùng để kiểm tra tính hợp lệ của phiên bản mô-đun này và được sử dụng trong 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

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

Tiện ích mô-đun

Tiện ích 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 repo bằng cách gọi các quy tắc repo. Các hàm này có chức năng tương tự như macro WORKSPACE hiện nay nhưng phù hợp hơn trong thế giới của các mô-đun và các phần phụ thuộc bắc cầu.

Các tiện ích mô-đun được xác định trong các tệp .bzl, giống như các quy tắc lưu trữ hoặc macro WORKSPACE. Chúng 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ẻ để phần mở rộng có thể đọc. Sau khi hoàn tất quá trình phân giải phiên bản mô-đun, các phần mở rộng của mô-đun sẽ được 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 mọi bản dựng thực sự xảy ra) và được đọ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, v.v. 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 mô-đun; một số thẻ được chỉ định cho phần mở rộng "maven" và một số thẻ được chỉ định cho "gogo". 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 trên D 1.3 nhưng đã được nâng cấp lên D 1.4 do C), các tiện ích "maven" sẽ được chạy và đọc tất cả các thẻ maven.* bằng cách sử dụng thông tin để quyết định nên tạo repos nào. Tương tự với phần mở rộng "hàng hóa".

Mức 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 một tiện ích trong mô-đun của mình, trước tiên, bạn cần thêm bazel_dep vào mô-đun đó, sau đó gọi hàm tích hợp use_extension để đưa nó vào phạm vi. Hãy xem xét ví dụ sau, đoạn mã từ tệp MODULE.bazel để sử dụng phần mở rộng "giả sử" giả định đã 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 cú pháp dấu chấm để chỉ định thẻ cho tiện ích đó. Xin lưu ý rằng các thẻ cần tuân theo giản đồ được xác định theo lớp thẻ tương ứng (xem định nghĩa phần mở rộng ở bên dưới). Sau đây là ví dụ về cách chỉ đị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 kho lưu trữ mà bạn muốn sử dụng trong mô-đun của mình, hãy sử dụng lệnh use_repo để khai báo các kho lưu trữ đó. Điều này nhằm đáp ứng điều kiện phần phụ thuộc nghiêm ngặt và tránh xung đột tên kho lưu trữ cục bộ.

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

Kho lưu trữ được tiện ích tạo ra là một phần của API, vì vậy 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 kho lưu trữ 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ể tuỳ ý đổi tên chúng trong phạm vi của mô-đun, chẳng hạn như "guava" tại đây.

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

Các phần mở rộng của mô-đun được xác định tương tự như các quy tắc kho lưu trữ, sử dụng hàm module_extension. Cả hai đều có hàm triển khai; nhưng mặc dù quy tắc lưu trữ có một số thuộc tính, nhưng phần mở rộng của mô-đun có một số tag_class, 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 thẻ mà tiện ích này sử dụng. Tiếp tục ví dụ của chúng tôi về phần mở rộng giả định "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 khai báo này nêu rõ rằng bạn có thể chỉ định 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 hàm này nhận đối tượng module_ctx, đối tượng này cấp quyền truy cập vào biểu đồ phần phụ thuộc và tất cả các thẻ liên quan. Sau đó, hàm triển khai sẽ gọi quy tắc kho lưu trữ để tạo 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 đi qua 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_module có trường tags hiển thị tất cả các thẻ maven.* trên mô-đun. Sau đó, chúng tôi gọi Công cụ tiện ích CLI để liên hệ với Maven và thực hiện giải pháp. Cuối cùng, chúng ta sử dụng kết quả giải quyết để tạo một số kho lưu trữ, sử dụng quy tắc giả định maven_single_jar giả định.