Cơ sở mã Bazel

Báo cáo vấn đề Xem nguồn Nightly/3}

Tài liệu này là nội dung mô tả về cơ sở mã và cấu trúc của Bazel. Ứng dụng này dành cho những người sẵn sàng đóng góp cho Bazel, không phải 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à khoảng 260 mã kiểm thử KLOC) và không ai biết rõ về toàn bộ khung cảnh: mọi người đều biết rất rõ thung lũng cụ thể của họ, nhưng ít ai biết nằm trên các ngọn đồi theo mọi hướng.

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

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

Các 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 trở lại GitHub.

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

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

Mỗi phiên bản máy chủ đều có một cây nguồn liên kết ("không gian làm việc") 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ể tránh né điều 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++ ("ứng dụng") sẽ được kiểm soát. Bot này thiết lập một quy trình máy chủ phù 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, hệ thống sẽ thực hiện việc đó. Đây là nơi bắt nguồn việc triển khai máy chủ.
  2. Kiểm tra xem có phiên bản máy chủ nào đang hoạt động hay không: phiên bản đó đang chạy, có các tuỳ chọn khởi động phù hợp và sử dụng đúng thư mục không gian làm việc. Công cụ 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á với cổng mà máy chủ đang theo dõi.
  3. Nếu cần, hãy tắt quy trình 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 một quy trình máy chủ phù hợp đã sẵn sàng, lệnh cần chạy sẽ được thông báo 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. Việc này được triển khai bằng cơ chế khoá chi tiết với các phần trong C++ và các phần trong Java. Bạn có thể dùng một số cơ sở hạ tầng để chạy nhiều lệnh song song, vì việc không thể chạy bazel version song song với một lệnh khác nên có phần không đẹp. Trình chặ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à ứng dụng cần trả về. Một vấn đề đáng chú ý là việc triển khai bazel run: công việc của lệnh này là chạy tác vụ 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ì lệnh này không có cửa sổ dòng lệnh. Thay vào đó, phương thức này sẽ cho ứng dụng biết tệp nhị phân nào nên ujexec() và với đối số nào.

Khi nhấn Ctrl-C, ứng dụng sẽ dịch thao tác này thành lệnh gọi Cancel (Huỷ) trên kết nối gRPC. Lệnh này sẽ cố gắng chấm dứt lệnh càng sớm càng tốt. Sau khi nhấn tổ hợp phím 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 các thư mục hơi phức tạp trong quá trình tạo bản dựng. Bạn có thể xem nội dung mô tả đầy đủ trong Bố cục thư mục đầu ra.

"Không gian làm việc" là cây nguồn mà Bazel điều hành. Nó thường tương ứng với nội dung bạn đã xem từ chế độ kiểm soát nguồn.

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

"Số lượt cài đặt" là nơi Bazel được trích xuất. Việc này được thực hiện tự động và mỗi phiên bản Bazel sẽ nhận được một thư mục con dựa trên giá trị tổng kiểm của thư mục đó trong số lượt cài đặt. Theo mặc định, giá trị này nằm tại $OUTPUT_USER_ROOT/install và bạn 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 liên kết với một không gian làm việc cụ thể ghi dữ liệu vào. Mỗi cơ sở đầu ra có tối đa một thực thể máy chủ Bazel có thể chạy bất cứ lúc nào. Thường thì lúc $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. Ngoài ra, tuỳ chọn này rất hữu ích để khắc phục giới hạn xảy ra khi chỉ một thực thể Bazel có thể chạy trong không gian làm việc bất kỳ tại bất kỳ thời điểm nào.

Thư mục đầu ra chứa các thành phần sau:

  • Các kho lưu trữ bên ngoài đã tìm nạp tại $OUTPUT_BASE/external.
  • Thư mục gốc exec, 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 này nằm tại $OUTPUT_BASE/execroot. Trong quá trình dựng, thư mục đang hoạt động là $EXECROOT/<name of main repository>. Chúng tôi đang dự định thay đổi thành $EXECROOT, mặc dù đây là một kế hoạch dài hạn vì thay đổi này rất không tương thích.
  • Tệp được tạo trong quá trình tạo bản dựng.

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

Sau khi máy chủ Bazel có quyền kiểm soát và nhận được thông báo về một lệnh mà máy chủ cần thực thi, trình tự 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. Lệnh này quyết định xem lệnh có cần không gian làm việc để chạy hay không (hầu hết mọi lệnh ngoại trừ những lệnh không liên quan gì đến mã nguồn, chẳng hạn như phiên bản hoặc trợ giúp) và liệu 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ú giải @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ó nhiều tuỳ chọn dòng lệnh, đượ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ố sự kiện trong số này được xuất ra bên ngoài Bazel theo sự bảo vệ của Giao thức sự kiện bản dựng để cho mọi người biết tiến trình của bản dựng.

  5. Lệnh này sẽ 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, mức độ sử dụng, 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/... sẽ được phân giải. Việc này được triển khai trong AnalysisPhaseRunner.evaluateTargetPatterns() và được 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 đồ không chu trình có hướng gồm 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 các mục tiêu cấp cao nhất được yêu cầu sẽ được chạy.

Tuỳ chọn dòng lệnh

Các tuỳ chọn dòng lệnh cho lệnh gọi Bazel được mô tả trong đối tượng OptionsParsingResult, đối tượng này chứa một bản đồ từ "các lớp tuỳ chọn" đến các giá trị của các tuỳ chọn. "Lớp tuỳ chọn" là một lớp con của OptionsBase, dùng để nhóm các tuỳ chọn dòng lệnh 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 sẽ là một lớp con của FragmentOptions và cuối cùng được gói vào một đối tượng BuildOptions.
  2. Các lựa chọn liên quan đến cách Bazel thực thi các thao tác (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++ có bao gồm cả quá trình quét hay không) sẽ được đọc trong giai đoạn thực thi, nhưng điều này luôn đòi hỏi phải có ống nước rõ ràng vì khi đó BuildConfiguration không có sẵn. Để biết thêm thông tin, hãy xem phần "Cấu hình".

CẢNH BÁO: Chúng tôi muốn giả định rằng các thực thể OptionsBase là không thể thay đổi và sử dụng chúng theo cách đó (chẳng hạn như một phần của SkyKeys). Trường hợp này không đúng và việc sửa đổi chúng là một cách rất hay để phá vỡ Bazel theo những cách tinh tế mà khó gỡ lỗi. Thật không may, làm cho 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 tạo trước khi bất kỳ ai khác có cơ hội giữ lại tham chiếu đến đối tượng đó và trước khi equals() hoặc hashCode() được gọi trên đối tượng này là chấp nhận được.)

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

  1. Một số thiết bị được nối cứng vào Bazel (CommonCommandOptions)
  2. Từ chú giải @Command trên mỗi lệnh Bazel
  3. Từ ConfiguredRuleClassProvider (đây là các tuỳ chọn dòng lệnh liên quan đến từng ngôn ngữ lập trình)
  4. Các quy tắc của 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 một lớp con FragmentOptions có chú thích @Option. Chú giải này 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 cho giá trị của tuỳ chọn dòng lệnh thường là một loại đơ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 cá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 kiểu dữ liệu là việc triển khai com.google.devtools.common.options.Converter.

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

Bazel kinh doanh phần mềm xây dựng, hoạt động 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 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 nơi một nhà phát triển làm việc; nó thường đại diện cho một dự án duy nhất. Đối tượng cấp trên của Bazel là Blaze, đã vận hành 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 từ đó được gọi là "kho lưu trữ chính", còn các kho lưu trữ khác được gọi là "kho lưu trữ bên ngoài".

Kho lưu trữ được đánh dấu bằng một tệp có tên là WORKSPACE (hoặc WORKSPACE.bazel) trong thư mục gốc của kho lưu trữ đó. Tệp này chứa thông tin "toàn cục" đối với toàn bộ bản dựng, ví dụ: tập hợp các kho lưu trữ bên ngoài có sẵn. Tệp này hoạt động giống như một tệp Starlark thông thường, tức là một tệp có thể load() các tệp Starlark khác. Tệp này thường dùng để lấy các kho lưu trữ cần thiết mà kho lưu trữ được tham chiếu rõ ràng (chúng tôi gọi đây là "mẫu deps.bzl")

Mã của 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 kết hợp với nhau; việc này được thực hiện bằng SymlinkForest. Thao tác này sẽ 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/.. (một gói có tên là external trong kho lưu trữ chính; đó là lý do chúng ta di chuyển khỏi kho lưu trữ đó)

Gói

Mỗi kho lưu trữ bao gồm các gói, một tập hợp các tệp có liên quan và thông số kỹ thuật của các phần phụ thuộc. Các giá trị này do một tệp có tên là BUILD hoặc BUILD.bazel chỉ định. Nếu cả hai đều tồn tại, Bazel ưu tiên BUILD.bazel; lý do khiến các tệp BUILD vẫn được chấp nhận là do đối tượng cấp trên của Bazel là 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 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: các 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 _thay đổi các gói khác, vì các khối cầu hồi quy dừng ở ranh giới gói và do đó, sự hiện diện của tệp BUILD sẽ dừng việc đệ quy.

Việc đánh giá tệp BUILD được gọi là "tải gói". Lệnh 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 bạn phải có 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. Chủ yếu là một bản đồ từ một chuỗi (tên của mục tiêu) đến chính mục tiêu đó.

Một phần lớn phức tạp trong quá trình tải gói là hiệu ứng cầu nối: Bazel không yêu cầu mọi tệp nguồn phải được liệt kê rõ ràng và thay vào đó có thể chạy các khối cầu (chẳng hạn như glob(["**/*.java"])). Không giống như shell, tính năng này hỗ trợ các khối cầu đệ quy xuống các thư mục con (nhưng không vào các gói con). Điều này đòi hỏi quyền truy cập vào hệ thống tệp và vì việc đó có thể chậm, nên chúng tôi sẽ triển khai tất cả các loại thủ thuật để hệ thống 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 vệt sáng liên quan đến Skyframe nhanh chóng và hạnh phúc
  • SkyframeHybridGlobber, một phiên bản sử dụng Skyframe và quay về hiệu ứng cầu vồng cũ để tránh việc "Skyframe khởi động lại" (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 tệp WORKSPACE và các thành phần này không phù hợp với các gói thực. Đây là một lỗi thiết kế vì các đối tượng mô tả gói thông thường không được chứa các trường mô tả nội dung khác. 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à nên có sự tách biệt hơn giữa việc phân tích cú pháp tệp WORKSPACE với việc phân tích cú pháp các gói thông thường để Package không cần phục vụ cho nhu cầu của cả hai. Thật không may, điều này rất khó thực hiện vì cả hai kết nối khá sâu sắc.

Nhãn, Mục tiêu và Quy tắc

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

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

Một loại quy tắc (chẳng hạn như "thư viện C++") được gọi là "lớp quy tắc". Các lớp quy tắc có thể được triển khai trong Starlark (hàm rule()) hoặc trong Java (được 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ố bộ quy tắc cũ (chẳng hạn như Java hoặc C++) hiện vẫn còn trong Java.

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 "bẩm sinh" được Baazel biết đế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 đó (chẳng hạn như srcs, deps): loại, giá trị mặc định, quy tắc ràng buộc, v.v.
  2. Chuyển đổi cấu hình và các khía cạnh đi kèm với mỗi thuộc tính, nếu có
  3. Triển khai quy tắc
  4. Trình cung cấp thông tin bắc cầu mà quy tắc "thường" tạo ra

Lưu ý về thuật ngữ: Trong cơ sở mã, chúng tôi 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ỉ nên được sử 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ù tên RuleClass có "lớp", nhưng không có mối quan hệ kế thừa Java giữa lớp quy tắc và mục tiêu thuộc loại đó.

Khung chân trời

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

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

Bất cứ khi nào SkyFunction yêu cầu một phần phụ thuộc không có sẵn, getValue() sẽ trả về giá trị rỗng. Sau đó, hàm này sẽ 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, rồi khởi động lại hàm này từ đầu — chỉ lần này, lệnh gọi getValue() mới thành công với kết quả không rỗng.

Hệ quả của việc này là mọi phép tính được thực hiện bên trong SkyFunction trước khi khởi động lại đều phải lặp lại. Tuy nhiên, việc này không bao gồm việc cần làm để đá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 hàng loạt (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 được tính toán theo các SkyFunction khác nhau để 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ì 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, bằng cách sử dụng SkyFunction.Environment.getState() hoặc giữ một bộ nhớ đệm tĩnh đặc biệt "phía sau Skyframe".

Về cơ bản, chúng tôi cần các loại giải pháp này vì chúng tôi thường có hàng trăm nghìn nút Skyframe trong khi bay và Java không hỗ trợ các luồng nhẹ.

Starlark

Starlark là ngôn ngữ dành riêng cho từng miền mà mọi người sử dụng để định cấu hình và mở rộng Bazel. Thư viện này được xem là một tập hợp con bị hạn chế của Python, có ít loại hơn nhiều, có nhiều hạn chế hơn đối với luồng điều khiển và quan trọng nhất là đảm bảo khả năng bất biến mạnh mẽ để cho phép đọc đồng thời. Tính năng này chưa phải là Turing-complete, vì điều này ngăn cản một số (nhưng không phải tất cả) người dùng cố gắng hoàn thành các tác vụ lập trình chung trong ngôn ngữ đó.

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

Starlark được sử dụng trong một số bối cảnh, chẳng hạn như:

  1. Ngôn ngữ BUILD. Đây là nơi các quy tắc mới được xác định. 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 chính mã này tải.
  2. Định nghĩa về quy tắc. Đây là cách xác định các quy tắc mới (chẳng hạn như khả năng hỗ trợ một ngôn ngữ mới). Mã Starlark chạy trong ngữ cảnh này có quyền truy cập vào cấu hình và dữ liệu do các phần phụ thuộc trực tiếp cung cấp (sẽ tìm hiểu thêm ở phần sau).
  3. Tệp WORKSPACE. Đây là nơi xác định các kho lưu trữ bên ngoài (mã không nằm trong cây nguồn chính).
  4. Định nghĩa quy tắc kho lưu trữ. Đây là nơi xác định các loại kho lưu trữ bên ngoài mới. Mã Starlark chạy trong ngữ cảnh này có thể chạy mã tuỳ ý trên máy mà Bazel đang chạy và tiếp cận bên ngoài không gian làm việ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. 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à khi 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ục tiêu đã định cấu hình", tức là một cặp (mục tiêu, cấu hình) khá hợp lý.

Đây được gọi là "giai đoạn tải/phân tích" vì giai đoạn này có thể chia thành 2 phần riêng biệt, thường được chuyển đổi tuần tự, nhưng giờ đây chúng có thể chồng chéo lên nhau theo thời gian:

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

Bạn phải phân tích từng mục tiêu được định cấu hình trong quá trình đóng bắc cầu của các mục tiêu đã định cấu hình được yêu cầu trên dòng lệnh từ dưới lên; tức là các nút lá trước tiên mới được phân tích đến các nút trên dòng lệnh. Dữ liệu đầu vào cho bản phân tích của một mục tiêu đã đị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ư những nội dung như tuỳ 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. Phần phụ thuộc trực tiếp. Trì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. Các tệp này được gọi như vậy vì cung cấp tính năng "tổng hợp" thông tin trong quá trình đóng tạm thời 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 thành tệp nhị phân C++)
  3. Chính mục tiêu. Đây là kết quả của việc tải gói chứa mục tiêu. Đối với các quy tắc, dữ liệu này bao gồm các thuộc tính tương ứng, thường là những gì quan trọng.
  4. Cách triển khai mục tiêu đã định cấu hình. Đối với các quy tắc, thuộc tính này có thể trong Starlark hoặc trong Java. Tất cả mục tiêu đã định cấu hình không có quy tắc đều được triển khai trong Java.

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

  1. Các trì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 dịch vụ đó có thể truy cập
  2. Các cấu phần phần mềm mà công cụ này có thể tạo và 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 ứng dụng này mạnh mẽ hơn, nhưng trong khi đó, thao tác Bad ThingsTM sẽ dễ dàng hơn, chẳng hạn như viết mã có độ phức tạp về thời gian hoặc không gian là bậc hai (hoặc tệ hơn), để gây ra sự cố máy chủ Bazel với ngoại lệ Java hoặc vi phạm các giá trị bất biến (chẳng hạn như vô tình sửa đổi thực thể Options hoặc làm cho mục tiêu đã định cấu hình có thể thay đổi)

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

Cấu hình

Cấu hình là "cách" xây dựng một mục tiêu: cho nền tảng nào, với các 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 hữu ích, chẳng hạn như khi sử dụng cùng một mã cho một công cụ chạy trong bản dựng và cho mã mục tiêu và chúng ta đang biên dịch chéo hoặc khi chúng ta đang xây dựng một ứng dụng Android lớn (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 gói bằng BuildConfiguration để cung cấp thêm các chức năng khác. Phương thức này truyền từ đầu biểu đồ phần phụ thuộc xuống đáy. Nếu có thay đổi, bạn cần phân tích lại bản dựng.

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

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

Cấu hình của một quy tắc không nhất thiết giống với cấu hình của quy tắc "gốc". Quá trình thay đổi cấu hình trong cạnh của 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 của phần 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 quá trình chuyển đổi xảy 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 bất kỳ cạnh nào sắp diễn ra đối với một mục tiêu đã định cấu hình. Các giá trị này được chỉ định trong RuleClass.Builder.cfg().

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

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

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

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

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

Nhà cung cấp thông tin trung gian

Trì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 để cho mọi người biết về các mục tiêu đã định cấu hình khác phụ thuộc vào điều đó. Lý do tại sao " bắc cầu" có tên là vì đây thường là một kiểu cuộn lên nào đó của quá trình đóng bắc cầu của một mục tiêu đã định cấu hình.

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à trình cung cấp thông tin chuyển đổi Starlark (ngoại lệ là DefaultInfo là sự kết hợp của FileProvider, FilesToRunProviderRunfilesProvider vì API đó được coi là giống Starlark hơn so với cách chuyển tự trực tiếp của Java). Khoá của họ là một trong những điều sau:

  1. Đối tượng lớp Java. Tính năng này chỉ dành cho các nhà cung cấp không 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à phương pháp cũ và không được khuyến khích vì nó dễ bị xung đột tên. Các trình cung cấp thông tin bắc cầu như vậy 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 hàm này qua 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.

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

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

Các mục tiêu đã định cấu hình được triển khai dưới dạng RuleConfiguredTargetFactory. Mỗi lớp quy tắc được triển khai trong Java đều có một lớp con. Các mục tiêu đã định cấu hình Starlark đượ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ề. Quá trình này bao gồm những thành phần sau:

  1. filesToBuild của họ, 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 đã định cấu hình nằm trên dòng lệnh hoặc trong phần src của một quy tắc gen.
  2. Các tệp runfile, thông thường và dữ liệu.
  3. Các nhóm đầu ra. Đâ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 nhóm tệp trong BUILD và sử dụng trình cung cấp OutputGroupInfo trong Java.

Tệp chạy

Một số tệp nhị phân cần có tệp dữ liệu để chạy. Một ví dụ nổi bật là các kiểm thử cần tệp đầu vào. Điều này được thể hiện trong Bazel bằng khái niệm "runfile". "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ể. Công cụ 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ây đầu ra.

Một tập hợp các tệp chạy đượ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 chạy tệp đến thực thể Artifact đại diện cho tệp đó. Điều 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 chạy tệp của một tệp giống với đường dẫn thực thi của tệp đó. Chúng ta dùng tuỳ chọn này để tiết kiệm RAM.
  • Có nhiều loại mục cũ trong cây runfile cũng cần được biểu thị.

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

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

  • Tệp kê khai các tệp runfile đầ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 tệp run 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 các tệp runfile đầu ra. Tính năng này được các thư viện thời gian chạy xử lý cây runfile, đặc biệt là trên Windows (đôi khi không hỗ trợ đường liên kết tượng trưng) sử dụng.
  • Trung gian cho các tệp runfile. Để có cây runfile, bạn cần xây dựng 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 của phần phụ thuộc, bạn có thể sử dụng trình trung gian runfile để đại diện cho tất cả các cạnh này.
  • Đối số dòng lệnh để chạy tệp nhị phân có các runfile mà đối tượng RunfilesSupport đại diện.

Khía cạnh

Các khía cạnh là một cách để "truyền bá phép tính xuống biểu đồ phần phụ thuộc". Các bài tập 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 xây dựng phương thức triển khai thông báo vùng đệm giao thức ("đơn vị cơ bản" của vùng đệm giao thức) trong bất kỳ ngôn ngữ lập trình nào cũng phải được kết hợp với quy tắc proto_library để nếu hai mục tiêu trong cùng một ngôn ngữ phụ thuộc vào cùng một vùng đệm giao thức thì chỉ được tạo một lần.

Cũng giống như các mục tiêu đã định cấu hình, các mục tiêu này được biểu thị trong Skyframe dưới dạng SkyValue và cách chúng được tạo 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 factory tên là ConfiguredAspectFactory có quyền truy cập vào RuleContext, nhưng không giống như nhà máy mục tiêu đã được định cấu hình, lớp này cũng biết về mục tiêu đã định cấu hình đi kèm và các nhà cung cấp tương ứng.

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

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

Một chức năng ngoài dự kiến là các khía cạnh có thể đính kèm vào các khía cạnh khác; ví dụ: một khung hì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 vài trong số đó là vùng đệm giao thức. Trong trường hợp đó, khía cạnh IDE sẽ muốn đính kèm vào cặp (quy tắc proto_library + khung hình proto Java).

Mức độ phức tạp của các khía cạnh trên các khía cạnh được ghi nhận 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 tạo bản dựng sẽ chạy và nhiều cấu trúc để tạo mã. Theo cách nói của Bazel, các cấu trúc này được gọi là nền tảng (tài liệu đầy đủ tại đây)

Một nền tảng được mô tả bằng ánh xạ khoá-giá trị từ chế độ cài đặt quy tắc ràng buộc (chẳng hạn như khái niệm "Cấu trúc CPU") đến 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ó "từ điển" về 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 được nhắm mục tiêu, người dùng 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 mục tiêu đế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 nền tảng mục tiêu và phương thức thực thi đã đặt (tài liệu về chuỗi công cụ tại đây).

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

  1. Quy tắc toolchain() mô tả tập hợp các quy tắc ràng buộc thực thi và mục tiêu mà một chuỗi công cụ hỗ trợ và cho biết loại công cụ (chẳng hạn như C++ hoặc Java) của chuỗi công cụ đó (quy tắc 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())

Việc này được thực hiện theo cách này vì chúng ta cần biết các quy tắc ràng buộc đối với 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() dành riêng cho ngôn ngữ chứa nhiều thông tin hơn thế nên sẽ mất nhiều thời gian tải hơn.

Nền tảng thực thi được chỉ định theo một trong những cách sau:

  1. Trong tệp WORKSPACE bằng hàm register_execution_platforms()
  2. Trên dòng lệnh, 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 trong RegisteredExecutionPlatformsFunction .

Nền tảng mục tiêu cho một mục tiêu đã định cấu hình được xác định theo PlatformOptions.computeTargetPlatform() . Đây là một 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 hiện chưa được triển khai.

Tập hợp các 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 WORKSPACE và cấu hình)
  • Nền tảng đích và nền tảng thực thi 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 điều kiện ràng buộc đối với nền tảng thực thi của mục tiêu đã định cấu hình (thuộc tính exec_compatible_with) và cấu hình (--experimental_add_exec_constraints_to_targets), trong UnloadedToolchainContextKey

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

Sau đó, các chuỗi công cụ thực sự được tải bằng cách sử dụng ResolvedToolchainContext.load() và sử dụng để 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 dần chuyển sang hệ thống nêu trên. Để xử lý 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 tính năng liên kết nền tảng để chuyển đổi giữa cờ cũ và các quy tắc hạn chế của nền tảng kiểu mới. Mã của họ viết 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. Rất tiếc là Bazel có nhiều cơ chế để đạt được mục tiêu này:

  • Các điều kiện ràng buộc theo quy tắc cụ thể
  • environment_group()/environment()
  • Các hạn chế về nền tảng

Các điều kiện ràng buộc riêng theo quy tắc chủ yếu được sử dụng trong Google cho các quy tắc Java; các quy tắc này đang được rút ra và không có trong Bazel, nhưng mã nguồn có thể chứa thông tin tham chiếu đến mã đó. Thuộc tính chi phối quá trình chuyển đổi này có tên là constraints= .

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

Các 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ả quy tắc bản dựng đều có thể khai báo "môi trường" có thể tạo quy tắc đó, trong đó "môi trường" là một thực thể của quy tắc environment().

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

  1. Thông qua thuộc tính restricted_to=. Đây là hình thức quy cách trực tiếp nhất; phương thức này khai báo tập hợp chính xác môi trường mà quy tắc hỗ trợ cho nhóm này.
  2. Thông qua thuộc tính compatible_with=. Thao tác này sẽ 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 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 theo 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"). Việc định nghĩa 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 hỗ trợ theo giá trị "mặc định" nếu không được các thuộc tính restricted_to= / environment() chỉ định khác. Quy tắc không có thuộc tính nào như vậy sẽ kế thừa tất cả giá trị mặc định.
  5. Thông qua một lớp quy tắc mặc định. Thao tác này sẽ ghi đè giá trị mặc định chung cho mọi phiên bản của lớp quy tắc nhất định. Bạn có thể sử dụng tính năng này chẳng hạn để làm cho tất cả các quy tắc *_test đều có thể kiểm thử được mà không cần mỗi thực thể phải khai báo rõ ràng tính năng này.

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

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

Quy trình kiểm tra điều kiện ràng buộc diễn ra trong RuleContextConstraintSemanticsTopLevelConstraintSemantics.

Các hạn chế về nền tảng

Cách "chính thức" hiện tại để mô tả nền tảng tương thích với một mục tiêu là sử dụng các quy tắc ràng buộc tương tự như dùng để mô tả chuỗi công cụ và nền tảng. Yêu cầu này đang được xem xét trong yêu cầu lấy #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 nên chú ý để ngăn những người khác tuỳ ý phụ thuộc 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 những hành vi mà bạn cho là chi tiết triển khai.

Bazel hỗ trợ việc này qua cơ chế có tên là chế độ hiển thị: bạn có thể khai báo rằng một mục tiêu cụ thể chỉ có thể phụ thuộc vào việc 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 danh sách nhãn, nhưng các nhãn này có thể mã hoá một mẫu trên tên gói thay vì con trỏ đến bất kỳ mục tiêu cụ thể nào. (Đúng, đây là một lỗi thiết kế.)

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

  • Giao diện RuleVisibility biểu thị phần khai báo chế độ hiển thị. Đó có thể là một hằng số (hoàn toàn công khai hoặc riêng tư hoàn toàn) hoặc một danh sách các nhãn.
  • Nhãn có thể tham chiếu đến các nhóm gói (danh sách các gói được xác định trước) đến các gói trực tiếp (//pkg:__pkg__) hoặc cây con của các 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 đã định cấu hình (PackageGroupConfiguredTarget). Nếu muốn, 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. Logic của chúng được triển khai với sự trợ giúp của: PackageSpecification, tương ứng với một mẫu duy nhất như //pkg/...; PackageGroupContents, tương ứng với một thuộc tính packages của package_group; và PackageSpecificationProvider (tổng hợp trên một package_groupincludes bắc cầu).
  • Việc chuyển đổi từ danh sách nhãn chế độ hiển thị sang phần phụ thuộc được thực hiện trong DependencyResolver.visitTargetVisibility và một vài vị trí khác.
  • Bước 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 riêng của nó và gói tập hợp đó vào một trình cung cấp thông tin bắc cầu để các mục tiêu đã định cấu hình phụ thuộc vào tập hợp đó cũng có thể thực hiện tương tự. Ví dụ:

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

Nếu chúng ta làm việc này theo cách đơn giản, chẳng hạn như sử dụng List hoặc Set, thì chúng ta sẽ sử dụng bộ nhớ bậc bốn: nếu có một chuỗi N quy tắc và mỗi quy tắc thêm một tệp, chúng ta sẽ có 1+2+...+N thành viên 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. Đó 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 của riêng nó, do đó tạo thành một đồ thị không chu trình của các tập hợp có hướng. Họ là bất biến và các thành viên của họ có thể lặp lại. Chúng tôi xác định nhiều thứ tự lặp lại (NestedSet.Order): đặt hàng trước, sau thứ tự, 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 nút phải giống nhau".

Cấu trúc dữ liệu này có tên 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 thực thể của lớp Action và các tệp được biểu thị dưới dạng thực thể của lớp Artifact. Chúng được sắp xếp trong một đồ thị hai phần, có hướng, không chu trình được gọi là "biểu đồ hành động".

Cấu phần phần mềm có 2 loại: cấu phần phần mềm nguồn (cấu phần 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 (cấu phần cần phải được tạo). Các cấu phần phần mềm phát sinh có thể có nhiều loại:

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

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

  • Trung gian tổng hợp được dùng để nhóm các cấu phần phần mềm lại với nhau. Tức là nếu nhiều thao tác sử dụng cùng một nhóm đầ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 cho phần phụ thuộc giúp đảm bảo rằng một hành động chạy trước một hành động khác. Các lớp này chủ yếu được dùng để tìm lỗi mã nguồn nhưng cũng dùng để biên dịch C++ (xem CcCompilationContext.createMiddleman() để biết nội dung giải thích)
  • Trình trung gian Runfiles được dùng để đảm bảo sự hiện diện của cây runfile sao cho cây chạy không cần phụ thuộc riêng vào tệp kê khai đầu ra và mọi cấu phần phần mềm được tham chiếu bởi cây runfile.

Hành động được hiểu rõ nhất là một lệnh cần 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ả 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 mà ứng dụng cần
  • 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) mà môi trườ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, chẳng hạn 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 các thao tác đều là SpawnAction hoặc StarlarkAction (giống nhau, các thao tác này không nên là các lớp riêng biệt), mặc dù Java và C++ có những loại thao tác riêng (JavaCompileAction, CppCompileActionCppLinkAction).

Cuối cùng, chúng ta muốn di chuyển mọi thứ sang SpawnAction; JavaCompileAction cũng khá gần, nhưng C++ là một trường hợp đặc biệt do có thể phân tích cú pháp tệp .d và bao gồm cả tính năng 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 liên kết từ cạnh phần phụ thuộc của biểu đồ hành động đến cạnh của phần phụ thuộc Skyframe được mô tả trong ActionExecutionFunction.getInputDeps()Artifact.key(), đồng thời có một vài điểm tối ưu hoá để duy trì số lượng cạnh của Skyframe thấp:

  • 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 hiểu khoá cho hành động tạo ra khoá đó
  • Các tập hợp lồng ghép sẽ 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 đặt các hành động phát sinh vào một thư mục được xác định theo cấu hình và gói của chúng (tuy nhiên, 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ể đưa các cấu phần phần mềm phát sinh ở bất cứ đâu.

Đây được coi là một tính năng sai, nhưng việc loại bỏ tính năng này thực sự rất khó vì sẽ 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 theo nhiều quy tắc (sóng tay). Việc này sẽ phải tốn một chút dung lượng RAM: mỗi thực thể của một hành động chung 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 đầu vào, cùng đầu ra và chạy cùng một dòng lệnh. Mối quan hệ tương đương này được triển khai trong Actions.canBeShared() và được xác minh giữa các giai đoạn phân tích và thực thi bằng cách xem xét mọi Hành động. Việc này được triển khai trong SkyframeActionExecutor.findAndStoreArtifactConflicts() và là một trong số ít nơi ở 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 thao tác tạo bản dựng, chẳng hạn như các lệnh tạo ra kết quả.

Điều đầu tiên Bazel làm sau giai đoạn phân tích là xác định Cấu phần phần mềm cần được tạo. Logic cho việc này được mã hoá trong TopLevelArtifactHelper; đạ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 để 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 gốc thực thi. Vì 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ộ với một cây nguồn đầy đủ. Việc này do 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 duy nhất liên kết tượng trưng cho mọi gói với một mục tiêu đã sử dụng từ vị trí thực tế của gói đó. Giải pháp thay thế là truyền đúng đường dẫn đến các lệnh (có tính đến --package_path). Đây là điều không mong muốn bởi vì:

  • API này thay đổi các 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 khác (đây là trường hợp phổ biến)
  • Điều này dẫn đến các dòng lệnh khác nhau nếu một hành động được chạy từ xa so với nếu hành động đó chạy cục bộ
  • Tính năng này yêu cầu một phép biến đổi dòng lệnh dành riêng cho công cụ đang sử dụng (hãy cân nhắc sự khác biệt giữa các đường dẫn lớp Java và C++, bao gồm cả cá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 của mục nhập 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 phần, biểu đồ có hướng bao gồm các hành động cũng như các cấu phần phần mềm đầu vào và đầu ra) và chạy các hành động. Quá trình thực thi mỗi hành động được biểu thị bằng một thực thể của lớp SkyValue ActionExecutionValue.

Vì việc chạy một thao tác sẽ tốn kém, nên chúng tôi có một vài lớp lưu vào bộ nhớ đệm có thể bị tấn công phía sau Skyframe:

  • ActionExecutionFunction.stateMap chứa dữ liệu để khiến Skyframe khởi động lại ActionExecutionFunction với 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 cũ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 phía sau Skyframe; ngay cả khi một thao tác được thực thi lại trong Skyframe, thì thao tác đó vẫn có thể là một lượt truy cập trong bộ nhớ đệm thao tác cục bộ. Nó thể hiện trạng thái của hệ thống tệp cục bộ và được chuyển đổi tuần tự sang ổ đĩa, có nghĩa là khi khởi động một máy chủ Bazel mới, người dùng có thể nhận được lượt truy cập bộ nhớ đệm hành động cục bộ ngay cả khi biểu đồ Skyframe trống.

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

Trái ngược với tên gọi, đây là một bản đồ từ đường dẫn của một cấu phần phần mềm phái sinh đến hành động đã tạo 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 và giá trị tổng kiểm của chúng
  2. "Khoá hành động" của lớp này thường là dòng lệnh được thực thi, nhưng nhìn chung, đại diện cho mọi thứ không được ghi lại bằng giá trị tổng kiểm của các tệp đầu vào (chẳng hạn như đối với FileWriteAction, đó là giá trị 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" có tính thử nghiệm cao vẫn đang được phát triển. Bộ nhớ này sử dụng các hàm băm bắc cầu để tránh chuyển vào bộ nhớ đệm nhiều lần.

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

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

Những thao tác này được triển khai bằng phương thức trên Action:

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

Khi bộ nhớ đệm thao tác trả về một lượt truy cập trên một thực thể mới của Hành động (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 dữ liệu đầu vào phản ánh kết quả của quá trình khám phá dữ liệu đầu vào và việc cắt giảm 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 được sử dụng thông qua đối số unused_inputs_list= của ctx.actions.run().

Có nhiều cách để chạy hành động: Strategies/ActionContexts

Bạn có thể chạy một số thao tác theo nhiều cách. Ví dụ: một dòng lệnh có thể được thực thi cục bộ, cục bộ, nhưng trong nhiều loại hộp cát hoặc từ xa. Khái niệm thể hiện điều này được gọi là ActionContext (hoặc Strategy, vì chúng tôi chỉ đổi tên thành công 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ề bối cảnh hành động mà các thực thể đó có. Điều này xảy ra trong hàm khởi tạo của ExecutionTool. Các loại ngữ cảnh hành động được xác định bằng một thực thể Class Java tham chiếu đến một giao diện phụ của ActionContext và giao diện mà ngữ cảnh hành động phải triển khai.
  2. Ngữ cảnh hành động thích hợp được chọn trong số các ngữ cảnh có sẵn và được chuyển tiếp đến ActionExecutionContextBlazeExecutor .
  3. 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ỉ có một cách để thực hiện việc này...)

Các chiến lược có thể tuỳ ý 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ộ và 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à chiến lược triển khai các quy trình của 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 sử dụng lại giữa các hành động thay vì bắt đầu một lần nữa cho mọi hành động (Điều này thể hiện một vấn đề tiềm ẩn về độ chính xác, vì Bazel dựa vào hứa hẹn về quy trình worker là nó không thể quan sát trạng thái giữa các yêu cầu riêng lẻ)

Nếu công cụ thay đổi, quy trình của worker cần được khởi động lại. Việc có thể sử dụng lại một worker hay không được xác định bằng cách tính toán tổng kiểm tra cho công cụ được dùng bằng WorkerFilesHash. Phương pháp này dựa vào việc biết dữ liệu đầu vào nào của hành động đại diện cho một phần của công cụ và đại diện cho dữ liệu đầ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à một 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!):

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

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

Bazel có thể chạy nhiều thao tác song song. Số lượng các thao tác cục bộ nên được chạy song song sẽ khác nhau giữa các hành động: hành động 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 gây quá tải cho máy cục bộ.

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

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

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

Mỗi hành động cần có một vị trí riêng trong thư mục đầu ra, nơi đặt đầ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ó 2 thuộc tính mong muốn xung đột:

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

Cho đến nay, chúng tôi vẫn chưa đưa ra một phương pháp theo nguyên tắc để giải quyết vấn đề này. Nó có nhiều điểm tương đồng với vấn đề 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 gây ra vấn đề là quy tắc Starlark (tác giả thường không quen thuộc với Bazel) và các khía cạnh, thêm một chiều khác vào không gian của những thứ có thể tạo ra "cùng một tệp đầu ra".

Phương pháp hiện tại là phân đoạn đường dẫn cho cấu hình là <CPU>-<compilation mode> với nhiều hậu tố được thêm vào để các 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 giá trị tổng kiểm của tập hợp các hiệu ứng chuyển đổi cấu hình Starlark được thêm vào để người dùng không gây ra xung đột hành động. Thật không thể hoàn hảo. Việc này được triển khai trong OutputDirectories.buildMnemonic() và dựa vào mỗi mảnh cấu hình thêm phần riêng của nó 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ông cụ 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 chương trình kiểm thử song song nhiều lần (để gỡ lỗi hoặc thu thập dữ liệu 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 kiểm thử trên nhiều quy trình để đảm bảo tốc độ)
  • Chạy lại kiểm thử mã không ổn định
  • Nhóm các bài kiểm thử vào 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ó một TestProvider, mô tả cách chạy kiểm thử:

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

Xác định xem nên chạy chương trình kiểm thử nào

Việc xác định những chương trình kiểm thử nào sẽ chạy là một quá trình công phu.

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

Sau đó, các bài kiểm thử được lọc theo kích thước, thẻ, thời gian chờ và ngôn ngữ theo tuỳ chọn dòng lệnh. Tính năng 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 tại sao các thuộc tính quy tắc có thể được lọc ra không thể định cấu hình là vì điều này xảy ra trước giai đoạn phân tích, do đó, không có sẵn cấu hình.

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

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

Đang chạy kiểm thử

Quá trình kiểm thử sẽ chạy 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 đó, việc này dẫn đến việc thực thi TestRunnerAction, cuối cùng sẽ gọi TestActionContext do tuỳ chọn dòng lệnh --test_strategy chọn để chạy kiểm thử theo cách được yêu cầu.

Các bài 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 quy trình kiểm thử biết những gì được mong đợi từ các biến đó. Bạn có thể xem nội dung mô tả chi tiết về những điều Bazel mong đợi từ các kiểm thử và những kiểm thử 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 mã khác có nghĩa là không thành công.

Ngoài tệp trạng thái bộ nhớ đệm, mỗi quy trình kiểm thử còn phát ra một số tệp khác. Các tệp này được đặt trong "thư mục nhật ký kiểm thử", đây 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 phép kiểm thử. stdout và stderr không được phân tách.
  • test.outputs, "thư mục đầu ra chưa được khai báo"; thư mục này được sử dụng bởi các chương trình kiểm thử muốn xuất các tệp ngoài nội dung mà chúng còn 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 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ố chương trình kiểm thử cần được thực thi ở chế độ độc quyền, ví dụ: không song song với các chương trình kiểm thử khác. Bạn có thể lấy đ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 lượt kiểm thử độc quyền được chạy bởi một lệnh gọi Skyframe riêng biệt, yêu cầu thực thi chương trình kiểm thử sau bản dựng "chính". Việc này được triển khai trong SkyframeExecutor.runExclusiveTest().

Không giống như các thao tác thông thường, có đầu ra trong thiết bị đầu cuối đượ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ử để họ được thông báo về tiến trình của một chương trình kiểm thử chạy trong thời gian dài. Điều này được chỉ định bởi tuỳ 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 nhiều loại kiểm thử không được xen kẽ.

Việc 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 đề cập, đồng thời kết xuất các byte mới vào thiết bị đầu cuối nơi quy tắc Bazel.

Kết quả của các bài kiểm thử đã thực thi sẽ có trên bus sự kiện bằng cách quan sát các sự kiện khác nhau (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 tới bảng điều khiển.

Thu thập dữ liệu mức độ phù hợp

Mức độ sử dụng được các bài kiểm thử báo cáo ở đị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 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ử để cho phép 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 mức độ sử dụng. Sau đó, công cụ này sẽ chạy quy trình kiểm thử. Một kiểm thử có thể tự 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 (với thời gian chạy thu thập mức độ che 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 được thực hiện bởi các chiến lược kiểm thử và đòi hỏi collect_coverage.sh phải nằm trong dữ liệu đầu vào của kiểm thử. Điều này có thêm nhờ 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à tính năng đo lường mức độ sử dụng được thêm vào thời điểm 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 sẽ đượ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 độ sử dụng cơ sở. Đây là phạm vi của thư viện, tệp nhị phân hoặc kiểm thử nếu không có mã nào trong đó được chạy. Vấn đề mà công cụ này giải quyết là nếu bạn muốn tính toán phạm vi kiểm thử cho một tệp nhị phân thì việc hợp nhất mức độ bao phủ của tất cả các kiểm thử là chưa đủ vì có thể có mã trong tệp nhị phân không được liên kết với bất kỳ kiểm thử nào. Do đó, điều chúng tôi làm là phát hành một tệp mức độ sử dụng cho mọi tệp nhị phân chỉ chứa các tệp chúng tôi thu thập mức độ sử dụng mà không có các dòng được bảo hiểm. Tệp mức độ sử dụng cơ sở của một mục tiêu nằm ở bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat . Ngoài việc kiểm thử, mã này còn được tạo cho các tệp nhị phân và thư viện nếu bạn truyền cờ --nobuild_tests_only đến Bazel.

Phạm vi bao phủ cơ sở hiện chưa có dữ liệu.

Chúng tôi theo dõi 2 nhóm tệp để thu thập mức độ sử dụng 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 để đo lường. Đối với môi trường thời gian chạy mức độ sử dụng trực tuyến, bạn có thể sử dụng thuộc tính này trong thời gian chạy để quyết định tệp nào cần đo lường. Phạm vi 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 về đo lường là tập hợp các tệp bổ sung mà một chương trình kiểm thử cần để tạo các tệp LCOV mà Bazel yêu cầu từ đó. Trên thực tế, quá trình 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 giá trị này được thêm vào tập hợp dữ liệu đầu vào của hành động kiểm thử nếu chế độ mức độ sử dụng được bật.

Việc liệu phạm vi có được thu thập hay không có được lưu trữ trong BuildConfiguration hay không. Cách này rất tiện lợi vì đây là cách dễ dàng để thay đổi hành động 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 hành mã có thể thu thập mức độ sử dụng, giúp giảm thiểu phần nào vấn đề này, vì dù sao cũng cần phân tích lại).

Các tệp hỗ trợ mức độ sử dụng phụ thuộc vào nhãn trong phần phụ thuộc ngầm ẩn để có thể bị chính sách gọi ghi đè, cho phép các tệp này khác nhau giữa các phiên bản Bazel. Tốt nhất là nên loại bỏ những khác biệt này 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 "báo cáo về mức độ phù hợp" hợp nhất phạm vi thu thập được cho mọi hoạt động 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() . Ứng dụng đó có quyền truy cập vào các công cụ cần thiết bằng cách xem thuộc tính :coverage_report_generator của kiểm thử đầu tiên được thực thi.

Công cụ truy vấn

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

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

Mỗi phương phá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 các kết quả truy vấn, thay vì thu thập các kết quả đó vào một cấu trúc dữ liệu nào đó, query2.engine.Callback được chuyển đến QueryFunction để gọi phương thức này cho các kết quả muốn trả về.

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

Một yêu cầu tinh vi đối với 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à quá trình tải gói cung cấp để người dùng có thể làm khác biệt kết quả và xác định xem một mục tiêu cụ thể có thay đổi hay không. Do đó, các giá trị thuộc tính cần phải được 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à giải pháp thoả mãn lắm, và sẽ rất tốt nếu nâng yêu cầu này lên.

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 bản sao của lịch sử Bazel khi trước đây đượ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 lệnh.

Các thư viện này 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ư phiên bản chúng tôi sử dụng tại Google) mới cần:

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

Tập hợp các điểm mở rộng mà BlazeModule cung cấp có phần không chính xác. Đừng dùng mô hình này làm ví dụ về các nguyên tắc thiết kế hiệu quả.

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à bằng xe buýt 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 về các sự kiện mà họ quan tâm. Ví dụ: những nội dung sau đây được biểu thị dưới dạng sự kiện:

  • Danh sách mục tiêu bản dựng sẽ được tạo đã được xác định (TargetParsingCompleteEvent)
  • Các 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 chưa (TargetCompleteEvent)
  • Đã chạy một thử nghiệm (TestAttempt, TestSummary)

Một số sự kiện này được biểu thị bên ngoài Bazel trong Giao thức sự kiện tạo (chú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 API 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à Build Event Service) để 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

Trong khi 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ứ mà người dùng cần xây dựng), thì Bazel lại sống trong một thế giới mà điều này không nhất thiết đúng. "Kho lưu trữ bên ngoài" là một khái niệm trừu tượng dùng để kết nối hai thế giới này: chúng biểu thị mã cần thiết cho bản dựng nhưng không có trong cây nguồn chính.

Tệp WORKSPACE

Tập hợp 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ụ: nội dung khai báo như sau:

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

Đã có kết quả trong kho lưu trữ có tên là @foo. Vấn đề này trở nên phức tạp hơn vì người ta có thể xác định các quy tắc mới về kho lưu trữ trong các tệp Starlark. Sau đó, bạn có thể dùng các quy tắc này để tải mã Starlark mới, cũng như xác định các quy tắc mới về kho lưu trữ, v.v.

Để xử lý trường hợp này, quá trình phân tích cú pháp tệp WORKSPACE (trong WorkspaceFileFunction) được chia thành các phần được 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 mã của kho lưu trữ có sẵn cho Bazel, bạn cần fetched mã này. Việc 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 đây:

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

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

  1. Có một bộ nhớ đệm cho các tệp đã tải xuống được khoá bằng giá trị tổng kiểm của chúng (RepositoryCache). Việc này yêu cầu tổng kiểm tra phải có trong tệp WORKSPACE, nhưng dù sao thì điều này cũng giúp đảm bảo tính ẩn giấu. Mọi phiên bản máy chủ Bazel trên cùng một máy trạm sẽ dùng chung dữ liệu này, bất kể các phiên bản đó đang chạy ở không gian làm việc hoặc cơ sở đầu ra nào.
  2. "Tệp đánh dấu" được viết cho mỗi kho lưu trữ trong $OUTPUT_BASE/external. Tệp này chứa giá trị tổng kiểm của quy tắc dùng để tìm nạp tệp đó. 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. Tính năng 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ác cấu phần phần mềm cần tải xuống. Điều này rất hữu ích trong các chế độ cài đặt của doanh nghiệp, trong đó Bazel không nên tìm nạp những nội dung ngẫu nhiên từ Internet. Việc này do DownloadManager triển khai .

Sau khi tải xuống kho lưu trữ, 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 các cấu phần phần mềm đó. 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ữ mà chúng đang 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. Việc này do ExternalFilesHelper xử lý.

Thư mục được quản lý

Đôi khi, các kho lưu trữ bên ngoài cần sửa đổi các tệp trong thư mục gốc của không gian làm việc (chẳng hạn như trình quản lý gói chứa các gói đã tải xuống trong một thư mục con của cây nguồn). Điều này mâu thuẫn với giả định Bazel cho rằng các tệp nguồn chỉ do người dùng sửa đổi chứ không phải do chính người dùng sửa đổi, đồng thời cho phép các gói tham chiếu đến mọi thư mục trong gốc của không gian làm việc. Để làm cho loại kho lưu trữ bên ngoài này hoạt động, Bazel thực hiện 2 việc:

  1. Cho phép người dùng chỉ định thư mục con của không gian làm việc mà Bazel không được phép truy cập vào. Các chức năng này được liệt kê trong một tệp có tên là .bazelignore và chức năng này được triển khai trong BlacklistedPackagePrefixesFunction.
  2. Chúng tôi mã hoá quá trình liên kết từ thư mục con của không gian làm việc đến kho lưu trữ bên ngoài mà chúng được xử lý vào ManagedDirectoriesKnowledge và xử lý các FileStateValue tham chiếu đến các thư mục đó theo cách tương tự như các kho lưu trữ bên ngoài thông thường.

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 ví dụ của "vấn đề về 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ó thể cả hai tệp nhị phân đó đều tham chiếu đến Guava có nhãn bắt đầu từ @guava// và dự kiến điều đó có nghĩa là các phiên bản khác nhau của Guava.

Do đó, Bazel cho phép một mối liên kết 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 tệp kia.

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

Việc ánh xạ được chỉ định trong tệp WORKSPACE làm thuộc tính repo_mapping của các định nghĩa riêng lẻ trong kho lưu trữ. Sau đó, nó xuất hiện trong Skyframe dưới dạng một thành viên của WorkspaceFileValue, trong đó nó được chuyển thẳng đế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 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 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ự làm hoặc không thể tự làm 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, chức năng kiểm soát quy trình và các hoạt động khác ở cấp thấp.

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

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

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

Việc phát ra kết quả đầu ra từ bảng điều khiển có vẻ là một điều đơn giản, nhưng việc kết hợp 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ó được một đầu ra thiết bị đầu cuối đẹp mắt, nhiều màu sắc và có một máy chủ chạy trong thời gian dài khiến việc này không hề quan trọng.

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

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

Thông báo ngắn (lỗi, cảnh báo, v.v.) được biểu thị thông qua giao diện EventHandler. Đáng chú ý là những nội dung này khác với nội dung mộ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ố lỗi khác) và chúng có thể có Location (vị trí trong mã nguồn đã khiến sự kiện xảy ra).

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

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

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

  • Xe buýt sự kiện
  • Luồng sự kiện được dẫn đến sự kiện đó thông qua Trình 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) phải 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. Thuộc tính này chỉ được dùng khi một lệnh cần kết xuất một lượng lớn dữ liệu nhị phân có thể có (chẳng hạn như bazel query).

Phân tích Bazel

Bazel rất nhanh. Bazel cũng chạy chậm vì các bản dựng có xu hướng phát triển cho đến khi chỉ vượt quá ranh giới có thể chấp nhận được. 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 tài nguyên cho các bản dựng và cho chính Bazel. Tính năng này được triển khai trong một lớp có tên phù hợp là Profiler. Tính năng này được bật theo mặc định, mặc dù tuỳ chọn này chỉ ghi lại dữ liệu rút gọn để mức hao tổn có thể chấp nhận được; Dòng lệnh --record_full_profiler_data giúp ghi lại mọi nội dung có thể.

Hồ sơ này tạo ra một hồ sơ ở định dạng trình phân tích tài nguyên của Chrome. Hồ sơ này sẽ hiển thị tốt nhất trong Chrome. Mô hình dữ liệu của ngăn xếp tác vụ là: một ngăn xếp tác vụ có thể bắt đầu và kết thúc tác vụ, đồng thời các tác vụ này phải đượ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: Hàm này hoạt động như thế nào với các thao tác và kiểu truyền tiếp tục?

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

Chúng ta cũng thực hiện việc phân tích bộ nhớ cơ bản trong MemoryProfiler. Trình quản lý 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 của GC.

Thử nghiệm Bazel

Bazel có hai loại thử nghiệm chính: loại thử nghiệm quan sát Bazel là một "hộp đen" và 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ử tích hợp" và "kiểm thử đơn vị" sau này, mặc dù các bài kiểm thử này giống với kiểm thử tích hợp 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ế nếu cần.

Trong các thử nghiệm tích hợp, chúng tôi có 2 loại:

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

Các bài 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 cào để 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 thông tin về kết quả phân tích.