Cơ sở mã Bazel

Báo cáo vấn đề Xem nguồn Nightly · 8.0 . 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Tài liệu này mô tả cơ sở mã và cách cấu trúc Bazel. Tài liệu này dành cho những người sẵn sàng đóng góp cho Bazel, chứ không dành cho người dùng cuối.

Giới thiệu

Cơ sở mã của Bazel rất lớn (~350KLOC mã sản xuất và ~260 KLOC mã kiểm thử) và không ai quen thuộc với toàn bộ cảnh quan: mọi người đều biết rất rõ về thung lũng cụ thể của mình, nhưng ít người biết những gì nằm trên các ngọn đồi theo mọi hướng.

Để những người đang ở giữa hành trình không bị lạc trong một khu rừng tối tăm với lối đi thẳng, tài liệu này cố gắng cung cấp thông tin tổng quan về cơ sở mã để dễ dàng bắt đầu làm việc với cơ sở mã đó.

Phiên bản công khai của mã nguồn Bazel nằm trên GitHub tại github.com/bazelbuild/bazel. Đây không phải là "nguồn đáng tin cậy"; mà là nguồn lấy từ cây nguồn nội bộ của Google, chứa chức năng bổ sung không hữu ích bên ngoài Google. Mục tiêu dài hạn là biến GitHub trở thành nguồn đáng tin cậy.

Các nội dung đóng góp được chấp nhận thông qua cơ chế yêu cầu lấy thông tin thông thường trên GitHub, sau đó được một nhân viên Google nhập thủ công vào cây nguồn nội bộ, rồi xuất lại về GitHub.

Cấu trúc máy khách/máy chủ

Phần lớn Bazel nằm trong một quy trình máy chủ lưu trữ trong RAM giữa các bản dựng. Điều này cho phép Bazel duy trì trạng thái giữa các bản dựng.

Đây là lý do dòng lệnh Bazel có hai loại tuỳ chọn: khởi động và lệnh. Trong dòng lệnh như sau:

    bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar

Một số tuỳ chọn (--host_jvm_args=) nằm trước tên lệnh cần chạy và một số tuỳ chọn nằm sau (-c opt); loại trước đây được gọi là "tuỳ chọn khởi động" và ảnh hưởng đến toàn bộ quá trình của máy chủ, trong khi loại sau, "tuỳ chọn lệnh", chỉ ảnh hưởng đến một lệnh duy nhất.

Mỗi phiên bản máy chủ có một không gian làm việc được liên kết (bộ sưu tập các cây nguồn được gọi là "kho lưu trữ") và mỗi không gian làm việc thường có một phiên bản máy chủ đang hoạt động. Bạn có thể tránh vấn đề này bằng cách chỉ định cơ sở đầu ra tuỳ chỉnh (xem phần "Bố cục thư mục" để biết thêm thông tin).

Bazel được phân phối dưới dạng một tệp thực thi ELF duy nhất, đồng thời cũng là một tệp .zip hợp lệ. Khi bạn nhập bazel, tệp thực thi ELF ở trên được triển khai trong C++ ("ứng dụng") sẽ có quyền kiểm soát. Phương thức này thiết lập một quy trình máy chủ thích hợp bằng cách làm theo các bước sau:

  1. Kiểm tra xem tệp đó đã tự giải nén hay chưa. Nếu không, ứng dụng sẽ thực hiện việc đó. Đây là nơi triển khai máy chủ.
  2. Kiểm tra xem có phiên bản máy chủ đang hoạt động nào hoạt động hay không: phiên bản đó đang chạy, có các tuỳ chọn khởi động phù hợp và sử dụng đúng thư mục không gian làm việc. Phương thức này tìm thấy máy chủ đang chạy bằng cách xem thư mục $OUTPUT_BASE/server có tệp khoá với cổng mà máy chủ đang nghe.
  3. Nếu cần, hãy chấm dứt quy trình máy chủ cũ
  4. Khởi động một quy trình máy chủ mới nếu cần

Sau khi một quy trình máy chủ phù hợp đã sẵn sàng, lệnh cần chạy sẽ được giao tiếp với quy trình đó qua giao diện gRPC, sau đó đầu ra của Bazel sẽ được chuyển về thiết bị đầu cuối. Chỉ có thể chạy một lệnh cùng một lúc. Việc này được triển khai bằng cách sử dụng một cơ chế khoá phức tạp với các phần trong C++ và các phần trong Java. Có một số cơ sở hạ tầng để chạy song song nhiều lệnh, vì không thể chạy bazel version song song với một lệnh khác là điều hơi đáng xấu hổ. Vấn đề chính là vòng đời của BlazeModule và một số trạng thái trong BlazeRuntime.

Ở cuối một lệnh, máy chủ Bazel sẽ truyền mã thoát mà ứng dụng sẽ trả về. Một điểm thú vị là việc triển khai bazel run: nhiệm vụ của lệnh này là chạy một nội dung nào đó mà Bazel vừa tạo, nhưng không thể thực hiện việc đó từ quy trình máy chủ vì không có thiết bị đầu cuối. Thay vào đó, ứng dụng sẽ cho ứng dụng biết tệp nhị phân nào cần ujexec() và với đối số nào.

Khi người dùng nhấn tổ hợp phím Ctrl-C, ứng dụng sẽ dịch tổ hợp phím này thành lệnh Huỷ trên kết nối gRPC. Lệnh này sẽ cố gắng chấm dứt lệnh càng sớm càng tốt. Sau lần nhấn tổ hợp phím Ctrl-C thứ ba, ứng dụng sẽ gửi SIGKILL đến máy chủ.

Mã nguồn của ứng dụng nằm trong src/main/cpp và giao thức dùng để giao tiếp với máy chủ nằm trong src/main/protobuf/command_server.proto .

Điểm truy cập chính của máy chủ là BlazeRuntime.main() và các lệnh gọi gRPC từ ứng dụng được GrpcServerImpl.run() xử lý.

Bố cục thư mục

Bazel tạo một tập hợp thư mục hơi phức tạp trong quá trình tạo bản dựng. Bạn có thể xem nội dung mô tả đầy đủ trong phần Bố cục thư mục đầu ra.

"Kho lưu trữ chính" là cây nguồn mà Bazel chạy trong đó. Thuộc tính này thường tương ứng với một nội dung mà bạn đã kiểm tra từ hệ thống quản lý nguồn. Thư mục gốc của thư mục này được gọi là "gốc không gian làm việc".

Bazel đặt tất cả dữ liệu của mình trong "output user root" (thư mục gốc của người dùng đầu ra). Giá trị này thường là $HOME/.cache/bazel/_bazel_${USER}, nhưng có thể được ghi đè bằng tuỳ chọn khởi động --output_user_root.

"Cơ sở cài đặt" là nơi Bazel được trích xuất. Việc này được thực hiện tự động và mỗi phiên bản Bazel sẽ nhận được một thư mục con dựa trên tổng kiểm của phiên bản đó trong cơ sở cài đặt. Theo mặc định, giá trị này là $OUTPUT_USER_ROOT/install và có thể được thay đổi bằng cách sử dụng tuỳ chọn dòng lệnh --install_base.

"Cơ sở đầu ra" là nơi thực thể Bazel đính kèm vào một không gian làm việc cụ thể ghi vào. Mỗi cơ sở đầu ra có tối đa một phiên bản máy chủ Bazel chạy bất cứ lúc nào. Tệp này thường nằm ở $OUTPUT_USER_ROOT/<checksum of the path to the workspace>. Bạn có thể thay đổi chế độ này bằng cách sử dụng tuỳ chọn khởi động --output_base. Ngoài ra, tuỳ chọn này còn hữu ích để khắc phục hạn chế chỉ có thể chạy một thực thể Bazel trong bất kỳ không gian làm việc nào tại một thời điểm nhất định.

Thư mục đầu ra chứa một số nội dung sau:

  • Các kho lưu trữ bên ngoài được tìm nạp tại $OUTPUT_BASE/external.
  • Thư mục gốc thực thi, một thư mục chứa các đường liên kết tượng trưng đến tất cả mã nguồn cho bản dựng hiện tại. Tệp này nằm ở $OUTPUT_BASE/execroot. Trong quá trình tạo bản dựng, thư mục đang hoạt động là $EXECROOT/<name of main repository>. Chúng tôi dự định thay đổi giá trị này thành $EXECROOT, mặc dù đây là kế hoạch dài hạn vì đây là thay đổi không tương thích.
  • Các tệp được tạo trong quá trình xây dựng.

Quy trình thực thi lệnh

Sau khi máy chủ Bazel có quyền kiểm soát và được thông báo về một lệnh cần thực thi, trình tự sự kiện sau đây sẽ diễn ra:

  1. BlazeCommandDispatcher được thông báo về yêu cầu mới. Phương thức này quyết định xem lệnh có cần không gian làm việc để chạy hay không (hầu hết các lệnh ngoại trừ các lệnh không liên quan gì đến mã nguồn, chẳng hạn như phiên bản hoặc trợ giúp) và liệu có lệnh nào khác đang chạy hay không.

  2. Đã tìm thấy lệnh phù hợp. Mỗi lệnh phải triển khai giao diện BlazeCommand và phải có chú thích @Command (đây là một chút mẫu ngược, sẽ rất tuyệt nếu tất cả siêu dữ liệu mà một lệnh cần được mô tả bằng các phương thức trên BlazeCommand)

  3. Các tuỳ chọn dòng lệnh được phân tích cú pháp. Mỗi lệnh có các tuỳ chọn dòng lệnh khác nhau, được mô tả trong chú thích @Command.

  4. Một bus sự kiện được tạo. Xe buýt sự kiện là một luồng cho các sự kiện xảy ra trong quá trình xây dựng. Một số trong số này được xuất ra bên ngoài Bazel theo mục đích của Giao thức sự kiện bản dựng để cho mọi người biết quá trình xây dựng diễn ra như thế nào.

  5. Lệnh này sẽ có quyền kiểm soát. Các lệnh thú vị nhất là những lệnh chạy bản dựng: bản dựng, kiểm thử, chạy, mức độ phù hợp, v.v.: chức năng này do BuildTool triển khai.

  6. Tập hợp các mẫu mục tiêu trên dòng lệnh được phân tích cú pháp và ký tự đại diện như //pkg:all//pkg/... được phân giải. Việc này được triển khai trong AnalysisPhaseRunner.evaluateTargetPatterns() và được tái hiện trong Skyframe dưới dạng TargetPatternPhaseValue.

  7. Giai đoạn tải/phân tích được chạy để tạo biểu đồ hành động (một biểu đồ không tuần hoàn có hướng của các lệnh cần thực thi cho bản dựng).

  8. Giai đoạn thực thi được chạy. Điều này có nghĩa là chạy mọi hành động cần thiết để tạo các mục tiêu cấp cao nhất được yêu cầu.

Tuỳ chọn dòng lệnh

Các tuỳ chọn dòng lệnh cho lệnh gọi Bazel được mô tả trong đối tượng OptionsParsingResult, đối tượng này chứa bản đồ từ "lớp tuỳ chọn" đến các giá trị của tuỳ chọn. "Lớp tuỳ chọn" là một lớp con của OptionsBase và nhóm các tuỳ chọn dòng lệnh lại với nhau có liên quan đến nhau. Ví dụ:

  1. Các tuỳ chọn liên quan đến một ngôn ngữ lập trình (CppOptions hoặc JavaOptions). Đây phải là một lớp con của FragmentOptions và cuối cùng được gói vào một đối tượng BuildOptions.
  2. Các tuỳ chọn liên quan đến cách Bazel thực thi các hành động (ExecutionOptions)

Các tuỳ chọn này được thiết kế để sử dụng trong giai đoạn phân tích và (thông qua RuleContext.getFragment() trong Java hoặc ctx.fragments trong Starlark). Một số trong số đó (ví dụ: có nên thực hiện việc quét C++ hay không) được đọc trong giai đoạn thực thi, nhưng điều đó luôn đòi hỏi phải có quy trình rõ ràng vì BuildConfiguration không có sẵn. Để biết thêm thông tin, hãy xem phần "Cấu hình".

CẢNH BÁO: Chúng ta muốn giả định rằng các thực thể OptionsBase là không thể thay đổi và sử dụng các thực thể đó theo cách đó (chẳng hạn như một phần của SkyKeys). Đây không phải là trường hợp và việc sửa đổi các thực thể đó là một cách rất hiệu quả để phá vỡ Bazel theo những cách tinh vi khó gỡ lỗi. Rất tiếc, việc thực sự làm cho các giá trị này không thể thay đổi là một công việc lớn. (Bạn có thể sửa đổi FragmentOptions ngay sau khi tạo trước khi bất kỳ ai khác có cơ hội giữ lại tệp tham chiếu đến FragmentOptions và trước khi equals() hoặc hashCode() được gọi.)

Bazel tìm hiểu về các lớp tuỳ chọn theo những cách sau:

  1. Một số được tích hợp sẵn vào Bazel (CommonCommandOptions)
  2. Từ chú thích @Command trên mỗi lệnh Bazel
  3. Từ ConfiguredRuleClassProvider (đây là các tuỳ chọn dòng lệnh liên quan đến từng ngôn ngữ lập trình)
  4. Các quy tắc Starlark cũng có thể xác định các tuỳ chọn riêng (xem tại đây)

Mỗi tuỳ chọn (ngoại trừ các tuỳ chọn do Starlark xác định) là một biến thành viên của lớp con FragmentOptions có chú thích @Option. Chú thích này chỉ định tên và loại tuỳ chọn dòng lệnh cùng với một số văn bản trợ giúp.

Loại Java của giá trị tuỳ chọn dòng lệnh thường là một giá trị đơn giản (chuỗi, số nguyên, Boolean, nhãn, v.v.). Tuy nhiên, chúng tôi cũng hỗ trợ các tuỳ chọn của các loại phức tạp hơn; trong trường hợp này, công việc chuyển đổi từ chuỗi dòng lệnh sang loại dữ liệu sẽ thuộc về việc triển khai com.google.devtools.common.options.Converter.

Cây nguồn mà Bazel nhìn thấy

Bazel chuyên về việc xây dựng phần mềm, việc này diễn ra bằng cách đọc và diễn giải mã nguồn. Toàn bộ mã nguồn mà Bazel hoạt động trên đó được gọi là "không gian làm việc" và được cấu trúc thành kho lưu trữ, gói và quy tắc.

Kho lưu trữ

"Kho lưu trữ" là một cây nguồn mà nhà phát triển làm việc; thường đại diện cho một dự án duy nhất. Blaze, tiền thân của Bazel, hoạt động trên một monorepo, nghĩa là một cây nguồn duy nhất chứa tất cả mã nguồn dùng để chạy bản dựng. Ngược lại, Bazel hỗ trợ các dự án có mã nguồn trải dài trên nhiều kho lưu trữ. Kho lưu trữ mà Bazel được gọi là "kho lưu trữ chính", còn các kho lưu trữ khác được gọi là "kho lưu trữ bên ngoài".

Kho lưu trữ được đánh dấu bằng tệp ranh giới kho lưu trữ (MODULE.bazel, REPO.bazel hoặc trong ngữ cảnh cũ, WORKSPACE hoặc WORKSPACE.bazel) trong thư mục gốc của kho lưu trữ. Kho lưu trữ chính là cây nguồn mà bạn đang gọi Bazel. Các kho lưu trữ bên ngoài được xác định theo nhiều cách; hãy xem tổng quan về phần phụ thuộc bên ngoài để biết thêm thông tin.

Mã của các kho lưu trữ bên ngoài được liên kết tượng trưng hoặc tải xuống trong $OUTPUT_BASE/external.

Khi chạy bản dựng, toàn bộ cây nguồn cần được ghép lại với nhau; việc này do SymlinkForest thực hiện, liên kết tượng trưng mọi gói trong kho lưu trữ chính với $EXECROOT và mọi kho lưu trữ bên ngoài với $EXECROOT/external hoặc $EXECROOT/...

Gói

Mỗi kho lưu trữ bao gồm các gói, một tập hợp các tệp có liên quan và thông số kỹ thuật của các phần phụ thuộc. Các tệp này được chỉ định bằng một tệp có tên là BUILD hoặc BUILD.bazel. Nếu cả hai đều tồn tại, Bazel sẽ ưu tiên BUILD.bazel; lý do tệp BUILD vẫn được chấp nhận là vì Blaze (tiền thân của Bazel) đã sử dụng tên tệp này. Tuy nhiên, đây lại là một đoạn đường dẫn thường dùng, đặc biệt là trên Windows, nơi tên tệp không phân biệt chữ hoa chữ thường.

Các gói độc lập với nhau: các thay đổi đối với tệp BUILD của một gói không thể làm thay đổi các gói khác. Việc thêm hoặc xoá tệp BUILD _có thể_ thay đổi các gói khác, vì các glob đệ quy dừng ở ranh giới gói và do đó, sự hiện diện của tệp BUILD sẽ dừng quá trình đệ quy.

Quá trình đánh giá tệp BUILD được gọi là "tải gói". Lớp này được triển khai trong lớp PackageFactory, hoạt động bằng cách gọi trình thông dịch Starlark và yêu cầu kiến thức về tập hợp các lớp quy tắc hiện có. Kết quả của quá trình tải gói là một đối tượng Package. Đây chủ yếu là một bản đồ từ một chuỗi (tên của mục tiêu) đến chính mục tiêu đó.

Một phần lớn sự phức tạp trong quá trình tải gói là việc gộp: Bazel không yêu cầu mọi tệp nguồn phải được liệt kê rõ ràng mà thay vào đó có thể chạy các tệp glob (chẳng hạn như glob(["**/*.java"])). Không giống như shell, Bazel hỗ trợ các tệp glob đệ quy đi xuống các thư mục con (nhưng không phải vào các gói con). Việc này yêu cầu quyền truy cập vào hệ thống tệp và vì việc này có thể diễn ra chậm, nên chúng tôi triển khai mọi loại thủ thuật để hệ thống tệp chạy song song và hiệu quả nhất có thể.

Tính năng gộp được triển khai trong các lớp sau:

  • LegacyGlobber, một globber nhanh và không biết Skyframe
  • SkyframeHybridGlobber, một phiên bản sử dụng Skyframe và quay lại globber cũ để tránh "Khởi động lại Skyframe" (như mô tả bên dưới)

Bản thân lớp Package chứa một số thành viên chỉ dùng để phân tích cú pháp gói "bên ngoài" (liên quan đến các phần phụ thuộc bên ngoài) và không phù hợp với các gói thực. Đây là một lỗi thiết kế vì các đối tượng mô tả gói thông thường không được chứa các trường mô tả nội dung khác. bao gồm:

  • Các mối liên kết kho lưu trữ
  • Chuỗi công cụ đã đăng ký
  • Các nền tảng thực thi đã đăng ký

Lý tưởng nhất là bạn nên phân tách việc phân tích cú pháp gói "bên ngoài" với việc phân tích cú pháp các gói thông thường để Package không cần đáp ứng nhu cầu của cả hai. Rất tiếc, việc này khó thực hiện vì hai khái niệm này liên quan chặt chẽ với nhau.

Nhãn, mục tiêu và quy tắc

Gói bao gồm các mục tiêu có các loại sau:

  1. Tệp: những nội dung là dữ liệu đầu vào hoặc đầu ra của bản dựng. Trong ngôn ngữ của Bazel, chúng tôi gọi các tệp này là cấu phần phần mềm (được thảo luận ở nơi khác). Không phải tất cả các tệp được tạo trong quá trình xây dựng đều là mục tiêu; thường thì đầu ra của Bazel không có nhãn liên kết.
  2. Quy tắc: mô tả các bước để lấy kết quả từ dữ liệu đầu vào. Các loại này thường liên kết với một ngôn ngữ lập trình (chẳng hạn như cc_library, java_library hoặc py_library), nhưng có một số loại không phụ thuộc vào ngôn ngữ (chẳng hạn như genrule hoặc filegroup)
  3. Nhóm gói: được thảo luận trong phần Chế độ hiển thị.

Tên của một mục tiêu được gọi là Nhãn. Cú pháp của nhãn là @repo//pac/kage:name, trong đó repo là tên của kho lưu trữ chứa Nhãn, pac/kage là thư mục chứa tệp BUILDname là đường dẫn của tệp (nếu nhãn tham chiếu đến một tệp nguồn) so với thư mục của gói. Khi tham chiếu đến một mục tiêu trên dòng lệnh, bạn có thể bỏ qua một số phần của nhãn:

  1. Nếu bạn bỏ qua kho lưu trữ, nhãn sẽ được đưa vào kho lưu trữ chính.
  2. Nếu phần gói bị bỏ qua (chẳng hạn như name hoặc :name), nhãn sẽ được đưa vào gói của thư mục đang hoạt động hiện tại (không được phép sử dụng đường dẫn tương đối chứa tham chiếu cấp cao (..))

Một loại quy tắc (chẳng hạn như "thư viện C++") được gọi là "lớp quy tắc". Các lớp quy tắc có thể được triển khai trong Starlark (hàm rule()) hoặc trong Java (còn gọi là "quy tắc gốc", loại RuleClass). Về lâu dài, mọi quy tắc dành riêng cho ngôn ngữ sẽ được triển khai trong Starlark, nhưng một số gia đình quy tắc cũ (chẳng hạn như Java hoặc C++) vẫn còn trong Java.

Bạn cần nhập các lớp quy tắc Starlark ở đầu tệp BUILD bằng câu lệnh load(), trong khi các lớp quy tắc Java được Bazel "tự nhiên" biết đến nhờ được đăng ký bằng ConfiguredRuleClassProvider.

Các lớp quy tắc chứa thông tin như:

  1. Các thuộc tính của nó (chẳng hạn như srcs, deps): loại, giá trị mặc định, quy tắc ràng buộc, v.v.
  2. Các chuyển đổi cấu hình và khía cạnh được đính kèm vào từng thuộc tính (nếu có)
  3. Việc triển khai quy tắc
  4. Trình cung cấp thông tin bắc cầu mà quy tắc "thường" tạo ra

Lưu ý về thuật ngữ: Trong cơ sở mã, chúng ta thường sử dụng "Quy tắc" để chỉ mục tiêu do một lớp quy tắc tạo. Tuy nhiên, trong Starlark và tài liệu dành cho người dùng, bạn chỉ nên sử dụng "Rule" (Quy tắc) để tham chiếu đến chính lớp quy tắc; mục tiêu chỉ là một "target" (mục tiêu). Ngoài ra, xin lưu ý rằng mặc dù RuleClass có "lớp" trong tên, nhưng không có mối quan hệ kế thừa Java nào giữa lớp quy tắc và các mục tiêu thuộc loại đó.

Skyframe

Khung đánh giá cơ bản của Bazel được gọi là Skyframe. Mô hình của công cụ này là mọi thứ cần được tạo trong một bản dựng được sắp xếp thành một biểu đồ không tuần hoàn có hướng với các cạnh trỏ từ bất kỳ phần dữ liệu nào đến các phần phụ thuộc của nó, tức là các phần dữ liệu khác cần được biết để tạo biểu đồ đó.

Các nút trong biểu đồ được gọi là SkyValue và tên của các nút này được gọi là SkyKey. Cả hai đều không thể thay đổi được; chỉ có thể truy cập các đối tượng không thể thay đổi từ các đối tượng này. Biến không đổi này hầu như luôn đúng và trong trường hợp không đúng (chẳng hạn như đối với các lớp tuỳ chọn riêng lẻ BuildOptions, là thành viên của BuildConfigurationValueSkyKey của lớp này), chúng ta cố gắng không thay đổi các lớp này hoặc chỉ thay đổi theo những cách không thể quan sát được từ bên ngoài. Do đó, mọi thứ được tính toán trong Skyframe (chẳng hạn như các mục tiêu được định cấu hình) cũng phải không thể thay đổi.

Cách thuận tiện nhất để quan sát biểu đồ Skyframe là chạy bazel dump --skyframe=deps. Thao tác này sẽ kết xuất biểu đồ, mỗi dòng là một SkyValue. Tốt nhất là bạn nên làm điều này cho các bản dựng nhỏ, vì bản dựng có thể khá lớn.

Skyframe nằm trong gói com.google.devtools.build.skyframe. Gói com.google.devtools.build.lib.skyframe có tên tương tự chứa quá trình triển khai Bazel trên Skyframe. Bạn có thể xem thêm thông tin về Skyframe tại đây.

Để đánh giá một SkyKey nhất định thành SkyValue, Skyframe sẽ gọi SkyFunction tương ứng với loại khoá. Trong quá trình đánh giá, hàm này có thể yêu cầu các phần phụ thuộc khác từ Skyframe bằng cách gọi nhiều phương thức nạp chồng của SkyFunction.Environment.getValue(). Điều này có tác dụng phụ là đăng ký các phần phụ thuộc đó vào biểu đồ nội bộ của Skyframe, nhờ đó Skyframe sẽ biết đánh giá lại hàm khi có bất kỳ phần phụ thuộc nào thay đổi. Nói cách khác, tính năng lưu vào bộ nhớ đệm và tính toán gia tăng của Skyframe hoạt động ở mức độ chi tiết của SkyFunctionSkyValue.

Bất cứ khi nào SkyFunction yêu cầu một phần phụ thuộc không có sẵn, getValue() sẽ trả về giá trị rỗng. Sau đó, hàm này sẽ trả về quyền kiểm soát cho Skyframe bằng cách tự trả về giá trị rỗng. Tại một thời điểm nào đó sau này, Skyframe sẽ đánh giá phần phụ thuộc không có sẵn, sau đó khởi động lại hàm từ đầu — chỉ lần này lệnh gọi getValue() mới thành công với kết quả không rỗng.

Hậu quả của việc này là mọi phép tính được thực hiện bên trong SkyFunction trước khi khởi động lại đều phải được lặp lại. Tuy nhiên, thời gian này không bao gồm công việc đã thực hiện để đánh giá phần phụ thuộc SkyValues được lưu vào bộ nhớ đệm. Do đó, chúng ta thường giải quyết vấn đề này bằng cách:

  1. Khai báo các phần phụ thuộc theo lô (bằng cách sử dụng getValuesAndExceptions()) để hạn chế số lần khởi động lại.
  2. Chia SkyValue thành các phần riêng biệt được tính toán bằng nhiều SkyFunction, để có thể tính toán và lưu vào bộ nhớ đệm một cách độc lập. Bạn nên thực hiện việc này một cách có chiến lược vì việc này có thể làm tăng mức sử dụng bộ nhớ.
  3. Lưu trữ trạng thái giữa các lần khởi động lại, sử dụng SkyFunction.Environment.getState() hoặc giữ bộ nhớ đệm tĩnh đặc biệt "ở phía sau Skyframe".

Về cơ bản, chúng ta cần những giải pháp này vì thường xuyên có hàng trăm nghìn nút Skyframe đang hoạt động và Java không hỗ trợ các luồng nhẹ.

Starlark

Starlark là ngôn ngữ chuyên biệt theo miền mà mọi người sử dụng để định cấu hình và mở rộng Bazel. Đây được coi là một tập hợp con bị hạn chế của Python có ít loại hơn nhiều, nhiều quy tắc hạn chế hơn về luồng điều khiển và quan trọng nhất là đảm bảo tính bất biến mạnh để cho phép đọc đồng thời. Ngôn ngữ này không hoàn chỉnh theo Turing, điều này khiến một số (nhưng không phải tất cả) người dùng không muốn cố gắng hoàn thành các nhiệm vụ lập trình chung trong ngôn ngữ này.

Starlark được triển khai trong gói net.starlark.java. Ứng dụng này cũng có cách triển khai Go độc lập tại đây. Cách triển khai Java được sử dụng trong Bazel hiện là một trình phiên dịch.

Starlark được dùng trong một số ngữ cảnh, bao gồm:

  1. Tệp BUILD. Đây là nơi xác định các mục tiêu bản dựng mới. Mã Starlark chạy trong ngữ cảnh này chỉ có quyền truy cập vào nội dung của chính tệp BUILD và các tệp .bzl do tệp này tải.
  2. Tệp MODULE.bazel. Đây là nơi xác định các phần phụ thuộc bên ngoài. Mã Starlark chạy trong ngữ cảnh này chỉ có quyền truy cập rất hạn chế vào một số lệnh được xác định trước.
  3. Tệp .bzl. Đây là nơi xác định các quy tắc bản dựng mới, quy tắc kho lưu trữ, tiện ích mô-đun. Mã Starlark tại đây có thể xác định các hàm mới và tải từ các tệp .bzl khác.

Các phương ngữ có sẵn cho tệp BUILD.bzl có chút khác biệt vì chúng thể hiện những nội dung khác nhau. Bạn có thể xem danh sách các điểm khác biệt tại đây.

Bạn có thể xem thêm thông tin về Starlark tại đây.

Giai đoạn tải/phân tích

Giai đoạn tải/phân tích là nơi Bazel xác định những hành động cần thiết để tạo một quy tắc cụ thể. Đơn vị cơ bản của nó là "mục tiêu đã định cấu hình", khá hợp lý là một cặp (mục tiêu, cấu hình).

Giai đoạn này được gọi là "giai đoạn tải/phân tích" vì có thể được chia thành hai phần riêng biệt, từng được tuần tự hoá nhưng giờ đây có thể trùng lặp theo thời gian:

  1. Tải gói, tức là chuyển các tệp BUILD thành các đối tượng Package đại diện cho các tệp đó
  2. Phân tích các mục tiêu đã định cấu hình, tức là chạy quá trình triển khai các quy tắc để tạo biểu đồ hành động

Bạn phải phân tích từng mục tiêu đã định cấu hình trong tập hợp đóng bắc cầu của các mục tiêu đã định cấu hình được yêu cầu trên dòng lệnh từ dưới lên; tức là trước tiên là các nút lá, sau đó là các nút trên dòng lệnh. Dữ liệu đầu vào cho việc phân tích một mục tiêu đã định cấu hình bao gồm:

  1. Cấu hình. ("cách" tạo quy tắc đó; ví dụ: nền tảng mục tiêu nhưng cũng bao gồm các tuỳ chọn dòng lệnh mà người dùng muốn được chuyển đến trình biên dịch C++)
  2. Các phần phụ thuộc trực tiếp. Các nhà cung cấp thông tin bắc cầu của chúng có sẵn cho quy tắc đang được phân tích. Các tệp này được gọi như vậy vì chúng cung cấp một "tệp cuộn lên" của thông tin trong tập hợp đóng bắc cầu của mục tiêu đã định cấu hình, chẳng hạn như tất cả tệp .jar trên đường dẫn lớp hoặc tất cả tệp .o cần được liên kết vào tệp nhị phân C++)
  3. Chính mục tiêu đó. Đây là kết quả của việc tải gói chứa mục tiêu. Đối với quy tắc, thuộc tính của quy tắc thường là yếu tố quan trọng.
  4. Triển khai mục tiêu đã định cấu hình. Đối với các quy tắc, mã này có thể ở dạng Starlark hoặc Java. Tất cả các mục tiêu được định cấu hình không phải quy tắc đều được triển khai trong Java.

Kết quả của việc phân tích một mục tiêu đã định cấu hình là:

  1. Các nhà cung cấp thông tin bắc cầu đã định cấu hình các mục tiêu phụ thuộc vào thông tin đó có thể truy cập
  2. Các cấu phần phần mềm mà công cụ này có thể tạo và các hành động tạo ra các cấu phần phần mềm đó.

API được cung cấp cho các quy tắc Java là RuleContext, tương đương với đối số ctx của các quy tắc Starlark. API của nó mạnh mẽ hơn, nhưng đồng thời, bạn cũng dễ dàng làm những việc xấu hơn, chẳng hạn như viết mã có độ phức tạp về thời gian hoặc không gian là bậc hai (hoặc tệ hơn), khiến máy chủ Bazel gặp sự cố với ngoại lệ Java hoặc vi phạm các hằng số không đổi (chẳng hạn như vô tình sửa đổi một thực thể Options hoặc bằng cách làm cho một mục tiêu đã định cấu hình có thể thay đổi)

Thuật toán xác định các phần phụ thuộc trực tiếp của một mục tiêu đã định cấu hình nằm trong DependencyResolver.dependentNodeMap().

Cấu hình

Cấu hình là "cách" tạo một mục tiêu: cho nền tảng nào, với tuỳ chọn dòng lệnh nào, v.v.

Bạn có thể tạo cùng một mục tiêu cho nhiều cấu hình trong cùng một bản dựng. Điều này rất hữu ích, ví dụ: khi cùng một mã được dùng cho một công cụ chạy trong quá trình xây dựng và cho mã mục tiêu, đồng thời chúng ta đang biên dịch chéo hoặc khi chúng ta đang tạo một ứng dụng Android lớn (ứng dụng chứa mã gốc cho nhiều kiến trúc CPU)

Về mặt khái niệm, cấu hình là một thực thể BuildOptions. Tuy nhiên, trong thực tế, BuildOptions được gói bằng BuildConfiguration để cung cấp thêm nhiều chức năng khác. Nó sẽ lan truyền từ đầu biểu đồ phần phụ thuộc đến cuối. Nếu giá trị này thay đổi, bạn cần phân tích lại bản dựng.

Điều này dẫn đến các trường hợp bất thường như phải phân tích lại toàn bộ bản dựng nếu, chẳng hạn như số lần chạy kiểm thử được yêu cầu thay đổi, mặc dù điều đó chỉ ảnh hưởng đến các mục tiêu kiểm thử (chúng tôi dự định "cắt bớt" các cấu hình để không xảy ra trường hợp này, nhưng chưa sẵn sàng).

Khi triển khai quy tắc cần một phần cấu hình, quy tắc đó cần khai báo phần cấu hình đó trong định nghĩa bằng cách sử dụng RuleClass.Builder.requiresConfigurationFragments(). Điều này vừa giúp tránh sai sót (chẳng hạn như quy tắc Python sử dụng mảnh Java) vừa hỗ trợ việc cắt bớt cấu hình để nếu các tuỳ chọn Python thay đổi, thì bạn không cần phân tích lại mục tiêu C++.

Cấu hình của một quy tắc không nhất thiết phải giống với cấu hình của quy tắc "mẹ". Quá trình thay đổi cấu hình trong một cạnh phần phụ thuộc được gọi là "chuyển đổi cấu hình". Điều này có thể xảy ra ở hai nơi:

  1. Trên cạnh phần phụ thuộc. Các quá trình chuyển đổi này được chỉ định trong Attribute.Builder.cfg() và là các hàm từ Rule (nơi diễn ra quá trình chuyển đổi) và BuildOptions (cấu hình ban đầu) đến một hoặc nhiều BuildOptions (cấu hình đầu ra).
  2. Trên bất kỳ cạnh nào đến một mục tiêu đã định cấu hình. Các giá trị này được chỉ định trong RuleClass.Builder.cfg().

Các lớp có liên quan là TransitionFactoryConfigurationTransition.

Các chuyển đổi cấu hình được sử dụng, ví dụ:

  1. Để khai báo rằng một phần phụ thuộc cụ thể được sử dụng trong quá trình xây dựng và do đó, phần phụ thuộc đó phải được tạo trong cấu trúc thực thi
  2. Để khai báo rằng một phần phụ thuộc cụ thể phải được tạo cho nhiều cấu trúc (chẳng hạn như cho mã gốc trong tệp APK Android lớn)

Nếu một quá trình chuyển đổi cấu hình dẫn đến nhiều cấu hình, thì quá trình đó được gọi là quá trình chuyển đổi phân tách.

Bạn cũng có thể triển khai quá trình chuyển đổi cấu hình trong Starlark (tài liệu tại đây)

Nhà cung cấp thông tin bắc cầu

Nhà cung cấp thông tin bắc cầu là một cách (và là _cách duy nhất_) để các mục tiêu đã định cấu hình cho biết thông tin về các mục tiêu đã định cấu hình khác phụ thuộc vào mục tiêu đó. Lý do tên của các lớp này có chứa từ "transitive" (mang tính bắc cầu) là vì đây thường là một số loại cuộn lên của phép đóng bắc cầu của một mục tiêu đã định cấu hình.

Thường thì có mối tương ứng 1:1 giữa trình cung cấp thông tin bắc cầu Java và trình cung cấp thông tin bắc cầu Starlark (ngoại lệ là DefaultInfo, là sự kết hợp của FileProvider, FilesToRunProviderRunfilesProvider vì API đó được coi là giống Starlark hơn là bản chuyển tự trực tiếp của API Java). Khoá của chúng là một trong những nội dung sau:

  1. Đối tượng Lớp Java. Tính năng này chỉ dành cho các nhà cung cấp không truy cập được từ Starlark. Các nhà cung cấp này là lớp con của TransitiveInfoProvider.
  2. Một chuỗi. Đây là cách cũ và không nên dùng vì dễ xảy ra xung đột tên. Những nhà cung cấp thông tin bắc cầu như vậy là các lớp con trực tiếp của build.lib.packages.Info .
  3. Biểu tượng nhà cung cấp. Bạn có thể tạo tệp này từ Starlark bằng hàm provider() và đây là cách được đề xuất để tạo trình cung cấp mới. Biểu tượng này được biểu thị bằng một thực thể Provider.Key trong Java.

Bạn nên triển khai các trình cung cấp mới được triển khai trong Java bằng BuiltinProvider. NativeProvider không còn được dùng nữa (chúng tôi chưa có thời gian xoá lớp này) và không thể truy cập vào các lớp con TransitiveInfoProvider từ Starlark.

Mục tiêu được định cấu hình

Các mục tiêu đã định cấu hình được triển khai dưới dạng RuleConfiguredTargetFactory. Có một lớp con cho mỗi lớp quy tắc được triển khai trong Java. Các mục tiêu được định cấu hình bằng Starlark được tạo thông qua StarlarkRuleConfiguredTargetUtil.buildRule() .

Các nhà máy mục tiêu được định cấu hình phải sử dụng RuleConfiguredTargetBuilder để tạo giá trị trả về. Tệp này bao gồm những thành phần sau:

  1. filesToBuild của họ, khái niệm mơ hồ về "nhóm tệp mà quy tắc này đại diện". Đây là những tệp được tạo khi mục tiêu được định cấu hình nằm trên dòng lệnh hoặc trong srcs của genrule.
  2. Tệp chạy, tệp thường và tệp dữ liệu.
  3. Nhóm đầu ra của chúng. Đây là nhiều "nhóm tệp khác" mà quy tắc có thể tạo. Bạn có thể truy cập vào các tệp này bằng cách sử dụng thuộc tính output_group của quy tắc filegroup trong BUILD và sử dụng trình cung cấp OutputGroupInfo trong Java.

Tệp chạy

Một số tệp nhị phân cần tệp dữ liệu để chạy. Một ví dụ nổi bật là các kiểm thử cần tệp đầu vào. Điều này được thể hiện trong Bazel bằng khái niệm "tệp chạy". "Cây runfiles" là cây thư mục của các tệp dữ liệu cho một tệp nhị phân cụ thể. Tệp này được tạo trong hệ thống tệp dưới dạng cây đường liên kết tượng trưng với các đường liên kết tượng trưng riêng lẻ trỏ đến các tệp trong nguồn của cây đầu ra.

Một tập hợp tệp chạy được biểu thị dưới dạng một thực thể Runfiles. Về mặt khái niệm, đây là một bản đồ từ đường dẫn của một tệp trong cây runfiles đến thực thể Artifact đại diện cho tệp đó. Cách này phức tạp hơn một chút so với một Map duy nhất vì hai lý do:

  • Trong hầu hết các trường hợp, đường dẫn runfiles của một tệp giống với execpath của tệp đó. Chúng ta sử dụng tính năng này để tiết kiệm một số RAM.
  • Có nhiều loại mục nhập cũ trong cây tệp chạy, các mục nhập này cũng cần được trình bày.

Tệp chạy được thu thập bằng RunfilesProvider: một thực thể của lớp này đại diện cho tệp chạy của một mục tiêu đã định cấu hình (chẳng hạn như thư viện) và các nhu cầu đóng vòng lặp bắc cầu của tệp chạy. Các tệp chạy này được thu thập như một tập hợp lồng nhau (thực tế, các tệp chạy này được triển khai bằng cách sử dụng các tập hợp lồng nhau trong lớp bao bọc): mỗi mục tiêu hợp nhất các tệp chạy của các phần phụ thuộc, thêm một số tệp chạy của riêng mục tiêu đó, sau đó gửi tập hợp kết quả lên trên trong biểu đồ phần phụ thuộc. Một thực thể RunfilesProvider chứa hai thực thể Runfiles, một thực thể cho trường hợp quy tắc được phụ thuộc thông qua thuộc tính "dữ liệu" và một thực thể cho mọi loại phần phụ thuộc sắp tới. Điều này là do một mục tiêu đôi khi hiển thị các tệp chạy khác nhau khi phụ thuộc vào một thuộc tính dữ liệu so với trường hợp khác. Đây là hành vi cũ không mong muốn mà chúng tôi chưa có cách để loại bỏ.

Tệp chạy của tệp nhị phân được biểu thị dưới dạng một thực thể của RunfilesSupport. Điều này khác với RunfilesRunfilesSupport có khả năng thực sự được tạo (không giống như Runfiles, chỉ là một ánh xạ). Điều này đòi hỏi các thành phần bổ sung sau:

  • Tệp kê khai tệp chạy đầu vào. Đây là nội dung mô tả tuần tự về cây runfiles. Tệp này được dùng làm proxy cho nội dung của cây tệp chạy và Bazel giả định rằng cây tệp chạy sẽ thay đổi nếu và chỉ khi nội dung của tệp kê khai thay đổi.
  • Tệp kê khai tệp chạy đầu ra. Thư viện thời gian chạy sử dụng loại này để xử lý cây tệp chạy, đặc biệt là trên Windows, đôi khi không hỗ trợ đường liên kết tượng trưng.
  • Người trung gian runfiles. Để cây tệp chạy tồn tại, bạn cần tạo cây đường liên kết tượng trưng và cấu phần phần mềm mà các đường liên kết tượng trưng trỏ đến. Để giảm số lượng cạnh phần phụ thuộc, bạn có thể sử dụng trình trung gian runfiles để biểu thị tất cả các cạnh này.
  • Đối số dòng lệnh để chạy tệp nhị phân mà đối tượng RunfilesSupport đại diện cho tệp chạy.

Khía cạnh

Phương diện là một cách để "truyền tính toán xuống biểu đồ phần phụ thuộc". Chúng được mô tả cho người dùng Bazel tại đây. Một ví dụ điển hình là vùng đệm giao thức: quy tắc proto_library không được biết về bất kỳ ngôn ngữ cụ thể nào, nhưng việc xây dựng việc triển khai thông báo vùng đệm giao thức ("đơn vị cơ bản" của vùng đệm giao thức) trong bất kỳ ngôn ngữ lập trình nào phải được ghép nối với quy tắc proto_library để nếu hai mục tiêu trong cùng một ngôn ngữ phụ thuộc vào cùng một vùng đệm giao thức, thì vùng đệm giao thức đó chỉ được tạo một lần.

Giống như các mục tiêu đã định cấu hình, các mục tiêu này được biểu thị trong Skyframe dưới dạng SkyValue và cách tạo các mục tiêu này rất giống với cách tạo các mục tiêu đã định cấu hình: các mục tiêu này có một lớp nhà máy có tên là ConfiguredAspectFactory có quyền truy cập vào RuleContext, nhưng không giống như các nhà máy mục tiêu đã định cấu hình, lớp này cũng biết về mục tiêu đã định cấu hình mà nó được đính kèm và các nhà cung cấp của mục tiêu đó.

Tập hợp các khía cạnh được truyền xuống biểu đồ phần phụ thuộc được chỉ định cho từng thuộc tính bằng cách sử dụng hàm Attribute.Builder.aspects(). Có một số lớp có tên gây nhầm lẫn tham gia vào quá trình này:

  1. AspectClass là cách triển khai tỷ lệ khung hình. Lớp này có thể ở trong Java (trong trường hợp đó là lớp con) hoặc trong Starlark (trong trường hợp đó là một thực thể của StarlarkAspectClass). Lớp này tương tự như RuleConfiguredTargetFactory.
  2. AspectDefinition là định nghĩa của khía cạnh; bao gồm các nhà cung cấp mà khía cạnh đó yêu cầu, các nhà cung cấp mà khía cạnh đó cung cấp và chứa thông tin tham chiếu đến cách triển khai khía cạnh đó, chẳng hạn như thực thể AspectClass thích hợp. Phương thức này tương tự như RuleClass.
  3. AspectParameters là một cách để tham số hoá một khía cạnh được truyền xuống biểu đồ phần phụ thuộc. Hiện tại, đây là một bản đồ chuỗi đến chuỗi. Một ví dụ điển hình về lý do tại sao vùng đệm giao thức lại hữu ích là: nếu một ngôn ngữ có nhiều API, thì thông tin về API nào cần tạo vùng đệm giao thức sẽ được truyền xuống biểu đồ phần phụ thuộc.
  4. Aspect đại diện cho tất cả dữ liệu cần thiết để tính toán một khía cạnh truyền xuống biểu đồ phần phụ thuộc. Tệp này bao gồm lớp khía cạnh, định nghĩa và tham số của lớp đó.
  5. RuleAspect là hàm xác định những khía cạnh mà một quy tắc cụ thể sẽ truyền tải. Đây là hàm Rule -> Aspect.

Một vấn đề phức tạp không mong muốn là các khía cạnh có thể đính kèm vào các khía cạnh khác; ví dụ: một khía cạnh thu thập đường dẫn lớp cho IDE Java có thể muốn biết về tất cả tệp .jar trên đường dẫn lớp, nhưng một số tệp trong số đó là vùng đệm giao thức. Trong trường hợp đó, khía cạnh IDE sẽ muốn đính kèm vào cặp (quy tắc proto_library + khía cạnh proto Java).

Mức độ phức tạp của các khía cạnh trên các khía cạnh được ghi lại trong lớp AspectCollection.

Nền tảng và chuỗi công cụ

Bazel hỗ trợ các bản dựng đa nền tảng, tức là các bản dựng có thể có nhiều cấu trúc để chạy các thao tác bản dựng và nhiều cấu trúc để tạo mã. Các cấu trúc này được gọi là nền tảng theo cách nói của Bazel (tài liệu đầy đủ tại đây)

Một nền tảng được mô tả bằng mối liên kết khoá-giá trị từ chế độ cài đặt quy tắc ràng buộc (chẳng hạn như khái niệm "cấu trúc CPU") đến giá trị quy tắc ràng buộc (chẳng hạn như một CPU cụ thể như x86_64). Chúng ta có một "từ điển" về các chế độ cài đặt và giá trị ràng buộc được sử dụng phổ biến nhất trong kho lưu trữ @platforms.

Khái niệm chuỗi công cụ xuất phát từ thực tế là tuỳ thuộc vào nền tảng mà bản dựng đang chạy và nền tảng được nhắm đến, bạn có thể cần sử dụng các trình biên dịch khác nhau; ví dụ: một chuỗi công cụ C++ cụ thể có thể chạy trên một hệ điều hành cụ thể và có thể nhắm đến một số hệ điều hành khác. Bazel phải xác định trình biên dịch C++ được sử dụng dựa trên nền tảng thực thi và nền tảng mục tiêu đã đặt (tài liệu về chuỗi công cụ tại đây).

Để làm việc này, các chuỗi công cụ được chú thích bằng tập hợp các điều kiện ràng buộc về nền tảng thực thi và mục tiêu mà chúng hỗ trợ. Để làm được việc này, định nghĩa về chuỗi công cụ được chia thành hai phần:

  1. Quy tắc toolchain() mô tả tập hợp các điều kiện ràng buộc thực thi và mục tiêu mà một chuỗi công cụ hỗ trợ, đồng thời cho biết loại chuỗi công cụ (chẳng hạn như C++ hoặc Java) (loại sau được biểu thị bằng quy tắc toolchain_type())
  2. Quy tắc dành riêng cho ngôn ngữ mô tả chuỗi công cụ thực tế (chẳng hạn như cc_toolchain())

Chúng ta thực hiện việc này vì cần biết các quy tắc ràng buộc cho mọi chuỗi công cụ để phân giải chuỗi công cụ và các quy tắc *_toolchain() dành riêng cho ngôn ngữ chứa nhiều thông tin hơn, do đó, các quy tắc này mất nhiều thời gian hơn để tải.

Bạn có thể chỉ định nền tảng thực thi theo một trong những cách sau:

  1. Trong tệp MODULE.bazel bằng hàm register_execution_platforms()
  2. Trên dòng lệnh bằng tuỳ chọn dòng lệnh --extra_execution_platforms

Tập hợp các nền tảng thực thi có sẵn được tính toán trong RegisteredExecutionPlatformsFunction .

Nền tảng mục tiêu cho một mục tiêu đã định cấu hình được xác định bằng PlatformOptions.computeTargetPlatform() . Đây là danh sách các nền tảng vì cuối cùng chúng tôi muốn hỗ trợ nhiều nền tảng mục tiêu, nhưng chưa triển khai.

Tập hợp chuỗi công cụ sẽ được sử dụng cho một mục tiêu đã định cấu hình được xác định bằng ToolchainResolutionFunction. Đây là hàm của:

  • Tập hợp các chuỗi công cụ đã đăng ký (trong tệp MODULE.bazel và cấu hình)
  • Nền tảng thực thi và nền tảng mục tiêu mong muốn (trong cấu hình)
  • Tập hợp các loại chuỗi công cụ mà mục tiêu đã định cấu hình yêu cầu (trong UnloadedToolchainContextKey)
  • Tập hợp các quy tắc ràng buộc của nền tảng thực thi của mục tiêu đã định cấu hình (thuộc tính exec_compatible_with) và cấu hình (--experimental_add_exec_constraints_to_targets) trong UnloadedToolchainContextKey

Kết quả là một UnloadedToolchainContext, về cơ bản là một bản đồ từ loại chuỗi công cụ (được biểu thị dưới dạng một thực thể ToolchainTypeInfo) đến nhãn của chuỗi công cụ đã chọn. Tệp này được gọi là "đã huỷ tải" vì không chứa các chuỗi công cụ mà chỉ chứa nhãn của các chuỗi công cụ đó.

Sau đó, các chuỗi công cụ thực sự được tải bằng ResolvedToolchainContext.load() và được sử dụng trong quá trình triển khai mục tiêu đã định cấu hình đã yêu cầu các chuỗi công cụ đó.

Chúng tôi cũng có một hệ thống cũ dựa trên việc có một cấu hình "máy chủ" duy nhất và các cấu hình mục tiêu được biểu thị bằng nhiều cờ cấu hình, chẳng hạn như --cpu . Chúng tôi đang từng bước chuyển sang hệ thống trên. Để xử lý các trường hợp mọi người dựa vào các giá trị cấu hình cũ, chúng tôi đã triển khai các ánh xạ nền tảng để dịch giữa các cờ cũ và các quy tắc ràng buộc nền tảng kiểu mới. Mã của chúng nằm trong PlatformMappingFunction và sử dụng một "ngôn ngữ nhỏ" không phải Starlark.

Giới hạn

Đôi khi, bạn chỉ muốn chỉ định một mục tiêu tương thích với một vài nền tảng. (Rất tiếc) Bazel có nhiều cơ chế để đạt được mục tiêu này:

  • Quy tắc ràng buộc theo quy tắc
  • environment_group()/environment()
  • Các quy tắc ràng buộc của nền tảng

Các quy tắc ràng buộc dành riêng cho quy tắc chủ yếu được sử dụng trong Google cho các quy tắc Java; các quy tắc này đang trong quá trình ngừng hoạt động và không có trong Bazel, nhưng mã nguồn có thể chứa các tệp tham chiếu đến quy tắc này. Thuộc tính điều chỉnh việc này được gọi là constraints= .

environment_group() và environment()

Đây là các quy tắc cũ và không được sử dụng rộng rãi.

Tất cả quy tắc bản dựng đều có thể khai báo "môi trường" mà chúng có thể được tạo, trong đó "môi trường" là một thực thể của quy tắc environment().

Có nhiều cách để chỉ định môi trường được hỗ trợ cho một quy tắc:

  1. Thông qua thuộc tính restricted_to=. Đây là hình thức thông số kỹ thuật trực tiếp nhất; thông số này khai báo chính xác tập hợp môi trường mà quy tắc hỗ trợ cho nhóm này.
  2. Thông qua thuộc tính compatible_with=. Thao tác này khai báo các môi trường mà quy tắc hỗ trợ ngoài các môi trường "tiêu chuẩn" được hỗ trợ theo mặc định.
  3. Thông qua các thuộc tính cấp gói default_restricted_to=default_compatible_with=.
  4. Thông qua thông số kỹ thuật mặc định trong quy tắc environment_group(). Mỗi môi trường thuộc về một nhóm các môi trường ngang hàng có liên quan theo chủ đề (chẳng hạn như "cấu trúc CPU", "phiên bản JDK" hoặc "hệ điều hành di động"). Định nghĩa của một nhóm môi trường bao gồm môi trường nào trong số này sẽ được hỗ trợ theo chế độ "mặc định" nếu các thuộc tính restricted_to= / environment() không chỉ định khác. Một quy tắc không có các thuộc tính như vậy sẽ kế thừa tất cả các giá trị mặc định.
  5. Thông qua lớp quy tắc mặc định. Thao tác này sẽ ghi đè các giá trị mặc định chung cho tất cả các thực thể của lớp quy tắc đã cho. Bạn có thể sử dụng tính năng này để kiểm thử tất cả quy tắc *_test mà không cần mỗi thực thể phải khai báo rõ ràng chức năng này.

environment() được triển khai dưới dạng quy tắc thông thường, trong khi environment_group() vừa là lớp con của Target nhưng không phải là Rule (EnvironmentGroup) vừa là hàm có sẵn theo mặc định từ Starlark (StarlarkLibrary.environmentGroup()) và cuối cùng tạo ra một mục tiêu cùng tên. Điều này là để tránh sự phụ thuộc tuần hoàn có thể phát sinh vì mỗi môi trường cần khai báo nhóm môi trường mà nó thuộc về và mỗi nhóm môi trường cần khai báo môi trường mặc định của nhóm đó.

Bạn có thể hạn chế một bản dựng cho một môi trường nhất định bằng tuỳ chọn dòng lệnh --target_environment.

Việc triển khai quy trình kiểm tra quy tắc ràng buộc nằm trong RuleContextConstraintSemanticsTopLevelConstraintSemantics.

Các quy tắc ràng buộc của nền tảng

Cách "chính thức" hiện tại để mô tả nền tảng mà một mục tiêu tương thích là bằng cách sử dụng các quy tắc ràng buộc giống như dùng để mô tả chuỗi công cụ và nền tảng. Yêu cầu này đang được xem xét trong yêu cầu lấy dữ liệu #10945.

Chế độ hiển thị

Nếu làm việc trên một cơ sở mã lớn với nhiều nhà phát triển (chẳng hạn như tại Google), bạn nên cẩn thận để ngăn mọi người tuỳ ý phụ thuộc vào mã của bạn. Nếu không, theo quy luật của Hyrum, mọi người sẽ dựa vào những hành vi mà bạn coi là chi tiết triển khai.

Bazel hỗ trợ việc này bằng cơ chế có tên là visibility (mức độ hiển thị): bạn có thể khai báo rằng chỉ có thể phụ thuộc vào một mục tiêu cụ thể bằng cách sử dụng thuộc tính visibility (mức độ hiển thị). Thuộc tính này có một chút đặc biệt vì mặc dù chứa danh sách nhãn, nhưng các nhãn này có thể mã hoá một mẫu trên tên gói thay vì con trỏ đến bất kỳ mục tiêu cụ thể nào. (Đúng vậy, đây là một lỗi thiết kế.)

Việc này được triển khai ở những vị trí sau:

  • Giao diện RuleVisibility thể hiện nội dung khai báo chế độ hiển thị. Đây có thể là một hằng số (hoàn toàn công khai hoặc hoàn toàn riêng tư) hoặc một danh sách nhãn.
  • Nhãn có thể tham chiếu đến các nhóm gói (danh sách gói được xác định trước), đến các gói trực tiếp (//pkg:__pkg__) hoặc các cây con của gói (//pkg:__subpackages__). Điều này khác với cú pháp dòng lệnh sử dụng //pkg:* hoặc //pkg/....
  • Các nhóm gói được triển khai dưới dạng mục tiêu riêng (PackageGroup) và mục tiêu được định cấu hình (PackageGroupConfiguredTarget). Chúng ta có thể thay thế các mục tiêu này bằng các quy tắc đơn giản nếu muốn. Logic của chúng được triển khai với sự trợ giúp của: PackageSpecification, tương ứng với một mẫu duy nhất như //pkg/...; PackageGroupContents, tương ứng với một thuộc tính packages của package_group; và PackageSpecificationProvider, tổng hợp trên một package_groupincludes bắc cầu của nó.
  • Việc chuyển đổi từ danh sách nhãn chế độ hiển thị sang phần phụ thuộc được thực hiện trong DependencyResolver.visitTargetVisibility và một số nơi khác.
  • Quá trình kiểm tra thực tế được thực hiện trong CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()

Tập hợp lồng nhau

Thông thường, một mục tiêu được định cấu hình sẽ tổng hợp một tập hợp tệp từ các phần phụ thuộc, thêm tập hợp tệp của riêng mục tiêu đó và gói tập hợp tổng hợp vào một nhà cung cấp thông tin bắc cầu để các mục tiêu được định cấu hình phụ thuộc vào mục tiêu đó cũng có thể làm như vậy. Ví dụ:

  • Tệp tiêu đề C++ dùng cho một bản dựng
  • Các tệp đối tượng đại diện cho phép đóng vi phạm của cc_library
  • Tập hợp các tệp .jar cần có trong đường dẫn lớp để một quy tắc Java có thể biên dịch hoặc chạy
  • Tập hợp các tệp Python trong tập hợp đóng bắc cầu của một quy tắc Python

Nếu thực hiện việc này theo cách đơn giản bằng cách sử dụng, ví dụ: List hoặc Set, thì chúng ta sẽ có mức sử dụng bộ nhớ bậc hai: nếu có một chuỗi gồm N quy tắc và mỗi quy tắc thêm một tệp, thì chúng ta sẽ có 1+2+...+N thành phần của tập hợp.

Để giải quyết vấn đề này, chúng tôi đã đưa ra khái niệm về NestedSet. Đây là một cấu trúc dữ liệu bao gồm các thực thể NestedSet khác và một số thành viên của chính nó, từ đó tạo thành một biểu đồ không tuần hoàn có hướng của các tập hợp. Các tập hợp này không thể thay đổi và các thành phần của tập hợp có thể được lặp lại. Chúng ta xác định nhiều thứ tự lặp lại (NestedSet.Order): thứ tự trước, thứ tự sau, thứ tự topo (một nút luôn nằm sau các nút cấp trên) và "không quan trọng, nhưng mỗi lần phải giống nhau".

Cấu trúc dữ liệu tương tự được gọi là depset trong Starlark.

Cấu phần phần mềm và Hành động

Bản dựng thực tế bao gồm một tập hợp các lệnh cần chạy để tạo ra kết quả mà người dùng muốn. Các lệnh được biểu thị dưới dạng thực thể của lớp Action và các tệp được biểu thị dưới dạng thực thể của lớp Artifact. Các hành động này được sắp xếp trong một biểu đồ hai phân đoạn, có hướng và không tuần hoàn được gọi là "biểu đồ hành động".

Cấu phần phần mềm có hai loại: cấu phần phần mềm nguồn (cấu phần phần mềm có sẵn trước khi Bazel bắt đầu thực thi) và cấu phần phần mềm phái sinh (cấu phần phần mềm cần được tạo). Bản thân cấu phần phần mềm phái sinh có thể có nhiều loại:

  1. **Cấu phần phần mềm thông thường. **Các tệp này được kiểm tra để đảm bảo mới nhất bằng cách tính toán tổng kiểm của chúng, với mtime làm lối tắt; chúng tôi không tính tổng kiểm của tệp nếu ctime của tệp đó không thay đổi.
  2. Cấu phần phần mềm đường liên kết tượng trưng chưa được phân giải. Các tệp này được kiểm tra xem có mới nhất hay không bằng cách gọi readlink(). Không giống như các cấu phần phần mềm thông thường, đây có thể là các đường liên kết tượng trưng bị treo. Thường được dùng trong trường hợp một tệp được đóng gói vào một loại tệp lưu trữ.
  3. Cấu phần phần mềm dạng cây. Đây không phải là các tệp đơn lẻ mà là cây thư mục. Các tệp này được kiểm tra để đảm bảo mới nhất bằng cách kiểm tra tập hợp các tệp trong đó và nội dung của các tệp đó. Các lớp này được biểu thị dưới dạng TreeArtifact.
  4. Cấu phần phần mềm siêu dữ liệu không đổi. Các thay đổi đối với các cấu phần phần mềm này không kích hoạt việc tạo lại. Thuộc tính này chỉ dùng cho thông tin dấu bản dựng: chúng ta không muốn tạo lại bản dựng chỉ vì thời gian hiện tại đã thay đổi.

Không có lý do cơ bản nào khiến cấu phần phần mềm nguồn không thể là cấu phần phần mềm cây hoặc cấu phần phần mềm đường liên kết tượng trưng chưa được phân giải, chỉ là chúng ta chưa triển khai cấu phần phần mềm đó (mặc dù chúng ta nên tham chiếu thư mục nguồn trong tệp BUILD là một trong số ít vấn đề không chính xác đã biết từ lâu với Bazel; chúng ta có một cách triển khai loại công việc này được bật bằng thuộc tính JVM BAZEL_TRACK_SOURCE_DIRECTORIES=1)

Một loại Artifact đáng chú ý là trung gian. Các giá trị này được biểu thị bằng các thực thể Artifact là kết quả của MiddlemanAction. Các hàm này được dùng để xử lý một số trường hợp đặc biệt:

  • Các bên trung gian tổng hợp được dùng để nhóm các cấu phần phần mềm lại với nhau. Điều này là để nếu nhiều hành động sử dụng cùng một tập hợp lớn đầu vào, chúng ta sẽ không có N*M cạnh phần phụ thuộc, chỉ có N+M (các cạnh này đang được thay thế bằng các tập hợp lồng nhau)
  • Việc lên lịch cho các phần phụ thuộc trung gian đảm bảo một hành động chạy trước một hành động khác. Các công cụ này chủ yếu được dùng để tìm lỗi mã nguồn nhưng cũng dùng để biên dịch C++ (xem CcCompilationContext.createMiddleman() để biết nội dung giải thích)
  • Trình trung gian tệp chạy được dùng để đảm bảo sự hiện diện của cây tệp chạy để không cần phụ thuộc riêng vào tệp kê khai đầu ra và mọi cấu phần phần mềm mà cây tệp chạy tham chiếu.

Bạn nên hiểu hành động là một lệnh cần chạy, môi trường cần thiết và tập hợp đầu ra mà lệnh đó tạo ra. Sau đây là các thành phần chính của nội dung mô tả một hành động:

  • Dòng lệnh cần chạy
  • Các cấu phần phần mềm đầu vào cần thiết
  • Các biến môi trường cần thiết lập
  • Chú thích mô tả môi trường (chẳng hạn như nền tảng) mà ứng dụng cần chạy trong đó \

Ngoài ra, còn có một số trường hợp đặc biệt khác, chẳng hạn như ghi một tệp có nội dung mà Bazel đã biết. Đây là lớp con của AbstractAction. Hầu hết các thao tác đều là SpawnAction hoặc StarlarkAction (tương tự, chúng không được coi là các lớp riêng biệt), mặc dù Java và C++ có các loại thao tác riêng (JavaCompileAction, CppCompileActionCppLinkAction).

Cuối cùng, chúng ta muốn chuyển mọi thứ sang SpawnAction; JavaCompileAction khá gần, nhưng C++ là một trường hợp đặc biệt do việc phân tích cú pháp tệp .d và bao gồm cả việc quét.

Biểu đồ hành động chủ yếu được "nhúng" vào biểu đồ Skyframe: về mặt khái niệm, việc thực thi một hành động được biểu thị dưới dạng lệnh gọi của ActionExecutionFunction. Việc liên kết từ cạnh phần phụ thuộc biểu đồ hành động đến cạnh phần phụ thuộc Skyframe được mô tả trong ActionExecutionFunction.getInputDeps()Artifact.key(), đồng thời có một số hoạt động tối ưu hoá để giảm thiểu số lượng cạnh Skyframe:

  • Cấu phần phần mềm phái sinh không có SkyValue riêng. Thay vào đó, Artifact.getGeneratingActionKey() được dùng để tìm khoá cho thao tác tạo khoá
  • Các tập hợp lồng nhau có khoá Skyframe riêng.

Hành động được chia sẻ

Một số hành động được tạo bởi nhiều mục tiêu được định cấu hình; các quy tắc Starlark bị hạn chế hơn vì chúng chỉ được phép đặt các hành động phái sinh vào một thư mục do cấu hình và gói của chúng xác định (nhưng ngay cả như vậy, các quy tắc trong cùng một gói có thể xung đột), nhưng các quy tắc được triển khai trong Java có thể đặt các cấu phần phần mềm phái sinh ở bất cứ đâu.

Đây được coi là một tính năng không mong muốn, nhưng việc loại bỏ tính năng này rất khó vì tính năng này giúp tiết kiệm đáng kể thời gian thực thi khi, ví dụ: một tệp nguồn cần được xử lý theo cách nào đó và tệp đó được tham chiếu bằng nhiều quy tắc (handwave-handwave). Điều này sẽ làm tiêu hao một lượng RAM: mỗi thực thể của một thao tác dùng chung cần được lưu trữ riêng trong bộ nhớ.

Nếu hai hành động tạo ra cùng một tệp đầu ra, thì các hành động đó phải giống hệt nhau: có cùng dữ liệu đầu vào, cùng dữ liệu đầu ra và chạy cùng một dòng lệnh. Mối quan hệ tương đương này được triển khai trong Actions.canBeShared() và được xác minh giữa các giai đoạn phân tích và thực thi bằng cách xem xét mọi Hành động. Việc này được triển khai trong SkyframeActionExecutor.findAndStoreArtifactConflicts() và là một trong số ít nơi trong Bazel yêu cầu chế độ xem "toàn cục" của bản dựng.

Giai đoạn thực thi

Đây là thời điểm Bazel thực sự bắt đầu chạy các hành động bản dựng, chẳng hạn như các lệnh tạo ra đầu ra.

Việc đầu tiên mà Bazel thực hiện sau giai đoạn phân tích là xác định những Cấu phần phần mềm cần được tạo. Logic cho việc này được mã hoá trong TopLevelArtifactHelper; nói một cách đơn giản, đó là filesToBuild của các mục tiêu đã định cấu hình trên dòng lệnh và nội dung của một nhóm đầu ra đặc biệt nhằm mục đích rõ ràng là thể hiện "nếu mục tiêu này nằm trên dòng lệnh, hãy tạo các cấu phần phần mềm này".

Bước tiếp theo là tạo thư mục gốc thực thi. Vì Bazel có thể đọc các gói nguồn từ nhiều vị trí trong hệ thống tệp (--package_path), nên cần cung cấp các thao tác được thực thi cục bộ bằng một cây nguồn đầy đủ. Việc này được xử lý bằng lớp SymlinkForest và hoạt động bằng cách ghi lại mọi mục tiêu được sử dụng trong giai đoạn phân tích và tạo một cây thư mục duy nhất liên kết tượng trưng mọi gói với mục tiêu đã sử dụng từ vị trí thực tế của gói đó. Một cách khác là truyền đường dẫn chính xác đến các lệnh (có tính đến --package_path). Điều này là không mong muốn vì:

  • Thay đổi dòng lệnh hành động khi một gói được di chuyển từ mục nhập đường dẫn gói sang một mục nhập khác (thường xảy ra)
  • Điều này dẫn đến các dòng lệnh khác nhau nếu một hành động được chạy từ xa so với khi chạy cục bộ
  • Phương thức này yêu cầu một phép biến đổi dòng lệnh dành riêng cho công cụ đang sử dụng (xem xét sự khác biệt giữa các đường dẫn lớp Java và đường dẫn bao gồm C++)
  • Việc thay đổi dòng lệnh của một hành động sẽ làm mất hiệu lực mục nhập bộ nhớ đệm hành động
  • --package_path đang dần ngừng hoạt động

Sau đó, Bazel bắt đầu duyệt qua biểu đồ hành động (biểu đồ hai phân đoạn, có hướng, bao gồm các hành động và cấu phần phần mềm đầu vào và đầu ra của các hành động đó) và chạy các hành động. Việc thực thi mỗi hành động được biểu thị bằng một thực thể của lớp SkyValue ActionExecutionValue.

Vì việc chạy một hành động tốn kém, nên chúng ta có một vài lớp lưu vào bộ nhớ đệm có thể được truy cập sau Skyframe:

  • ActionExecutionFunction.stateMap chứa dữ liệu để giúp Skyframe khởi động lại ActionExecutionFunction một cách tiết kiệm
  • Bộ nhớ đệm thao tác cục bộ chứa dữ liệu về trạng thái của hệ thống tệp
  • Các hệ thống thực thi từ xa thường cũng chứa bộ nhớ đệm riêng

Bộ nhớ đệm hành động cục bộ

Bộ nhớ đệm này là một lớp khác nằm sau Skyframe; ngay cả khi một hành động được thực thi lại trong Skyframe, hành động đó vẫn có thể là một lượt truy cập trong bộ nhớ đệm hành động cục bộ. Tệp này đại diện cho trạng thái của hệ thống tệp cục bộ và được chuyển đổi tuần tự sang ổ đĩa, nghĩa là khi khởi động một máy chủ Bazel mới, bạn có thể nhận được các lượt truy cập vào bộ nhớ đệm hành động cục bộ ngay cả khi biểu đồ Skyframe trống.

Bộ nhớ đệm này được kiểm tra các lượt truy cập bằng phương thức ActionCacheChecker.getTokenIfNeedToExecute() .

Trái với tên gọi, đây là một bản đồ từ đường dẫn của một cấu phần phần mềm phái sinh đến hành động đã phát ra cấu phần phần mềm đó. Hành động này được mô tả là:

  1. Tập hợp các tệp đầu vào và đầu ra cùng với tổng kiểm của các tệp đó
  2. "Khoá hành động" của nó thường là dòng lệnh đã được thực thi, nhưng nói chung, đại diện cho mọi thứ không được tổng kiểm của tệp đầu vào ghi lại (chẳng hạn như đối với FileWriteAction, đó là tổng kiểm của dữ liệu đã ghi)

Ngoài ra, còn có một "bộ nhớ đệm thao tác từ trên xuống" thử nghiệm cao vẫn đang trong quá trình phát triển. Bộ nhớ đệm này sử dụng hàm băm bắc cầu để tránh truy cập vào bộ nhớ đệm nhiều lần.

Khám phá dữ liệu đầu vào và cắt bớt dữ liệu đầu vào

Một số hành động phức tạp hơn khi chỉ có một tập hợp dữ liệu đầu vào. Các thay đổi đối với tập hợp dữ liệu đầu vào của một hành động có hai dạng:

  • Một hành động có thể khám phá dữ liệu đầu vào mới trước khi thực thi hoặc quyết định rằng một số dữ liệu đầu vào thực sự không cần thiết. Ví dụ chuẩn là C++, trong đó tốt hơn là bạn nên dự đoán những tệp tiêu đề mà tệp C++ sử dụng từ phép đóng vi mô để chúng ta không cần gửi mọi tệp đến trình thực thi từ xa; do đó, chúng ta có thể chọn không đăng ký mọi tệp tiêu đề làm "đầu vào", mà quét tệp nguồn để tìm các tiêu đề được đưa vào một cách bắc cầu và chỉ đánh dấu các tệp tiêu đề đó là đầu vào được đề cập trong câu lệnh #include (chúng ta đánh giá quá cao để không cần triển khai trình xử lý trước C đầy đủ) Tuỳ chọn này hiện được kết nối cứng với "false" trong Bazel và chỉ được sử dụng tại Google.
  • Một hành động có thể nhận ra rằng một số tệp không được sử dụng trong quá trình thực thi. Trong C++, đây được gọi là "tệp .d": trình biên dịch cho biết tệp tiêu đề nào được sử dụng sau khi thực tế và để tránh sự xấu hổ khi có mức tăng dần kém hơn Make, Bazel sử dụng thực tế này. Phương thức này đưa ra số liệu ước tính tốt hơn so với trình quét bao gồm vì nó dựa vào trình biên dịch.

Các hành động này được triển khai bằng cách sử dụng các phương thức trên Hành động:

  1. Action.discoverInputs() được gọi. Hàm này sẽ trả về một tập hợp các Cấu phần phần mềm lồng nhau được xác định là bắt buộc. Đây phải là các cấu phần phần mềm nguồn để không có cạnh phần phụ thuộc nào trong biểu đồ hành động không có cạnh tương đương trong biểu đồ mục tiêu đã định cấu hình.
  2. Thao tác này được thực thi bằng cách gọi Action.execute().
  3. Ở cuối Action.execute(), thao tác có thể gọi Action.updateInputs() để cho Bazel biết rằng không phải tất cả dữ liệu đầu vào đều cần thiết. Điều này có thể dẫn đến các bản dựng tăng dần không chính xác nếu dữ liệu đầu vào đã sử dụng được báo cáo là không sử dụng.

Khi bộ nhớ đệm hành động trả về một lượt truy cập trên một thực thể Hành động mới (chẳng hạn như được tạo sau khi khởi động lại máy chủ), Bazel sẽ tự gọi updateInputs() để tập hợp đầu vào phản ánh kết quả của việc khám phá và cắt bớt đầu vào đã thực hiện trước đó.

Các thao tác Starlark có thể sử dụng cơ sở này để khai báo một số dữ liệu đầu vào là không sử dụng bằng cách sử dụng đối số unused_inputs_list= của ctx.actions.run().

Nhiều cách để chạy hành động: Chiến lược/ActionContexts

Bạn có thể chạy một số thao tác theo nhiều cách. Ví dụ: một dòng lệnh có thể được thực thi cục bộ, cục bộ nhưng trong nhiều loại hộp cát hoặc từ xa. Khái niệm thể hiện điều này được gọi là ActionContext (hoặc Strategy, vì chúng ta chỉ thực hiện được một nửa việc đổi tên thành công…)

Vòng đời của ngữ cảnh hành động như sau:

  1. Khi bắt đầu giai đoạn thực thi, các thực thể BlazeModule sẽ được hỏi về ngữ cảnh hành động mà chúng có. Điều này xảy ra trong hàm khởi tạo của ExecutionTool. Các loại ngữ cảnh hành động được xác định bằng một thực thể Class Java tham chiếu đến một giao diện phụ của ActionContext và giao diện mà ngữ cảnh hành động phải triển khai.
  2. Ngữ cảnh hành động thích hợp được chọn trong số các ngữ cảnh có sẵn và được chuyển tiếp đến ActionExecutionContextBlazeExecutor .
  3. Các hành động yêu cầu ngữ cảnh bằng ActionExecutionContext.getContext()BlazeExecutor.getStrategy() (thực sự chỉ có một cách để thực hiện việc này…)

Các chiến lược có thể tự do gọi các chiến lược khác để thực hiện công việc của mình; ví dụ: chiến lược này được sử dụng trong chiến lược động bắt đầu các hành động cả cục bộ và từ xa, sau đó sử dụng bất kỳ hành động nào kết thúc trước.

Một chiến lược đáng chú ý là chiến lược triển khai các quy trình worker liên tục (WorkerSpawnStrategy). Ý tưởng là một số công cụ có thời gian khởi động lâu, do đó, bạn nên sử dụng lại các công cụ này giữa các thao tác thay vì bắt đầu lại một công cụ cho mỗi thao tác (Đây là vấn đề về độ chính xác tiềm ẩn, vì Bazel dựa vào lời hứa của quy trình worker rằng quy trình này không mang trạng thái có thể quan sát được giữa các yêu cầu riêng lẻ)

Nếu công cụ thay đổi, bạn cần khởi động lại quy trình worker. Việc một worker có thể được sử dụng lại hay không được xác định bằng cách tính toán tổng kiểm cho công cụ được sử dụng bằng WorkerFilesHash. Phương thức này dựa vào việc biết dữ liệu đầu vào nào của hành động đại diện cho một phần của công cụ và dữ liệu đầu vào nào đại diện cho dữ liệu đầu vào; điều này do nhà sáng tạo của Hành động xác định: Spawn.getToolFiles() và tệp chạy của Spawn được tính là một phần của công cụ.

Thông tin khác về chiến lược (hoặc ngữ cảnh hành động!):

  • Bạn có thể xem thông tin về nhiều chiến lược để chạy hành động tại đây.
  • Thông tin về chiến lược động, chiến lược mà chúng ta chạy một thao tác cả trên máy và từ xa để xem thao tác nào kết thúc trước có sẵn tại đây.
  • Bạn có thể xem thông tin về các vấn đề phức tạp khi thực thi hành động cục bộ tại đây.

Trình quản lý tài nguyên cục bộ

Bazel có thể chạy song song nhiều thao tác. Số lượng hành động cục bộ mà bạn nên chạy song song sẽ khác nhau tuỳ theo hành động: hành động càng cần nhiều tài nguyên thì càng ít thực thể chạy cùng một lúc để tránh quá tải máy cục bộ.

Điều này được triển khai trong lớp ResourceManager: mỗi hành động phải được chú thích bằng số liệu ước tính về tài nguyên cục bộ mà hành động đó cần ở dạng thực thể ResourceSet (CPU và RAM). Sau đó, khi ngữ cảnh hành động thực hiện một thao tác cần có tài nguyên cục bộ, các ngữ cảnh này sẽ gọi ResourceManager.acquireResources() và bị chặn cho đến khi có sẵn tài nguyên cần thiết.

Bạn có thể xem nội dung mô tả chi tiết hơn về việc quản lý tài nguyên cục bộ tại đây.

Cấu trúc của thư mục đầu ra

Mỗi thao tác cần có một vị trí riêng trong thư mục đầu ra để đặt đầu ra. Vị trí của các cấu phần phần mềm phái sinh thường như sau:

$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>

Tên của thư mục liên kết với một cấu hình cụ thể được xác định như thế nào? Có hai thuộc tính mong muốn xung đột với nhau:

  1. Nếu hai cấu hình có thể xuất hiện trong cùng một bản dựng, thì chúng phải có các thư mục khác nhau để cả hai có thể có phiên bản riêng của cùng một hành động; nếu không, nếu hai cấu hình không đồng ý về một hành động, chẳng hạn như dòng lệnh của một hành động tạo ra cùng một tệp đầu ra, thì Bazel sẽ không biết nên chọn hành động nào (một "xung đột hành động")
  2. Nếu hai cấu hình đại diện cho "gần như" cùng một nội dung, thì chúng phải có cùng tên để các hành động được thực thi trong một cấu hình có thể được sử dụng lại cho cấu hình còn lại nếu các dòng lệnh khớp nhau: ví dụ: các thay đổi đối với tuỳ chọn dòng lệnh cho trình biên dịch Java không được dẫn đến việc các hành động biên dịch C++ được chạy lại.

Cho đến nay, chúng tôi vẫn chưa đưa ra được cách giải quyết nguyên tắc cho vấn đề này, vấn đề này có những điểm tương đồng với vấn đề cắt bớt cấu hình. Bạn có thể xem nội dung thảo luận chi tiết hơn về các tuỳ chọn tại đây. Các vấn đề chính là các quy tắc Starlark (tác giả thường không quen thuộc với Bazel) và các khía cạnh, thêm một phương diện khác vào không gian của những thứ có thể tạo ra "cùng một" tệp đầu ra.

Phương pháp hiện tại là phân đoạn đường dẫn cho cấu hình là <CPU>-<compilation mode> với nhiều hậu tố được thêm vào để các chuyển đổi cấu hình được triển khai trong Java không dẫn đến xung đột hành động. Ngoài ra, một tổng kiểm của tập hợp các chuyển đổi cấu hình Starlark sẽ được thêm vào để người dùng không thể gây ra xung đột hành động. Nó còn rất xa mới hoàn hảo. Việc này được triển khai trong OutputDirectories.buildMnemonic() và dựa vào từng mảnh cấu hình thêm phần riêng vào tên của thư mục đầu ra.

Thử nghiệm

Bazel có nhiều tính năng hỗ trợ chạy kiểm thử. API này hỗ trợ:

  • Chạy kiểm thử từ xa (nếu có phần phụ trợ thực thi từ xa)
  • Chạy song song nhiều lần kiểm thử (để loại bỏ lỗi hoặc thu thập dữ liệu về thời gian)
  • Phân đoạn kiểm thử (phân tách các trường hợp kiểm thử trong cùng một kiểm thử trên nhiều quy trình để tăng tốc độ)
  • Chạy lại các kiểm thử không ổn định
  • Nhóm các bài kiểm thử thành bộ kiểm thử

Kiểm thử là các mục tiêu được định cấu hình thông thường có TestProvider, mô tả cách chạy kiểm thử:

  • Các cấu phần phần mềm có kết quả bản dựng trong quá trình chạy kiểm thử. Đây là tệp "trạng thái bộ nhớ đệm" chứa thông báo TestResultData đã chuyển đổi tuần tự
  • Số lần chạy kiểm thử
  • Số lượng phân đoạn mà kiểm thử sẽ được chia thành
  • Một số thông số về cách chạy kiểm thử (chẳng hạn như thời gian chờ kiểm thử)

Xác định kiểm thử nào sẽ chạy

Xác định kiểm thử nào sẽ chạy là một quá trình phức tạp.

Trước tiên, trong quá trình phân tích cú pháp mẫu mục tiêu, các bộ kiểm thử được mở rộng đệ quy. Tiện ích mở rộng được triển khai trong TestsForTargetPatternFunction. Một điều đáng ngạc nhiên là nếu một bộ kiểm thử không khai báo kiểm thử nào, thì bộ kiểm thử đó sẽ tham chiếu đến mọi kiểm thử trong gói của bộ kiểm thử đó. Việc này được triển khai trong Package.beforeBuild() bằng cách thêm một thuộc tính ngầm ẩn có tên là $implicit_tests vào các quy tắc của bộ kiểm thử.

Sau đó, các chương trình kiểm thử sẽ được lọc theo kích thước, thẻ, thời gian chờ và ngôn ngữ theo các tuỳ chọn dòng lệnh. Phương thức này được triển khai trong TestFilter và được gọi từ TargetPatternPhaseFunction.determineTests() trong quá trình phân tích cú pháp mục tiêu, kết quả được đưa vào TargetPatternPhaseValue.getTestsToRunLabels(). Lý do bạn không thể định cấu hình các thuộc tính quy tắc có thể được lọc là vì việc này xảy ra trước giai đoạn phân tích, do đó, cấu hình không có sẵn.

Sau đó, dữ liệu này được xử lý thêm trong BuildView.createResult(): các mục tiêu có phân tích không thành công sẽ bị lọc ra và các kiểm thử được chia thành kiểm thử độc quyền và không độc quyền. Sau đó, tệp này được đưa vào AnalysisResult. Đây là cách ExecutionTool biết cần chạy kiểm thử nào.

Để mang lại sự minh bạch cho quy trình phức tạp này, toán tử truy vấn tests() (được triển khai trong TestsFunction) có sẵn để cho biết kiểm thử nào sẽ chạy khi một mục tiêu cụ thể được chỉ định trên dòng lệnh. Rất tiếc, đây là một quá trình triển khai lại, vì vậy, có thể sẽ khác với nội dung trên theo nhiều cách tinh tế.

Chạy kiểm thử

Cách chạy kiểm thử là yêu cầu cấu phần phần mềm trạng thái bộ nhớ đệm. Sau đó, thao tác này sẽ dẫn đến việc thực thi TestRunnerAction, cuối cùng sẽ gọi TestActionContext do tuỳ chọn dòng lệnh --test_strategy chọn, tuỳ chọn này sẽ chạy kiểm thử theo cách được yêu cầu.

Các chương trình kiểm thử được chạy theo một giao thức phức tạp sử dụng các biến môi trường để cho các chương trình kiểm thử biết kết quả dự kiến. Bạn có thể xem nội dung mô tả chi tiết về những gì Bazel yêu cầu đối với kiểm thử và những gì kiểm thử có thể mong đợi từ Bazel tại đây. Ở mức đơn giản nhất, mã thoát là 0 có nghĩa là thành công, mọi giá trị khác có nghĩa là không thành công.

Ngoài tệp trạng thái bộ nhớ đệm, mỗi quy trình kiểm thử sẽ phát ra một số tệp khác. Các tệp này được đặt trong "thư mục nhật ký kiểm thử", là thư mục con có tên testlogs của thư mục đầu ra của cấu hình mục tiêu:

  • test.xml, một tệp XML kiểu JUnit, trình bày chi tiết từng trường hợp kiểm thử trong phân đoạn kiểm thử
  • test.log, đầu ra của bảng điều khiển của kiểm thử. stdout và stderr không được tách riêng.
  • test.outputs, "thư mục đầu ra chưa khai báo"; thư mục này được các chương trình kiểm thử sử dụng để xuất tệp ngoài những gì chúng in ra thiết bị đầu cuối.

Có hai điều có thể xảy ra trong quá trình thực thi kiểm thử mà không thể xảy ra trong quá trình tạo mục tiêu thông thường: thực thi kiểm thử độc quyền và truyền phát đầu ra.

Một số chương trình kiểm thử cần được thực thi ở chế độ độc quyền, chẳng hạn như không chạy song song với các chương trình kiểm thử khác. Bạn có thể kích hoạt điều này bằng cách thêm tags=["exclusive"] vào quy tắc kiểm thử hoặc chạy kiểm thử bằng --test_strategy=exclusive . Mỗi chương trình kiểm thử độc quyền được chạy bằng một lệnh gọi Skyframe riêng biệt yêu cầu thực thi chương trình kiểm thử sau bản dựng "chính". Việc này được triển khai trong SkyframeExecutor.runExclusiveTest().

Không giống như các thao tác thông thường, trong đó đầu ra của thiết bị đầu cuối được kết xuất khi thao tác hoàn tất, người dùng có thể yêu cầu truyền phát đầu ra của các chương trình kiểm thử để họ nhận được thông báo về tiến trình của một chương trình kiểm thử chạy trong thời gian dài. Điều này được chỉ định bằng tuỳ chọn dòng lệnh --test_output=streamed và ngụ ý việc thực thi kiểm thử độc quyền để kết quả của các kiểm thử khác nhau không bị xen kẽ.

Tính năng này được triển khai trong lớp StreamedTestOutput được đặt tên phù hợp và hoạt động bằng cách thăm dò ý kiến về các thay đổi đối với tệp test.log của kiểm thử có liên quan và kết xuất các byte mới vào thiết bị đầu cuối nơi có quy tắc Bazel.

Bạn có thể xem kết quả của các chương trình kiểm thử đã thực thi trên bus sự kiện bằng cách quan sát nhiều sự kiện (chẳng hạn như TestAttempt, TestResult hoặc TestingCompleteEvent). Các kết quả này được chuyển vào Giao thức sự kiện bản dựng và được AggregatingTestListener phát vào bảng điều khiển.

Thu thập thông tin về mức độ phù hợp

Mức độ sử dụng được báo cáo bằng các bài kiểm thử ở định dạng LCOV trong các tệp bazel-testlogs/$PACKAGE/$TARGET/coverage.dat .

Để thu thập mức độ sử dụng, mỗi lần thực thi kiểm thử được gói trong một tập lệnh có tên là collect_coverage.sh .

Tập lệnh này thiết lập môi trường kiểm thử để cho phép thu thập mức độ sử dụng và xác định vị trí(các) thời gian chạy mức độ sử dụng ghi tệp mức độ sử dụng. Sau đó, công cụ này sẽ chạy kiểm thử. Bản thân một chương trình kiểm thử có thể chạy nhiều quy trình con và bao gồm các phần được viết bằng nhiều ngôn ngữ lập trình khác nhau (với thời gian chạy thu thập phạm vi riêng biệt). Tập lệnh trình bao bọc chịu trách nhiệm chuyển đổi các tệp kết quả sang định dạng LCOV nếu cần và hợp nhất các tệp đó thành một tệp duy nhất.

Việc chèn collect_coverage.sh do các chiến lược kiểm thử thực hiện và yêu cầu collect_coverage.sh phải nằm trong dữ liệu đầu vào của kiểm thử. Việc này được thực hiện bằng thuộc tính ngầm :coverage_support được phân giải thành giá trị của cờ cấu hình --coverage_support (xem TestConfiguration.TestOptions.coverageSupport)

Một số ngôn ngữ thực hiện đo lường ngoại tuyến, nghĩa là đo lường phạm vi sử dụng được thêm vào thời gian biên dịch (chẳng hạn như C++) và một số ngôn ngữ khác thực hiện đo lường trực tuyến, nghĩa là đo lường phạm vi sử dụng được thêm vào thời gian thực thi.

Một khái niệm cốt lõi khác là mức độ phù hợp cơ sở. Đây là mức độ sử dụng của một thư viện, tệp nhị phân hoặc kiểm thử nếu không có mã nào trong đó được chạy. Vấn đề mà công cụ này giải quyết là nếu bạn muốn tính toán mức độ kiểm thử cho một tệp nhị phân, thì việc hợp nhất mức độ kiểm thử của tất cả các kiểm thử là chưa đủ vì có thể có mã trong tệp nhị phân không được liên kết với bất kỳ kiểm thử nào. Do đó, chúng ta sẽ tạo một tệp mức độ sử dụng cho mọi tệp nhị phân chỉ chứa các tệp mà chúng ta thu thập mức độ sử dụng mà không có dòng nào được bao phủ. Tệp mức độ sử dụng cơ sở cho một mục tiêu nằm ở bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat . Tệp này cũng được tạo cho các tệp nhị phân và thư viện ngoài các chương trình kiểm thử nếu bạn truyền cờ --nobuild_tests_only đến Bazel.

Mức độ sử dụng cơ sở hiện đang bị lỗi.

Chúng tôi theo dõi hai nhóm tệp để thu thập mức độ sử dụng cho mỗi quy tắc: tập hợp các tệp được đo lường và tập hợp các tệp siêu dữ liệu đo lường.

Tập hợp các tệp được đo lường chỉ là một tập hợp các tệp để đo lường. Đối với thời gian chạy phạm vi trực tuyến, bạn có thể sử dụng tính năng này trong thời gian chạy để quyết định những tệp cần đo lường. Tệp này cũng được dùng để triển khai mức độ sử dụng cơ sở.

Tập hợp các tệp siêu dữ liệu đo lường là tập hợp các tệp bổ sung mà một chương trình kiểm thử cần có để tạo các tệp LCOV mà Bazel yêu cầu. Trong thực tế, tệp này bao gồm các tệp dành riêng cho thời gian chạy; ví dụ: gcc phát ra các tệp .gcno trong quá trình biên dịch. Các hành động này được thêm vào tập hợp dữ liệu đầu vào của các hành động kiểm thử nếu chế độ phạm vi được bật.

Liệu phạm vi phủ sóng có đang được thu thập hay không sẽ được lưu trữ trong BuildConfiguration. Điều này rất hữu ích vì đây là cách dễ dàng để thay đổi hành động kiểm thử và biểu đồ hành động tuỳ thuộc vào bit này, nhưng cũng có nghĩa là nếu bit này bị lật, tất cả các mục tiêu cần được phân tích lại (một số ngôn ngữ, chẳng hạn như C++ yêu cầu các tuỳ chọn trình biên dịch khác nhau để phát mã có thể thu thập phạm vi sử dụng, điều này giúp giảm bớt vấn đề này, vì dù sao bạn cũng cần phân tích lại).

Các tệp hỗ trợ mức độ sử dụng phụ thuộc vào các nhãn trong một phần phụ thuộc ngầm ẩn để có thể được ghi đè bằng chính sách gọi, cho phép các tệp này khác nhau giữa các phiên bản Bazel. Lý tưởng nhất là các điểm khác biệt này sẽ bị xoá và chúng tôi đã chuẩn hoá một trong số đó.

Chúng ta cũng tạo một "báo cáo mức độ sử dụng" để hợp nhất mức độ sử dụng được thu thập cho mọi chương trình kiểm thử trong lệnh gọi Bazel. Việc này do CoverageReportActionFactory xử lý và được gọi từ BuildView.createResult() . Công cụ này có quyền truy cập vào các công cụ cần thiết bằng cách xem thuộc tính :coverage_report_generator của kiểm thử đầu tiên được thực thi.

Công cụ truy vấn

Bazel có một ngôn ngữ nhỏ dùng để hỏi về nhiều thứ liên quan đến các biểu đồ. Các loại truy vấn sau đây được cung cấp:

  • bazel query được dùng để điều tra biểu đồ mục tiêu
  • bazel cquery được dùng để điều tra biểu đồ mục tiêu đã định cấu hình
  • bazel aquery được dùng để điều tra biểu đồ hành động

Mỗi lớp trong số này được triển khai bằng cách tạo lớp con AbstractBlazeQueryEnvironment. Bạn có thể thực hiện các hàm truy vấn bổ sung bằng cách tạo lớp con QueryFunction. Để cho phép truyền trực tuyến kết quả truy vấn, thay vì thu thập kết quả truy vấn vào một số cấu trúc dữ liệu, query2.engine.Callback sẽ được truyền đến QueryFunction. Phương thức này sẽ gọi kết quả truy vấn mà nó muốn trả về.

Kết quả của truy vấn có thể được phát theo nhiều cách: nhãn, nhãn và lớp quy tắc, XML, protobuf, v.v. Các lớp này được triển khai dưới dạng lớp con của OutputFormatter.

Một yêu cầu tinh tế của một số định dạng đầu ra truy vấn (chắc chắn là proto) là Bazel cần phát _tất cả _thông tin mà quá trình tải gói cung cấp để người dùng có thể so sánh đầu ra và xác định xem một mục tiêu cụ thể có thay đổi hay không. Do đó, các giá trị thuộc tính cần phải có thể chuyển đổi tuần tự. Đó là lý do tại sao chỉ có một số ít loại thuộc tính không có thuộc tính nào có giá trị Starlark phức tạp. Giải pháp thông thường là sử dụng nhãn và đính kèm thông tin phức tạp vào quy tắc bằng nhãn đó. Đây không phải là giải pháp thỏa đáng và sẽ rất tuyệt nếu bạn có thể gỡ bỏ yêu cầu này.

Hệ thống mô-đun

Bạn có thể mở rộng Bazel bằng cách thêm các mô-đun vào đó. Mỗi mô-đun phải có lớp con BlazeModule (tên này là di tích của quá trình phát triển Bazel khi trước đây được gọi là Blaze) và nhận thông tin về nhiều sự kiện trong quá trình thực thi lệnh.

Các lớp này chủ yếu được dùng để triển khai nhiều phần chức năng "không phải cốt lõi" mà chỉ một số phiên bản Bazel (chẳng hạn như phiên bản chúng tôi sử dụng tại Google) cần:

  • Giao diện cho các hệ thống thực thi từ xa
  • Lệnh mới

Tập hợp các điểm mở rộng mà BlazeModule cung cấp có phần lộn xộn. Đừng sử dụng ví dụ này làm ví dụ về các nguyên tắc thiết kế tốt.

Xe buýt sự kiện

Cách chính để BlazeModules giao tiếp với phần còn lại của Bazel là thông qua một bus sự kiện (EventBus): một thực thể mới được tạo cho mỗi bản dựng, nhiều phần của Bazel có thể đăng sự kiện lên bus này và các mô-đun có thể đăng ký trình nghe cho các sự kiện mà chúng quan tâm. Ví dụ: những điều sau đây được biểu thị dưới dạng sự kiện:

  • Danh sách các mục tiêu bản dựng cần tạo đã được xác định (TargetParsingCompleteEvent)
  • Các cấu hình cấp cao nhất đã được xác định (BuildConfigurationEvent)
  • Đã tạo một mục tiêu, thành công hay không (TargetCompleteEvent)
  • Đã chạy một kiểm thử (TestAttempt, TestSummary)

Một số sự kiện trong số này được biểu thị bên ngoài Bazel trong Build Event Protocol (Giao thức sự kiện bản dựng) (chúng là BuildEvent). Điều này không chỉ cho phép BlazeModule mà còn cho phép các thành phần bên ngoài quy trình Bazel quan sát bản dựng. Bạn có thể truy cập các tệp này dưới dạng một tệp chứa thông báo giao thức hoặc Bazel có thể kết nối với một máy chủ (được gọi là Dịch vụ sự kiện bản dựng) để truyền trực tuyến sự kiện.

Việc này được triển khai trong các gói Java build.lib.buildeventservicebuild.lib.buildeventstream.

Kho lưu trữ bên ngoài

Mặc dù ban đầu Bazel được thiết kế để sử dụng trong một monorepo (một cây nguồn duy nhất chứa mọi thứ cần thiết để xây dựng), nhưng Bazel hoạt động trong một thế giới mà điều này không nhất thiết phải đúng. "Kho lưu trữ bên ngoài" là một khái niệm trừu tượng dùng để kết nối hai thế giới này: chúng đại diện cho mã cần thiết cho bản dựng nhưng không có trong cây nguồn chính.

Tệp WORKSPACE

Tập hợp các kho lưu trữ bên ngoài được xác định bằng cách phân tích cú pháp tệp WORKSPACE. Ví dụ: một nội dung khai báo như sau:

    local_repository(name="foo", path="/foo/bar")

Có kết quả trong kho lưu trữ có tên @foo. Điều phức tạp ở đây là bạn có thể xác định các quy tắc mới của kho lưu trữ trong tệp Starlark, sau đó sử dụng các quy tắc này để tải mã Starlark mới, từ đó xác định các quy tắc mới của kho lưu trữ, v.v.

Để xử lý trường hợp này, quá trình phân tích cú pháp tệp WORKSPACE (trong WorkspaceFileFunction) được chia thành các phần được xác định bằng câu lệnh load(). Chỉ mục đoạn được biểu thị bằng WorkspaceFileKey.getIndex() và tính toán WorkspaceFileFunction cho đến khi chỉ mục X có nghĩa là đánh giá chỉ mục đó cho đến câu lệnh load() thứ X.

Tìm nạp kho lưu trữ

Trước khi có thể cung cấp mã của kho lưu trữ cho Bazel, bạn cần phải tìm nạp mã đó. Điều này khiến Bazel tạo một thư mục trong $OUTPUT_BASE/external/<repository name>.

Quá trình tìm nạp kho lưu trữ diễn ra theo các bước sau:

  1. PackageLookupFunction nhận ra rằng nó cần một kho lưu trữ và tạo một RepositoryName dưới dạng SkyKey, gọi RepositoryLoaderFunction
  2. RepositoryLoaderFunction chuyển tiếp yêu cầu đến RepositoryDelegatorFunction vì lý do không rõ ràng (mã này cho biết là để tránh tải lại các nội dung trong trường hợp khởi động lại Skyframe, nhưng đây không phải là lý do vững chắc)
  3. RepositoryDelegatorFunction tìm ra quy tắc kho lưu trữ mà nó được yêu cầu tìm nạp bằng cách lặp lại các phần của tệp WORKSPACE cho đến khi tìm thấy kho lưu trữ được yêu cầu
  4. Tìm thấy RepositoryFunction thích hợp triển khai tính năng tìm nạp kho lưu trữ; đó là cách triển khai Starlark của kho lưu trữ hoặc bản đồ được mã hoá cứng cho các kho lưu trữ được triển khai trong Java.

Có nhiều lớp lưu vào bộ nhớ đệm vì việc tìm nạp kho lưu trữ có thể rất tốn kém:

  1. Có một bộ nhớ đệm cho các tệp đã tải xuống được khoá bằng tổng kiểm của tệp (RepositoryCache). Điều này yêu cầu tổng kiểm phải có trong tệp WORKSPACE, nhưng dù sao thì điều đó cũng tốt cho tính kín đáo. Mỗi thực thể máy chủ Bazel trên cùng một máy trạm đều chia sẻ thông tin này, bất kể chúng đang chạy trong không gian làm việc hay cơ sở đầu ra nào.
  2. Một "tệp điểm đánh dấu" được ghi cho mỗi kho lưu trữ trong $OUTPUT_BASE/external chứa tổng kiểm của quy tắc được dùng để tìm nạp tệp đó. Nếu máy chủ Bazel khởi động lại nhưng tổng kiểm không thay đổi, thì tổng kiểm đó sẽ không được tìm nạp lại. Việc này được triển khai trong RepositoryDelegatorFunction.DigestWriter .
  3. Tuỳ chọn dòng lệnh --distdir chỉ định một bộ nhớ đệm khác dùng để truy vấn các cấu phần phần mềm cần tải xuống. Điều này hữu ích trong chế độ cài đặt doanh nghiệp, trong đó Bazel không được tìm nạp các nội dung ngẫu nhiên trên Internet. Việc này do DownloadManager triển khai .

Sau khi tải một kho lưu trữ xuống, các cấu phần phần mềm trong kho lưu trữ đó sẽ được coi là cấu phần phần mềm nguồn. Điều này gây ra vấn đề vì Bazel thường kiểm tra tính mới nhất của các cấu phần phần mềm nguồn bằng cách gọi stat() trên các cấu phần phần mềm đó và các cấu phần phần mềm này cũng không hợp lệ khi định nghĩa của kho lưu trữ chứa các cấu phần phần mềm đó thay đổi. Do đó, các FileStateValue cho một cấu phần phần mềm trong kho lưu trữ bên ngoài cần phụ thuộc vào kho lưu trữ bên ngoài của chúng. Việc này do ExternalFilesHelper xử lý.

Ánh xạ kho lưu trữ

Có thể xảy ra trường hợp nhiều kho lưu trữ muốn phụ thuộc vào cùng một kho lưu trữ, nhưng ở các phiên bản khác nhau (đây là một thực thể của "vấn đề phần phụ thuộc kim cương"). Ví dụ: nếu hai tệp nhị phân trong các kho lưu trữ riêng biệt trong bản dựng muốn phụ thuộc vào Guava, thì cả hai tệp nhị phân đó đều sẽ tham chiếu đến Guava bằng các nhãn bắt đầu bằng @guava// và dự kiến đó sẽ là các phiên bản khác nhau của Guava.

Do đó, Bazel cho phép ánh xạ lại các nhãn kho lưu trữ bên ngoài để chuỗi @guava// có thể tham chiếu đến một kho lưu trữ Guava (chẳng hạn như @guava1//) trong kho lưu trữ của một tệp nhị phân và một kho lưu trữ Guava khác (chẳng hạn như @guava2//) là kho lưu trữ của tệp nhị phân còn lại.

Ngoài ra, bạn cũng có thể sử dụng hàm này để kết hợp các viên kim cương. Nếu một kho lưu trữ phụ thuộc vào @guava1// và một kho lưu trữ khác phụ thuộc vào @guava2//, thì tính năng liên kết kho lưu trữ cho phép liên kết lại cả hai kho lưu trữ để sử dụng kho lưu trữ @guava// chính tắc.

Việc liên kết được chỉ định trong tệp WORKSPACE dưới dạng thuộc tính repo_mapping của các định nghĩa kho lưu trữ riêng lẻ. Sau đó, thành phần này sẽ xuất hiện trong Skyframe dưới dạng một thành phần của WorkspaceFileValue, trong đó thành phần này được kết nối với:

  • Package.Builder.repositoryMapping dùng để chuyển đổi các thuộc tính có giá trị nhãn của quy tắc trong gói bằng RuleClass.populateRuleAttributeValues()
  • Package.repositoryMapping được dùng trong giai đoạn phân tích (để giải quyết các vấn đề như $(location) không được phân tích cú pháp trong giai đoạn tải)
  • BzlLoadFunction để phân giải nhãn trong câu lệnh load()

Bit JNI

Máy chủ của Bazel chủ yếu được viết bằng Java. Trường hợp ngoại lệ là những phần mà Java không thể tự làm hoặc không thể tự làm khi chúng ta triển khai. Điều này chủ yếu giới hạn ở việc tương tác với hệ thống tệp, kiểm soát quy trình và nhiều thứ cấp thấp khác.

Mã C++ nằm trong src/main/native và các lớp Java có phương thức gốc là:

  • NativePosixFilesNativePosixFileSystem
  • ProcessUtils
  • WindowsFileOperationsWindowsFileProcesses
  • com.google.devtools.build.lib.platform

Kết quả xuất ra trên bảng điều khiển

Việc phát ra đầu ra của bảng điều khiển có vẻ như là một việc đơn giản, nhưng sự kết hợp của việc chạy nhiều quy trình (đôi khi từ xa), lưu vào bộ nhớ đệm chi tiết, mong muốn có đầu ra của thiết bị đầu cuối đẹp và đầy màu sắc cũng như có máy chủ chạy trong thời gian dài khiến việc này không hề đơn giản.

Ngay sau khi lệnh gọi RPC đến từ ứng dụng, hai thực thể RpcOutputStream sẽ được tạo (dành cho stdout và stderr) để chuyển tiếp dữ liệu được in vào các thực thể đó đến ứng dụng. Sau đó, các dòng này được gói trong OutErr (một cặp (stdout, stderr)). Mọi nội dung cần in trên bảng điều khiển đều phải đi qua các luồng này. Sau đó, các luồng này được chuyển giao cho BlazeCommandDispatcher.execExclusively().

Theo mặc định, kết quả được in bằng các chuỗi thoát ANSI. Khi không mong muốn (--color=no), các thuộc tính này sẽ bị AnsiStrippingOutputStream xoá bỏ. Ngoài ra, System.outSystem.err được chuyển hướng đến các luồng đầu ra này. Điều này là để có thể in thông tin gỡ lỗi bằng cách sử dụng System.err.println() và vẫn kết thúc trong đầu ra của thiết bị đầu cuối của ứng dụng (khác với đầu ra của máy chủ). Hãy cẩn thận nếu một quy trình tạo ra đầu ra nhị phân (chẳng hạn như bazel query --output=proto), thì không có hoạt động kết hợp nào của stdout diễn ra.

Thông báo ngắn (lỗi, cảnh báo và các thông báo tương tự) được thể hiện thông qua giao diện EventHandler. Đáng chú ý là những nội dung này khác với nội dung mà người dùng đăng lên EventBus (điều này gây nhầm lẫn). Mỗi Event có một EventKind (lỗi, cảnh báo, thông tin và một số loại khác) và có thể có Location (vị trí trong mã nguồn đã gây ra sự kiện).

Một số cách triển khai EventHandler lưu trữ các sự kiện mà chúng nhận được. Phương thức này được dùng để phát lại thông tin cho giao diện người dùng do nhiều loại hoạt động xử lý được lưu vào bộ nhớ đệm gây ra, chẳng hạn như cảnh báo do mục tiêu được định cấu hình trong bộ nhớ đệm phát ra.

Một số EventHandler cũng cho phép đăng các sự kiện cuối cùng sẽ tìm thấy đường đến xe buýt sự kiện (Event thông thường _không_ xuất hiện ở đó). Đây là các cách triển khai ExtendedEventHandler và mục đích chính của các cách này là phát lại các sự kiện EventBus đã lưu vào bộ nhớ đệm. Các sự kiện EventBus này đều triển khai Postable, nhưng không phải mọi nội dung được đăng lên EventBus đều triển khai giao diện này; chỉ những nội dung được ExtendedEventHandler lưu vào bộ nhớ đệm (điều này sẽ rất tốt và hầu hết các nội dung đều thực hiện; tuy nhiên, điều này không bắt buộc)

Đầu ra của thiết bị đầu cuối chủ yếu được phát qua UiEventHandler. Thành phần này chịu trách nhiệm về tất cả định dạng đầu ra và báo cáo tiến trình mà Bazel thực hiện. Hàm này có hai đầu vào:

  • Xe buýt sự kiện
  • Luồng sự kiện được chuyển vào đó thông qua Reporter

Kết nối trực tiếp duy nhất mà cơ chế thực thi lệnh (ví dụ: phần còn lại của Bazel) có với luồng RPC đến máy khách là thông qua Reporter.getOutErr(), cho phép truy cập trực tiếp vào các luồng này. Phương thức này chỉ được dùng khi một lệnh cần kết xuất một lượng lớn dữ liệu nhị phân có thể có (chẳng hạn như bazel query).

Phân tích tài nguyên Bazel

Bazel có tốc độ nhanh. Bazel cũng chậm, vì các bản dựng có xu hướng phát triển cho đến khi chỉ còn một chút dung lượng có thể chịu đựng được. Vì lý do này, Bazel bao gồm một trình phân tích tài nguyên có thể được dùng để phân tích tài nguyên cho các bản dựng và chính Bazel. Phương thức này được triển khai trong một lớp có tên là Profiler. Tính năng này được bật theo mặc định, mặc dù chỉ ghi lại dữ liệu rút gọn để chi phí hao tổn có thể chấp nhận được; Dòng lệnh --record_full_profiler_data giúp tính năng này ghi lại mọi thứ có thể.

Trình phân tích tài nguyên này sẽ tạo một hồ sơ ở định dạng trình phân tích tài nguyên Chrome; bạn nên xem hồ sơ này trong Chrome. Mô hình dữ liệu của nó là mô hình ngăn xếp tác vụ: người dùng có thể bắt đầu và kết thúc tác vụ và các tác vụ này phải được lồng ghép gọn gàng với nhau. Mỗi luồng Java sẽ có một ngăn xếp tác vụ riêng. VIỆC CẦN LÀM: Cách hoạt động của tính năng này với các thao tác và kiểu truyền tiếp tục như thế nào?

Trình phân tích tài nguyên được khởi động và dừng tương ứng trong BlazeRuntime.initProfiler()BlazeRuntime.afterCommand(), đồng thời cố gắng hoạt động lâu nhất có thể để chúng ta có thể phân tích mọi thứ. Để thêm nội dung vào hồ sơ, hãy gọi Profiler.instance().profile(). Phương thức này trả về một Closeable, trong đó phần đóng biểu thị việc kết thúc tác vụ. Tốt nhất bạn nên sử dụng với câu lệnh try-with-resources.

Chúng ta cũng thực hiện việc phân tích bộ nhớ cơ bản trong MemoryProfiler. Chế độ này cũng luôn bật và chủ yếu ghi lại kích thước vùng nhớ khối xếp tối đa và hành vi của GC.

Kiểm thử Bazel

Bazel có hai loại kiểm thử chính: những kiểm thử quan sát Bazel dưới dạng "hộp đen" và những kiểm thử chỉ chạy giai đoạn phân tích. Chúng ta gọi loại kiểm thử trước là "kiểm thử tích hợp" và loại kiểm thử sau là "kiểm thử đơn vị", mặc dù chúng giống như kiểm thử tích hợp nhưng ít tích hợp hơn. Chúng ta cũng có một số kiểm thử đơn vị thực tế, khi cần thiết.

Có hai loại kiểm thử tích hợp:

  1. Những lệnh được triển khai bằng một khung kiểm thử bash rất phức tạp trong src/test/shell
  2. Những hàm được triển khai trong Java. Các lớp này được triển khai dưới dạng lớp con của BuildIntegrationTestCase

BuildIntegrationTestCase là khung kiểm thử tích hợp được ưu tiên vì khung này được trang bị đầy đủ cho hầu hết các trường hợp kiểm thử. Vì là một khung Java, nên khung này có khả năng gỡ lỗi và tích hợp liền mạch với nhiều công cụ phát triển phổ biến. Có nhiều ví dụ về lớp BuildIntegrationTestCase trong kho lưu trữ Bazel.

Các kiểm thử phân tích được triển khai dưới dạng lớp con của BuildViewTestCase. Có một hệ thống tệp tạm mà bạn có thể sử dụng để ghi tệp BUILD, sau đó nhiều phương thức trợ giúp có thể yêu cầu các mục tiêu đã định cấu hình, thay đổi cấu hình và xác nhận nhiều thông tin về kết quả của quá trình phân tích.