Cơ sở mã Bazel

Báo cáo vấn đề Xem nguồn Hằng đêm · 7,3 · 7,2 · 7.1 · 7 · 6,5

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

Giới thiệu

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

Để những người đang đi giữa hành trình không thấy được chính mình 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 dễ dàng bắt đầu sử dụng đang xử lý nó.

Phiên bản công khai của mã nguồn của Bazel nằm trên GitHub tại github.com/bazelbuild/bazel Đây không phải là "nguồn đáng tin cậy"; Mã 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. Chiến lược phát hành đĩa đơn mục tiêu dài hạn là biến GitHub 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 (pull request) thông thường của GitHub. và được nhân viên của Google nhập thủ công vào cây nguồn nội bộ, sau đó tái xuất sang 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ó hai loại tuỳ chọn: khởi động và . 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=) ở trước tên của lệnh sẽ chạy và một số nằm sau (-c opt); loại trước đây được gọi là "tuỳ chọn khởi động" và ảnh hưởng đến toàn bộ quy trình máy chủ, trong khi loại sau này, lệnh "lệnh tuỳ chọn", chỉ ảnh hưởng đến một lệnh duy nhất.

Mỗi phiên bản máy chủ có một không gian làm việc được liên kết (tập hợp 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ủ. Bạn có thể tránh né điều này bằng cách chỉ định cơ sở đầu ra tuỳ chỉnh (hãy 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++ (cụm từ "client") nắm quyền kiểm soát. Công cụ này thiết lập một quy trình máy chủ phù hợp bằng cách sử dụng 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 đó. Chiến dịch nà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 ứng dụng 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. Nó tìm máy chủ đang chạy bằng cách xem thư mục $OUTPUT_BASE/server có một tệp khoá với cổng mà máy chủ đang nghe.
  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 quy trình máy chủ phù hợp đã sẵn sàng, lệnh cần chạy là truyền đến nó qua giao diện gRPC, sau đó đầu ra của Bazel được chuyển trở lại đến thiết bị đầu cuối. Chỉ có thể chạy một lệnh cùng lúc. Đây là đượ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. Có một số cơ sở hạ tầng để chạy song song nhiều lệnh, vì không thể chạy bazel version song song với một lệnh khác hơi ngượng ngùng. 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 truyền mã thoát mà máy khách sẽ được trả về. Một nếp nhăn thú vị là việc triển khai bazel run: phương thức công việc của lệnh này là chạy thứ gì đó mà Bazel vừa xây dựng, nhưng nó không thể thực hiện việc đó từ quá trình máy chủ vì nó không có thiết bị đầu cuối. Thay vào đó, công cụ này cho biết cho máy khách tệp nhị phân nào nên exec() và với các đối số nào.

Khi một người nhấn Ctrl-C, ứng dụng sẽ dịch thao tác đó thành lệnh gọi Cancel (Huỷ) trên gRPC Kết nối này sẽ cố gắng chấm dứt lệnh càng sớm càng tốt. Sau Ctrl-C thứ ba, máy khách sẽ gửi một SIGKILL đến máy chủ.

Mã nguồn của ứng dụng thuộc src/main/cpp và giao thức được dùng để giao tiếp với máy chủ 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 khách 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. Đầy có trong Bố cục thư mục đầu ra.

"Kho lưu trữ chính" là cây nguồn Bazel được chạy trong đó. Nó thường tương ứng với nội dung mà bạn đã xem từ chế độ kiểm soát nguồn. Thư mục gốc của thư mục này là có tên là "thư mục gốc của workspace".

Bazel đặt tất cả dữ liệu vào "thư mục gốc của người dùng đầu ra". Việc này thường $HOME/.cache/bazel/_bazel_${USER}, nhưng có thể ghi đè bằng cách sử dụ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 đều nhận được một thư mục con dựa trên giá trị tổng kiểm của nó trong số lượt cài đặt. Địa chỉ tại $OUTPUT_USER_ROOT/install theo mặc định 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 đính kèm với một Workspace 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ường thì lúc $OUTPUT_USER_ROOT/<checksum of the path to the workspace>. Bạn có thể thay đổi tệp này bằng cách sử dụng tuỳ chọn khởi động --output_base, Điều này, cùng với nhiều tính năng khác, hữu ích để vượt qua hạn chế mà chỉ một phiên bản Bazel có thể chạy trong bất kỳ không gian làm việc nào tại 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ả nguồn cho bản dựng hiện tại. Địa điểm này nằm tại $OUTPUT_BASE/execroot. Trong bản dựng, thư mục đang hoạt động là $EXECROOT/<name of main repository>. Chúng tôi dự định thay đổi thành $EXECROOT, mặc dù đây là kế hoạch dài hạn vì đó là sự thay đổi 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 nhận được quyền kiểm soát và được thông báo về một lệnh mà máy chủ cần thực thi thì chuỗi sự kiện sau đây sẽ xảy ra:

  1. BlazeCommandDispatcher đã nhận được thông báo về yêu cầu mới. Điều này quyết định liệu 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ừ cho các thư không có 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ú 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 là đượ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ó một 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 xây dựng để cho mọi người biết phiên bản sẽ đi.

  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 build: tạo, kiểm thử, chạy, mức độ sử dụng, v.v.: chức năng này là 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 giải quyết. Việc này được triển khai trong AnalysisPhaseRunner.evaluateTargetPatterns() và đã sửa lại trong Skyframe thành TargetPatternPhaseValue.

  7. Giai đoạn tải/phân tích được chạy để tạo biểu đồ hành động (một giai đoạn đồ thị không chu trình 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. Điều này có nghĩa là chạy mọi hành động cần thiết để tạo các mục tiêu cấp cao nhất được yêu cầu rồi 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 một Đối tượng OptionsParsingResult chứa bản đồ từ "option lớp học" vào giá trị của các lựa chọn. "Lớp tùy chọn" là lớp con của OptionsBase và nhóm các tuỳ chọn dòng lệnh lại với nhau có liên quan đến mỗi tuỳ chọn khác. 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à một lớp con của FragmentOptions và cuối cùng sẽ được gói vào đố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à (hoặc 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 quét hay không) được đọc trong giai đoạn thực thi nhưng việc này luôn đòi hỏi bạn phải thực hiện rõ ràng vì Khi đó, bạn không thể sử dụng BuildConfiguration. Để biết thêm thông tin, hãy xem phần "Cấu hình".

CẢNH BÁO: Chúng ta muốn giả định rằng các thực thể OptionsBase là không thể thay đổi và sử dụng chúng theo cách đó (chẳng hạn như một phần của SkyKeys). Không đúng như vậy và sửa đổi chúng là một cách rất hay để phá vỡ Bazel theo những cách tinh tế nhưng 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. (Sửa đổi FragmentOptions ngay sau khi xây dựng trước khi bất kỳ ai khác có cơ hội để giữ lại một tệp tham chiếu đến tệp đó và trước ngày equals() hoặc hashCode() CANNOT TRANSLATE

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 sang ngôn ngữ lập trình riêng lẻ)
  4. Các quy tắc của Starlark cũng có thể xác định các tuỳ chọn riêng của chú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, trong đó chỉ định tên và loại tuỳ chọn dòng lệnh cùng với một số văn bản trợ giúp.

Loại giá trị Java cho giá trị của tuỳ chọn dòng lệnh thường là kiểu đơ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 lựa chọn thuộc loại phức tạp hơn; trong trường hợp này, công việc chuyển đổi từ Chuỗi dòng lệnh cho loại dữ liệu rơi vào việc triển khai com.google.devtools.common.options.Converter.

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

Bazel kinh doanh trong lĩnh vực xây dựng phần mềm, 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 trên có tên 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ữ

Một "kho lưu trữ" là cây nguồn mà nhà phát triển sử dụng; thường thì đại diện cho một dự án duy nhất. Tổ tiên của Bazel, 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ữ mới. Kho lưu trữ nơi Bazel được gọi ra được gọi là "main kho lưu trữ", các kho lưu trữ còn lại được gọi là "kho lưu trữ bên ngoài".

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

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 ghép nối với nhau; này do SymlinkForest thực hiện, giúp liên kết mọi gói trong kho lưu trữ chính vào $EXECROOT và mọi kho lưu trữ bên ngoài vào $EXECROOT/external hoặc $EXECROOT/...

Gói

Mỗi kho lưu trữ bao gồm các gói, một tập hợp các tệp có liên quan và thông số kỹ thuật của các phần phụ thuộc. Các giá trị 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 ưu tiên BUILD.bazel; lý do tại sao các tệp BUILD vẫn được chấp nhận là đối tượng cấp trên của Bazel là Blaze đã sử dụng thuộc tính này tên tệp. Tuy nhiên, hoá ra đó lại là một phân đoạn đường dẫn thường được sử dụng, đặc biệt 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 thể khiến các gói khác thay đổi. Thêm hoặc xoá BUILD tệp _can _thay đổi các gói khác, vì các khối cầu đệ quy dừng ở ranh giới gói và do đó, sự hiện diện của tệp BUILD sẽ dừng đệ quy.

Việc đánh giá tệp BUILD được gọi là "tải gói". Đã 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 gói đang tả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) cho 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à lỗ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ư lớp shell, lớp này hỗ trợ các khối cầu đệ quy chuyển xuống các thư mục con (nhưng không chuyển vào các gói con). Ứng dụng này cần có 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 triển khai tất cả các loại thủ thuật để làm cho chiến dịch đó 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ề hình ảnh cầu cũ để tránh thông báo "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 dùng riêng để phân tích cú pháp "bên ngoài" gó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ế do 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 rõ ràng hơn giữa việc phân tích cú pháp các yếu tố "bên ngoài" gói hàng 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 hợp với nhau 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. Ngang bằng Theo cách nói của 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 là mục tiêu; kết quả của Bazel không có nhãn liên kết.
  2. Quy tắc: mô tả các bước để lấy kết quả từ dữ liệu đầu vào. Chúng thường được liên kết với ngôn ngữ lập trình (ví dụ: cc_library, java_library hoặc py_library), nhưng có một số ứng dụ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ữ mà Nhãn là tên pac/kage là thư mục chứa tệp BUILDname là đường dẫn của tệp (nếu nhãn đề cập đến tệp nguồn) tương ứng với thư mục của . Khi tham chiếu đến một mục tiêu trên dòng lệnh, một số phần của nhãn có thể bỏ qua:

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

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à "native rules", nhập RuleClass). Về lâu dài, mọi ngôn ngữ cụ thể quy tắc này sẽ được triển khai trong Starlark, nhưng một số họ quy tắc cũ (chẳng hạn như Java hoặc C++) vẫn ở trong Java tại thời điểm này.

Bạn cần nhập các lớp quy tắc Starlark ở đầu tệp BUILD bằng cách sử dụng câu lệnh load(), trong khi các lớp quy tắc Java là "bẩm sinh" được biết đến bởi Bazel, nhờ đã đă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 của chúng, 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. Nhà cung cấp thông tin bắc cầu theo quy tắc "thường" số lần tạo

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 lớp quy tắc tạo. Nhưng trong Starlark và trong tài liệu dành cho người dùng, "Quy tắc" chỉ nên dùng để tham chiếu đến lớp quy tắc; mục tiêu chỉ là "mục tiêu". Cũng xin lưu ý rằng mặc dù RuleClass có "lớp học" trong nên 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 công cụ này là mọi thứ cần xây dựng trong quá trình xây dựng. Mọi thứ cần được sắp xếp theo đồ thị không chu trình có các cạnh trỏ từ bất kỳ phần dữ liệu nào đến các phần phụ thuộc của nó, tức là các phần dữ liệu khác cần được biết để 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 SkyKey giây. Cả hai đều không thể thay đổi sâu sắc; chỉ nên sử dụng các đối tượng không thể thay đổi có thể truy cập được từ họ. Bất biến này hầu như luôn được giữ lại, và trong trường hợp không (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 rất cố gắng để không thay đổi hoặc chỉ thay đổi chúng theo những cách mà không thể quan sát từ bên ngoài. Từ đó, mọi thứ đượ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à không thể thay đổi.

Cách thuận tiện nhất để quan sát biểu đồ Skyframe là chạy bazel dump --skyframe=deps để kết xuất biểu đồ, một SkyValue trên mỗi dòng. Tốt nhất để làm điều đó 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. Chiến lược phát hành đĩa đơn gói có tên tương tự com.google.devtools.build.lib.skyframe chứa thực hiện Bazel trên Skyframe. Thông tin khác về Skyframe là có sẵn tại đây.

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

Bất cứ khi nào SkyFunction yêu cầu một phần phụ thuộc không có sẵn, getValue() sẽ trả về giá trị rỗng. Sau đó, hàm này sẽ tạo lại quyền kiểm soát cho Skyframe bằng cách chính nó trả về giá trị rỗng. Tại một thời điểm sau đó, Skyframe sẽ đánh giá phần phụ thuộc không có sẵn, hãy khởi động lại hàm từ đầu — chỉ điều này bất cứ khi nào lệnh gọi getValue() 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 phải lặp lại trước khi khởi động lại. Nhưng chỉ số này không bao gồm việc thực hiện đánh giá phần phụ thuộc SkyValues, được lưu vào bộ nhớ đệm. Do đó, chúng tôi thường hợp tác xung quanh vấn đề này bằng cách:

  1. Khai báo phần phụ thuộc theo loạt (bằng cách sử dụng getValuesAndExceptions()) để hạn chế số lần khởi động lại.
  2. Chia SkyValue thành các phần riêng biệt được tính theo các giá trị khác nhau SkyFunction để có thể được tính toán và lưu vào bộ nhớ đệm một cách độc lập. Chiến dịch này nên được thực hiện một cách có chiến lược, vì phương pháp này có khả năng làm tăng bộ nhớ mức sử dụng.
  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 lưu một bộ nhớ đệm tĩnh đột xuất "phía sau Skyframe". Với SkyFunctions phức tạp, quản lý trạng thái giữa các lần khởi động lại có thể khó khăn, vì vậy StateMachine được giới thiệu cho một phương pháp có cấu trúc đối với tình trạng đồ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 một SkyFunction. Ví dụ: DependencyResolver#computeDependencies sử dụng StateMachine với getState() để tính toán tập hợp có thể rất lớn phần phụ thuộc trực tiếp của mục tiêu đã định cấu hình, mà có thể dẫn đến việc khởi động lại tốn kém tài nguyên.

Về cơ bản, Bazel cần các loại giải pháp này vì hàng trăm hàng nghìn nút Skyframe trên máy bay là phổ biến và sự hỗ trợ của Java luồng nhẹ không hoạt động tốt hơn Triển khai StateMachine kể từ năm 2023.

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. Nó được xem là một tập hợp con bị hạn chế của Python và có ít loại hơn rất nhiều, nhiều hạn chế hơn đối với luồng điều khiển và quan trọng nhất là tính bất biến mạnh mẽ đảm bảo cho phép đọc đồng thời. Đó không phải là Turing-complete, 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 việc chung chung nhiệm vụ lập trình bằng ngôn ngữ đó.

Starlark được triển khai trong gói net.starlark.java. Nền tảng này cũng có cách triển khai Go độc lập tại đây. Java Hoạt động triển khai được dùng trong Bazel hiện là thông dịch viên.

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

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

Phương ngữ có sẵn cho tệp BUILD.bzl hơi khác bởi vì chúng thể hiện những điều khác nhau. Có danh sách những đ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 là "mục tiêu được định cấu hình", tức là một cặp (mục tiêu, cấu hình).

Đây được gọi là "giai đoạn tải/phân tích" vì nó có thể được chia thành hai các phần riêng biệt, trước đây đượ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. Đang tải gói, tức là chuyển tệp BUILD thành đối tượng Package đại diện cho chúng
  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

Mỗi 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 phải được phân tích từ dưới lên; tức là các nút lá trước tiên rồi đến lệnh trên dòng lệnh. Dữ liệu đầu vào cho việc phân tích một mục tiêu được định cấu hình là:

  1. Cấu hình. ("cách thức" xây dựng quy tắc đó; ví dụ: mục tiêu nền tảng mà còn những thứ 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. Nhà cung cấp thông tin bắc cầu của họ có sẵn đối với quy tắc đang được phân tích. Chúng được gọi như vậy vì chúng cung cấp "tổng hợp" của thông tin trong quá trình đóng bắc cầu của cấu hình đích, 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 một tệp nhị phân C++)
  3. Chính mục tiêu. Đây là kết quả của việc tải gói mà mục tiêu đã tham gia. Đối với quy tắc, phần tử này bao gồm các thuộc tính của quy tắc, thường là rất quan trọng.
  4. Cách triển khai mục tiêu đã định cấu hình. Đối với quy tắc, thao tác này có thể có trong Starlark hoặc Java. Tất cả mục tiêu được đị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. Những nhà cung cấp thông tin bắc cầu đã thiết lập các mục tiêu phụ thuộc vào nền tảng này 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 lớp này mạnh mẽ hơn, nhưng đồng thời thì sẽ dễ dàng hơn để làm Bad ThingsTM, ví dụ: viết mã dành cho người có thời gian hoặc độ phức tạp của không gian là cấp hai (hoặc tệ hơn), khiến máy chủ Bazel gặp sự cố khi Trường hợp ngoại lệ đối với Java hoặc vi phạm các giá trị bất biến (chẳng hạn như do vô tình sửa đổi một thuộc tính thực thể Options hoặc bằng cách thiết lập một mục tiêu đã định cấu hình có thể thay đổi)

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

Cấu hình

Cấu hình là "cách thức" về xây dựng mục tiêu: dành cho nền tảng nào, với những gì tuỳ chọn dòng lệnh, 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. Chiến dịch này rất hữu ích, ví dụ: khi sử dụng cùng một mã cho công cụ chạy trong cho bản dựng và cho mã mục tiêu, đồng thời chúng tôi đang biên dịch chéo hoặc khi chúng tôi 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 CPU kiến trúc)

Về mặt lý thuyết, cấu hình này là một thực thể BuildOptions. Tuy nhiên, trong thực hành, BuildOptions được gói bằng BuildConfiguration để cung cấp các chức năng bổ sung. Nó truyền từ đầu biểu đồ phần phụ thuộc ở dưới cùng. Nếu có thay đổi, thì bản dựng cần phải 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, ví dụ: số lần chạy kiểm thử được yêu cầu thay đổi, mặc dù điều đó chỉ ảnh hưởng đến các mục tiêu kiểm thử (chúng tôi dự định "cắt" cấu hình để 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, quá trình đó cần khai báo nó trong định nghĩa bằng cách sử dụng RuleClass.Builder.requiresConfigurationFragments() của Google. 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 để 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ác mục tiêu 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 phải 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 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. Những quá trình chuyển đổi này được chỉ định trong Attribute.Builder.cfg() và là các hàm từ Rule (trong đó quá trình chuyển đổi diễn ra) và BuildOptions (cấu hình ban đầu) thành một trở lên 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 thông tin 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 và phần phụ thuộc đó do đó cần đượ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ư mã gốc trong các APK Android lớn)

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

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

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

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 biết các thông tin về các mục tiêu đã thiết lập khác phụ thuộc vào loại chiến dịch đó. Lý do "ngoại động từ" là ở tên gọi của mình, đó thường là một kiểu cuộn lại đó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 trong Java và Starlark (ngoại lệ là DefaultInfo, là sự kết hợp giữa FileProvider, FilesToRunProviderRunfilesProvider vì API đó được cho là giống Starlark hơn là 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 phải là có 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 đối tượng này từ Starlark bằng cách sử dụng provider() và là cách nên dùng để tạo trình cung cấp mới. Ký hiệu là đượ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 dùng nữa (chúng tôi chưa có thời gian để xoá dịch vụ 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 đượ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 đã định cấu hình Starlark đều đượ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ề của chúng. 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 của quy tắc này đại diện." Đây là những 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 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" quy tắc có thể bản dựng. Bạn có thể truy cập vào các thuộc tính 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à những thử nghiệm cần tệp đầu vào. Điều này được thể hiện trong Bazel bằng khái niệm "runfile". Đáp "cây runfiles" là cây thư mục của các tệp dữ liệu cho một tệp nhị phân cụ thể. Nó đượ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 liên kết tượng trưng riêng lẻ trỏ đến các tệp trong cây nguồn đầ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à ánh xạ từ đường dẫn của một tệp trong cây runfile đến thực thể Artifact biểu thị nó. Việc này phức tạp hơn một chút so với một Map cho hai 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 nhập cũ trong cây runfile, cũng cần được thể hiện.

Các tệp chạy được thu thập bằng RunfilesProvider: một thực thể của lớp này đại diện cho các tệp runfile mà một mục tiêu đã định cấu hình (chẳng hạn như một thư viện) và bắc cầu của nó và chúng được tập hợp như một tập hợp con lồng ghép (trên thực tế, chúng là đượ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 liên kết mục tiêu với các tệp runfile phần phụ thuộc của nó, thêm một số phần phụ thuộc riêng, sau đó gửi kết quả thiết lập lên trên trong biểu đồ phần phụ thuộc. Một thực thể RunfilesProvider chứa hai Runfiles trường hợp thứ nhất là khi quy tắc phụ thuộc vào "dữ liệu" và một cho mỗi loại phần phụ thuộc sắp tới. Đó là vì một mục tiêu đôi khi trình bày các tệp chạy khác nhau khi phụ thuộc vào một thuộc tính dữ liệu so với cách khác. Đây là hành vi cũ không mong muốn mà chúng tôi chưa xử lý đang 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. Chiến dịch này khác với RunfilesRunfilesSupport có khả năng thực sự được tạo (không giống như Runfiles, chỉ là một bản đồ). Chiến dịch này cần có các thành phần bổ sung sau:

  • Tệp kê khai các tệp runfile đầu vào. Đây là phần mô tả được chuyển đổi tuần tự về cây runfile. Tệp này được dùng làm proxy cho nội dung của cây runfile và Bazel giả định rằng cây runfile thay đổi khi và chỉ khi nội dung về thay đổi trong tệp kê khai.
  • 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 sử dụng xử lý các 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.
  • 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. Đơn đặt hàng để giảm số lượng cạnh phụ thuộc, trung gian runfile có thể là được dùng để biểu thị tất cả những thông tin này.
  • Đối số dòng lệnh để chạy tệp nhị phân có các runfile Đại diện cho đối tượng RunfilesSupport.

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". Đó là được mô tả cho người dùng Bazel tại đây. Tốt ví dụ thúc đẩy 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 xây dựng cách triển khai một giao thức thông báo vùng đệm ("đơn vị cơ bản" của vùng đệm giao thức) trong bất kỳ chương trình nào ngôn 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, nó chỉ được tạo một lần.

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

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

Một chức năng khá bất ngờ là các khía cạnh có thể đính kèm vào các khía cạnh khác; ví dụ: một khía cạnh thu thập đường dẫn lớp cho một IDE Java có thể sẽ có thể 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ố đó 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).

Sự phức tạp của các khía cạnh về các khía cạnh được thể hiệ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 chạy các hành động tạo bản dựng và nhiều cấu trúc cho mã nào được tạo. Các kiến trúc này được gọi là nền tảng ở Bazel cách diễn đạt (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ị trong 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") để giới hạn các giá trị (chẳng hạn như một CPU cụ thể như x86_64). Chúng tôi có một "từ điển" quy tắc ràng buộc thường dùng nhất các chế độ cài đặt và giá trị 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 nào bản dựng đó đang chạy trên và nền tảng nào được nhắm mục tiêu thì người dùng có thể cần sử dụng trình biên dịch khác nhau; ví dụ: một chuỗi công cụ C++ cụ thể có thể chạy trên một hệ điều hành cụ thể và có thể nhắm đến một số hệ điều hành khác. Bazel phải xác định mã C++ trình biên dịch được dùng dựa trên việc thực thi tập hợp và nền tảng mục tiêu (tài liệu về chuỗi công cụ tại đây).

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

Thực hiện theo cách này vì chúng ta cần biết những ràng buộc đối với mỗi để thực hiện việc phân giải chuỗi công cụ và phân giải theo ngôn ngữ cụ thể *_toolchain() quy tắc chứa nhiều thông tin hơn thế, vì vậy, chúng cần nhiều thông tin hơn thời gian tải.

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

  1. Trong tệp MODULE.bazel bằng hàm register_execution_platforms()
  2. Trên dòng lệnh, sử dụng dòng lệnh --extra_execution_platforms lựa chọn

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

Nền tảng mục tiêu cho 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ì chúng tôi cuối cùng muốn hỗ trợ nhiều nền tảng mục tiêu, nhưng không được triển khai chưa.

Tập hợp chuỗi công cụ dùng cho một mục tiêu đã định cấu hình được xác định theo ToolchainResolutionFunction. Đâ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 đí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 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 nó là một UnloadedToolchainContext, về cơ bản là một bản đồ từ loại chuỗi công cụ (được biểu thị dưới dạng một thực thể ToolchainTypeInfo) cho nhãn của chuỗi công cụ đã chọn. Cuộc gọi này được gọi là "đã huỷ tải" vì không chứa chính chuỗi công cụ, chỉ với nhãn của chúng.

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

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

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 vài 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; chúng và không có ở Bazel, nhưng mã nguồn có thể chứa tham chiếu đến nó. Thuộc tính chi phối thuộc tính này được gọi 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" nào có thể được xây dựng khi "môi trường" là một bản sao 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 trực tiếp nhất của thông số kỹ thuật; hệ thống sẽ khai báo tập hợp chính xác cá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 môi trường là một quy tắc hỗ trợ bên cạnh "tiêu chuẩn" những môi trường được hỗ trợ bởi 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 thuộc về một nhóm các ứng dụng ngang hàng có liên quan theo chủ đề (chẳng hạn như "CPU kiến trúc", "phiên bản JDK" hoặc "hệ điều hành trên thiết bị di động"). Chiến lược phát hành đĩa đơn định nghĩa nhóm môi trường bao gồm môi trường nào trong số này phải được hỗ trợ theo "mặc định" nếu không được chỉ định khác bởi Thuộc tính restricted_to= / environment(). Quy tắc không có các thuộc tính kế thừa tất cả các giá trị mặc định.
  5. Thông qua một lớp quy tắc mặc định. Tùy chọn này sẽ ghi đè giá trị mặc định chung cho tất cả các bản sao của lớp quy tắc nhất định. Ví dụ: điều này có thể được sử dụng để tất cả quy tắc *_test đều có thể kiểm thử được mà mỗi thực thể không phải thực hiện rõ ràng khai báo khả năng này.

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

Một bản dựng có thể bị hạn chế ở một môi trường nhất định thông qua 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 đang ở RuleContextConstraintSemanticsTopLevelConstraintSemantics.

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

Từ "chính thức" hiện tại cách mô tả nền tảng mà một mục tiêu tương thích bằng cách sử dụng các quy tắc ràng buộc tương tự 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 #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 muốn chăm sóc để ngăn những người khác không tuỳ tiện phụ thuộc vào . Nếu không, theo định luật Hyrum, mọi người sẽ dựa vào những hành vi mà bạn coi là triển khai chi tiết.

Bazel hỗ trợ điều này bằng cơ chế có tên là chế độ hiển thị: bạn có thể khai báo rằng mục tiêu cụ thể chỉ có thể phụ thuộc vào việc sử dụng chế độ hiển thị. Chiến dịch này hơi đặc biệt bởi vì, mặc dù nó chứa danh sách các nhãn, có thể mã hoá một mẫu trên tên gói thay vì một con trỏ tới bất kỳ mục tiêu cụ thể. (Đú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ị. Chiến dịch này có thể là hằng số (hoàn toàn công khai hoặc hoàn toàn riêng tư) hoặc một danh sách 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) để đóng gói trực tiếp (//pkg:__pkg__) hoặc cây con của gói (//pkg:__subpackages__). Cú pháp 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 đã thiết lập (PackageGroupConfiguredTarget). Chúng ta có thể thay thế chúng bằng các quy tắc đơn giản nếu muốn. Logic của họ được triển khai với sự trợ giúp của: PackageSpecification, tương ứng với một mẫu đơn như //pkg/...; PackageGroupContents, tương ứng vào một thuộc tính packages của package_group; và PackageSpecificationProvider, tổng hợp trên package_groupincludes bắc cầu.
  • Quá trình 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 mục khác địa điểm.
  • Việ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ó, tự thêm và gói tập hợp tổng hợp vào 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 mã này cũng có thể làm 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 ngây thơ bằng cách sử dụng, chẳng hạn như List hoặc Set, chúng ta sẽ kết thúc sử dụng bộ nhớ bậc hai: nếu có một chuỗi N quy tắc và mỗi quy tắc thêm một , chúng tôi 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 NestedSet. Đây là một cấu trúc dữ liệu bao gồm các NestedSet khác thực thể và một số thành phần của riêng nó, từ đó tạo thành đồ thị không chu trình có hướng tập hợp. 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, đặt hàng 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 nút đó phải là như nhau mỗi lần".

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 đầu ra mà người dùng muốn. Các lệnh được biểu thị dưới dạng bản sao 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ị lưỡng 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ó hai loại: cấu phần phần mềm nguồn (loại có sẵn trước khi Bazel bắt đầu thực thi) và các cấu phần phần mềm phát sinh (những cấu phần cần phải xây dựng). 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 để đảm bảo tính cập nhật bằng công nghệ điện toán giá trị tổng kiểm của chúng, trong đó 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 bắt đầu 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 đơn vị này sẽ kiểm tra tính cập nhật của đang 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ơ lửng đường liên kết tượng trưng. Thường được sử dụng trong trường hợp sau đó gói một số tệp vào một một số kiểu tệp lưu trữ.
  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. Chúng đượ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à . 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 tạo lại. Tham số này được sử dụng riêng cho thông tin về dấu bản dựng: chúng tôi không muốn 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 giải quyết, chỉ là chúng tôi chưa triển khai nó (chúng tôi nên -- việc tham chiếu thư mục nguồn trong tệp BUILD là một trong một số vấn đề không chính xác đã được biết đến từ lâu với Bazel; chúng ta có một Loại công việc nào được kích hoạt bởi BAZEL_TRACK_SOURCE_DIRECTORIES=1 thuộc tính JVM)

Một loại Artifact đáng chú ý là bên trung gian. Các vị trí này được biểu thị bằng Artifact thực thể là kết quả của MiddlemanAction. Chúng được dùng để 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. Điều này 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 không có N*M cạnh phần phụ thuộc, chỉ N+M (các cạnh này 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. Chúng chủ yếu được dùng để tìm lỗi mã nguồn nhưng cũng trong quá trình biên dịch C++ (xem CcCompilationContext.createMiddleman() để được giải thích)
  • Trình trung gian Runfile được dùng để đảm bảo sự hiện diện của cây runfile. Do đó, mã đó 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 duy nhất được tham chiếu bởi cây chạy tệp.

Hành động được hiểu rõ nhất là một lệnh cần chạy, môi trường nó cần và tập hợp đầu ra mà nó tạo ra. Sau đây là những yếu tố chính thành phần mô tả của 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 được Bazel biết đến. Chúng là lớp con của AbstractAction. Hầu hết các thao tác SpawnAction hoặc StarlarkAction (tương tự như vậy, có lẽ chúng không nên riêng biệt), mặc dù Java và C++ có các kiểu thao tác riêng (JavaCompileAction, CppCompileActionCppLinkAction).

Cuối cùng, chúng tôi muốn di chuyển mọi thứ sang SpawnAction; JavaCompileAction là 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ả quét.

Biểu đồ hành động chủ yếu là "đượ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ời gọi ActionExecutionFunction. Á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() và có một vài các tối ưu hoá để duy trì số 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 ra khoá cho hành động tạo ra quảng cáo đó
  • 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ố hành động do nhiều mục tiêu đã định cấu hình tạo ra; Quy tắc của Starlark vì chúng chỉ được phép đặt các hành động phát sinh vào thư mục được xác định theo cấu hình và gói của chúng (nhưng kể cả vậy, các quy tắc trong cùng một gói có thể xung đột), nhưng các quy tắc được triển khai trong Java có thể đặt cấu phần phần mềm phát sinh ở bất cứ đâu.

Đây được coi là một sai tính năng, nhưng việc loại bỏ nó 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 tệp nguồn cần được xử lý theo cách nào đó và tệp đó được tham chiếu bằng nhiều quy tắc (sóng tay). Bạn phải trả chi phí một dung lượng RAM: mỗi thực thể của hành động đượ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 đầu vào, cùng đầu ra và chạy cùng một dòng lệnh. Chiến dịch này thì quan hệ tương đương được triển khai trong Actions.canBeShared() và được xác minh giữa giai đoạn phân tích và giai đoạn 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 đòi hỏi phải có chiến lược "toàn cầu" chế độ xem 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 trong bản dựng, chẳng hạn như các lệnh tạo ra đầu ra.

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

  • Thay đổi các dòng lệnh hành động khi một gói được di chuyển khỏi đường dẫn gói mục nhập vào một mục khác (từng là trường hợp thường gặp)
  • Đ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 quy trình đó chạy trên máy
  • Phương thức này yêu cầu một phép biến đổi dòng lệnh dành riêng cho công cụ đang sử dụng (hãy cân nhắc sự khác biệt giữa các đường dẫn lớp Java và C++, chẳng hạn như 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 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 qua đồ thị hành động (đồ thị lưỡng phần, đồ thị có hướng) bao gồm các thao tác cùng các cấu phần phần mềm đầu vào và đầu ra) 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 thực thể của SkyValue lớp ActionExecutionValue.

Vì việc chạy một hành động khá 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ị va đập phía sau Skyframe:

  • ActionExecutionFunction.stateMap chứa dữ liệu để khởi động lại Skyframe trong số ActionExecutionFunction 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 hành động sau khi thực thi lại trong Skyframe, thì nó vẫn có thể là một lượt truy cập trong bộ nhớ đệm hành động cục bộ. Nó biểu thị 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 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 bộ nhớ đệm thao tác cục bộ số lượt truy cập mặc dù biểu đồ Skyframe trống.

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

Trái với tên của nó, đây là một bản đồ từ đường dẫn của cấu phần phần mềm phát sinh đến hành động tạo ra quảng cáo đó. 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", thường là dòng lệnh được thực thi, nhưng nói chung, đại diện cho tất cả những gì không được ghi lại bằng giá trị tổng kiểm của tệp đầu vào (chẳng hạn như đối với FileWriteAction, đây là giá trị tổng kiểm của dữ liệu được viết ra)

Ngoài ra còn có "bộ nhớ đệm thao tác từ trên xuống" mang tính thử nghiệm cao vẫn còn dưới mức Sử dụng 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 thông tin đầ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 đầu vào không thực sự cần thiết. Ví dụ chuẩn là C++, trong đó tốt hơn là nên đoán một cách có cơ sở về tệp tiêu đề nào trong C++ tệp sử dụng từ đóng bắc cầu của nó để chúng tôi không phải chú ý đến việc gửi tệp cho người 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 tệp "đầu vào", nhưng quét tệp nguồn để chuyển đổi và chỉ đánh dấu các tệp tiêu đề đó làm dữ liệu đầu vào được đề cập trong các câu lệnh #include (chúng tôi ước tính cao hơn để không cần triển khai bộ tiền xử lý C đầy đủ) Lựa chọn này hiện được kết nối cứng để "sai" ở Bazel và chỉ được dùng tại Google.
  • 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. Ngang bằng C++, tệp này được gọi là ".d files": trình biên dịch cho biết tệp tiêu đề nào được sử dụng sau sự kiện và để tránh xấu hổ khi có mức độ gia tăng hơn Make, Bazel tận dụng thực tế này. Điều này giúp cải thiện ước tính so với trình quét bao gồm vì công cụ 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. Hàm này sẽ trả về một tập hợp các giá trị được lồng Tệp phần mềm được xác định là bắt buộc. Đây phải là các cấu phần phần mềm nguồn để không có cạnh phụ thuộc nào trong biểu đồ hành động không có 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. Ở cuối Action.execute(), thao tác có thể gọi Action.updateInputs() để cho Bazel biết rằng không phải tất cả dữ liệu đầu vào đều được cần thiết. Điều này có thể dẫn đến các bản dựng gia tăng không chính xác nếu dữ liệu đầu vào đã sử dụng là được báo cáo là không được sử dụng.

Khi bộ nhớ đệm của thao tác 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ư đã tạo) sau khi khởi động lại máy chủ), Bazel tự gọi updateInputs() để tập hợp 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à cắt bớt trước đó.

Các hành động của Starlark có thể tận dụng cơ sở này để khai báo một số dữ liệu đầu vào là không dùng đến sử dụng đố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ể thực thi cục bộ, cục bộ nhưng trong nhiều loại hộp cát hoặc từ xa. Chiến lược phát hành đĩa đơn khái niệm thể hiện điều này được gọi là ActionContext (hoặc Strategy, vì chúng ta chỉ hoàn tất một nửa 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ề nội dung ngữ cảnh hành động mà chúng có. Điều này xảy ra trong hàm khởi tạo của ExecutionTool. Các loại ngữ cảnh thao tác được xác định bằng Class Java là giao diện phụ của ActionContext và 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ừ những ngữ cảnh có sẵn và đã chuyển tiếp đến ActionExecutionContextBlazeExecutor .
  3. Thao tác yêu cầu bối cảnh bằng ActionExecutionContext.getContext()BlazeExecutor.getStrategy() (chỉ có một cách để thực hiện nó...)

Người đưa ra chiến lược có thể thoải mái kêu gọi các chiến lược khác thực hiện công việc của họ; URL này được dùng cho ví dụ: trong chiến lược động bắt đầu hành động cả cục bộ và từ xa, sau đó sử dụng giá trị 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 hành động mới cho mọi hành động (điều này thể hiện một vấn đề có thể xảy ra với tính chính xác, vì Bazel dựa vào sự hứa hẹn của quy trình thực thi mà nó không mang dữ liệu 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. Liệu một worker Giá trị có thể được sử dụng lại được xác định bằng cách tính giá trị tổng kiểm cho công cụ được sử dụng bằng cách sử dụng WorkerFilesHash. Chiến lược này phụ thuộc vào việc biết hành động đầu vào nào đại diện cho của công cụ và đại diện cho dữ liệu đầu vào; điều này là do nhà sáng tạo xác định của Hành động: Spawn.getToolFiles() và các tệp run của Spawn là đượ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!):

  • Bạn có thể xem thông tin về nhiều chiến lược để chạy hành động tại đây.
  • Thông tin về chiến lược động, trong đó chúng ta chạy hành động tại địa phương và từ xa để xem có phương án nào kết thúc trước không tại đây.
  • Có thông tin về những vấn đề phức tạp khi thực hiện các hành động trên máy tại đây.

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

Bazel có thể chạy nhiều hành động song song. Số hành động liên quan đến địa điểm thực tế nên chạy song song khác nhau giữa các hành động: càng nhiều tài nguyên yêu cầu hành động nào, ít thực thể hơn nên 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 thao tác phải chú thích bằng thông tin ước tính về tài nguyên địa phương cần thiết dưới dạng Thực thể của ResourceSet (CPU và RAM). Sau đó, khi ngữ cảnh hành động làm gì đó cần có tài nguyên cục bộ, nên các dịch vụ này sẽ gọi ResourceManager.acquireResources() và bị chặn cho đến khi có các tài nguyên cần thiết.

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

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

Mỗi thao tác cần một vị trí riêng trong thư mục đầu ra nơi thao tác đó đặt kết quả đầu ra. 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 ứng dụng cụ thể là như thế nào đã xác định được cấu hình chưa? 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, chúng phải có các thư mục khác nhau để cả hai đều có thể 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ư lệnh của một hành động tạo ra cùng một tệp đầu ra, Bazel không biết hành động cần chọn ("xung đột hành động")
  2. Nếu hai cấu hình đại diện cho "khoảng" họ cũng nên có cùng một tên để có thể sử dụng lại các hành động được thực hiện trong một hành động các dòng lệnh khớp: ví dụ: thay đổi tuỳ chọn dòng lệnh thành trình biên dịch Java sẽ không dẫn đến việc các hành động biên dịch C++ được chạy lại.

Cho đến nay, chúng tôi vẫn chưa tìm ra một phương pháp nguyên tắc để giải quyết vấn đề này, có nhiều điểm tương đồng với vấn đề cắt cấu hình. Cuộc thảo luận dài hơn có sẵn tại đây. Các vấn đề chính gây ra vấn đề là các quy tắc Starlark (mà tác giả thường không phải là quen thuộc với Bazel) và các khía cạnh khác, điều này bổ sung thêm một khía cạnh 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 <CPU>-<compilation mode> cùng với nhiều hậu tố được thêm vào để cấu hình đó chuyển đổi đượ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 quá trình chuyển đổi cấu hình Starlark được thêm vào để người dùng không được 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 từng mảnh cấu hình thêm phần riêng vào tên của thư mục đầu ra.

Thử nghiệm

Bazel có 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 thông tin 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ử cho nhiều quy trình) về 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ó TestProvider, mô tả cách chạy bài 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à "bộ nhớ đệm trạng thái" tệp 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. Chiến lược phát hành đĩa đơn được triển khai trong TestsForTargetPatternFunction. Có phần nào nếp nhăn đáng ngạc nhiên là nếu một bộ kiểm thử khai báo rằng không có kiểm thử nào, thì nó tham chiếu đến mọi bài kiểm thử trong gói của mình. Việc này được triển khai trong Package.beforeBuild() bằng thêm một thuộc tính ngầm ẩn có tên là $implicit_tests để kiểm thử các quy tắc của bộ kiểm thử.

Sau đó, thử nghiệm đượ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 khi phân tích cú pháp mục tiêu và kết quả sẽ được đưa vào TargetPatternPhaseValue.getTestsToRunLabels(). Lý do tại sao lại không thể định cấu hình các thuộc tính quy tắc có thể lọc ra xảy ra trước giai đoạn phân tích, do đó, cấu hình không được sẵn có.

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

Để tăng cường tính minh bạch cho quy trình công phu này, tests() toán tử truy vấn (được triển khai trong TestsFunction) có sẵn để cho biết kiểm thử nào sẽ chạy khi một mục tiêu cụ thể được chỉ định trên dòng lệnh. Bây giờ rất tiếc là việc triển khai lại, vì vậy việc này có thể khác với cách ở trên trong 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 đó, dẫn đến việc thực thi TestRunnerAction, cuối cùng sẽ gọi phương thức TestActionContext được chọn theo tuỳ chọn dòng lệnh --test_strategy chạy chương trình 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 chương trình kiểm thử những gì được mong đợi từ đó. Mô tả chi tiết về điều Bazel kỳ vọng từ các thử nghiệm và những thử nghiệm có thể mong đợi từ Bazel có sẵn tại đây. Tại đơn giản nhất, mã thoát bằng 0 có nghĩa là thành công, còn bất cứ điều gì khác có nghĩa là thất bại.

Ngoài tệp trạng thái bộ nhớ đệm, mỗi quá trình kiểm thử còn tạo ra một số tệp. 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 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, kết quả kiểm thử trên bảng điều khiển. stdout và stderr không phải là đã tách riêng.
  • test.outputs, "thư mục dữ liệu đầu ra chưa được khai báo"; mã này được sử dụng bởi các bài kiểm thử muốn xuất các tệp bên cạnh những nội dung chúng in ra thiết bị đầu cuối.

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

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 thử nghiệm khác. Bạn có thể kích hoạt điều này bằng cách thêm tags=["exclusive"] vào quy tắc kiểm thử hoặc chạy kiểm thử bằng --test_strategy=exclusive . Mỗi cấp độ độ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 thử nghiệm sau đoạn mã "chính" bản dựng. 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 của thiết bị đầu cuối được kết xuất khi thao tác đó Khi hoàn tất, người dùng có thể yêu cầu truyền trực tuyến kết quả kiểm thử để được thông báo về tiến trình của một bài 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ụ ý kiểm thử độc quyền sao cho đầu ra của các bài kiểm thử khác nhau không được xen kẽ.

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

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). Chúng được kết xuất vào Giao thức sự kiện bản dựng và được phát tới bảng điều khiển của AggregatingTestListener.

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 độ sử dụng, 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ử để 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 viết 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 phụ và bao gồm các phần được viết bằng nhiều ngôn ngữ lập trình khác nhau (với thời gian chạy thu thập mức độ phù hợp). Tập lệnh trình bao bọc chịu trách nhiệm chuyển đổi các tệp thu được thành định dạng LCOV nếu cần và hợp nhất chúng thành một .

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

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

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 bao phủ của một thư viện, hoặc kiểm thử xem có mã nào trong đó được chạy hay không. Vấn đề mà giải pháp này giải quyết là nếu bạn nếu 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 độ phù hợp của tất cả các bài kiểm thử vì có thể có mã trong tệp nhị phân không liên kết với bất kỳ thử nghiệm nào. Do đó, những gì 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 dữ liệu mà không có đường. Tệp mức độ phù hợp cơ sở của một mục tiêu đang ở bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat . Quá trình này cũng được tạo cho các tệp nhị phân và thư viện ngoài các bài kiểm thử nếu bạn vượt qua --nobuild_tests_only gắn cờ cho Bazel.

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

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 đo lường và tập hợp 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. Cho môi trường thời gian chạy, có thể được sử dụng trong thời gian chạy để quyết định tệp nào cần công cụ. Phạm vi này cũng được dùng để triển khai mức độ phù hợp cơ sở.

Tập hợp tệp siêu dữ liệu về đ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 tệp LCOV mà Bazel yêu cầu từ đó. Trong 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 là bật.

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

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

Công cụ truy vấn

Bazel có ít ngôn từ thường hỏi công ty nhiều câu về các biểu đồ khác nhau. Các kiểu 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 bổ sung bằng cách phân lớp con QueryFunction của Google. Để cho phép truyền trực tuyến các kết quả truy vấn, thay vì thu thập chúng cho một số cấu trúc dữ liệu, query2.engine.Callback được truyền đến QueryFunction, gọi phương thức đó để có kết quả mà thiết bị muốn trả về.

Kết quả của truy vấn có thể được đưa ra theo nhiều cách khác nhau: nhãn, nhãn và quy tắc class, 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 của truy vấn (proto, chắc chắn) là Bazel cần phát _all _thông tin mà quá trình tải gói cung cấp để bạn có thể làm khác biệt đầu ra và xác định xem một mục tiêu cụ thể có thay đổi hay không. Do vậy, 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ó bất kỳ thuộc tính nào có Starlark phức tạp giá trị. Cách giải quyết thông thường là sử dụng nhãn và đính kèm vào quy tắc có nhãn đó. Đây không phải là một giải pháp khiến người dùng hài lòng và tốt nhất là 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 là lớp con BlazeModule (tên này là một di tích của lịch sử Bazel khi nó từng được có tên 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 phần "không cốt lõi" chức năng 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) 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. Không nên làm hãy lấy dữ liệu đó 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à mô-đun có thể đăng ký trình nghe cho 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 Tạo giao thức sự kiện (chúng là BuildEvent). Việc này không chỉ cho phép BlazeModule mà còn cho phép mọi thứ bên ngoài quy trình Bazel để quan sát bản dựng. Chúng 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áy chủ (được gọi là Xây dựng dịch vụ sự kiện) để truyền trực tuyến các sự kiện.

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

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

Trong khi đó, ban đầu Bazel được thiết kế để sử dụng trong một kho lưu trữ đơn (monorepo) (một nguồn duy nhất) cây chứa mọi thứ mà người ta cần xây dựng), Bazel sống trong một thế giới nơi đ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 để cầu nối hai thế giới này: chúng đại diện cho mã cần thiết cho việc tạo bản dựng nhưng không nằm 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. Kết quả phức tạp là việc có thể xác định các quy tắc mới của kho lưu trữ trong các tệp Starlark. sau đó có thể được sử dụng để tải mã Starlark mới, có thể được dùng để xác định quy tắc kho lưu trữ, v.v.

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

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

Trước khi mã của kho lưu trữ có sẵn cho Bazel, mã này cần phải đã tìm nạp. 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 ứng dụng này cần có kho lưu trữ và tạo một RepositoryName dưới dạng SkyKey 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 nó đến tránh tải xuống lại các thứ trong trường hợp Skyframe khởi động lại, nhưng đó không phải là lập luận rất chắc chắn)
  3. RepositoryDelegatorFunction tìm ra quy tắc kho lưu trữ mà nó được yêu cầu tìm nạp bằng cách lặp lại các phần của tệp WORKSPACE cho đến khi được yêu cầu đã tìm thấy kho lưu trữ
  4. Tìm thấy RepositoryFunction thích hợp sẽ triển khai kho lưu trữ fetching; đó là triển khai Starlark cho 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ể đắt:

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

Sau khi tải xuống kho lưu trữ, cấu phần phần mềm trong kho lưu trữ đó sẽ được coi là nguồn cấu phần phần mềm. Đ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á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ữ đang thay đổi. Do đó, Các FileStateValue của một cấu phần phần mềm trong kho lưu trữ bên ngoài cần phụ thuộc vào kho lưu trữ bên ngoài của mình. Việc này do ExternalFilesHelper xử lý.

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

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

Do đó, Bazel cho phép 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 khác.

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

Việc liên kết được chỉ định trong tệp WORKSPACE làm thuộc tính repo_mapping của từng định nghĩa kho lưu trữ. Sau đó, nó xuất hiện trong Skyframe với tư cách là thành viên của WorkspaceFileValue, trong đó nó được chuyển thẳng đến:

  • Package.Builder.repositoryMapping được dùng để biến đổi giá trị của nhãn của các quy tắc trong gói bằng cách RuleClass.populateRuleAttributeValues()
  • Package.repositoryMapping được dùng trong giai đoạn phân tích (cho giải quyết những vấn đề như $(location) không được phân tích cú pháp trong khi tải pha)
  • 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. Ngoại lệ là những phần Java không thể tự làm hoặc không thể tự làm khi chúng ta triển khai. Chiến dịch này chủ yếu bị giới hạn ở việc tương tác với hệ thống tệp, kiểm soát quy trình và cấp thấp khác.

Mã C++ nằm trong lớp src/main/native và các lớp Java có mã gốc bao gồm:

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

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

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

Ngay sau khi lệnh gọi RPC đến từ ứng dụng, hai RpcOutputStream các thực thể được tạo (cho stdout và stderr) để chuyển tiếp dữ liệu được in vào chúng cho khách hàng. Sau đó, các thành phần này được gói trong một OutErr (stdout, stderr) ghép nối). Bất cứ nội dung nào cần in trên bảng điều khiển đều phải trải qua các bước này phát trực tuyến. Sau đó, những luồng này được chuyển cho BlazeCommandDispatcher.execExclusively().

Theo mặc định, đầu ra được in bằng chuỗi ký tự thoát ANSI. Khi những yêu cầu này không được áp dụng mong muốn (--color=no), chúng sẽ bị AnsiStrippingOutputStream xoá. Ngang bằng Ngoài ra, System.outSystem.err được chuyển hướng đến các luồng đầu ra này. Việc này là để có thể in thông tin gỡ lỗi bằng System.err.println() mà vẫn kết thúc trong đầu ra đầu cuối của ứng dụng (khác với máy chủ). Cần chú ý rằng nếu quá trình tạo ra đầu ra nhị phân (chẳng hạn như bazel query --output=proto), không kết nối với stdout diễn ra.

Thông báo ngắn (lỗi, cảnh báo, v.v.) được thể hiện thông qua Giao diện EventHandler. Đáng chú ý là những nội dung này khác với những nội dung mà một bài đăng EventBus (điều này khó hiểu). Mỗi Event có một EventKind (lỗi, cảnh báo, thông tin và một số thông tin khác) và chúng có thể có Location (địa điểm 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. Thông tin này đã được sử dụng phát lại thông tin tới giao diện người dùng do nhiều loại xử lý bộ nhớ đệm gây ra, ví dụ: 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 đưa ra.

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

Đầu ra đầu cuối chủ yếu được phát qua UiEventHandler, tức là 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 Bazel làm. 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 truyền luồng RPC đến ứng dụng thông qua Reporter.getOutErr(), cho phép truy cập trực tiếp vào các luồng 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 hoạt động chậm vì các bản dựng có xu hướng phát triển cho đến khi của những gì có thể chịu được. Vì lý do này, Bazel bao gồm một trình phân tích tài nguyên có thể dùng để lập hồ sơ 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 đặt tên phù hợp là Profiler. Chế độ này được bật theo mặc định, mặc dù chế độ này chỉ ghi lại dữ liệu rút gọn để chấp nhận được mức hao tổn; Dòng lệnh --record_full_profiler_data giúp ghi lại mọi thứ có thể.

Chrome tạo một hồ sơ ở định dạng trình phân tích tài nguyên của Chrome; video đó được xem tốt nhất trên Chrome. Mô hình dữ liệu của ngăn xếp công việc: người dùng có thể bắt đầu công việc, kết thúc công việc và chúng phải được lồng vào nhau một cách gọn gàng. Mỗi luồng Java sẽ nhận được ngăn xếp tác vụ riêng. TODO: Cách thực hiện việc này bằng các hành động và câu lệnh tiếp tục không?

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

Chúng ta cũng thực hiện việc 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 của GC.

Thử nghiệm Bazel

Bazel có hai loại thử nghiệm chính: loại thử nghiệm thấy Bazel là một "hộp đen" và những giải pháp chỉ chạy giai đoạn phân tích. Chúng tôi gọi các quy trình trước đây là "kiểm thử tích hợp" và "kiểm thử đơn vị" sau, mặc dù chúng giống với kiểm thử tích hợp ít được tích hợp hơn. Chúng tôi cũng có một số bài kiểm thử đơn vị thực tế, trong đó 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 mô hình được triển khai bằng khung kiểm thử bash rất chi tiết theo 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 được ưu tiên dùng được trang bị tốt cho hầu hết các tình huống thử nghiệm. Vì là khung Java, nên 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 và các công cụ lập mô hình tuỳ chỉnh. Có nhiều ví dụ về 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. Có một hệ thống tệp Scratch mà bạn có thể dùng để viết tệp BUILD, sau đó là nhiều trình 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.