Cơ sở mã Bazel

Báo cáo sự cố Xem nguồn

Tài liệu này mô tả cơ sở mã và cấu trúc của Bazel. Nội dung 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 (mã sản xuất khoảng 350KLOC và mã kiểm thử khoảng 260 KLOC) 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õ thung lũng cụ thể của họ, nhưng ít ai biết có gì nằm trên những ngọn đồi theo mọi hướng.

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

Phiên bản công khai của mã nguồn của Bazel có trên GitHub tại github.com/bazelbuild/bazel. Đây không phải là "nguồn đáng tin cậy". Thông tin này bắt nguồn 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 trở thành nguồn đáng tin cậy.

Khoản đóng góp được chấp nhận thông qua cơ chế yêu cầu lấy dữ liệu GitHub thông thường và được nhân viên của Google nhập thủ công vào cây nguồn nội bộ, sau đó xuất lại sang GitHub.

Cấu trúc ứng dụng/máy chủ

Phần lớn Bazel nằm trong một quy trình máy chủ 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ố tuỳ chọn (--host_jvm_args=) nằm trước tên lệnh sẽ chạy và một số nằm sau (-c opt); loại trước được gọi là "tuỳ chọn khởi động" và ảnh hưởng đến toàn bộ quá trình máy chủ, trong khi loại thứ hai là "tuỳ chọn lệnh", chỉ ảnh hưởng đến một lệnh duy nhất.

Mỗi thực thể máy chủ có một không gian làm việc liên kết duy nhất (tập hợp 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 thực thể máy chủ đang hoạt động. Bạn có thể tránh được 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 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++ ("máy khách") sẽ được kiểm soát. Trình 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ự trích xuất hay chưa. Nếu không, ứng dụng sẽ thực hiện việc này. Đây là nơi bắt nguồn hoạt động triển khai máy chủ.
  2. Kiểm tra xem có thực thể máy chủ nào đang hoạt động hay không: thực thể đó đ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. Trình này tìm máy chủ đang chạy bằng cách xem thư mục $OUTPUT_BASE/server, nơi có tệp khoá có cổng mà máy chủ đang nghe.
  3. Nếu cần, hãy loại bỏ quy trình của máy chủ cũ
  4. Nếu cần, hãy khởi động một quy trình máy chủ mới

Sau khi 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 trở lại thiết bị đầu cuối. Chỉ có thể chạy một lệnh cùng lúc. Tính năng này được triển khai bằng 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ì việc không thể chạy bazel version song song với một lệnh khác sẽ có chút khó khăn. Trình chặn chính là vòng đời của các 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à ứng dụng sẽ trả về. Một điểm hạn chế thú vị là cách triển khai bazel run: nhiệm vụ của lệnh này là chạy nội dung 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 đó, hàm này sẽ cho ứng dụng biết tệp nhị phân nào nên ujexec() và bằng đối số nào.

Khi nhấn Ctrl-C, ứng dụng sẽ chuyển thành lệnh gọi Huỷ trên kết nối gRPC. Thao tác 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 Ctrl-C thứ ba, ứng dụng sẽ gửi một 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 do 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 xây dựng. Bạn có thể xem nội dung mô tả đầy đủ trong Bố cục thư mục đầu ra.

"Kho lưu trữ chính" là cây nguồn mà Bazel chạy. Nó thường tương ứng với nội dung mà bạn đã thanh toán qua chế độ kiểm soát 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 mọi dữ liệu của mình vào "gốc người dùng đầu ra". Giá trị này thường là $HOME/.cache/bazel/_bazel_${USER}, nhưng bạn có thể ghi đè giá trị này bằng cách sử dụng tuỳ chọn khởi động --output_user_root.

"Cơ sở cài đặt" là nơi Bazel được trích xuất. Quá trình 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 thư mục đó trong cơ sở cài đặt. Thuộc tính này nằm ở $OUTPUT_USER_ROOT/install theo mặc định và có thể 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 được đính kèm với một không gian làm việc cụ thể được ghi vào. Mỗi cơ sở đầu ra có tối đa một thực thể máy chủ Bazel chạy bất cứ lúc nào. Thời gian thường là $OUTPUT_USER_ROOT/<checksum of the path to the workspace>. Bạn có thể thay đổi chính sách này bằng cách sử dụng tuỳ chọn khởi động --output_base. Đây là một cách hữu ích để khắc phục hạn chế về việc chỉ một thực thể Bazel có thể chạy trong mọi không gian làm việc tại một thời điểm bất kỳ.

Thư mục đầu ra chứa:

  • Các kho lưu trữ bên ngoài được tìm nạp tại $OUTPUT_BASE/external.
  • 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 của bản dựng hiện tại. Địa điểm tại $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 thay đổi này thành $EXECROOT, mặc dù đây là kế hoạch dài hạn vì đây là một thay đổi rất không tương thích.
  • Tệp được tạo trong quá trình tạo.

Quá trình thực thi một lệnh

Sau khi máy chủ Bazel có đượ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ự các sự kiện sau đây sẽ diễn ra:

  1. BlazeCommandDispatcher đã nhận được thông báo về yêu cầu mới. Hàm này quyết định xem lệnh có cần không gian làm việc để chạy hay không (gần như 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 bên phải. 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 phản mẫu, sẽ tốt hơn 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 xe buýt sự kiện đượ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 bản dựng. Một số tệp trong số này được xuất ra bên ngoài Bazel theo aegis của Giao thức sự kiện bản dựng để cho mọi người biết cách bản dựng hoạt động.

  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 một bản dựng: tạo, kiểm thử, chạy, bao phủ, 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à các ký tự đại diện như //pkg:all//pkg/... cũng được phân giải. Việc này được triển khai trong AnalysisPhaseRunner.evaluateTargetPatterns() và sửa đổi 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 đồ tuần hoàn có hướng dẫn của các lệnh cần được thực thi cho bản dựng).

  8. Giai đoạn thực thi đang chạy. Tức là chạy mọi hành động cần thiết để tạo 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, từ đó 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 và có liên quan với nhau. Ví dụ:

  1. Các tuỳ chọn liên quan đến ngôn ngữ lập trình (CppOptions hoặc JavaOptions). Đây phải là lớp con của FragmentOptions và cuối cùng được gói vào đố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ố lệnh trong số đó (ví dụ: liệu có nên quét C++ hay không) được đọc trong giai đoạn thực thi, nhưng việc đó luôn yêu cầu hệ thống ống nước rõ ràng vì BuildConfiguration chưa 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 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, việc này không đúng và sửa đổi các thực thể đó là một cách rất hay để phá vỡ Bazel theo những cách tinh vi mà khó gỡ lỗi. Thật không may, việc biến chúng thực sự bất biến là một nỗ lực lớn. (Việc sửa đổi FragmentOptions ngay sau khi xây dựng xong trước khi bất kỳ ai khác có cơ hội tham chiếu đến tham số đó 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ố kết nối trực tiếp 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 có 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 phần của lớp con FragmentOptions có chú thích @Option, chỉ định tên và loại của 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ị của 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 thuộc kiểu 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ẽ chuyển thành cách triển khai com.google.devtools.common.options.Converter.

Cây nguồn, như Bazel nhìn thấy

Bazel kinh doanh xây dựng phần mềm. Quá trình 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 đượ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à cây nguồn nơi nhà phát triển hoạt động; kho lưu trữ này thường đại diện cho một dự án duy nhất. Tổ tiên của Bazel là Blaze đã vận hành một monorepo (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à từ đó Bazel được gọi đượ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".

Một 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. Kho lưu trữ chính là cây nguồn nơi bạn gọi Bazel. Các kho lưu trữ bên ngoài được xác định theo nhiều cách; xem bài viết 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 đượ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 khẳng định với nhau. Việc này được thực hiện bởi SymlinkForest, liên kết 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à thông số kỹ thuật của các phần phụ thuộc. Các đối số này được chỉ định bởi 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 các tệp BUILD vẫn được chấp nhận là vì đối tượng cấp trên của Bazel, Blaze, đã sử dụng tên tệp này. Tuy nhiên, hoá ra đây là một phân đoạn đường dẫn thường được sử dụng, đặc biệt là trên Windows, trong đó 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: thay đổi đối với tệp BUILD của một gói không được khiến các gói khác thay đổi. Việc thêm hoặc xoá các tệp BUILD _can _change các gói khác, vì các khối đệ quy dừng ở ranh giới gói, và do đó, sự hiện diện của tệp BUILD sẽ dừng đệ quy.

Quá trình đánh giá tệp BUILD được gọi là "tải gói". Bộ quy tắ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 quá trình tải gói là một đối tượng Package. Đây chủ yếu là mục ánh xạ từ một chuỗi (tên của mục tiêu) đến chính mục tiêu đó.

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

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

  • LegacyGlobber, một khối toàn cảnh không nhận biết trong Skyframe tốc độ cao và hạnh phúc
  • SkyframeHybridGlobber, một phiên bản sử dụng Skyframe và quay lại toàn bộ khung hình cũ để tránh "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ỉ 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ả các gói thông thường không được chứa các trường mô tả nội dung khác. Những quốc gia/khu vực này bao gồm:

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

Tốt nhất là bạn nên tách riêng gói "bên ngoài" khi phân tích cú pháp các gói thông thường để Package không cần phục vụ nhu cầu của cả hai. Thật không may, điều này rất khó thực hiện vì hai phần tử ăn khớp với nhau khá sâu sắc.

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

Các gói bao gồm mục tiêu có các loại sau đây:

  1. Files: (Tệp): những tệp là dữ liệu đầu vào hoặc đầu ra của bản dựng. Theo cách nói của Bazel, chúng tôi gọi chúng 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ả tệp được tạo trong quá trình tạo bản dựng đều là mục tiêu; thông thường, đầu ra của Bazel không có nhãn liên kết.
  2. Rules (Quy tắc): mô tả các bước để lấy kết quả từ dữ liệu đầu vào. Các lớp 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ũng có một số ngôn ngữ 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ụ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 là 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 kho lưu trữ bị bỏ qua, nhãn sẽ được lấy ở trong 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 được đưa vào gói của thư mục đang làm việc hiện tại (không cho phép đường dẫn tương đối chứa các 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 theo ngôn ngữ cụ thể sẽ được triển khai trong Starlark, nhưng một số bộ quy tắc cũ (chẳng hạn như Java hoặc C++) hiện vẫn nằm trong Java.

Các lớp quy tắc Starlark cần được nhập vào đầ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 đến "vốn tự nhiên" do được đăng ký với 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 lớp này (chẳng hạn như srcs, deps): loại, giá trị mặc định, quy tắc ràng buộc, v.v.
  2. Quá trình chuyển đổi cấu hình và các khía cạnh đính kèm với mỗi 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 tôi thường sử dụng "Quy tắc" để có nghĩa là mục tiêu do một lớp quy tắc tạo ra. Tuy nhiên, trong Starlark và trong tài liệu dành cho người dùng, "Quy tắc" chỉ nên được dùng để tham chiếu đến chính lớp quy tắc đó; mục tiêu chỉ là một "mục tiêu". Ngoài ra, xin lưu ý rằng mặc dù RuleClass có tên là "lớp", nhưng không có mối quan hệ kế thừa Java nào giữa lớp quy tắc và mục tiêu của loại đó.

Khung trời

Khung đánh giá cơ bản của Bazel được gọi là Skyframe. Mô hình của lớp này là mọi thứ cần được tạo trong quá trình tạo bản dựng được sắp xếp thành một biểu đồ tuần hoàn định 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à những phần dữ liệu khác cần biết để tạo.

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 sâu sắc; chỉ có thể tiếp cận các đối tượng bất biến từ các đối tượng đó. Bất biến này hầu như luôn giữ nguyên và trong trường hợp thì không (chẳng hạn như đối với các lớp tuỳ chọn riêng lẻ BuildOptions, một thành phần của BuildConfigurationValueSkyKey của nó), chúng ta sẽ cố gắng không thay đổi chúng hoặc chỉ thay đổi chúng theo những cách không thể quan sát được từ bên ngoài. Theo đó, mọi thành phần được tính toán trong Skyframe (chẳng hạn như mục tiêu đã đị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, phương thức này sẽ kết xuất biểu đồ, một SkyValue trên mỗi dòng. Tốt nhất là bạn nên làm việc này đối với các bản dựng nhỏ vì nó 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 phương thức triển khai Bazel trên Skyframe. Thông tin thêm về Skyframe có tại đây.

Để đánh giá một SkyKey nhất định trong 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 các phương thức nạp chồng khác nhau của SkyFunction.Environment.getValue(). Việc này có tác dụng phụ là việc đăng ký các phần phụ thuộc đó vào biểu đồ nội bộ của Skyframe để Skyframe sẽ biết và đánh giá lại hàm khi có bất kỳ phần phụ thuộc nào của nó thay đổi. Nói cách khác, chức năng lưu vào bộ nhớ đệm và tính toán gia tăng của Skyframe hoạt động ở độ 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 sử dụng được, getValue() sẽ trả về giá trị rỗng. Sau đó, hàm này sẽ mang lại quyền kiểm soát cho Skyframe bằng cách tự trả về giá trị rỗng. 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ác rỗng.

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

Về cơ bản, Bazel cần các giải pháp giải quyết này vì có hàng trăm nghìn nút Skyframe đang diễn ra là rất phổ biến và khả năng hỗ trợ các luồng nhẹ của Java không tốt hơn việc triển khai StateMachine tính đến năm 2023.

Starlark

Starlark là ngôn ngữ riêng cho miền mà mọi người sử dụng để định cấu hình và mở rộng tiếng Bazel. Nó được hình thành như 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 đồng thời. Đây không phải là Turing-complete, nó khiến một số (nhưng không phải tất cả) người dùng không muốn 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. Phương thức triển khai Java được sử dụng trong Bazel hiện là một thông dịch viên.

Starlark được sử dụng trong một số 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 mà mã đó 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 vài lệnh được xác định trước.
  3. .bzl tệp. Đây là nơi xác định các quy tắc tạo bản dựng, quy tắc kho lưu trữ, phần mở rộng mô-đun mới. Mã Starlark ở đây có thể định nghĩa 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 hơi khác nhau vì chúng thể hiện những thứ khác nhau. Bạn có thể xem danh sách các điểm khác biệt tại đây.

Thông tin thêm về Starlark có 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 lớp này là một "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).

Giai đoạn nà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 được chuyển đổi tuần tự), nhưng giờ đây các phần này có thể chồng chéo nhau theo thời gian:

  1. Tải gói, tức là chuyển các tệp BUILD thành đố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 quy trình triển khai quy tắc để tạo biểu đồ hành động

Mỗi mục tiêu được định cấu hình trong đó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 phải được phân tích từ dưới lên; tức là các nút lá trước, sau đó đến các nút trên dòng lệnh. Các đầu vào cho bản phân tích của một mục tiêu được định cấu hình là:

  1. Cấu hình. ("cách" tạo quy tắc đó; ví dụ: nền tảng mục tiêu cũng như các tuỳ chọn như tuỳ chọn dòng lệnh mà người dùng muốn 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 thông tin "tổng hợp" trong quá trình đó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 classpath hoặc tất cả các tệp .o cần liên kết với 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 có chứa mục tiêu. Đối với quy tắc, hàm này bao gồm các thuộc tính tương ứng, 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, quy tắc này có thể ở trong Starlark hoặc trong Java. Tất cả các mục tiêu được định cấu hình không theo quy tắc đều được triển khai trong Java.

Kết quả phân tích một mục tiêu đã thiết lập là:

  1. 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 đó có thể truy cập
  2. Các cấu phần phần mềm mà nó có thể tạo ra và các 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 của nó mạnh mẽ hơn, nhưng đồng thời, sẽ dễ thực hiện Bad ThingsTM hơn, chẳng hạn như viết mã có thời gian hoặc độ phức tạp 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 bất biến (chẳng hạn như vô tình sửa đổi thực thể Options hoặc tạo mục tiêu đã định cấu hình có thể thay đổi được)

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 sẽ tồn tại trong DependencyResolver.dependentNodeMap().

Cấu hình

Cấu hình là "cách thức" tạo mục tiêu: dành 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, chẳng hạn như khi cùng một mã được sử dụng cho một công cụ chạy trong quá trình tạo bản dựng và cho mã mục tiêu, đồng thời chúng ta biên dịch chéo hoặc khi chúng ta xây dựng một ứng dụng Android chất lượng cao (một ứng dụng chứa mã gốc cho nhiều kiến trúc CPU)

Về mặt lý thuyết, cấu hình này là một thực thể BuildOptions. Tuy nhiên, trên thực tế, BuildOptions được BuildConfiguration gói để cung cấp thêm các chức năng lặt vặt. Khung này truyền từ đầu biểu đồ phần phụ thuộc xuống dưới cùng. Nếu có thay đổi, bản dựng cần được phân tích lại.

Điều này dẫn đến các đ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ượt chạy kiểm thử được yêu cầu thay đổi, mặc dù điều đó chỉ ảnh hưởng đến mục tiêu kiểm thử (chúng tôi có kế hoạch "cắt" cấu hình để trường hợp này không đúng như vậy nhưng vẫn chưa sẵn sàng).

Khi cần triển khai một phần của cấu hình, quá trình triển khai quy tắc cần phải 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 nhằm tránh sai sót (chẳng hạn như các quy tắc Python sử dụng mảnh Java) và để hỗ trợ việc cắt bỏ cấu hình, chẳng hạn như nếu các tuỳ chọn Python thay đổi, các mục tiêu C++ không cần được phân tích lại.

Cấu hình của quy tắc không nhất thiết phải giống với quy tắc "gốc". Quá trình thay đổi cấu hình trong 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) thành một hoặc nhiều BuildOptions (cấu hình đầu ra).
  2. Trên bất kỳ cạnh nào được chia sẻ với một mục tiêu đã định cấu hình. Các toán tử này được chỉ định trong RuleClass.Builder.cfg().

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

Quá trình 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 dùng trong quá trình tạo bản dựng và do đó, phần phụ thuộc đó phải được xây dựng 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ư đối với mã gốc trong các tệp APK lớn của Android)

Nếu 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à chuyển đổi phân tách.

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

Nhà cung cấp thông tin chuyển tuyến

Nhà cung cấp thông tin chuyển tiếp là một cách (và _only _way) cho các mục tiêu đã định cấu hình để thông báo về các mục tiêu được định cấu hình khác phụ thuộc vào các mục tiêu đó. Lý do " bắc cầu" có trong tên của chúng là vì đây thường là một kiểu tổng hợp quá trình đóng bắc cầu của một mục tiêu đã định cấu hình.

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

  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 thể truy cập từ Starlark. Các trình cung cấp này là một lớp con của TransitiveInfoProvider.
  2. Một chuỗi. Đây là hành vi cũ và không được khuyến khích vì rất dễ xảy ra xung đột tên. Các trình cung cấp thông tin chuyển tiếp đó là 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 đối tượng này từ Starlark bằng cách sử dụng hàm provider() và 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.

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

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

Các mục tiêu đã đị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 do Starlark định cấu hình sẽ được tạo thông qua StarlarkRuleConfiguredTargetUtil.buildRule() .

Nhà máy mục tiêu đã định cấu hình phải sử dụng RuleConfiguredTargetBuilder để tạo giá trị trả về. bao gồm những yếu tố 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à các 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 quy tắc sinh.
  2. Các tệp runfile, thông thường và dữ liệu của chúng.
  3. Các nhóm đầu ra của chúng. Đây là các "nhóm tệp khác" mà quy tắc có thể tạo. Bạn có thể truy cập các thành phần này bằng thuộc tính output_group của quy tắc nhóm tệp trong hàm BUILD và sử dụng trình cung cấp OutputGroupInfo trong Java.

Tệp Run

Một số tệp nhị phân cần có tệp dữ liệu để chạy. Một ví dụ nổi bật là các bài kiểm thử cần có tệp đầu vào. Điều này được thể hiện ở Bazel bằng khái niệm "runfiles". "Cây runfiles" là cây thư mục của các tệp dữ liệu dành cho một tệp nhị phân cụ thể. Chuỗi này được tạo trong hệ thống tệp dưới dạng cây 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 các tệp runfile được biểu thị dưới dạng một thực thể Runfiles. Về mặt lý thuyết, đâ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 vì 2 lý do:

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

Runfile được thu thập bằng cách sử dụng RunfilesProvider: một thực thể của lớp này đại diện cho các runfile mà mục tiêu đã định cấu hình (chẳng hạn như thư viện) và nhu cầu đóng bắc cầu của nó được tập hợ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 dưới bìa): mỗi mục tiêu sẽ hợp nhất các runfile của phần phụ thuộc, thêm một số mục tiêu, sau đó gửi kết quả lên trên biểu đồ phần phụ thuộc. Thực thể RunfilesProvider chứa hai thực thể Runfiles, một thực thể khi quy tắc được phụ thuộc vào thông qua thuộc tính "dữ liệu" và một thực thể cho các loại phần phụ thuộc sắp tới khác. Nguyên nhân là do đôi khi, một mục tiêu sẽ biểu thị nhiều tệp chạy khác nhau khi phụ thuộc vào thông qua một thuộc tính dữ liệu. Đây là hành vi cũ không mong muốn mà chúng tôi chưa xoá bỏ.

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

  • Tệp kê khai runfiles đầu vào. Đây là nội dung mô tả chuyển đổi tuần tự của cây runfiles. Tệp 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 thay đổi khi và chỉ khi nội dung của tệp kê khai thay đổi.
  • Tệp kê khai runfiles đầu ra. Tính năng này được các thư viện thời gian chạy sử dụng để xử lý cây runfile, đặc biệt là trên Windows, vốn đôi khi không hỗ trợ đường liên kết tượng trưng.
  • Người trung gian runfiles. Để tồn tại cây runfiles, bạn cần tạo cây liên kết tượng trưng và cấu phần phần mềm mà các 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 runfile để đại diện cho tất cả những giá trị này.
  • Đối số dòng lệnh để chạy tệp nhị phân có tệp runfile mà đối tượng RunfilesSupport đại diện.

Khía cạnh

Khung là một cách để "truyền bá tính toán xuống biểu đồ phần phụ thuộc". Các thao tác này đượ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 nên biết về bất kỳ ngôn ngữ cụ thể nào, như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) bằng bất kỳ ngôn ngữ lập trình nào cũng phải được ghép nối với quy tắc proto_library sao cho nếu hai mục tiêu bằng cùng một ngôn ngữ phụ thuộc vào cùng một vùng đệm giao thức, thì quy tắc đó chỉ được tạo một lần.

Cũng giống như các mục tiêu được định cấu hình, chúng được biểu thị trong Skyframe dưới dạng SkyValue và cách xây dựng chúng rất giống với cách tạo các mục tiêu đã định cấu hình: chúng có một lớp nhà máy 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 được định cấu hình, mục tiêu này cũng biết về mục tiêu đã định cấu hình được đính kèm và các nhà cung cấp của mục tiêu đó.

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

Một chức năng có thể không mong muốn là các khía cạnh có thể đính kèm 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 Java IDE có thể sẽ muốn biết về tất cả các tệp .jar trên classpath, nhưng một số tệp trong số đó là vùng đệm giao thức. Trong trường hợp đó, thành phần IDE sẽ cần đính kèm vào cặp (quy tắc proto_library + thành phần giao diện 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 hành động trong bản dựng sẽ chạy 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 liên kết khoá-giá trị từ các chế độ cài đặt ràng buộc (chẳng hạn như khái niệm "cấu trúc CPU") đến các 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 giá trị và chế độ cài đặt ràng buộc thường dùng 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 nhắm đến, người dùng có thể cần phải 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 mục tiêu 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 quá trình thực thi tập hợp và nền tảng đích (tài liệu về chuỗi công cụ tại đây).

Để làm được điều này, các chuỗi công cụ được chú thích bằng tập hợp các quy tắc ràng buộc thực thi và nền tảng mục tiêu mà các chuỗi công cụ hỗ trợ. Để thực hiện việc này, phần đị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 quy tắc thực thi và ràng buộc mục tiêu mà một chuỗi công cụ hỗ trợ, đồng thời cho biết đó là loại chuỗi công cụ nào (chẳng hạn như C++ hoặc Java) (quy tắc thứ hai được biểu thị bằng quy tắc toolchain_type())
  2. Quy tắc theo ngôn ngữ cụ thể mô tả chuỗi công cụ thực tế (chẳng hạn như cc_toolchain())

Điều này được thực hiện theo cách này vì chúng ta cần biết những hạn chế cho mọi chuỗi công cụ để có thể phân giải chuỗi công cụ và các quy tắc *_toolchain() theo ngôn ngữ chứa nhiều thông tin hơn thế nên sẽ mất nhiều thời gian hơn để tải.

Các 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 cách sử dụng hàm register_execution_platforms()
  2. Trên dòng lệnh, hãy sử dụ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 của một mục tiêu đã định cấu hình được xác định theo PlatformOptions.computeTargetPlatform() . Đây là danh sách các nền tảng vì cuối cùng chúng tôi cũng muốn hỗ trợ nhiều nền tảng mục tiêu, nhưng danh sách này chưa được triển khai.

Tập hợp chuỗi công cụ dùng cho một mục tiêu đã định cấu hình là do ToolchainResolutionFunction xác định. Đây là một chức năng 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 đích 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 giới hạn 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ả của mẫu này là một UnloadedToolchainContext, về cơ bản là bản đồ từ loại chuỗi công cụ (được biểu thị dưới dạng thực thể ToolchainTypeInfo) đến nhãn của chuỗi công cụ đã chọn. Phương thức này được gọi là "đã huỷ tải" vì không chứa chuỗi công cụ mà chỉ chứa nhãn của các 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 bằng cách 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 chuyển đổi dần sang hệ thống nêu trên. Để xử lý trường hợp mọi người sử dụng các giá trị cấu hình cũ, chúng tôi đã triển khai mối liên kết nền tảng để chuyển đổi giữa các cờ cũ và các quy tắc ràng buộc của nền tảng kiểu mới. Mã của họ bằng PlatformMappingFunction và sử dụng một "ngôn ngữ nhỏ" không phải Starlark.

Giới hạn

Đôi khi, một người 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 quy tắc ràng buộc cụ thể theo quy tắc
  • environment_group()/environment()
  • Các quy tắc ràng buộc đối với nền tảng

Các quy tắc ràng buộc cụ thể theo quy tắc chủ yếu được sử dụng trong Google đối với 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ó sẵn trong Bazel, nhưng mã nguồn có thể chứa thông tin tham chiếu đến đó. Thuộc tính chi phối việc này được gọi là constraints= .

môi trường_group() và môi trường()

Những quy tắc nà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 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 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; biểu mẫu 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=. Đoạn mã này khai báo những 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 thông số kỹ thuật mặc định trong quy tắc environment_group(). Mỗi môi trường thuộc một nhóm các ứng dụng ngang hàng có liên quan về chủ đề (chẳng hạn như "Cấu trúc CPU", "phiên bản JDK" hoặc "hệ điều hành trên thiết bị di động"). Định nghĩa về nhóm môi trường bao gồm những môi trường trong số này sẽ được "mặc định" hỗ trợ nếu không được thuộc tính restricted_to= / environment() chỉ định. Một quy tắc không 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 giá trị 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 nhất định. Ví dụ: bạn có thể sử dụng tính năng này để khiến tất cả các quy tắc *_test có thể kiểm thử được mà không cần từng thực thể phải khai báo rõ ràng chức năng này.

environment() được triển khai theo quy tắc thông thường, trong khi environment_group() vừa là lớp con của Target mà không phải Rule (EnvironmentGroup) và là một hàm có sẵn theo mặc định từ Starlark (StarlarkLibrary.environmentGroup()) mà 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 phần phụ thuộc tuần hoàn phát sinh vì mỗi môi trường cần phải khai báo nhóm môi trường chứa nó và mỗi nhóm môi trường cần khai báo các môi trường mặc định.

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

Quy trình triển khai quy trình kiểm tra ràng buộc diễn ra trong RuleContextConstraintSemanticsTopLevelConstraintSemantics.

Các quy tắc ràng buộc đối với nền tảng

Cách "chính thức" hiện tại để mô tả những nền tảng tương thích với mục tiêu là sử dụng cùng các điều kiện ràng buộc 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 cần chú ý ngăn chặn việc người khác tuỳ ý dựa vào mã của bạn. Nếu không, theo luật của Hyrum, mọi người sẽ dựa vào các 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à hiển thị: bạn có thể khai báo rằng một mục tiêu cụ thể chỉ có thể được phụ thuộc vào bằng cách sử dụng thuộc tính chế độ 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ẫu trên tên gói thay vì con trỏ đến bất kỳ mục tiêu cụ thể nào. (Đúng, đây là lỗi thiết kế.)

Việc này được triển khai ở những nơi sau:

  • Giao diện RuleVisibility biểu thị một nội dung khai báo về chế độ hiển thị. Đó có thể là một hằng số (công khai hoàn toàn 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 nhóm gói (danh sách gói được xác định trước), đến trực tiếp các gói (//pkg:__pkg__) hoặ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 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 qua 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 các phần phụ thuộc được thực hiện trong DependencyResolver.visitTargetVisibility và một vài vị trí khác.
  • Quá trình kiểm tra thực tế được thực hiện trong CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()

Các tập hợp lồng ghép

Thông thường, một mục tiêu đã đị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 mục tiêu riêng 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 mục tiêu đó có thể thực hiện điều tương tự. Ví dụ:

  • Tệp tiêu đề C++ dùng cho bản dựng
  • Các tệp đối tượng đại diện cho phần đóng bắc cầu của cc_library
  • Tập hợp các tệp .jar cần nằm trên classpath để quy tắc Java biên dịch hoặc chạy
  • Tập hợp các tệp Python trong phần đó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 cơ bản, chẳng hạn như sử dụng List hoặc Set, chúng ta sẽ sử dụng bộ nhớ bậc hai: nếu có một chuỗi N và mỗi quy tắc thêm một tệp, chúng ta sẽ có từ 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 đã nghĩ ra khái niệm về NestedSet. Đây là 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 của nó, do đó tạo thành một đồ thị tuần hoàn có hướng của các tập hợp. Đó là những thành phần bất biến và bạn có thể lặp lại thành phần. Chúng tôi xác định nhiều thứ tự lặp lại (NestedSet.Order): đặt hàng trước, thứ tự sau, cấu trúc liên kết (một nút luôn đứng sau đối tượng cấp trên) và "không quan tâm, nhưng mỗi lần đều 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 kết quả mà người dùng muốn. Các lệnh được biểu thị dưới dạng các bản sao của lớp Action và tệp được biểu thị dưới dạng các bản sao của lớp Artifact. Các chỉ số này được sắp xếp trong một biểu đồ tuần hoàn, có hướng và hai bên được gọi là "biểu đồ hành động".

Các cấu phần phần mềm có hai loại: cấu phần phần mềm nguồn (một loại 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át sinh (là cấu phần phần mềm cần được tạo). Các cấu phần phần mềm phát sinh có thể thuộ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 tính cập nhật bằng cách tính toán tổng kiểm tra với mtime dưới dạng lối tắt. Chúng tôi không kiểm tra tổng của tệp nếu thời gian của tệp không thay đổi.
  2. Các cấu phần phần mềm trong đường liên kết tượng trưng chưa được giải quyết. Bạn có thể kiểm tra tính cập 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, chúng có thể là các liên kết tượng trưng lơ lửng. Thường được sử dụng trong trường hợp một tệp rồi gói một số tệp vào một kho lưu trữ theo kiểu nào đó.
  3. Cấu phần phần mềm cây. Đây không phải là tệp đơn lẻ mà là cây thư mục. Bạn có thể kiểm tra các tệp này để cập nhật thông tin mới nhất bằng cách kiểm tra tập hợp tệp trong đó và nội dung của tệp. Chúng được biểu thị dưới dạng TreeArtifact.
  4. Cấu phần phần mềm siêu dữ liệu cố định. Các thay đổi đối với những cấu phần phần mềm này không kích hoạt quá trình tạo lại. Hàm này chỉ được dùng cho thông tin dấu bản dựng: chúng ta không muốn 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 dạng cây hoặc 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, chỉ là chúng ta chưa triển khai nó (tuy nhiên, chúng ta nên – việc tham chiếu một thư mục nguồn trong tệp BUILD là một trong một trong số ít vấn đề đã biết từ lâu và không chính xác với Bazel; chúng tôi đã triển khai loại công việc được kích hoạt bởi thuộc tính JVM BAZEL_TRACK_SOURCE_DIRECTORIES=1)

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

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

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

  • Dòng lệnh cần chạy
  • 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 đặt
  • Chú giải mô tả môi trường (chẳng hạn như nền tảng) cần chạy trong \

Ngoài ra, còn có một vài trường hợp đặc biệt khác, như viết một tệp có nội dung mà Bazel biết. Chúng là lớp con của AbstractAction. Hầu hết thao tác đều là SpawnAction hoặc StarlarkAction (giống nhau, nên được cho là không nên 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 cũng 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 lý thuyết, việc thực thi một hành động được biểu thị dưới dạng lệnh gọi ActionExecutionFunction. Việc ánh xạ từ cạnh phần phụ thuộc của biểu đồ hành động đến cạnh phần phụ thuộc của Skyframe được mô tả trong ActionExecutionFunction.getInputDeps()Artifact.key(), đồng thời có một số cách tối ưu hoá để giảm số lượng cạnh của Skyframe:

  • Các cấu phần phần mềm phát sinh không có SkyValue riêng. Thay vào đó, Artifact.getGeneratingActionKey() được dùng để tìm khoá cho hành động tạo ra khoá này
  • Các nhóm lồng ghép 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 được định cấu hình; các quy tắc Starlark bị hạn chế hơn vì các quy tắc này chỉ được phép đưa các hành động phát sinh vào một thư mục do cấu hình và gói của chúng xác định (thậm chí, 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 bắt nguồn ở bất cứ đâu.

Đây được coi là một tính năng không chính xác, nhưng việc loại bỏ tính năng này thực sự khó khăn vì nó giúp tiết kiệm đáng kể thời gian thực thi, chẳng hạn như khi một tệp nguồn cần được xử lý theo cách nào đó và tệp đó được tham chiếu bởi nhiều quy tắc (sóng tay). Việc này sẽ tốn một số dung lượng RAM: mỗi phiên bản của một thao tác được chia sẻ cần được lưu trữ riêng trong bộ nhớ.

Nếu 2 hành động tạo ra cùng một tệp đầu ra, thì chú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 ở SkyframeActionExecutor.findAndStoreArtifactConflicts() và là một trong số ít địa điểm ở 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à khi Bazel thực sự bắt đầu chạy các hành động tạo bản dựng, chẳng hạn như các lệnh tạo kết quả đầu ra.

Việc đầu tiên Bazel làm 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 xây dựng. Logic cho việc này được mã hoá bằng TopLevelArtifactHelper; nói một cách khái quát, đó là filesToBuild của mục tiêu được định cấu hình trên dòng lệnh và nội dung của nhóm đầu ra đặc biệt nhằm mục đích thể hiện rõ ràng "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 Bazel cần cung cấp các hành động được thực thi cục bộ bằng một cây nguồn đầy đủ. Việc này được lớp SymlinkForest xử lý và hoạt động bằng cách ghi lại mọi mục tiêu dùng trong giai đoạn phân tích, đồng thời xây dựng một cây thư mục liên kết tượng trưng cho mọi gói có một mục tiêu đã sử dụng tại vị trí thực tế. Một cách 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à ngoài mong muốn bởi vì:

  • Tệp này thay đổi các 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 khác (thường là trường hợp xuất hiện phổ biến)
  • Đ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 trên máy
  • Thao tác này yêu cầu chuyể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 những đường dẫn lớp Java và C++ bao gồm cả đường dẫn)
  • 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 trong bộ nhớ đệm của thao tác
  • --package_path đang dần ngừng hoạt động

Sau đó, Bazel bắt đầu truyền tải biểu đồ hành động (biểu đồ hai bên, có định hướng bao gồm các thao tác cũng như các cấu phần phần mềm đầu vào và đầu ra của chúng) và các thao tác đang chạy. Quá trình thực thi mỗi hành động được biểu thị bằng một bản sao của lớp SkyValue ActionExecutionValue.

Vì việc chạy một thao tác rất 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ể bị truy cập phía sau Skyframe:

  • ActionExecutionFunction.stateMap chứa dữ liệu để giúp Skyframe khởi động lại ActionExecutionFunction với mức giá rẻ
  • 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 chứa bộ nhớ đệm riêng

Bộ nhớ đệm thao tác cục bộ

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, nó vẫn có thể là một lượt truy cập trong bộ nhớ đệm thao tác cục bộ. Phương thứ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ự vào ổ đĩa. Nghĩa là khi khởi động một máy chủ Bazel mới, người dùng có thể nhận được các lượt truy cập vào bộ nhớ đệm của thao tác 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 ngược với tên gọi, bản đồ này còn là bản đồ từ đường dẫn của một cấu phần phần mềm bắt nguồn đế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, đầu ra và tổng kiểm của chúng
  2. "Khoá hành động", thường là dòng lệnh được thực thi, nhưng nhìn chung, đại diện cho mọi nội dung không được tổng kiểm tra 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 được ghi)

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

Loại bỏ dữ liệu đầu vào và khám phá dữ liệu đầu vào

Một số hành động phức tạp hơn so với việc 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ó 2 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 là không thực sự cần thiết. Ví dụ chuẩn là C++, trong đó bạn nên đoán đơn giản về những tệp tiêu đề mà tệp C++ sử dụng sau khi đóng bắc cầu để chúng ta không cần gửi mọi tệp đến các bộ thực thi từ xa; do đó, chúng ta có tuỳ chọn không đăng ký mọi tệp tiêu đề là "triển khai", mà chỉ quét tệp nguồn để các tiêu đề đi kèm và chỉ đánh dấu các tệp tiêu đề được sử dụng trong các câu lệnh #include được đề cập quá mức trong tuỳ chọn #include được đề cập quá mức trong tuỳ chọn #include
  • Khi thực thi, thao tác có thể nhận thấy một số tệp không được dùng trong quá trình thực thi. Trong C++, tệp này được gọi là "tệp .d": trình biên dịch cho biết các tệp tiêu đề nào được sử dụng sau dữ kiện, và để tránh gặp khó khăn khi có sự gia tăng kém hơn Make, Bazel tận dụng dữ kiện này. Phương thức này đưa ra thông tin ướ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 phương thức này được triển khai bằng các phương thức trên Hành động:

  1. Action.discoverInputs() sẽ được gọi. Thuộc tính này sẽ trả về một tập hợp Cấu phần phần mềm lồng ghép được xác định là bắt buộc. Đây phải là cấu phần phần mềm nguồn để biểu đồ hành động không có cạnh phụ thuộc 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. Khi kết thúc Action.execute(), hành động này có thể gọi Action.updateInputs() để cho Bazel biết rằng không cần tất cả dữ liệu đầu vào. Đ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 một dữ liệu đầu vào đã sử dụng được báo cáo là chưa dùng đến.

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 máy chủ khởi động lại), Bazel sẽ tự gọi updateInputs() để tập hợp dữ liệu đầu vào phản ánh kết quả khám phá đầu vào và lược bớt đã thực hiện trước đó.

Các hành động 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: Strategies/ActionContexts

Một số thao tác có thể chạy 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 này được gọi là ActionContext (hoặc Strategy, vì chúng ta chỉ đi được một nửa sau khi đổi tên...)

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ề bối cảnh hành động mà thực thể đó có. Việc 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 của Java tham chiếu đến 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 từ các ngữ cảnh có sẵn và được chuyển tiếp đến ActionExecutionContextBlazeExecutor .
  3. Hành động 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 đó...)

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 chúng. 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ộ lẫn từ xa, sau đó sử dụng chiến lược nào kết thúc trước.

Một chiến lược đáng chú ý là triển khai các quy trình trình thực thi liên tục (WorkerSpawnStrategy). Ý tưởng là một số công cụ có thời gian khởi động lâu và do đó nên được sử dụng lại giữa các hành động thay vì bắt đầu lại cho mỗi hành động (Điều này biểu thị một vấn đề tiềm ẩn về độ chính xác, vì Bazel dựa vào lời hứa về quy trình trình thực thi rằng nó 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 thì bạn cần khởi động lại quy trình worker. Việc một trình thực thi 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 tra cho công cụ dùng bằng WorkerFilesHash. Điều này phụ thuộc vào việc biết đầu vào nào của hành động đại diện cho một phần của công cụ và đầu vào nào đại diện cho đầu vào; điều này là 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ề các chiến lược khác nhau để thực hiện các hành động tại đây.
  • Thông tin về chiến lược động, trong đó chúng tôi chạy một hành động cả cục bộ và từ xa để xem chiến lược nào hoàn thành trước đã có tại đây.
  • Bạn có thể xem thông tin về những chi tiết phức tạp của việc thực thi các thao tác 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 hành động. Số lượng thao tác cục bộ nên chạy song song giữa các thao tác: càng yêu cầu nhiều tài nguyên thì càng ít thực thể chạy cùng lúc để tránh làm quá tải máy cục bộ.

Việc này được triển khai trong lớp ResourceManager: mỗi hành động phải được chú thích với thông tin ước tính về tài nguyên cục bộ mà hành động đó yêu cầu dưới dạng một thực thể ResourceSet (CPU và RAM). Sau đó, khi ngữ cảnh hành động thực hiện việc gì đó yêu cầu 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ề 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 yêu cầu một vị trí riêng biệt trong thư mục đầu ra nơi đặt các đầu ra của thao tác đó. Vị trí của các cấu phần phần mềm phát 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 mâu thuẫn nhau:

  1. Nếu hai cấu hình có thể xảy ra trong cùng một bản dựng, chúng nên có các thư mục khác nhau để cả hai đều có phiên bản riêng của cùng một hành động; ngược lại, nếu hai cấu hình không thống nhất về nhau, 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 ("xung đột hành động")
  2. Nếu hai cấu hình đại diện "gần giống nhau", 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 sử dụng lại cho nhau nếu các dòng lệnh khớp. Ví dụ: việc thay đổi tuỳ chọn dòng lệnh đối với trình biên dịch Java sẽ không dẫn đến việc các thao tác biên dịch C++ sẽ không chạy lại được.

Cho đến nay, chúng tôi chưa đưa ra cách thức nguyên tắc để giải quyết vấn đề này. Cách này có những điểm tương đồng với vấn đề về việc cắt bỏ cấu hình. Bạn có thể xem nội dung thảo luận dài hơn về các lựa chọn tại đây. Các khía cạnh chính có vấn đề là các quy tắc Starlark (các tác giả thường không quen thuộc với Bazel) và các khía cạnh bổ sung một chiều khác cho 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, tổng kiểm tra tập hợp các lượt chuyển đổi cấu hình Starlark sẽ được thêm để người dùng không gây ra xung đột hành động. Chưa bao giờ 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.

Kiểm thử

Bazel có tính năng hỗ trợ phong phú cho việc chạy kiểm thử. Cách 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 kiểm thử nhiều lần song song (để loại bỏ hoặc thu thập dữ liệu về thời gian)
  • Kiểm thử phân đoạn (chia các trường hợp kiểm thử trong cùng một bài kiểm thử cho nhiều quy trình để tăng tốc độ)
  • Chạy lại kiểm thử không ổn định
  • Nhóm các bài kiểm thử thành các 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ấu phần phần mềm có bản dựng dẫn đến quá trình kiểm thử đang chạy. Đâ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 kiểm thử sẽ được chạy
  • Số lượng phân đoạn mà kiểm thử sẽ đượ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 kiểm thử cần chạy

Việc xác định loại hoạt động kiểm thử cần 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. Việc mở rộng được triển khai trong TestsForTargetPatternFunction. Một điểm hạn chế đáng ngạc nhiên là nếu một bộ kiểm thử khai báo không có bài kiểm thử nào, thì đó là mọi bài kiểm thử trong gói. Việc này được triển khai trong Package.beforeBuild() bằng cách thêm 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 hoạt động kiểm thử đượ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. 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 không thể định cấu hình các thuộc tính quy tắc có thể được lọc là vì quá trình này xảy ra trước giai đoạn phân tích nên cấu hình sẽ không có sẵn.

Sau đó, quy trình này sẽ đượ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ẽ được lọc ra và các thử nghiệm được chia thành các chương trình kiểm thử độc quyền và không độc quyền. Sau đó, dữ liệu này được đưa vào AnalysisResult, giúp ExecutionTool biết cần chạy chương trình kiểm thử nào.

Để quy trình chi tiết này diễn ra rõ ràng hơn, toán tử truy vấn tests() (được triển khai trong TestsFunction) có sẵn để cho biết quy trình kiểm thử nào sẽ được chạy khi một mục tiêu cụ thể được chỉ định trên dòng lệnh. Thật không may, đây là một việc triển khai lại, vì vậy có thể việc này khác với những điều trên theo nhiều cách tinh vi.

Chạy kiểm thử

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

Hoạt động kiểm thử được chạy theo một giao thức chi tiết sử dụng các biến môi trường để cho biết các hoạt động kiểm thử dự kiến. 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ó thể mong đợi từ Bazel tại đây. Đơn giản nhất, mã thoát bằng 0 có nghĩa là thành công, còn mọi thứ 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. Những tệp này được đặt trong "thư mục nhật ký kiểm thử", là thư mục con có tên là 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 nêu chi tiết các trường hợp kiểm thử riêng lẻ trong phân đoạn kiểm thử
  • test.log, đầu ra trên bảng điều khiển của chương trình kiểm thử. stdout và stderr không được phân tách.
  • test.outputs, "thư mục đầu ra chưa khai báo"; thư mục này được sử dụng cho các chương trình kiểm thử muốn xuất tệp ngoài nội dung chúng in ra thiết bị đầu cuối.

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

Một số kiểm thử cần được thực thi ở chế độ độc quyền, ví dụ: không song song với các kiểm thử khác. Bạn có thể gợi ý đ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 quy 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 quy 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 có kết quả đầu ra được kết xuất khi thao tác kết thúc, người dùng có thể yêu cầu truyền trực tuyến kết quả kiểm thử để nhận thông báo về tiến trình của một chương trình kiểm thử diễn ra trong thời gian dài. Điều này được chỉ định bởi tuỳ chọn dòng lệnh --test_output=streamed và ngụ ý quá trình thực thi kiểm thử độc quyền để kết quả của nhiều kiểm thử không được đặt xen kẽ.

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

Bạn có thể xem kết quả của các hoạt động 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 sự kiện này được kết xuất vào Giao thức sự kiện bản dựng và được AggregatingTestListener phát lên bảng điều khiển.

Tập hợp mức độ phù hợp

Mức độ phù hợp được báo cáo bởi 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ượt chạy 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ử để bật tính năng thu thập mức độ sử dụng và xác định vị trí các tệp mức độ sử dụng được ghi bởi(các) thời gian chạy về mức độ sử dụng. Sau đó, hệ thống sẽ chạy kiểm thử. Bài kiểm thử có thể tự chạy nhiều quy trình phụ và bao gồm các phần được viết bằng nhiều ngôn ngữ lập trình (với các thời gian chạy thu thập phạm vi bao phủ 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 xen kẽ của collect_coverage.sh do các chiến lược kiểm thử thực hiện và đòi hỏi collect_coverage.sh phải có trong dữ liệu đầu vào của bài kiểm thử. Điều này được bổ sung bởi thuộc tính ngầm ẩn :coverage_support. Thuộc tính này đượ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 mức độ sử dụng được thêm vào thời gian biên dịch (chẳng hạn như C++) và các ngôn ngữ khác thực hiện đo lường trực tuyến, nghĩa là khả năng đo lường mức độ sử dụng được thêm vào thời điểm thực thi.

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

Phạm vi cơ sở hiện đang bị hỏng.

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 theo mức độ phù hợp trực tuyến, bạn có thể sử dụng tuỳ chọn này trong thời gian chạy để quyết định tệp nào cần đo lường. Dữ liệu này cũng được dùng để triển khai mức độ phù hợp 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à chương trình kiểm thử cần để tạo các tệp LCOV mà Bazel yêu cầu từ đó. 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 các tệp .gcno trong quá trình biên dịch. Các phương thức 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 bạn bật chế độ mức độ sử dụng.

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

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

Chúng tôi cũng tạo một "báo cáo mức độ sử dụng", hợp nhất mức độ sử dụng đã thu thập được cho mọi lượt kiểm thử trong lệnh gọi Bazel. Phương thức này do CoverageReportActionFactory xử lý và được gọi từ BuildView.createResult() . Lớp này 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 lượt kiểm thử đầu tiên được thực thi.

Công cụ truy vấn

Bazel có một chút ngôn ngữ dùng để hỏi Trợ lý những điều khác nhau về các biểu đồ. Các loại truy vấn sau sẽ đượ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 phân lớp con AbstractBlazeQueryEnvironment. Bạn có thể thực hiện thêm các hàm truy vấn khác bằng cách phân 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ả vào một số cấu trúc dữ liệu, query2.engine.Callback sẽ được chuyển vào QueryFunction để gọi kết quả mà nó muốn trả về.

Kết quả của một truy vấn có thể được phát hành 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 nhỏ 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 _all _thông tin mà hoạt động tải gói cung cấp để người dùng có thể khác biệt kết quả và xác định xem một mục tiêu cụ thể đã thay đổi hay chưa. Do đó, các giá trị thuộc tính cần phải có khả năng chuyển đổi tuần tự. Đó là lý do chỉ có rất í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. Cách giải quyết 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à cách giải quyết thoả đáng và sẽ rất tốt nếu bạn nên nâng cấp 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 phân lớp con BlazeModule (tên này là một di tích của lịch sử Bazel khi mô-đun này từng được gọi là Blaze) và nhận thông tin về các sự kiện khác nhau trong quá trình thực thi một lệnh.

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

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

Bộ điểm mở rộng mà BlazeModule cung cấp có phần lộn xộn. Đừng coi đó là ví dụ về các nguyên tắc thiết kế hay.

Xe buýt 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 luồng 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 đó và các mô-đun có thể đăng ký trình nghe cho những sự kiện mà họ quan tâm. Ví dụ: những sự kiện 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 sẽ được tạo đã được xác định (TargetParsingCompleteEvent)
  • Cấu hình cấp cao nhất đã được xác định (BuildConfigurationEvent)
  • Mục tiêu đã được tạo, thành công hay không (TargetCompleteEvent)
  • Đã chạy thử nghiệm (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 (các sự kiện này là BuildEvent). Điều này không chỉ cho phép BlazeModule mà còn cho phép cả những phần tử 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 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ù 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 để xây dựng), nhưng Bazel số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 yếu tố trừu tượng dùng để làm cầu nối cho 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 nội dung khai báo như sau:

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

Hiện đã có kết quả trong kho lưu trữ có tên là @foo. Trong trường hợp này trở nên phức tạp, 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ác tệp này có thể được dùng để tải mã Starlark mới. Mã này có thể dùng để xác định các quy tắc 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 phần được mô tả bằng câu lệnh load(). Chỉ mục phân đ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.

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

Trước khi có thể sử dụng mã của kho lưu trữ cho Bazel, bạn cần phải fetched mã này. Điều này dẫn đến việc 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 cần có kho lưu trữ và tạo 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 cần tránh tải xuống lại các mục trong trường hợp Skyframe khởi động lại, nhưng đây không phải là một lý do quá chắc chắn)
  3. RepositoryDelegatorFunction tìm ra quy tắc kho lưu trữ mà công cụ này yêu cầu tìm nạp bằng cách lặp lại trên 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 sẽ triển khai quá trình tìm nạp kho lưu trữ; đó là cách triển khai Starlark cho 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. Bộ nhớ đệm cho các tệp đã tải xuống được khoá theo giá trị tổng kiểm (RepositoryCache). Việc này đòi hỏi phải có giá trị tổng kiểm trong tệp WORKSPACE nhưng điều đó vẫn tốt cho tính bảo mật. Mọi thực thể máy chủ Bazel đều chia sẻ dữ liệu này trên cùng một máy trạm, bất kể chúng đang chạy trên không gian làm việc hay cơ sở đầu ra nào.
  2. Một "tệp đánh dấu" được ghi cho mỗi kho lưu trữ trong $OUTPUT_BASE/external chứa giá trị tổng kiểm của quy tắc đã dùng để tìm nạp kho lưu trữ đó. Nếu máy chủ Bazel khởi động lại nhưng giá trị tổng kiểm không thay đổi, thì máy chủ 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 để tra cứu cấu phần phần mềm cần tải xuống. Điều này sẽ hữu ích trong các chế độ cài đặt doanh nghiệp khi Bazel không tìm nạp các nội dung ngẫu nhiên từ Internet. Việc này do DownloadManager triển khai .

Sau khi một kho lưu trữ được tải 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 một vấn đề vì Bazel thường kiểm tra tính cập 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 chúng và các cấu phần phần mềm này cũng không hợp lệ khi định nghĩa về kho lưu trữ thay đổi. Do đó, FileStateValue của 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. Do ExternalFilesHelper xử lý.

Liên kết kho lưu trữ

Có thể xảy ra việc nhiều kho lưu trữ muốn phụ thuộc vào cùng một kho lưu trữ, nhưng trong các phiên bản khác nhau (đây là một trường hợp 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ó lẽ cả hai tệp nhị phân đó sẽ tham chiếu đến Guava với các nhãn bắt đầu từ @guava// và muốn nghĩa là các phiên bản khác nhau của Guava.

Do đó, Bazel cho phép một bản á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//) kho lưu trữ của kho lưu trữ còn lại.

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

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

  • Package.Builder.repositoryMapping dùng để biến đổi các thuộc tính có giá trị nhãn của các 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 nội dung như $(location) không được phân tích cú pháp trong giai đoạn tải)
  • BzlLoadFunction để phân giải các 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ự 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 bị giới hạn ở hoạt động tương tác với hệ thống tệp, kiểm soát quy trình và nhiều yếu tố 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 dữ liệu đầu ra từ bảng điều khiển có vẻ như là một việc đơn giản, nhưng 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ó một đầu ra thiết bị đầu cuối đẹp mắt và đầy màu sắc cũng như có một máy chủ chạy trong thời gian dài khiến việc này không còn đơn giản.

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

Theo mặc định, kết quả được in bằng chuỗi ký tự thoát ANSI. Khi không mong muốn (--color=no), các giá trị này sẽ bị AnsiStrippingOutputStream xoá. Ngoài ra, System.outSystem.err được chuyển hướng đến các luồng đầu ra này. Việc này nhằm đảm bảo thông tin gỡ lỗi có thể được in bằng System.err.println() mà vẫn được đưa vào đầ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 chú ý đến việc 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ó quá trình tải stdout nào diễn ra.

Thông báo ngắn (lỗi, cảnh báo và các nội dung 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 những gì một bài đă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ố khác) và chúng có thể có Location (vị trí trong mã nguồn đã gây ra sự kiện).

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

Một số EventHandler cũng cho phép đăng các sự kiện mà cuối cùng cũng sẽ tìm được xe buýt sự kiện (các Event thông thường _not _xuất hiện ở đó). Đây là các cách triển khai ExtendedEventHandler và mục đích chính 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 sự kiện được đăng lên EventBus đều triển khai giao diện này; chỉ những sự kiện được ExtendedEventHandler lưu vào bộ nhớ đệm (mặc dù điều này sẽ tốt và hầu hết mọi thứ đã được thực thi; tuy nhiên, điều này không được thực thi)

Đầu ra của dòng lệnh chủ yếu được phát thông qua UiEventHandler. Tệp này chịu trách nhiệm về tất cả định dạng đầu ra ưa thích cũng như báo cáo tiến trình mà Bazel thực hiện. Báo cáo có hai dữ liệu đầu vào:

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

Kết nối trực tiếp duy nhất mà bộ máy thực thi lệnh (ví dụ: phần còn lại của Bazel) có với luồng RPC đến ứng dụng là thông qua Reporter.getOutErr(). Phương thức này cho phép truy cập trực tiếp vào các luồng này. Hàm 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).

Lập hồ sơ Bazel

Bazel rất nhanh. Bazel cũng có tốc độ chậm, vì các bản dựng có xu hướng phát triển cho đến khi đạt đến ngưỡng của tính năng dễ hiểu. Vì lý do này, Bazel thêm một trình phân tích tài nguyên có thể dùng để phân tích 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 thích hợp là Profiler. Tính năng này được bật theo mặc định, mặc dù tính năng này chỉ ghi lại dữ liệu rút gọn để mức hao tổn của thuộc tính có thể chấp nhận được. Dòng lệnh --record_full_profiler_data giúp trình phân tích cú pháp ghi lại mọi thứ có thể.

Ứng dụng cung cấp một hồ sơ ở định dạng trình phân tích tài nguyên trên Chrome. Bạn có thể xem hồ sơ này tốt nhất trong Chrome. Mô hình dữ liệu là mô hình của các ngăn xếp tác vụ: một ngăn xếp có thể bắt đầu và kết thúc tác vụ và chúng được lồng ghép gọn gàng vào nhau. Mỗi luồng Java sẽ có ngăn xếp tác vụ riêng. TODO: Chức năng này hoạt động như thế nào với các hành động và kiểu truyền tiếp tục?

Trình phân tích tài nguyên bắt đầu và dừng tương ứng trong BlazeRuntime.initProfiler()BlazeRuntime.afterCommand(), đồng thời cố gắng tồn tại lâu nhất có thể để chúng tôi 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 có trạng thái đóng đại diện cho việc kết thúc tác vụ. Bạn nên sử dụng câu lệnh này với câu lệnh try-with-resources.

Chúng ta cũng thực hiện phân tích bộ nhớ cơ bản trong MemoryProfiler. Tính năng 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 cũng như hành vi GC.

Thử nghiệm Bazel

Bazel có 2 loại kiểm thử chính: một loại kiểm thử quan sát Bazel là một "hộp đen" và một loại chỉ chạy giai đoạn phân tích. Chúng tôi gọi các quy trình kiểm thử tích hợp trước đây là "kiểm thử đơn vị" và "kiểm thử đơn vị", mặc dù chúng giống với các quy trình 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ế cần thiết.

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

  1. Các tính năng được triển khai bằng khung kiểm thử bash rất công phu trong src/test/shell
  2. Một số giá trị đượ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 trang bị đầy đủ cho hầu hết các trường hợp kiểm thử. Vì là khung Java, nên khung này 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ề lớp BuildIntegrationTestCase trong kho lưu trữ Bazel.

Các hoạt động kiểm thử phân tích được triển khai dưới dạng lớp con của BuildViewTestCase. Bạn có thể dùng một hệ thống tệp Scratch để 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 được đị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ả phân tích.