Cơ sở mã Bazel

Báo cáo vấn đề Xem nguồn Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

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

Giới thiệu

Cơ sở mã của Bazel có quy mô lớn (khoảng 350 nghìn dòng mã sản xuất và khoảng 260 nghìn dòng mã kiểm thử) và không ai nắm rõ toàn bộ: mọi người đều biết rất rõ về thung lũng cụ thể của họ, 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 trong quá trình này không bị lạc trong một khu rừng tối tăm và mất lối đi thẳng, tài liệu này cố gắng đưa ra thông tin tổng quan về cơ sở mã để bạn dễ dàng bắt đầu làm việc trên đó.

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à được lấy từ một cây nguồn nội bộ của Google có 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 thành nguồn đáng tin cậy.

Các đóng góp được chấp nhận thông qua cơ chế yêu cầu kéo thông thường của GitHub và được một nhân viên Google nhập thủ công vào cây nguồn nội bộ, sau đó xuất lại ra 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ủ vẫn nằm 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ó 2 loại tuỳ chọn: khởi động và lệnh. Trong một dòng lệnh như sau:

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

Một số lựa chọn (--host_jvm_args=) nằm trước tên của lệnh sẽ chạy và một số nằm sau (-c opt); loại trước được gọi là "lựa chọn khởi động" và ảnh hưởng đến toàn bộ quy trình máy chủ, trong khi loại sau, "lựa 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 duy nhất (tập hợ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 duy nhất. Bạn có thể khắc phục vấn đề này bằng cách chỉ định một 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 bằng C++ (là "máy khách") sẽ được kiểm soát. Thao tá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, nó sẽ làm như vậy. Đâ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 hay không: phiên bản đó đang chạy, có các lựa chọn khởi động phù hợp và sử dụng thư mục không gian làm việc phù hợp. Nó tìm thấy máy chủ đang chạy bằng cách xem thư mục $OUTPUT_BASE/server, nơi có một tệp khoá với cổng mà máy chủ đang nghe.
  3. Nếu cần, hãy huỷ 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 quá trình máy chủ phù hợp đã sẵn sàng, lệnh cần chạy sẽ được truyền đến quá trình đó thông qua giao diện gRPC, sau đó đầu ra của Bazel sẽ được chuyển ngược lại vào thiết bị đầu cuối. Mỗi lần, bạn chỉ có thể chạy một lệnh. Điều này được triển khai bằng cơ chế khoá phức tạp với các phần bằng C++ và các phần bằng Java. Có một số cơ sở hạ tầng để chạy song song nhiều lệnh, vì việc không thể chạy bazel version song song với một lệnh khác là điều khá bất tiện. Rào cản chính là vòng đời của BlazeModule và một số trạng thái trong BlazeRuntime.

Khi kết thúc một lệnh, máy chủ Bazel sẽ truyền mã thoát mà máy khách 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 thứ mà Bazel vừa tạo, nhưng lệnh này 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. Vì vậy, thay vào đó, nó cho máy khách biết nên exec() tệp nhị phân nào và với những đối số nào.

Khi người dùng nhấn tổ hợp phím Ctrl-C, ứng dụng sẽ chuyển 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 kết thúc lệnh càng sớm càng tốt. Sau lần nhấn 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ừ máy khách được xử lý bởi GrpcServerImpl.run().

Bố cục thư mục

Bazel tạo một tập hợp thư mục có phần phức tạp trong quá trình tạo. 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 đó. Thành phần này thường tương ứng với nội dung mà bạn đã kiểm tra từ hệ thống kiểm soát nguồn. Thư mục gốc của thư mục này được gọi là "thư mục gốc của không gian làm việc".

Bazel đặt tất cả dữ liệu của mình trong "thư mục gốc đầu ra của người dùng". Đây thường là $HOME/.cache/bazel/_bazel_${USER}, nhưng bạn có thể ghi đè bằng cách dùng lựa chọn khởi động --output_user_root.

"Thư mục cài đặt" là nơi Bazel được trích xuất. Thao tác này được thực hiện tự động và mỗi phiên bản Bazel sẽ 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ể thay đổi bằng cách sử dụng tuỳ chọn dòng lệnh --install_base.

"Thư mục đầu ra" là nơi mà phiên bản Bazel được đính kèm vào một không gian làm việc cụ thể sẽ 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 tại bất kỳ thời điểm nào. Thư mục này thường nằm trong $OUTPUT_USER_ROOT/<checksum of the path to the workspace>. Bạn có thể thay đổi tuỳ chọn này bằng cách sử dụng tuỳ chọn khởi động --output_base. Tuỳ chọn này rất hữu ích (trong số những việc khác) để khắc phục hạn chế rằng chỉ một phiên bản Bazel có thể chạy 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 những 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. Nơi 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 thành $EXECROOT, mặc dù đây là kế hoạch dài hạn vì đây là một thay đổi không tương thích.
  • Các tệp được tạo trong quá trình tạo.

Quy trình thực thi một 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, chuỗi sự kiện sau đây sẽ diễn ra:

  1. BlazeCommandDispatcher sẽ được thông báo về yêu cầu mới. Nó quyết định xem lệnh có cần một không gian làm việc để chạy hay không (hầu hết mọi lệnh, ngoại trừ những lệnh không liên quan đến mã nguồn, chẳng hạn như phiên bản hoặc trợ giúp) và liệu một lệnh khác 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 mẫu chống, sẽ rất tố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 lựa chọn dòng lệnh khác nhau, được mô tả trong chú thích @Command.

  4. Một bus sự kiện sẽ được tạo. Bus sự kiện là một luồng cho các sự kiện xảy ra trong quá trình tạo. Một số trong số này được xuất ra bên ngoài Bazel theo sự bảo trợ của Build Event Protocol để cho thế giới biết quá trình tạo 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: build, test, run, coverage, v.v.: chức năng này được triển khai bằng BuildTool.

  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à các 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 hiện thực hoá 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 (biểu đồ có hướng không có chu kỳ của các lệnh cần được 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à mọi thao tác cần thiết để tạo các mục tiêu cấp cao nhất được yêu cầu đều được thực hiện.

Tuỳ chọn dòng lệnh

Các lựa chọn dòng lệnh cho một lệnh gọi Bazel được mô tả trong đối tượng OptionsParsingResult. Đối tượng này lần lượt chứa một bản đồ từ "option classes" (các lớp lựa chọn) đến các giá trị của các lựa 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 với nhau có liên quan đến nhau. Ví dụ:

  1. Các lựa 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 bao bọc trong một đối tượng BuildOptions.
  2. Các lựa chọn liên quan đến cách Bazel thực thi các thao tác (ExecutionOptions)

Các lựa 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ố này (ví dụ: có quét C++ include hay không) được đọc trong giai đoạn thực thi, nhưng điều đó luôn đòi hỏi phải có đường dẫn rõ ràng vì BuildConfiguration không có sẵn vào thời điểm đó. Để biết thêm thông tin, hãy xem phần "Cấu hình".

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

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

  1. Một số được cố định vào Bazel (CommonCommandOptions)
  2. Từ chú thích @Command trên mỗi lệnh Bazel
  3. Từ ConfiguredRuleClassProvider (đây là các lựa 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 lựa chọn riêng (xem tại đây)

Mỗi lựa chọn (ngoại trừ các lựa chọn do Starlark xác định) là một biến thành phần của lớp con FragmentOptions có chú thích @Option, trong đó chỉ định tên và loại của lựa 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ị của một tuỳ chọn dòng lệnh thường là một thứ gì đó đơn giản (một chuỗi, một số nguyên, một giá trị Boolean, một nhãn, v.v.). Tuy nhiên, chúng tôi cũng hỗ trợ các lựa chọn thuộc loại phức tạp hơn; trong trường hợp này, nhiệm vụ chuyển đổi từ chuỗi dòng lệnh sang loại dữ liệu sẽ thuộc về một cách triển khai com.google.devtools.common.options.Converter.

Cây nguồn, theo Bazel

Bazel có nhiệm vụ tạo phần mềm bằng cách đọc và diễn giải mã nguồn. Toàn bộ mã nguồn mà Bazel hoạt động được gọi là "không gian làm việc" và được cấu trúc thành các 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 trên đó; kho lưu trữ 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, tức 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 rộng 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á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 một tệp ranh giới kho lưu trữ (MODULE.bazel, REPO.bazel hoặc trong các bối cảnh cũ, WORKSPACE hoặc WORKSPACE.bazel) trong thư mục gốc. 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ề các 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 được thực hiện bằng SymlinkForest, giúp 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 liên quan và một quy cách về 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 khiến các tệp BUILD vẫn được chấp nhận là vì tổ tiên của Bazel, Blaze, đã sử dụng tên tệp này. Tuy nhiên, đây lại là một phân đ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: những thay đổi đối với tệp BUILD của một gói không thể khiến các gói khác thay đổi. 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 đệ quy.

Việc đánh giá một tệp BUILD được gọi là "tải gói". Phương thức 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 có sẵn. Kết quả của việc 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ột mục tiêu) đến chính mục tiêu đó.

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

Globbing được triển khai trong các lớp sau:

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

Bản thân lớp Package chứa một số thành phần chỉ được 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 có ý nghĩa đối 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ả các gói thông thường không được chứa các trường mô tả một thứ khác. bao gồm:

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

Lý tưởng nhất là nên tách riêng 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, điều này khó thực hiện vì hai yếu tố này có mối liên hệ khá sâu sắc.

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

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

  1. Tệp: những thứ là đầu vào hoặc đầu ra của bản dựng. Theo thuật ngữ Bazel, chúng tôi gọi chúng là hiện vật (đượ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 tạo đều là mục tiêu; thông thường, đầu ra của Bazel sẽ không có nhãn được liên kết.
  2. Quy tắc: mô tả các bước để lấy đầu ra từ đầu vào. Chúng thường được 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ố ngôn ngữ độc lập (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ữ mà Nhãn nằm trong đó, pac/kage là thư mục chứa tệp BUILDname là đường dẫn của tệp (nếu nhãn đề cập đến một tệp nguồn) tương ứng với thư mục của gói. Khi đề cập đế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 coi là nằm trong kho lưu trữ chính.
  2. Nếu bạn bỏ qua phần gói (chẳng hạn như name hoặc :name), nhãn sẽ được coi là nằm trong 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 có chứa các tham chiếu cấp trên (..))

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ố họ quy tắc cũ (chẳng hạn như Java hoặc C++) vẫn ở trong Java trong thời gian này.

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

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

  1. Các thuộc tính của thành phần (chẳng hạn như srcs, deps): các loại, giá trị mặc định, ràng buộc, v.v.
  2. Các khía cạnh và quá trình chuyển đổi cấu hình được đính kèm với từng thuộc tính (nếu có)
  3. Việc triển khai quy tắc
  4. Các 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 dùng "Quy tắc" để chỉ mục tiêu do một lớp quy tắc tạo ra. Nhưng trong Starlark và trong tài liệu dành cho người dùng, "Quy tắc" chỉ được dùng để đề cập đến chính lớp quy tắc; mục tiêu chỉ là "mục tiêu". Cũng xin lưu ý rằng mặc dù RuleClass có "class" trong tên, nhưng không có mối quan hệ kế thừa Java giữa một 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 nó là mọi thứ cần được tạo trong quá trình tạo đều được sắp xếp thành một biểu đồ có hướng không chu kỳ với các cạnh trỏ từ mọi phần dữ liệu đến các phần phụ thuộc của nó, tức là những phần dữ liệu khác cần được biết để tạo ra phần dữ liệu đó.

Các nút trong biểu đồ được gọi là SkyValue và tên của các nút được gọi là SkyKey. Cả hai đều không thể thay đổi sâu; chỉ các đối tượng không thể thay đổi mới có thể truy cập được từ các đối tượng này. Bất biến này hầu như luôn được duy trì và trong trường hợp không duy trì được (chẳng hạn như đối với các lớp lựa chọn riêng lẻ BuildOptions, là một thành phần của BuildConfigurationValueSkyKey của lớp này), chúng tôi sẽ cố gắng hết sức để không thay đổi hoặc chỉ thay đổi theo những cách không thể quan sát được từ bên ngoài. Từ đó, 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 là bất biến.

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 SkyValue trên một dòng. Tốt nhất là bạn nên thực hiện việc này cho các bản dựng nhỏ, vì tệp có thể khá lớn.

Skyframe nằm trong gói com.google.devtools.build.skyframe. Gói có tên tương tự com.google.devtools.build.lib.skyframe 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 một SkyValue, Skyframe sẽ gọi SkyFunction tương ứng với loại khoá. Trong quá trình đánh giá hàm, 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 hàm 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, để Skyframe biết cách đánh giá lại hàm khi có bất kỳ phần phụ thuộc nào của hàm 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 sẽ tự động trả về giá trị rỗng để nhường quyền kiểm soát lại cho Skyframe. Vào 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() sẽ thành công với kết quả khác rỗng.

Hậu quả của việc này là mọi hoạt động tính toán đượ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, điều này không bao gồm công việc được thực hiện để đánh giá SkyValues phụ thuộc (được lưu vào bộ nhớ đệm). Do đó, chúng tôi 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()) để giới hạn số lần khởi động lại.
  2. Chia một SkyValue thành các phần riêng biệt do các SkyFunction khác nhau tính toán, để chúng có thể được 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ì nó có khả năng 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, bằng cách sử dụng SkyFunction.Environment.getState() hoặc giữ bộ nhớ đệm tĩnh đặc biệt "phía sau Skyframe". Với SkyFunctions phức tạp, việc quản lý trạng thái giữa các lần khởi động lại có thể trở nên khó khăn, vì vậy, StateMachines đã được giới thiệu để có một phương pháp có cấu trúc cho tính đồng thời logic, bao gồm cả các lệnh gọi để tạm ngưng và tiếp tục các phép tính phân cấp trong một SkyFunction. Ví dụ: DependencyResolver#computeDependencies sử dụng StateMachine với getState() để tính toán tập hợp có thể rất lớn gồm 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ếu không, điều này có thể dẫn đến việc khởi động lại tốn kém.

Về cơ bản, Bazel cần những giải pháp này vì hàng trăm nghìn nút Skyframe đang hoạt động là điều phổ biến và khả năng hỗ trợ các luồng siêu nhẹ của Java không vượt trội hơn việc triển khai StateMachine tính đến năm 2023.

Starlark

Starlark là ngôn ngữ dành riêng cho miền mà mọi người dùng để định cấu hình và mở rộng Bazel. Đây là một tập hợp con bị hạn chế của Python, có ít loại hơn, nhiều 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 mẽ để cho phép các lượt đọ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 tác vụ lập trình chung trong ngôn ngữ này.

Starlark được triển khai trong gói net.starlark.java. Thư viện này cũng có một quy trình triển khai Go độc lập tại đây. Quy trình triển khai Java được dùng trong Bazel hiện là một trình thông dịch.

Starlark được dùng trong nhiều ngữ cảnh, bao gồm:

  1. BUILD tệp. Đâ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 các phần phụ thuộc bên ngoài được xác định. Mã Starlark chạy trong bối cảnh này chỉ có quyền truy cập rất hạn chế vào một số chỉ thị được xác định trước.
  3. .bzl tệp. Đâ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 ở đâ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ó trong tệp BUILD.bzl hơi khác nhau vì chúng thể hiện những điều 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à giai đoạn 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 được định cấu hình", tức là một cặp (mục tiêu, cấu hình).

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

  1. Tải các 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 ra đồ thị hành động

Mỗi mục tiêu được định cấu hình trong bao đóng bắc cầu của các mục tiêu được định cấu hình được yêu cầu trên dòng lệnh phải được phân tích từ dưới lên; tức là các nút lá trước, sau đó lên đến các nút trên dòng lệnh. Đầu vào cho hoạt động phân tích một mục tiêu được định cấu hình duy nhất là:

  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 có những thứ như các lựa chọn dòng lệnh mà người dùng muốn được truyề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 họ có sẵn cho quy tắc đang được phân tích. Chúng được gọi như vậy vì chúng cung cấp một "bản tổng hợp" thông tin trong bao đóng bắc cầu của mục tiêu đã định cấu hình, chẳng hạn như tất cả các tệp .jar trên đường dẫn lớp hoặc tất cả các tệp .o cần được liên kết vào một 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 mà đích đến nằm trong đó. Đối với các quy tắc, điều này bao gồm cả các thuộc tính của quy tắc, thường là những gì quan trọng.
  4. Việc triển khai mục tiêu đã định cấu hình. Đối với các quy tắc, bạn có thể sử dụng Starlark hoặc Java. Tất cả các mục tiêu không được định cấu hình theo quy tắc đều được triển khai bằng 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à nó có thể tạo và những thao tác 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 này mạnh mẽ hơn, nhưng đồng thời, bạn cũng dễ dàng làm Điều xấu™, 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 một ngoại lệ Java hoặc vi phạm các bất biến (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ục tiêu được đị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 được định cấu hình nằm trong DependencyResolver.dependentNodeMap().

Cấu hình

Cấu hình là "cách thức" để tạo một mục tiêu: cho nền tảng nào, với những lựa 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 hữu ích, chẳng hạn như khi cùng một mã được dùng cho một công cụ chạy trong quá trình tạo 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 này là một thực thể BuildOptions. Tuy nhiên, trên thực tế, BuildOptions được bao bọc bởi BuildConfiguration, cung cấp thêm nhiều chức năng khác. Thao tác này lan truyền từ đầu biểu đồ phần phụ thuộc xuống cuối. Nếu thay đổi, bản dựng cần được phân tích lại.

Điều này dẫn đến những điểm 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ượng lần chạy 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 có kế hoạch "cắt" các cấu hình để điều này không xảy ra, nhưng kế hoạch này chưa sẵn sàng).

Khi cần một phần cấu hình, quá trình triển khai quy tắc cần khai báo 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ư các quy tắc Python sử dụng đoạn mã Java) vừa giúp việc cắt tỉa cấu hình trở nên dễ dàng hơn để nếu các lựa chọn Python thay đổi, các mục tiêu C++ không cần phải được phân tích lại.

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ụ 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 một cạnh phụ thuộc. Các hiệu ứng chuyển đổi này được chỉ định trong Attribute.Builder.cfg() và là các hàm từ Rule (nơi hiệu ứng chuyển đổi diễn ra) 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 mọi cạnh đến của một mục tiêu được đị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.

Ví dụ: các hiệu ứng chuyển đổi cấu hình được dùng cho:

  1. Để khai báo rằng một phần phụ thuộc cụ thể được dùng trong quá trình tạo 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ư mã gốc trong các 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ì đó được gọi là quá trình chuyển đổi phân chia.

Bạn cũng có thể triển khai các 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 về phương tiện công cộng

Trì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 được định cấu hình tìm hiểu thông tin về những mục tiêu được định cấu hình khác mà chúng phụ thuộc vào, đồng thời là cách duy nhất để cho những mục tiêu được định cấu hình khác biết thông tin về chính chúng (những mục tiêu phụ thuộc vào chúng). Lý do khiến "transitive" xuất hiện trong tên của các mục tiêu này là vì đây thường là một loại tổng hợp nào đó của bao đóng bắc cầu của một mục tiêu đã định cấu hình.

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

  1. Một đối tượng Lớp Java. Điều này chỉ áp dụng cho những nhà cung cấp không truy cập được từ Starlark. Các nhà cung cấp này là một lớp con của TransitiveInfoProvider.
  2. Một chuỗi. Đây là phương thức 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 của nhà cung cấp. Bạn có thể tạo đối tượng này từ Starlark bằng hàm provider() và đây là cách nên dùng để tạo nhà cung cấp mới. Ký hiệu này được biểu thị bằng một thực thể Provider.Key trong Java.

Các nhà cung cấp mới được triển khai trong Java phải được triển khai bằng BuiltinProvider. NativeProvider không được dùng nữa (chúng tôi chưa có thời gian để xoá thuộc tính này) và bạn 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 được định cấu hình sẽ đượ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 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ề. Nội dung này bao gồm những thành phần sau:

  1. filesToBuild của chúng, khái niệm mơ hồ về "tập hợp các 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 một genrule.
  2. Runfile của chúng, thông thường và dữ liệu.
  3. Nhóm đầu ra của họ. Đâ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.

Runfiles

Một số tệp nhị phân cần có tệp dữ liệu để chạy. Một ví dụ điển hình 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 "runfiles". "Cây runfiles" là một cây thư mục của các tệp dữ liệu cho một tệp nhị phân cụ thể. Thư mục này được tạo trong hệ thống tệp dưới dạng một cây symlink với các symlink riêng lẻ trỏ đến các tệp trong cây nguồn hoặc cây đầu ra.

Một tập hợp runfile được biểu thị dưới dạng một thực thể Runfiles. Về cơ bản, đâ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 đó. Việc này phức tạp hơn một chút so với một Map vì 2 lý do:

  • Thông thường, đường dẫn runfiles của một tệp sẽ giống với execpath của tệp đó. Chúng tôi sử dụng tính năng này để tiết kiệm một phần RAM.
  • Có nhiều loại mục nhập cũ trong cây runfiles, bạn cũng cần phải biểu thị.

Runfile được thu thập bằng cách sử dụng RunfilesProvider: một phiên bản của lớp này đại diện cho runfile mà một mục tiêu đã định cấu hình (chẳng hạn như một thư viện) và bao đóng bắc cầu của nó cần và chúng được thu thập như một tập hợp lồng nhau (trên thực tế, chúng được triển khai bằng cách sử dụng các tập hợp lồng nhau): mỗi mục tiêu hợp nhất runfile của các phần phụ thuộc, thêm một số phần phụ thuộc của riêng nó, 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 2 thực thể Runfiles, một thực thể cho trường hợp quy tắc phụ thuộc thông qua thuộc tính "data" và một thực thể cho mọi loại phần phụ thuộc đến khác. Lý do là vì đôi khi một mục tiêu sẽ trình bày các tệp thực thi khác nhau khi phụ thuộc vào thông qua một thuộc tính dữ liệu so với các trường hợp khác. Đây là hành vi không mong muốn của phiên bản cũ mà chúng tôi chưa loại bỏ được.

Runfile của các tệp nhị phân được biểu thị dưới dạng một phiên bản 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 mối liên kết). Điều này đòi hỏi các thành phần bổ sung sau:

  • Tệp kê khai runfile đầu vào. Đây là nội dung mô tả được chuyển đổi tuần tự của cây runfiles. Thư mục này được dùng làm proxy cho nội dung của cây runfiles và Bazel giả định rằng cây runfiles sẽ thay đổi nếu và chỉ khi nội dung của tệp kê khai thay đổi.
  • Tệp kê khai runfile đầu ra. Thư viện này được dùng bởi các thư viện thời gian chạy xử lý cây runfile, đặc biệt là trên Windows. Đôi khi, Windows không hỗ trợ các đường liên kết tượng trưng.
  • Đố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

Khía cạnh là một cách để "truyền tính toán xuống biểu đồ phần phụ thuộc". Các quy tắc này được mô tả cho người dùng Bazel tại đây. Một ví dụ hay về động lực là bộ đệ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 tạo bản triển khai thông báo bộ đệm giao thức ("đơn vị cơ bản" của bộ đệm giao thức) bằng bất kỳ ngôn ngữ lập trình nào phải được liên kết 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 bộ đệm giao thức, thì mục tiêu đó chỉ được tạo một lần.

Giống như các mục tiêu đã định cấu hình, chúng được biểu thị trong Skyframe dưới dạng SkyValue và cách chúng được tạo rất giống với cách các mục tiêu đã định cấu hình được tạo: chúng 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, nó 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 quy trình này:

  1. AspectClass là cách triển khai khía cạnh. Đây có thể là trong Java (trong trường hợp đó, đây là một lớp con) hoặc trong Starlark (trong trường hợp đó, đây là một thực thể của StarlarkAspectClass). Nó tương tự như RuleConfiguredTargetFactory.
  2. AspectDefinition là định nghĩa về khía cạnh; nó bao gồm các trình cung cấp mà khía cạnh đó yêu cầu, các trình cung cấp mà khía cạnh đó cung cấp và chứa một thông tin tham chiếu đến việc triển khai của khía cạnh đó, chẳng hạn như thực thể AspectClass thích hợp. Nó 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. Đây hiện là một bản đồ chuỗi đến chuỗi. Một ví dụ điển hình về lý do khiến việc này hữu ích là bộ đệm giao thức: nếu một ngôn ngữ có nhiều API, thì thông tin về API mà bộ đệm giao thức cần được tạo 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. Nó bao gồm lớp khía cạnh, định nghĩa và các 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 điểm phức tạp có phần bất ngờ là các khía cạnh có thể gắn với 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 một IDE Java có thể sẽ muốn biết về tất cả các tệp .jar trên đường dẫn lớp, nhưng một số tệp trong số đó là các 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).

Độ 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 nơi các thao tác xây dựng chạy và nhiều cấu trúc mà mã được xây dựng. Các cấu trúc này được gọi là nền tảng trong thuật ngữ Bazel (tài liệu đầy đủ tại đây)

Nền tảng được mô tả bằng một bản đồ khoá-giá trị từ chế độ cài đặt ràng buộc (chẳng hạn như khái niệm "cấu trúc CPU") đến giá trị ràng buộc (chẳng hạn như một CPU cụ thể như x86_64). Chúng tôi có một "từ điển" gồm các chế độ cài đặt và giá trị ràng buộc thường dùng nhất trong kho lưu trữ @platforms.

Khái niệm về 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, người ta 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 dùng dựa trên quá trình 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 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 điều này, định nghĩa về một chuỗi công cụ được chia thành hai phần:

  1. Một quy tắc toolchain() mô tả tập hợp các ràng buộc về việ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. Một 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 tôi thực hiện theo cách này vì cần biết các ràng buộc cho mọi chuỗi công cụ để thực hiện việ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 đó, chúng mất nhiều thời gian hơn để tải.

Nền tảng thực thi được chỉ định 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 cách sử dụng lựa 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 đích cho một đích được đị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 các chuỗi công cụ sẽ được dùng cho một mục tiêu đã định cấu hình do ToolchainResolutionFunction xác định. Đây là một 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 được định cấu hình yêu cầu (trong UnloadedToolchainContextKey)
  • Tập hợp các ràng buộc của nền tảng thực thi của mục tiêu được đị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ả của thao tác này 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. Thư mục này được gọi là "unloaded" (chưa tải) vì không chứa chính các chuỗi công cụ mà chỉ chứa nhãn của chúng.

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

Chúng tôi cũng có một hệ thống cũ dựa vào một cấu hình "máy chủ lưu trữ" 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 dần chuyển sang hệ thống nêu 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 ràng buộc nền tảng theo kiểu mới. Mã của họ nằm trong PlatformMappingFunction và sử dụng "ngôn ngữ nhỏ" không phải Starlark.

Giới hạn

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

  • Các hạn chế dành riêng cho quy tắc
  • environment_group()/environment()
  • Các quy tắc ràng buộc về nền tảng

Các ràng buộc dành riêng cho quy tắc chủ yếu được dùng trong các quy tắc của Google cho Java; chúng sắp hết hạn và không có trong Bazel, nhưng mã nguồn có thể chứa các thông tin tham chiếu đến ràng buộc này. Thuộc tính chi phối việc này được gọi là constraints= .

environment_group() và environment()

Đây là một cơ chế cũ và không được sử dụng rộng rãi.

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

Có nhiều cách để chỉ định các 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 đặc tả trực tiếp nhất; nó khai báo chính xác tập hợp các môi trường mà quy tắc hỗ trợ.
  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à một 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 các quy cách mặc định trong quy tắc environment_group(). Mọi môi trường đều thuộc một nhóm các thành phần 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 về một nhóm môi trường bao gồm những môi trường nào trong số này sẽ được "mặc định" hỗ trợ nếu không được chỉ định bằng các thuộc tính restricted_to= / environment(). 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 chế độ mặc định của lớp quy tắc. Thao tác này sẽ ghi đè các giá trị mặc định chung cho tất cả các phiên bản của lớp quy tắc đã cho. Ví dụ: bạn có thể dùng cách này để kiểm thử tất cả các quy tắc *_test mà không cần từng thực thể phải khai báo rõ ràng khả năng này.

environment() được triển khai dưới dạng một quy tắc thông thường, trong khi environment_group() vừa là một lớp con của Target nhưng không phải Rule (EnvironmentGroup) vừa là một hàm có sẵn theo mặc định từ Starlark (StarlarkLibrary.environmentGroup()) và cuối cùng sẽ tạo ra một mục tiêu cùng tên. Điều này nhằm tránh sự phụ thuộc theo chu kỳ sẽ 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 các môi trường mặc định của nhóm đó.

Bạn có thể hạn chế bản dựng trong 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 ràng buộc nằm trong RuleContextConstraintSemanticsTopLevelConstraintSemantics.

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

Cách "chính thức" hiện tại để mô tả những 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 ràng buộc tương tự được dùng để mô tả chuỗi công cụ và nền tảng. Tính năng này được triển khai trong yêu cầu kéo #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 (như tại Google), bạn cần phải cẩn thận để ngăn mọi người khác 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ợ điều này bằng cơ chế được gọi là khả năng hiển thị: bạn có thể giới hạn những mục tiêu 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 khả năng hiển thị. Thuộc tính này hơi đặc biệt vì mặc dù chứa một 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ì một con trỏ đến bất kỳ mục tiêu cụ thể nào. (Có, đây là một lỗi thiết kế.)

Điều này được triển khai ở những nơi sau:

  • Giao diện RuleVisibility thể hiện một khai báo về khả năng hiển thị. Đó 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ể đề cập đến các nhóm gói (danh sách gói được xác định trước), trực tiếp đến các gói (//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 nhóm gói này bằng các quy tắc đơn giản nếu muốn. Logic của các thành phần này đượ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 duy nhất của package_group; và PackageSpecificationProvider, tổng hợp trên package_groupincludes bắc cầu của thành phần này.
  • Việc chuyển đổi từ danh sách nhãn hiển thị sang các 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 các tệp từ các phần phụ thuộc của nó, thêm các tệp của riêng nó và gói tập hợp tổng hợp vào một trì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 nó có thể làm điều tương tự. Ví dụ:

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

Nếu làm theo cách đơn giản bằng cách sử dụng List hoặc Set, chẳng hạn, thì chúng ta sẽ có mức sử dụng bộ nhớ bậc hai: nếu có 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 viên trong bộ sưu tậ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 phần riêng, do đó tạo thành một đồ thị có hướng không chu trình của các tập hợp. Chúng là bất biến và các thành viên của chúng có thể được lặp lại. Chúng tôi xác định nhiều thứ tự lặp lại (NestedSet.Order): thứ tự trước, thứ tự sau, thứ tự theo cấu trúc liên kết (một nút luôn xuất hiện sau các nút tổ tiê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à thao tác

Bản dựng thực tế bao gồm một tập hợp các lệnh cần được chạy để tạo ra đầu ra mà người dùng muốn. Các lệnh được biểu thị dưới dạng các thực thể của lớp Action và các tệp được biểu thị dưới dạng các thực thể của lớp Artifact. Chúng được sắp xếp trong một đồ thị hai phần, có hướng và không chu trình, được gọi là "đồ thị 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 (những 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 (những cấu phần phần mềm cần được tạo). Bản sao phái sinh có thể có nhiều loại:

  1. Cấu phần phần mềm thông thường. Chúng được kiểm tra tính cập nhật bằng cách tính tổng kiểm của chúng, với mtime là một lối tắt; chúng tôi không tính tổng kiểm cho tệp nếu ctime của tệp không thay đổi.
  2. Các cấu phần phần mềm liên kết tượng trưng chưa được giải quyết. Các tệp này được kiểm tra để đảm bảo luôn mới nhất 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, các tệp này có thể là các symlink không hợp lệ. Thường được dùng trong trường hợp người dùng đóng gói một số tệp vào một loại tệp lưu trữ nào đó.
  3. Cấu phần phần mềm cây. Đây không phải là các tệp riêng lẻ mà là các cây thư mục. Chúng được kiểm tra để đảm bảo luôn mới bằng cách kiểm tra bộ tệp trong đó và nội dung của chúng. Chúng được biểu thị bằng TreeArtifact.
  4. Cấu phần phần mềm siêu dữ liệu không đổi. Những thay đổi đối với các cấu phần phần mềm này không kích hoạt quá trình tạo lại. Điều này chỉ được dùng cho thông tin về dấu thời gian của bản dựng: chúng ta không muốn thực hiện quy trình tạo lại chỉ vì thời gian hiện tại thay đổi.

Không có lý do cơ bản nào khiến các 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 symlink chưa được phân giải, chỉ là chúng tôi chưa triển khai (mặc dù chúng tôi nên triển khai – việc tham chiếu một 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 tôi có một cách triển khai hoạt động theo kiểu được bật bằng thuộc tính JVM BAZEL_TRACK_SOURCE_DIRECTORIES=1)

Bạn nên hiểu rõ các thao tác như một lệnh cần được chạy, môi trường cần thiết và tập hợp các đầu ra mà thao tác đó tạo ra. Sau đây là các thành phần chính của nội dung mô tả về 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 được 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 \

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à một lớp con của AbstractAction. Hầu hết các thao tác đều là SpawnAction hoặc StarlarkAction (giống nhau, có thể chúng không phải 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 tôi 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à quét tệp include.

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 một lệnh gọi ActionExecutionFunction. Mối liên kết từ một cạnh phần phụ thuộc của biểu đồ thao tác đến một cạnh phần phụ thuộc Skyframe được mô tả trong ActionExecutionFunction.getInputDeps()Artifact.key(), đồng thời có một số điểm tối ưu hoá để giữ cho số lượng cạnh Skyframe ở mức thấp:

  • Các 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 ra khoá cho hành động tạo ra khoá đó
  • Các tập hợp lồng nhau có khoá Skyframe riêng.

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

Một số thao tác được tạo bởi nhiều mục tiêu đã định cấu hình; các quy tắc Starlark bị hạn chế hơn vì chỉ được phép đặt các thao tác 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 kỳ đâu.

Đây được coi là một tính năng không mong muốn, nhưng rất khó loại bỏ vì nó 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 nhiều quy tắc tham chiếu (handwave-handwave). Điều này sẽ tiêu tốn một chút RAM: mỗi phiên bản của một thao tác dùng chung cần được lưu trữ riêng trong bộ nhớ.

Nếu hai thao tác tạo ra cùng một tệp đầu ra, thì chúng phải hoàn toàn giống 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 Thao tác. Thao tác này được triển khai trong SkyframeActionExecutor.findAndStoreArtifactConflicts() và là một trong số ít vị trí trong Bazel yêu cầu chế độ xem "toàn cầu" 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 thao tác xây dựng, chẳng hạn như các lệnh tạo ra đầu ra.

Điều đầu tiên Bazel làm sau giai đoạn phân tích là xác định những Artifact cần được tạo. Logic cho việc này được mã hoá trong TopLevelArtifactHelper; nói một cách đại khái, đó là filesToBuild của các mục tiêu được đị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 cho 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ó lựa chọn đọc các gói nguồn từ nhiều vị trí trong hệ thống tệp (--package_path), nên Bazel cần cung cấp cho các thao tác được thực thi cục bộ 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 chú mọi mục tiêu được 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 đã dùng từ vị trí thực của gói. Một lựa chọn khác là truyền các đườ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ì:

  • Thao tác này sẽ thay đổi dòng lệnh hành động khi một gói được di chuyển từ một mục nhập đường dẫn gói sang một mục nhập khác (trước đây 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 thao tác được chạy từ xa so với khi thao tác đó được chạy cục bộ
  • Việc này đòi hỏi một quá trình chuyển đổi dòng lệnh dành riêng cho công cụ đang sử dụng (hãy xem xét sự khác biệt giữa các đường dẫn bao gồm C++ và đường dẫn lớp Java)
  • Việc thay đổi dòng lệnh của một thao tác sẽ làm mất hiệu lực mục nhập bộ nhớ đệm thao tác của thao tác đó
  • --package_path đang dần bị ngừng sử dụng

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

Vì việc chạy một thao tác tốn nhiều chi phí, nên chúng tôi có một vài lớp lưu vào bộ nhớ đệm có thể được truy cập ở chế độ nền Skyframe:

  • ActionExecutionFunction.stateMap chứa dữ liệu để khởi động lại Skyframe của ActionExecutionFunction với chi phí thấp
  • 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 liên quan đến địa điểm thực tế

Bộ nhớ đệm này là một lớp khác nằm sau Skyframe; ngay cả khi một thao tác được thực thi lại trong Skyframe, thao tác đó vẫn có thể là một lượt truy cập vào bộ nhớ đệm thao tác cục bộ. Thư mục 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 để tìm lượt truy cập bằng phương thức ActionCacheChecker.getTokenIfNeedToExecute() .

Ngược lại với tên của nó, đâ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ả như sau:

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

Ngoài ra, còn có một "bộ nhớ đệm hành động từ trên xuống" đang trong giai đoạn thử nghiệm cao và vẫn đang được phát triển. Bộ nhớ đệm này sử dụng các 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á đầu vào và cắt tỉa đầu vào

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

  • Một thao tác có thể phát hiện các đầu vào mới trước khi thực thi hoặc quyết định rằng một số đầu vào của thao tác đó không thực sự cần thiết. Ví dụ kinh điển là C++, trong đó tốt hơn là đưa ra một phỏng đoán có căn cứ về những tệp tiêu đề mà một tệp C++ sử dụng từ bao đóng bắc cầu để chúng ta không cần gửi mọi tệp đến các trình thực thi từ xa; do đó, chúng ta có lựa 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 những tệp tiêu đề đó làm đầu vào được đề cập trong các câu lệnh #include (chúng ta đánh giá quá cao để không cần triển khai một trình tiền xử lý C đầy đủ) Lựa chọn này hiện được cố định thành "false" trong Bazel và chỉ được dùng tại Google.
  • Một thao tác có thể nhận ra rằng một số tệp không được dùng trong quá trình thực thi. Trong C++, việc này được gọi là "tệp .d": trình biên dịch cho biết những tệp tiêu đề đã được sử dụng sau đó và để tránh tình trạng gia tăng kém hơn Make, Bazel sẽ tận dụng thực tế này. Điều này giúp đưa ra thông tin ước tính chính xác hơn so với trình quét include vì thông tin này dựa vào trình biên dịch.

Các thao tác này được triển khai bằng cách sử dụng các phương thức trên Thao tác:

  1. Action.discoverInputs() sẽ được gọi. Hàm này sẽ trả về một tập hợp lồng nhau gồm các Artifact (Hiện vật) đượ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ụ thuộc nào trong biểu đồ hành động không có cấu phần phần mềm tương đương trong biểu đồ mục tiêu đã định cấu hình.
  2. Hành động này được thực thi bằng cách gọi Action.execute().
  3. Vào cuối Action.execute(), thao tác này có thể gọi Action.updateInputs() để cho Bazel biết rằng không phải tất cả các đầu vào của thao tác đều cần thiết. Điều này có thể dẫn đến các bản dựng gia tăng không chính xác nếu một đầu vào đã dùng được báo cáo là chưa dùng.

Khi bộ nhớ đệm của hành động trả về một lượt truy cập vào một phiên bản 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ẽ gọi chính updateInputs() để tập hợp các đầu vào phản ánh kết quả của quá trình khám phá và cắt tỉa đầ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ố đầu vào là không dùng đến bằng cách sử dụng đối số unused_inputs_list= của ctx.actions.run().

Nhiều cách chạy các thao tác: Chiến lược/ActionContext

Một số thao tác có thể được chạy theo nhiều cách. Ví dụ: bạn có thể thực thi một dòng lệnh tại chỗ, tại chỗ 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ỉ đổi tên được một nửa...)

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

  1. Khi giai đoạn thực thi bắt đầu, 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 đề cập đế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 phù 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 thao tác yêu cầu ngữ cảnh bằng cách sử dụng ActionExecutionContext.getContext()BlazeExecutor.getStrategy() (thực sự chỉ nên có một cách để thực hiện việc này...)

Các chiến lược có thể gọi các chiến lược khác để thực hiện công việc của chúng; ví dụ: chiến lược này được dùng trong chiến lược động bắt đầu các thao tác cả cục bộ và từ xa, sau đó sử dụng thao tác hoàn thành 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 dài và do đó, nên được dùng lại giữa các thao tác thay vì bắt đầu một công cụ mới cho mọi thao tác (Điều này thể hiện một vấn đề tiềm ẩn về tính chính xác, 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, quy trình worker cần được khởi động lại. Việc có thể sử dụng lại worker hay không được xác định bằng cách tính tổng kiểm cho công cụ được dùng bằng WorkerFilesHash. Việc này phụ thuộc vào việc biết những đầu vào nào của hành động đại diện cho một phần của công cụ và những đầu vào nào đại diện cho đầu vào; điều này do người tạo Hành động xác định: Spawn.getToolFiles() và các tệp chạy của Spawn được tính là các phần của công cụ.

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

  • Bạn có thể xem thông tin về nhiều chiến lược để chạy các hành động tại đây.
  • Thông tin về chiến lược động, trong đó chúng ta chạy một thao tác cả cục bộ và từ xa để xem thao tác nào hoàn thành trước, có tại đây.
  • Bạn có thể xem thông tin về sự phức tạp của việc thực thi các hành động tại địa phương 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ộ cần chạy song song khác nhau tuỳ theo từng hành động: hành động càng cần nhiều tài nguyên thì càng ít phiên bản chạy cùng lúc để tránh làm quá tải máy cục bộ.

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

Bạn có thể xem nội dung mô tả chi tiết hơn về hoạt động 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 đều cần 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 đượ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 thao tác; nếu không, nếu hai cấu hình không đồng ý về chẳng hạn như dòng lệnh của một thao tác tạo ra cùng một tệp đầu ra, thì Bazel sẽ không biết nên chọn thao tác nào ("xung đột thao tác")
  2. Nếu hai cấu hình đại diện cho "gần như" cùng một thứ, thì chúng phải có cùng tên để các thao tác được thực thi trong một cấu hình có thể được dùng lại cho cấu hình kia nếu các dòng lệnh khớp nhau: ví dụ: các thay đổi đối với các lựa chọn dòng lệnh cho trình biên dịch Java không được dẫn đến việc chạy lại các thao tác biên dịch C++.

Cho đến nay, chúng tôi vẫn chưa tìm ra cách giải quyết vấn đề này một cách có nguyên tắc. Vấn đề này tương tự như vấn đề cắt tỉa cấu hình. Bạn có thể xem thêm thông tin về các lựa chọn tại đây. Các vấn đề chính là quy tắc Starlark (thường là do những tác giả không quen thuộc với Bazel) và các khía cạnh, những yếu tố này sẽ thêm một khía cạnh khác vào không gian của những thứ có thể tạo ra tệp đầu ra "giống nhau".

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 quá trình 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 tra của tập hợp các quá trình 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. Tuy nhiên, tính năng này chưa thể hoàn hảo. Thao tá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ử. Chiến dịch này hỗ trợ:

  • Chạy kiểm thử từ xa (nếu có một phần phụ trợ thực thi từ xa)
  • Chạy song song nhiều lần (để giảm thiểu sự không ổn định hoặc thu thập dữ liệu về thời gian)
  • Phân đoạn kiểm thử (chia các trường hợp kiểm thử trong cùng một kiểm thử thành 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 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ả dựng là thử nghiệm đang chạy. Đây là tệp "cache status" chứa thông báo TestResultData được chuyển đổi tuần tự
  • Số lần cần chạy kiểm thử
  • Số lượng phân đoạn mà thử nghiệm cần được chia thành
  • Một số tham số về cách chạy kiểm thử (chẳng hạn như thời gian chờ kiểm thử)

Xác định những kiểm thử cần chạy

Việc xác định những kiểm thử sẽ chạy là một quy 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ử sẽ được mở rộng một cách đệ quy. Việc mở rộng được triển khai trong TestsForTargetPatternFunction. Một điểm đá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ẽ đề cập đến mọi kiểm thử trong gói của bộ kiểm thử đó. Điều này được triển khai trong Package.beforeBuild() bằng cách thêm một thuộc tính ngầm định có tên là $implicit_tests vào các quy tắc bộ kiểm thử.

Sau đó, các kiểm thử sẽ được lọc theo kích thước, thẻ, thời gian chờ và ngôn ngữ theo các lựa chọn trên dòng lệnh. Việ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 và kết quả được đưa vào TargetPatternPhaseValue.getTestsToRunLabels(). Lý do khiến bạn không thể định cấu hình các thuộc tính quy tắc có thể lọc là vì điều này xảy ra trước giai đoạn phân tích, do đó, bạn không thể định cấu hình.

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

Để quá trình phức tạp này trở nên minh bạch hơn, bạn có thể dùng toán tử truy vấn tests() (được triển khai trong TestsFunction) để biết những kiểm thử nào được 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ể quá trình này sẽ khác với quá trình trên theo nhiều cách tinh tế.

Chạy kiểm thử

Cách chạy các kiểm thử là bằng cách yêu cầu các 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 một 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 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 biết những gì được mong đợi từ các kiểm thử. Bạn có thể xem nội dung mô tả chi tiết về những gì Bazel mong đợi từ các kiểm thử và những gì các kiểm thử có thể mong đợi từ Bazel tại đây. Đơn giản nhất, mã thoát 0 có nghĩa là thành công, mọi mã khác có nghĩa là thất bại.

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. Chúng đượ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 theo kiểu JUnit nêu 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 trong quá trình kiểm thử. stdout và stderr không tách biệt.
  • test.outputs, "thư mục đầu ra chưa khai báo"; thư mục này được dùng cho những kiểm thử muốn 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 các mục tiêu thông thường: thực thi kiểm thử độc quyền và truyền trực tuyến đầu ra.

Một số kiểm thử cần được thực thi ở chế độ độc quyền, chẳng hạn như không song song với các kiểm thử khác. Bạn có thể kích hoạt tính năng 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 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 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 (đầu ra của thao tác này sẽ bị loại bỏ khi thao tác kết thúc), người dùng có thể yêu cầu truyền trực tuyến đầu ra của các kiểm thử để được thông báo về tiến trình của một kiểm thử chạy trong thời gian dài. Điều này được chỉ định bằng lựa chọn dòng lệnh --test_output=streamed và ngụ ý việc thực thi kiểm thử độc quyền để đầu ra của các kiểm thử khác nhau không bị xen kẽ.

Điều này được triển khai trong lớp StreamedTestOutput có tên phù hợp và hoạt động bằng cách thăm dò 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ó các quy tắc Bazel.

Kết quả của các kiểm thử đã thực thi có 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 kết xuất vào Build Event Protocol và được AggregatingTestListener phát ra bảng điều khiển.

Bộ sưu tập phạm vi

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

Để thu thập mức độ phù hợp, mỗi lần thực thi kiểm thử sẽ được bao bọc 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 độ phù hợp và xác định nơi các tệp mức độ phù hợp được ghi bởi(các) thời gian chạy mức độ phù hợp. Sau đó, nó sẽ chạy kiểm thử. Bản thân một 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 các thời gian chạy thu thập mức độ phù hợp 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 được thực hiện bằng các chiến lược kiểm thử và yêu cầu collect_coverage.sh nằm trên các đầu vào của quy trình 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, tức là hoạt động đo lường mức độ phù hợp được thêm vào tại thời điểm 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, tức là hoạt động đo lường mức độ phù hợp được thêm vào tại thời điểm thực thi.

Một khái niệm cốt lõi khác là mức độ phù hợp cơ bản. Đây là mức độ phù hợp 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à tính năng 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à không đủ 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ẽ phát ra một tệp mức độ phù hợp cho mọi tệp nhị phân. Tệp này chỉ chứa những tệp mà chúng ta thu thập mức độ phù hợp mà không có dòng nào được bao phủ. Tệp phạm vi cơ sở mặc định cho một mục tiêu nằm ở bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat, nhưng các quy tắc được khuyến khích tạo tệp phạm vi cơ sở riêng với nội dung có ý nghĩa hơn ngoài tên của các tệp nguồn.

Chúng tôi theo dõi 2 nhóm tệp để thu thập thông tin về mức độ phù hợp cho từng 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 cần đo lường. Đối với thời gian chạy phạm vi hoạt động trực tuyến, bạn có thể dùng tính năng này trong thời gian chạy để quyết định tệp nào cần đo lường. Thẻ này cũng được dùng để triển khai mức độ phù hợp cơ bản.

Tập hợp các tệp siêu dữ liệu về hoạt động đo lường là tập hợp các tệp bổ sung mà một quy trình kiểm thử cần để tạo các tệp LCOV mà Bazel yêu cầu. Trên thực tế, việc 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 tệp này được thêm vào tập hợp đầu vào của các thao tác kiểm thử nếu bạn bật chế độ mức độ phù hợp.

Thông tin về việc có thu thập dữ liệu về mức độ phù hợp hay không được lưu trữ trong BuildConfiguration. Điều này rất hữu ích vì đây là một cách dễ dàng để thay đổi thao tác kiểm thử và biểu đồ thao tác tuỳ thuộc vào bit này, nhưng điều này cũng có nghĩa là nếu bit này bị đảo ngược, thì 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 lựa chọn trình biên dịch khác nhau để phát ra mã có thể thu thập mức độ phù hợp, điều này giúp giảm thiểu phần nào vấn đề này, vì sau đó dù sao thì cũng cần phân tích lại).

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

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

Công cụ truy vấn

Bazel có một ngôn ngữ nhỏ được dùng để hỏi Bazel nhiều điều về nhiều biểu đồ. Sau đây là các loại truy vấn được cung cấp:

  • bazel query được dùng để kiểm tra biểu đồ mục tiêu
  • bazel cquery được dùng để kiểm 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 thành phần 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 khác bằng cách phân lớp phụ QueryFunction. Để cho phép truyền trực tuyến kết quả truy vấn, thay vì thu thập chúng vào một cấu trúc dữ liệu nào đó, query2.engine.Callback sẽ được truyền đến QueryFunction, gọi query2.engine.Callback cho các kết quả mà QueryFunction muốn trả về.

Kết quả của một truy vấn có thể được phát theo nhiều cách: nhãn, nhãn và các 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 ra _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 được chuyển đổi tuần tự. Đó là lý do chỉ có một số ít loại thuộc tính mà không có thuộc tính nào có giá trị Starlark phức tạp. Giải pháp thường dùng là sử dụng nhãn và đính kèm thông tin phức tạp vào quy tắc có nhãn đó. Đây không phải là một giải pháp thay thế thoả đáng và sẽ rất tốt nếu yêu cầu này được gỡ bỏ.

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à một di tích trong lịch sử của Bazel khi nó từng được gọi là Blaze) và nhận thông tin về nhiều sự kiện trong quá trình thực thi một lệnh.

Chúng chủ yếu được dùng để triển khai nhiều phần chức năng "không phải là chức năng 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 dùng tại Google) cần:

  • Giao diện với 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 BlazeModule được cung cấp có phần ngẫu nhiên. Đừng dùng nó làm ví dụ về các nguyên tắc thiết kế phù hợp.

Bus sự kiện

Cách chính mà 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 phiên bản mới được tạo cho mỗi bản dựng, nhiều phần của Bazel có thể đăng các sự kiện lên đó 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 việc sau đây được biểu thị dưới dạng sự kiện:

  • Danh sách các mục tiêu 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 thành công hoặc không thành công một mục tiêu (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 Giao thức sự kiện bản dựng (đó là các BuildEvent). Điều này không chỉ cho phép BlazeModule mà còn cho phép những thứ bên ngoài quy trình Bazel quan sát bản dựng. Các sự kiện này có thể truy cập được 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ủ (gọi là Dịch vụ sự kiện bản dựng) để truyền trực tuyến các 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ù Bazel ban đầu được thiết kế để sử dụng trong một kho lưu trữ đơn (một cây nguồn duy nhất chứa mọi thứ cần thiết để tạo), nhưng Bazel tồn tại 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 được 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 nằm 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 khai báo như sau:

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

Kết quả có trong kho lưu trữ có tên là @foo. Vấn đề phức tạp ở chỗ người ta có thể xác định các quy tắc kho lưu trữ mới trong tệp Starlark, sau đó có thể dùng để tải mã Starlark mới, mã này có thể dùng để xác định các quy tắc kho lưu trữ mới, 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 khối được phân định bằng các câu lệnh load(). Chỉ mục khối được biểu thị bằng WorkspaceFileKey.getIndex() và việc tính toán WorkspaceFileFunction cho đến chỉ mục X có nghĩa là đánh giá chỉ mục đó cho đến câu lệnh load() thứ X.

Đang tìm nạp kho lưu trữ

Trước khi mã của kho lưu trữ có sẵn cho Bazel, mã đó cần được tìm nạp. Thao tác này sẽ khiến Bazel tạo một thư mục trong $OUTPUT_BASE/external/<repository name>.

Việc 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ì những lý do không rõ ràng (mã cho biết là để tránh tải lại nội dung trong trường hợp Skyframe khởi động lại, nhưng đây không phải là lý do thuyết phụ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 đoạn của tệp WORKSPACE cho đến khi tìm thấy kho lưu trữ được yêu cầu
  4. RepositoryFunction thích hợp được tìm thấy để triển khai quá trình tìm nạp kho lưu trữ; đó có thể là quá trình triển khai Starlark của kho lưu trữ hoặc một bản đồ được mã hoá cứng cho các kho lưu trữ được triển khai bằng Java.

Có nhiều lớp lưu vào bộ nhớ đệm vì việc tìm nạp một 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 tra (RepositoryCache). Điều này yêu cầu tổng kiểm tra phải có trong tệp WORKSPACE, nhưng điều đó dù sao cũng tốt cho tính khép kín. Thư mục này được chia sẻ bởi mọi phiên bản máy chủ Bazel trên cùng một máy trạm, bất kể chúng đang chạy trong không gian làm việc hoặc cơ sở đầu ra nào.
  2. "Tệp đánh dấu" được ghi cho mỗi kho lưu trữ trong $OUTPUT_BASE/external chứa tổng kiểm tra 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ì máy chủ sẽ không 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 được dùng để tra cứu các cấu phần phần mềm cần tải xuống. Điều này hữu ích trong các chế độ cài đặt doanh nghiệp, nơi Bazel không được tìm nạp các nội dung ngẫu nhiên trên Internet. Thao tác này được 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 đó 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 sẽ không hợp lệ khi định nghĩa về kho lưu trữ mà chúng nằm trong thay đổi. Do đó, FileStateValue cho một cấu phần phần mềm trong kho lưu trữ bên ngoài cần phải phụ thuộc vào kho lưu trữ bên ngoài của chúng. Vấn đề này do ExternalFilesHelper xử lý.

Mối liên kết 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 trường hợp của "vấn đề về phần phụ thuộc hình 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ó lẽ cả hai sẽ tham chiếu đến Guava bằng các nhãn bắt đầu bằng @guava// và mong đợi điều đó có nghĩa là các phiên bản khác nhau của Guava.

Do đó, Bazel cho phép người dùng ánh xạ lại 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//) trong kho lưu trữ của tệp nhị phân còn lại.

Ngoài ra, bạn cũng có thể dùng cách 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ì việc lập bản đồ kho lưu trữ cho phép một kho lưu trữ ánh xạ lại cả hai kho lưu trữ để sử dụng một kho lưu trữ @guava// chính tắc.

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

  • Package.Builder.repositoryMapping được 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 những 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()

Các bit JNI

Máy chủ của Bazel chủ yếu được viết bằng Java. Ngoại lệ là những phần mà Java không thể tự thực hiện hoặc không thể tự thực hiện khi chúng tôi 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ứ khác ở cấp độ thấp.

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

Đầu ra trên bảng điều khiển

Việc phát đầu ra của bảng điều khiển có vẻ đơn giản, nhưng sự kết hợp giữa việc chạy nhiều quy trình (đôi khi từ xa), 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ư việc có một máy chủ chạy trong thời gian dài khiến việc này trở nên 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 (cho stdout và stderr) để chuyển tiếp dữ liệu được in vào chúng đến ứng dụng. Sau đó, các giá trị này được bao bọc trong một OutErr (một cặp (stdout, stderr)). Mọi thứ cần được in trên bảng điều khiển đều phải trải qua các luồng này. Sau đó, các luồng này sẽ được chuyển cho BlazeCommandDispatcher.execExclusively().

Theo mặc định, đầu ra được in bằng các chuỗi ký tự thoát ANSI. Khi không mong muốn (--color=no), các thuộc tính này sẽ bị loại bỏ bằng một AnsiStrippingOutputStream. 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à để thông tin gỡ lỗi có thể được in bằng System.err.println() và vẫn xuất hiện 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ủ). Cần lưu ý rằng 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ì sẽ không có thao tác nào đối với stdout.

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

Một số hoạt động triển khai EventHandler lưu trữ các sự kiện mà chúng nhận được. Thao tá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 quy trình xử lý được lưu vào bộ nhớ đệm gây ra, ví dụ: cảnh báo do một 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ẽ xuất hiện trên bus sự kiện (Event thông thường _không_ xuất hiện ở đó). Đây là các phương thức triển khai của ExtendedEventHandler và mục đích chính của chúng là phát lại các sự kiện EventBus được lưu vào bộ nhớ đệm. Tất cả các sự kiện EventBus này đều triển khai Postable, nhưng không phải mọi thứ được đăng lên EventBus đều nhất thiết phải triển khai giao diện này; chỉ những sự kiện được lưu vào bộ nhớ đệm bởi ExtendedEventHandler (sẽ rất tốt và hầu hết mọi thứ đều làm như vậy; tuy nhiên, điều này không bắt buộc)

Đầu ra của thiết bị đầu cuối hầu hết được phát ra thông qua UiEventHandler, 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ó 2 đầu vào:

  • Bus 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. Lệnh này chỉ được dùng khi 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 tăng lên cho đến khi đạt đến giới hạn chịu đựng. Vì lý do này, Bazel có một trình phân tích tài nguyên có thể dùng để phân tích tài nguyên các bản dựng và chính Bazel. Thao tác này được triển khai trong một lớp có tên phù hợp là Profiler. Theo mặc định, tính năng này sẽ bật, mặc dù chỉ ghi lại dữ liệu rút gọn để có thể chịu được mức hao tổn; Dòng lệnh --record_full_profiler_data sẽ ghi lại mọi thứ có thể.

Công cụ này phát ra một hồ sơ ở định dạng trình phân tích tài nguyên Chrome; tốt nhất là 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ụ, đồng thời các tác vụ này được lồng vào nhau một cách gọn gàng. Mỗi luồng Java sẽ có ngăn xếp tác vụ riêng. VIỆC CẦN LÀM: Tính năng này hoạt động như thế nào với các thao tác và kiểu truyền nối tiếp?

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 càng lâu càng tốt để 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(). Hàm này trả về một Closeable, trong đó đóng biểu thị phần cuối của tác vụ. Tốt nhất là bạn nên dùng câu lệnh try-with-resources.

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

Kiểm thử Bazel

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

Trong số các kiểm thử tích hợp, chúng ta có 2 loại:

  1. Các kiểm thử được triển khai bằng cách sử dụng một khung kiểm thử bash rất công phu trong src/test/shell
  2. Những phương thức đượ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ì có đầy đủ các tính năng cho hầu hết các trường hợp kiểm thử. Vì là một khung Java, nên Compose cung cấp 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ề các 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 thời mà bạn có thể dùng để ghi các 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 điều về kết quả phân tích.