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 (~350KLOC mã sản xuất và ~260 KLOC mã kiểm thử) và không ai quen thuộc với toàn bộ khung cảnh: mọi người đều biết rất rõ về thung lũng cụ thể của mình, nhưng ít người biết những gì nằm trên các ngọn đồi theo mọi hướng.
Để những người đang ở giữa hành trình không bị lạc trong một khu rừng tối tăm với lối đi thẳng 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ã để dễ dàng bắt đầu làm việc với cơ sở mã đó.
Phiên bản công khai của mã nguồn của Bazel có trên GitHub tại github.com/bazelbuild/bazel. Đây không phải là "nguồn đáng tin cậy"; 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 máy khách/máy chủ
Phần lớn Bazel nằm trong một quy trình máy chủ lưu trữ trong RAM giữa các bản dựng. Điều này cho phép Bazel duy trì trạng thái giữa các bản dựng.
Đây là lý do tại sao dòng lệnh Bazel có 2 loại tuỳ chọn: khởi động và lệnh. Trong dòng lệnh như sau:
bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar
Một số tuỳ chọn (--host_jvm_args=
) nằm trước tên lệnh cần chạy và một số tuỳ chọn nằm sau (-c opt
); loại trước đây được gọi là "tuỳ chọn khởi động" và ảnh hưởng đến toàn bộ quá trình của máy chủ, trong khi loại sau, "tuỳ chọn lệnh", chỉ ảnh hưởng đến một lệnh duy nhất.
Mỗi phiên bản máy chủ đề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 vấn đề này bằng cách chỉ định cơ sở đầu ra tuỳ chỉnh (xem phần "Bố cục thư mục" để biết thêm thông tin).
Bazel được phân phối dưới dạng một tệp thực thi ELF duy nhất cũng là một tệp .zip hợp lệ.
Khi bạn nhập bazel
, tệp thực thi ELF ở trên được triển khai trong C++ ("ứ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:
- Kiểm tra xem tệp đó đã tự giải nén hay chưa. Nếu không, hệ thống sẽ thực hiện việc đó. Đây là nơi triển khai máy chủ.
- Kiểm tra xem có phiên bản máy chủ đang hoạt động nào hoạt động hay không: phiên bản đó đang chạy, có các tuỳ chọn khởi động phù hợp và sử dụng đúng thư mục không gian làm việc. Phương thức này tìm thấy máy chủ đang chạy bằng cách xem thư mục
$OUTPUT_BASE/server
có tệp khoá với cổng mà máy chủ đang nghe. - Nếu cần, hãy tắt quy trình máy chủ cũ
- 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 giao tiếp với quy trình đó qua giao diện gRPC, sau đó đầu ra của Bazel sẽ được chuyển về thiết bị đầu cuối. Chỉ có thể chạy một lệnh cùng một lúc. Việc này được triển khai bằng cách sử dụng một cơ chế khoá phức tạp với các phần trong C++ và các phần trong Java. Có một số cơ sở hạ tầng để chạy song song nhiều lệnh, vì việc không thể chạy bazel version
song song với một lệnh khác là điều hơi đáng xấu hổ. Vấn đề chính là vòng đời của BlazeModule
và một số trạng thái trong BlazeRuntime
.
Ở cuối một lệnh, máy chủ Bazel sẽ truyền mã thoát mà ứng dụng sẽ trả về. Một điểm thú vị là việc triển khai bazel run
: nhiệm vụ của lệnh này là chạy một nội dung nào đó mà Bazel vừa tạo, nhưng không thể thực hiện việc đó từ quy trình máy chủ vì không có thiết bị đầu cuối. Thay vào đó, ứng dụng sẽ cho ứng dụng biết tệp nhị phân nào cần ujexec()
và với đối số nào.
Khi người dùng nhấn tổ hợp phím Ctrl-C, ứng dụng sẽ dịch tổ hợp phím này thành lệnh Huỷ trên kết nối gRPC. Lệnh này sẽ cố gắng chấm dứt lệnh càng sớm càng tốt. Sau lần nhấn tổ hợp phím Ctrl-C thứ ba, ứng dụng sẽ gửi SIGKILL đến máy chủ.
Mã nguồn của ứng dụng nằm trong src/main/cpp
và giao thức dùng để giao tiếp với máy chủ nằm trong src/main/protobuf/command_server.proto
.
Điểm truy cập chính của máy chủ là BlazeRuntime.main()
và các lệnh gọi gRPC từ ứng dụng được GrpcServerImpl.run()
xử lý.
Bố cục thư mục
Bazel tạo một nhóm thư mục hơi phức tạp trong quá trình tạo bản dựng. Bạn có thể xem nội dung mô tả đầy đủ trong phần Bố cục thư mục đầu ra.
"Không gian làm việc" là cây nguồn mà Bazel chạy trong đó. Thuộc tính này thường tương ứng với một nội dung mà bạn đã kiểm tra từ hệ thống quản lý nguồn.
Bazel đặt tất cả dữ liệu của mình trong "thư mục gốc của người dùng đầu ra". Giá trị này thường là $HOME/.cache/bazel/_bazel_${USER}
, nhưng có thể được ghi đè bằng tuỳ chọn khởi động --output_user_root
.
"Cơ sở cài đặt" là nơi Bazel được trích xuất. Việc này được thực hiện tự động và mỗi phiên bản Bazel sẽ có một thư mục con dựa trên tổng kiểm của phiên bản đó trong cơ sở cài đặt. Theo mặc định, giá trị này là $OUTPUT_USER_ROOT/install
và có thể được thay đổi bằng cách sử dụng tuỳ chọn dòng lệnh --install_base
.
"Cơ sở đầu ra" là nơi thực thể Bazel đính kèm vào một không gian làm việc cụ thể ghi vào. Mỗi cơ sở đầu ra có tối đa một phiên bản máy chủ Bazel chạy bất cứ lúc nào. Tệp này thường nằm ở $OUTPUT_USER_ROOT/<checksum of the path
to the workspace>
. Bạn có thể thay đổi chế độ này bằng cách sử dụng tuỳ chọn khởi động --output_base
. Ngoài ra, tuỳ chọn này còn hữu ích để khắc phục hạn chế chỉ có thể chạy một thực thể Bazel trong bất kỳ không gian làm việc nào tại một thời điểm nhất định.
Thư mục đầu ra chứa, trong số những thứ khác:
- 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. Tệp này nằm ở
$OUTPUT_BASE/execroot
. Trong quá trình tạo bản dựng, thư mục đang hoạt động là$EXECROOT/<name of main repository>
. Chúng tôi đang dự định thay đổi thành$EXECROOT
, mặc dù đây là một kế hoạch dài hạn vì đây là một thay đổi rất không tương thích. - Tệp được tạo trong quá trình tạo bản dựng.
Quy trình thực thi lệnh
Sau khi máy chủ Bazel có quyền kiểm soát và được thông báo về một lệnh cần thực thi, trình tự sự kiện sau đây sẽ diễn ra:
BlazeCommandDispatcher
được thông báo về yêu cầu mới. Phương thức này quyết định xem lệnh có cần không gian làm việc để chạy hay không (hầu hết các lệnh ngoại trừ các lệnh không liên quan gì đến mã nguồn, chẳng hạn như phiên bản hoặc trợ giúp) và liệu có lệnh nào khác đang chạy hay không.Đã tìm thấy lệnh phù hợp. Mỗi lệnh phải triển khai giao diện
BlazeCommand
và phải có chú thích@Command
(đây là một chút mẫu ngược, sẽ rất tuyệt nếu tất cả siêu dữ liệu mà một lệnh cần được mô tả bằng các phương thức trênBlazeCommand
)Các tuỳ chọn dòng lệnh được phân tích cú pháp. Mỗi lệnh có các tuỳ chọn dòng lệnh khác nhau, được mô tả trong chú thích
@Command
.Một bus sự kiện được tạo. Xe buýt sự kiện là một luồng cho các sự kiện xảy ra trong quá trình xây dựng. Một số trong số này được xuất ra bên ngoài Bazel theo mục đích của Giao thức sự kiện bản dựng để cho mọi người biết quá trình xây dựng diễn ra như thế nào.
Lệnh này sẽ có quyền kiểm soát. Các lệnh thú vị nhất là những lệnh chạy bản dựng: bản dựng, kiểm thử, chạy, mức độ phù hợp, v.v.: chức năng này do
BuildTool
triển khai.Tập hợp các mẫu mục tiêu trên dòng lệnh được phân tích cú pháp và ký tự đại diện như
//pkg:all
và//pkg/...
được phân giải. Việc này được triển khai trongAnalysisPhaseRunner.evaluateTargetPatterns()
và được tái hiện trong Skyframe dưới dạngTargetPatternPhaseValue
.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).
Giai đoạn thực thi được chạy. Điều này có nghĩa là chạy mọi hành động cần thiết để tạo các mục tiêu cấp cao nhất được yêu cầu.
Tuỳ chọn dòng lệnh
Các tuỳ chọn dòng lệnh cho lệnh gọi Bazel được mô tả trong đối tượng OptionsParsingResult
, đối tượng này chứa bản đồ từ "lớp tuỳ chọn" đến các giá trị của tuỳ chọn. "Lớp tuỳ chọn" là một lớp con của OptionsBase
và nhóm các tuỳ chọn dòng lệnh lại với nhau có liên quan đến nhau. Ví dụ:
- Các tuỳ chọn liên quan đến ngôn ngữ lập trình (
CppOptions
hoặcJavaOptions
). Đây sẽ là một lớp con củaFragmentOptions
và cuối cùng được gói vào một đối tượngBuildOptions
. - Các tuỳ chọn liên quan đến cách Bazel thực thi các hành động (
ExecutionOptions
)
Các tuỳ chọn này được thiết kế để sử dụng trong giai đoạn phân tích và (thông qua RuleContext.getFragment()
trong Java hoặc ctx.fragments
trong Starlark).
Một số trong số đó (ví dụ: có nên thực hiện việc quét C++ hay không) được đọc trong giai đoạn thực thi, nhưng điều đó luôn đòi hỏi phải có quy trình rõ ràng vì BuildConfiguration
không có sẵn. Để biết thêm thông tin, hãy xem phần "Cấu hình".
CẢNH BÁO: Chúng ta muốn giả định rằng các thực thể OptionsBase
là không thể thay đổi và sử dụng các thực thể đó theo cách đó (chẳng hạn như một phần của SkyKeys
). Đây không phải là trường hợp và việc sửa đổi các thực thể đó là một cách rất hiệu quả để phá vỡ Bazel theo những cách tinh vi khó gỡ lỗi. Rất tiếc, việc thực sự làm cho các giá trị này không thể thay đổi là một nhiệm vụ lớn.
(Bạn có thể sửa đổi FragmentOptions
ngay sau khi tạo trước khi bất kỳ ai khác có cơ hội giữ lại tệp tham chiếu đến FragmentOptions
và trước khi equals()
hoặc hashCode()
được gọi.)
Bazel tìm hiểu về các lớp tuỳ chọn theo những cách sau:
- Một số thiết bị được gắn cứng vào Bazel (
CommonCommandOptions
) - Từ chú thích
@Command
trên mỗi lệnh Bazel - 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) - Các quy tắc Starlark cũng có thể xác định các tuỳ chọn riêng (xem tại đây)
Mỗi tuỳ chọn (ngoại trừ các tuỳ chọn do Starlark xác định) là một biến thành viên của lớp con FragmentOptions
có chú thích @Option
. Chú thích này chỉ định tên và loại tuỳ chọn dòng lệnh cùng với một số văn bản trợ giúp.
Loại Java của giá trị tuỳ chọn dòng lệnh thường là một giá trị đơn giản (chuỗi, số nguyên, Boolean, nhãn, v.v.). Tuy nhiên, chúng tôi cũng hỗ trợ các tuỳ chọn của các loại phức tạp hơn; trong trường hợp này, việc chuyển đổi từ chuỗi dòng lệnh sang loại dữ liệu sẽ thuộc về việc triển khai com.google.devtools.common.options.Converter
.
Cây nguồn mà Bazel nhìn thấy
Bazel 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 mà nhà phát triển làm việc; 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 dài trên nhiều kho lưu trữ. Kho lưu trữ mà Bazel được gọi là "kho lưu trữ chính", cá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. Tệp này chứa thông tin "chung" cho toàn bộ bản dựng, ví dụ: tập hợp các kho lưu trữ bên ngoài hiện có. Tệp này hoạt động giống như một tệp Starlark thông thường, nghĩa là bạn có thể load()
các tệp Starlark khác.
Phương thức này thường được dùng để lấy các kho lưu trữ mà một kho lưu trữ cần đến và được tham chiếu rõ ràng (chúng tôi gọi đây là "mẫu deps.bzl
")
Mã của các kho lưu trữ bên ngoài được liên kết tượng trưng hoặc tải xuống trong $OUTPUT_BASE/external
.
Khi chạy bản dựng, toàn bộ cây nguồn cần được ghép lại với nhau; việc này do SymlinkForest
thực hiện, liên kết tượng trưng mọi gói trong kho lưu trữ chính với $EXECROOT
và mọi kho lưu trữ bên ngoài với $EXECROOT/external
hoặc $EXECROOT/..
(tất nhiên, trước đây không thể có gói nào có tên là external
trong kho lưu trữ chính; đó là lý do chúng tôi đang 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 tệp này được chỉ định bằng một tệp có tên là BUILD
hoặc BUILD.bazel
. Nếu cả hai đều tồn tại, Bazel sẽ ưu tiên BUILD.bazel
; lý do tệp BUILD
vẫn được chấp nhận là vì Blaze (tiền thân của Bazel) đã sử dụng tên tệp này. Tuy nhiên, đây lại là một đoạn đường dẫn thường dùng, đặc biệt là trên Windows, nơi tên tệp không phân biệt chữ hoa chữ thường.
Các gói độc lập với nhau: các thay đổi đối với tệp BUILD
của một gói không được khiến các gói khác thay đổi. Việc thêm hoặc xoá tệp BUILD
_có thể_ thay đổi các gói khác, vì các glob đệ quy dừng ở ranh giới gói và do đó, sự hiện diện của tệp BUILD
sẽ dừng quá trình đệ quy.
Quá trình đánh giá tệp BUILD
được gọi là "tải gói". Lớp này được triển khai trong lớp PackageFactory
, hoạt động bằng cách gọi trình thông dịch Starlark và yêu cầu kiến thức về tập hợp các lớp quy tắc hiện có. Kết quả của quá trình tải gói là một đối tượng Package
. Đây chủ yếu là một bản đồ từ một chuỗi (tên của mục tiêu) đến chính mục tiêu đó.
Một phần lớn sự phức tạp trong quá trình tải gói là việc gộp: Bazel không yêu cầu mọi tệp nguồn phải được liệt kê rõ ràng mà thay vào đó có thể chạy các tệp glob (chẳng hạn như glob(["**/*.java"])
). Không giống như shell, Bazel hỗ trợ các tệp glob đệ quy đi xuống các thư mục con (nhưng không phải vào các gói con). Việc này yêu cầu quyền truy cập vào hệ thống tệp và vì việc này có thể diễn ra chậm, nên chúng tôi triển khai mọi loại thủ thuật để hệ thống tệp chạy song song và hiệu quả nhất có thể.
Tính năng gộp được triển khai trong các lớp sau:
LegacyGlobber
, một globber nhanh và không biết SkyframeSkyframeHybridGlobber
, một phiên bản sử dụng Skyframe và quay lại globber cũ để tránh "Khởi động lại Skyframe" (như mô tả bên dưới)
Bản thân lớp Package
chứa một số thành viên chỉ dùng để phân tích cú pháp tệp WORKSPACE và không phù hợp với các gói thực. Đây là một lỗi thiết kế vì các đối tượng mô tả gói thông thường không được chứa các trường mô tả nội dung khác. bao gồm:
- Các mối liên kết kho lưu trữ
- Chuỗi công cụ đã đăng ký
- 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. Rất tiếc, việc này khó thực hiện vì hai khái niệm này liên quan chặt chẽ với nhau.
Nhãn, Mục tiêu và Quy tắc
Gói bao gồm các mục tiêu, có các loại sau:
- Tệp: những nội dung là dữ liệu đầu vào hoặc đầu ra của bản dựng. Trong ngôn ngữ của Bazel, chúng tôi gọi các tệp này là cấu phần phần mềm (được thảo luận ở nơi khác). Không phải tất cả các tệp được tạo trong quá trình xây dựng đều là mục tiêu; thường thì đầu ra của Bazel không có nhãn liên kết.
- Quy tắc: mô tả các bước để lấy kết quả từ dữ liệu đầu vào. Các loại này thường liên kết với một ngôn ngữ lập trình (chẳng hạn như
cc_library
,java_library
hoặcpy_library
), nhưng có một số loại không phụ thuộc vào ngôn ngữ (chẳng hạn nhưgenrule
hoặcfilegroup
) - Nhóm gói: được thảo luận trong phần Chế độ hiển thị.
Tên của một mục tiêu được gọi là Nhãn. Cú pháp của nhãn là @repo//pac/kage:name
, trong đó repo
là tên của kho lưu trữ chứa Nhãn, pac/kage
là thư mục chứa tệp BUILD
và name
là đường dẫn của tệp (nếu nhãn tham chiếu đến một tệp nguồn) so với thư mục của gói. Khi tham chiếu đến một mục tiêu trên dòng lệnh, bạn có thể bỏ qua một số phần của nhãn:
- Nếu kho lưu trữ bị bỏ qua, nhãn sẽ được lấy trong kho lưu trữ chính.
- Nếu phần gói bị bỏ qua (chẳng hạn như
name
hoặc:name
), nhãn sẽ được đưa vào gói của thư mục đang hoạt động hiện tại (không được phép sử dụng đường dẫn tương đối chứa tham chiếu cấp cao (..))
Một loại quy tắc (chẳng hạn như "thư viện C++") được gọi là "lớp quy tắc". Các lớp quy tắc có thể được triển khai trong Starlark (hàm rule()
) hoặc trong Java (còn gọi là "quy tắc gốc", loại RuleClass
). Về lâu dài, mọi quy tắc dành riêng cho ngôn ngữ sẽ được triển khai trong Starlark, nhưng một số gia đình quy tắc cũ (chẳng hạn như Java hoặc C++) vẫn còn trong Java.
Bạn cần nhập các lớp quy tắc Starlark ở đầu tệp BUILD
bằng câu lệnh load()
, trong khi các lớp quy tắc Java được Bazel "tự nhiên" biết đến nhờ được đăng ký bằng ConfiguredRuleClassProvider
.
Các lớp quy tắc chứa thông tin như:
- Các thuộc tính của nó (chẳng hạn như
srcs
,deps
): loại, giá trị mặc định, quy tắc ràng buộc, v.v. - 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ó
- Việc triển khai quy tắc
- Trình cung cấp thông tin bắc cầu mà quy tắc "thường" tạo ra
Lưu ý về thuật ngữ: Trong cơ sở mã, chúng ta thường sử dụng "Quy tắc" để chỉ mục tiêu do một lớp quy tắc tạo. Tuy nhiên, trong Starlark và tài liệu dành cho người dùng, bạn chỉ nên sử dụng "Rule" (Quy tắc) để tham chiếu đến chính lớp quy tắc; mục tiêu chỉ là một "target" (mục tiêu). Ngoài ra, xin lưu ý rằng mặc dù RuleClass
có "lớp" trong tên, nhưng không có mối quan hệ kế thừa Java nào giữa lớp quy tắc và mục tiêu thuộc loại đó.
Skyframe
Khung đánh giá cơ bản của Bazel được gọi là Skyframe. Mô hình của 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 này được gọi là SkyKey
. Cả hai đều không thể thay đổi được; chỉ có thể truy cập các đối tượng không thể thay đổi từ các đối tượng này. Biến không đổi này hầu như luôn đúng và trong trường hợp không đúng (chẳng hạn như đối với các lớp tuỳ chọn riêng lẻ BuildOptions
, là thành viên của BuildConfigurationValue
và SkyKey
của lớp này), chúng ta cố gắng không thay đổi các lớp này hoặc chỉ thay đổi theo những cách không thể quan sát được từ bên ngoài.
Do đó, mọi thứ được tính toán trong Skyframe (chẳng hạn như các mục tiêu được định cấu hình) cũng phải không thể thay đổi.
Cách thuận tiện nhất để quan sát biểu đồ Skyframe là chạy bazel dump
--skyframe=deps
. Thao tác này sẽ kết xuất biểu đồ, mỗi dòng là một SkyValue
. Tốt nhất là bạn nên làm điều này cho các bản dựng nhỏ, vì bản dựng có thể khá lớn.
Skyframe nằm trong gói com.google.devtools.build.skyframe
. Gói com.google.devtools.build.lib.skyframe
có tên tương tự chứa quá trình triển khai Bazel trên Skyframe. Bạn có thể xem thêm thông tin về Skyframe tại đây.
Để đánh giá một SkyKey
nhất định thành SkyValue
, Skyframe sẽ gọi SkyFunction
tương ứng với loại khoá. Trong quá trình đánh giá, hàm này có thể yêu cầu các phần phụ thuộc khác từ Skyframe bằng cách gọi nhiều phương thức nạp chồng của SkyFunction.Environment.getValue()
. 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 gia tăng của Skyframe hoạt động ở mức độ chi tiết của SkyFunction
và SkyValue
.
Bất cứ khi nào SkyFunction
yêu cầu một phần phụ thuộc không có sẵn, getValue()
sẽ trả về giá trị rỗng. Sau đó, hàm này sẽ trả về quyền kiểm soát cho Skyframe bằng cách tự trả về giá trị rỗng. Tại một thời điểm nào đó sau này, Skyframe sẽ đánh giá phần phụ thuộc không có sẵn, sau đó khởi động lại hàm từ đầu — chỉ lần này lệnh gọi getValue()
mới thành công với kết quả không rỗng.
Hậu quả của việc này là mọi phép tính được thực hiện bên trong SkyFunction
trước khi khởi động lại đều phải được lặp lại. Tuy nhiên, 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 ta thường giải quyết vấn đề này bằng cách:
- Khai báo các phần phụ thuộc theo lô (bằng cách sử dụng
getValuesAndExceptions()
) để hạn chế số lần khởi động lại. - Chia
SkyValue
thành các phần riêng biệt được tính toán bằng nhiềuSkyFunction
, để có thể tính toán và lưu vào bộ nhớ đệm một cách độc lập. Bạn nên thực hiện việc này một cách có chiến lược vì việc này có thể làm tăng mức sử dụng bộ nhớ. - Lưu trữ trạng thái giữa các lần khởi động lại, sử dụng
SkyFunction.Environment.getState()
hoặc giữ bộ nhớ đệm tĩnh đặc biệt "ở phía sau Skyframe".
Về cơ bản, chúng ta cần những giải pháp này vì thường xuyên có hàng trăm nghìn nút Skyframe đang hoạt động và Java không hỗ trợ các luồng nhẹ.
Starlark
Starlark là ngôn ngữ 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. Đây được coi là một tập hợp con bị hạn chế của Python có ít loại hơn nhiều, nhiều quy tắc hạn chế hơn về luồng điều khiển và quan trọng nhất là đảm bảo tính bất biến mạnh để cho phép đọc đồng thời. 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
.
Ứng dụng này cũng có cách triển khai Go độc lập
tại đây. Cách triển khai Java được sử dụng trong Bazel hiện là một trình phiên dịch.
Starlark được dùng trong một số ngữ cảnh, bao gồm:
- Ngôn ngữ
BUILD
. Đây là nơi xác định các quy tắc mới. Mã Starlark chạy trong ngữ cảnh này chỉ có quyền truy cập vào nội dung của chính tệpBUILD
và các tệp.bzl
do tệp này tải. - Đị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ư hỗ trợ 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ẽ nói thêm về vấn đề này sau).
- 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).
- Đị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 đang chạy Bazel và truy cập bên ngoài không gian làm việc.
Các phương ngữ có sẵn cho tệp BUILD
và .bzl
có chút khác biệt vì chúng thể hiện những nội dung khác nhau. Bạn có thể xem danh sách các điểm khác biệt tại đây.
Bạn có thể xem thêm thông tin về Starlark tại đây.
Giai đoạn tải/phân tích
Giai đoạn tải/phân tích là nơi Bazel xác định những hành động cần thiết để tạo một quy tắc cụ thể. Đơn vị cơ bản của nó là "mục tiêu được định cấu hình", khá hợp lý 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ì giai đoạn này có thể chia thành hai 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:
- Tải các gói, tức là chuyển tệp
BUILD
thành các đối tượngPackage
đại diện cho các gói đó - Phân tích các mục tiêu đã định cấu hình, tức là chạy quá trình triển khai các quy tắc để tạo biểu đồ hành động
Bạn phải phân tích từng mục tiêu đã định cấu hình trong tập hợp đóng bắc cầu của các mục tiêu đã định cấu hình được yêu cầu trên dòng lệnh từ dưới lên; tức là trước tiên là các nút lá, sau đó là các nút trên dòng lệnh. Dữ liệu đầu vào cho việc phân tích một mục tiêu đã định cấu hình bao gồm:
- 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++)
- Phần phụ thuộc trực tiếp. Các nhà cung cấp thông tin bắc cầu của chúng có sẵn cho quy tắc đang được phân tích. Các tệp này được gọi như vậy vì chúng cung cấp một "tệp cuộn lên" của thông tin trong tập hợp đóng bắc cầu của mục tiêu đã định cấu hình, chẳng hạn như tất cả tệp .jar trên đường dẫn lớp hoặc tất cả tệp .o cần được liên kết vào tệp nhị phân C++)
- Chính mục tiêu đó. Đây là kết quả của việc tải gói chứa mục tiêu. Đối với quy tắc, thuộc tính của quy tắc thường là yếu tố quan trọng.
- Triển khai mục tiêu đã định cấu hình. Đối với các quy tắc, bạn có thể sử dụng Starlark hoặc Java. Tất cả mục tiêu được định cấu hình không phải quy tắc đều được triển khai trong Java.
Kết quả phân tích một mục tiêu đã định cấu hình là:
- Các nhà cung cấp thông tin bắc cầu đã định cấu hình các mục tiêu phụ thuộc vào thông tin đó có thể truy cập
- Các cấu phần phần mềm mà công cụ này có thể tạo và các hành động tạo ra các cấu phần phần mềm đó.
API được cung cấp cho các quy tắc Java là RuleContext
, tương đương với đối số ctx
của các quy tắc Starlark. API của nó mạnh mẽ hơn, nhưng đồng thời, bạn cũng dễ dàng làm những việc xấu hơn, chẳng hạn như viết mã có độ phức tạp về thời gian hoặc không gian là bậc hai (hoặc tệ hơn), khiến máy chủ Bazel gặp sự cố với ngoại lệ Java hoặc vi phạm các hằng số không đổi (chẳng hạn như vô tình sửa đổi một thực thể Options
hoặc bằng cách làm cho một mục tiêu đã định cấu hình có thể thay đổi)
Thuật toán xác định các phần phụ thuộc trực tiếp của một mục tiêu đã định cấu hình nằm trong DependencyResolver.dependentNodeMap()
.
Cấu hình
Cấu hình là "cách" tạo một mục tiêu: cho nền tảng nào, với tuỳ chọn dòng lệnh nào, v.v.
Bạn có thể tạo cùng một mục tiêu cho nhiều cấu hình trong cùng một bản dựng. Điều này rất hữu ích, ví dụ: khi cùng một mã được dùng cho một công cụ chạy trong quá trình xây dựng và cho mã mục tiêu, đồng thời chúng ta đang biên dịch chéo hoặc khi chúng ta đang tạo một ứng dụng Android lớn (ứng dụng chứa mã gốc cho nhiều kiến trúc CPU)
Về mặt khái niệm, cấu hình là một thực thể BuildOptions
. Tuy nhiên, 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. Nó sẽ lan truyền từ đầu biểu đồ phần phụ thuộc đến cuối. Nếu giá trị này thay đổi, bạn cần phân tích lại bản dựng.
Điều này dẫn đến các trường hợp bất thường như phải phân tích lại toàn bộ bản dựng nếu, chẳng hạn như số lần chạy kiểm thử được yêu cầu thay đổi, mặc dù điều đó chỉ ảnh hưởng đến các mục tiêu kiểm thử (chúng tôi dự định "cắt bớt" các cấu hình để không xảy ra trường hợp này, nhưng chưa sẵn sàng).
Khi triển khai quy tắc cần một phần cấu hình, quy tắc đó cần khai báo phần cấu hình đó trong định nghĩa bằng cách sử dụng RuleClass.Builder.requiresConfigurationFragments()
. 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ớt 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:
- Trên cạnh 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ềuBuildOptions
(cấu hình đầu ra). - Trên bất kỳ cạnh nào đến một mục tiêu đã định cấu hình. Các giá trị này được chỉ định trong
RuleClass.Builder.cfg()
.
Các lớp có liên quan là TransitionFactory
và ConfigurationTransition
.
Các chuyển đổi cấu hình được sử dụng, ví dụ:
- Để khai báo rằng một phần phụ thuộc cụ thể được sử dụng trong quá trình xây dựng và do đó, phần phụ thuộc đó phải được tạo trong cấu trúc thực thi
- Để khai báo rằng một phần phụ thuộc cụ thể phải được tạo cho nhiều cấu trúc (chẳng hạn như cho mã gốc trong tệp APK Android lớn)
Nếu một quá trình chuyển đổi cấu hình dẫn đến nhiều cấu hình, thì quá trình đó được gọi là quá trình chuyển đổi phân tách.
Bạn cũng có thể triển khai quá trình chuyển đổi cấu hình trong Starlark (tài liệu tại đây)
Nhà cung cấp thông tin bắc cầu
Nhà cung cấp thông tin bắc cầu là một cách (và là _cách duy nhất_) để các mục tiêu đã định cấu hình cho biết thông tin về các mục tiêu đã định cấu hình khác phụ thuộc vào mục tiêu đó. Lý do tạ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 bắc cầu Starlark (ngoại trừ DefaultInfo
là sự kết hợp của FileProvider
, FilesToRunProvider
và RunfilesProvider
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 chúng là một trong những nội dung sau:
- Đối tượng Lớp Java. Tính năng này chỉ dành cho các nhà cung cấp không truy cập được từ Starlark. Các nhà cung cấp này là lớp con của
TransitiveInfoProvider
. - Một chuỗi. Đây là cách cũ và không nên dùng vì dễ xảy ra xung đột tên. Những nhà cung cấp thông tin bắc cầu như vậy là các lớp con trực tiếp của
build.lib.packages.Info
. - Biểu tượng nhà cung cấp. Bạn có thể tạo hàm này từ Starlark bằng hàm
provider()
và đây là cách được đề xuất để tạo trình cung cấp mới. Biểu tượng này được biểu thị bằng một thực thểProvider.Key
trong Java.
Bạn nên triển khai các trình cung cấp mới được triển khai trong Java bằng BuiltinProvider
.
NativeProvider
không còn được dùng nữa (chúng tôi chưa có thời gian xoá lớp này) và không thể truy cập vào các lớp con TransitiveInfoProvider
từ Starlark.
Mục tiêu đã thiết lập
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 của Starlark được tạo thông qua StarlarkRuleConfiguredTargetUtil.buildRule()
.
Các nhà máy mục tiêu được định cấu hình phải sử dụng RuleConfiguredTargetBuilder
để tạo giá trị trả về. Tệp này bao gồm những thành phần sau:
filesToBuild
của họ, khái niệm mơ hồ về "nhóm tệp mà quy tắc này đại diện". Đây là những tệp được tạo khi mục tiêu đã định cấu hình nằm trên dòng lệnh hoặc trong srcs của genrule.- Tệp chạy, tệp thường và tệp dữ liệu.
- 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 nhóm này bằng cách sử dụng thuộc tính output_group của quy tắc filegroup trong BUILD và sử dụng trình cung cấp
OutputGroupInfo
trong Java.
Tệp chạy
Một số tệp nhị phân cần 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 "tệp chạy". "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ể. Tệp này được tạo trong hệ thống tệp dưới dạng cây đường liên kết tượng trưng với các đường liên kết tượng trưng riêng lẻ trỏ đến các tệp trong nguồn của cây đầu ra.
Một tập hợp tệp chạy được biểu thị dưới dạng một thực thể Runfiles
. Về mặt khái niệm, đây là một bản đồ từ đường dẫn của một tệp trong cây runfiles đến thực thể Artifact
đại diện cho tệp đó. Cách này phức tạp hơn một chút so với một Map
duy nhất vì hai lý do:
- Trong hầu hết các trường hợp, đường dẫn runfiles của tệp giống với execpath của tệp đó. Chúng ta sử dụng tính năng này để tiết kiệm một số RAM.
- Có nhiều loại mục cũ trong cây runfile cũng cần được biểu thị.
Tệp chạy được thu thập bằng RunfilesProvider
: một thực thể của lớp này đại diện cho tệp chạy của một mục tiêu đã định cấu hình (chẳng hạn như thư viện) và các nhu cầu đóng vòng lặp bắc cầu của tệp chạy. Các tệp chạy này được thu thập như một tập hợp lồng nhau (thực tế, các tệp chạy này được triển khai bằng cách sử dụng các tập hợp lồng nhau trong lớp bao bọc): mỗi mục tiêu hợp nhất các tệp chạy của các phần phụ thuộc, thêm một số tệp chạy của riêng mục tiêu đó, sau đó gửi tập hợp kết quả lên trên trong biểu đồ phần phụ thuộc. Một thực thể RunfilesProvider
chứa hai thực thể Runfiles
, một thực thể cho trường hợp quy tắc được phụ thuộc thông qua thuộc tính "dữ liệu" và một thực thể cho mọi loại phần phụ thuộc sắp tới. Điều này là do một mục tiêu đôi khi 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 có cách để loại bỏ.
Tệp chạy của tệp nhị phân được biểu thị dưới dạng một thực thể của RunfilesSupport
. Điều này khác với Runfiles
vì RunfilesSupport
có khả năng thực sự được tạo (không giống như Runfiles
, chỉ là một ánh xạ). Điều này đòi hỏi các thành phần bổ sung sau:
- Tệp kê khai các tệp runfile đầu vào. Đây là nội dung mô tả tuần tự về cây runfiles. Tệp này được dùng làm proxy cho nội dung của cây tệp chạy và Bazel giả định rằng cây tệp chạy sẽ thay đổi nếu và chỉ khi nội dung của tệp kê khai thay đổi.
- Tệp kê khai tệp chạy đầu ra. Thư viện thời gian chạy sử dụng tính năng này để xử lý cây tệp chạy, đặc biệt là trên Windows, đôi khi không hỗ trợ đường liên kết tượng trưng.
- Người trung gian runfiles. Để cây tệp chạy tồn tại, bạn cần tạo cây đường liên kết tượng trưng và cấu phần phần mềm mà các đường liên kết tượng trưng trỏ đến. Để giảm số lượng cạnh 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
Phương diện là một cách để "truyền tính toán xuống biểu đồ phần phụ thuộc". Chúng được mô tả cho người dùng Bazel tại đây. Một ví dụ điển hình là vùng đệm giao thức: quy tắc proto_library
không được biết về bất kỳ ngôn ngữ cụ thể nào, nhưng việc xây dựng việc triển khai thông báo vùng đệm giao thức ("đơn vị cơ bản" của vùng đệm giao thức) trong bất kỳ ngôn ngữ lập trình nào phải được ghép nối với quy tắc proto_library
để nếu hai mục tiêu trong cùng một ngôn ngữ phụ thuộc vào cùng một vùng đệm giao thức, thì vùng đệm giao thức đó chỉ được tạo một lần.
Giống như các mục tiêu đã định cấu hình, các mục tiêu này được biểu thị trong Skyframe dưới dạng SkyValue
và cách tạo các mục tiêu này rất giống với cách tạo các mục tiêu đã định cấu hình: các mục tiêu này có một lớp nhà máy có tên là ConfiguredAspectFactory
có quyền truy cập vào RuleContext
, nhưng không giống như các nhà máy mục tiêu đã định cấu hình, lớp này cũng biết về mục tiêu đã định cấu hình mà nó được đính kèm và các nhà cung cấp của mục tiêu đó.
Tập hợp các khía cạnh được truyền xuống biểu đồ phần phụ thuộc được chỉ định cho từng thuộc tính bằng cách sử dụng hàm Attribute.Builder.aspects()
. Có một vài lớp có tên gây nhầm lẫn tham gia vào quá trình này:
AspectClass
là cách triển khai tỷ lệ khung hình. Lớp này có thể ở trong Java (trong trường hợp đó là lớp con) hoặc trong Starlark (trong trường hợp đó là một thực thể củaStarlarkAspectClass
). Lớp này tương tự nhưRuleConfiguredTargetFactory
.AspectDefinition
là định nghĩa của khía cạnh; bao gồm các nhà cung cấp mà khía cạnh đó yêu cầu, các nhà cung cấp mà khía cạnh đó cung cấp và chứa thông tin tham chiếu đến việc triển khai khía cạnh đó, chẳng hạn như thực thểAspectClass
thích hợp. Phương thức này tương tự nhưRuleClass
.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.Aspect
đại diện cho tất cả dữ liệu cần thiết để tính toán một khía cạnh truyền xuống biểu đồ phần phụ thuộc. Tệp này bao gồm lớp khía cạnh, định nghĩa và tham số của lớp đó.RuleAspect
là hàm xác định những khía cạnh mà một quy tắc cụ thể sẽ truyền tải. Đây là một hàmRule
->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
+ khía cạnh proto Java).
Mức độ phức tạp của các khía cạnh trên các khía cạnh được ghi 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 để chạy các hành động bản dựng và nhiều cấu trúc để tạo mã. Các cấu trúc này được gọi là nền tảng theo cách nói của Bazel (tài liệu đầy đủ tại đây)
Một nền tảng được mô tả bằng mối liên kết khoá-giá trị từ chế độ cài đặt quy tắc ràng buộc (chẳng hạn như khái niệm "cấu trúc CPU") đến giá trị quy tắc ràng buộc (chẳng hạn như một CPU cụ thể như x86_64). Chúng ta có một "từ điển" về các chế độ cài đặt và giá trị ràng buộc được sử dụng phổ biến nhất trong kho lưu trữ @platforms
.
Khái niệm chuỗi công cụ xuất phát từ thực tế là tuỳ thuộc vào nền tảng mà bản dựng đang chạy và nền tảng được nhắm đến, bạn có thể cần sử dụng các trình biên dịch khác nhau; ví dụ: một chuỗi công cụ C++ cụ thể có thể chạy trên một hệ điều hành cụ thể và có thể nhắm đến một số hệ điều hành khác. Bazel phải xác định trình biên dịch C++ được sử dụng dựa trên nền tảng thực thi và nền tảng mục tiêu đã đặt (tài liệu về chuỗi công cụ tại đây).
Để làm việc này, các chuỗi công cụ được chú thích bằng tập hợp các điều kiện ràng buộc về nền tảng thực thi và mục tiêu mà chúng hỗ trợ. Để làm được việc này, định nghĩa về chuỗi công cụ được chia thành hai phần:
- 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ắctoolchain_type()
) - Quy tắc dành riêng cho ngôn ngữ mô tả chuỗi công cụ thực tế (chẳng hạn như
cc_toolchain()
)
Chúng ta thực hiện việc này vì cần biết các quy tắc ràng buộc cho mọi chuỗi công cụ để phân giải chuỗi công cụ và các quy tắc *_toolchain()
dành riêng cho ngôn ngữ chứa nhiều thông tin hơn thế, vì vậy, các quy tắc này mất nhiều thời gian hơn để tải.
Bạn có thể chỉ định nền tảng thực thi theo một trong những cách sau:
- Trong tệp WORKSPACE bằng hàm
register_execution_platforms()
- Trên dòng lệnh bằng tuỳ chọn dòng lệnh --extra_execution_platforms
Tập hợp các nền tảng thực thi có sẵn được tính toán trong RegisteredExecutionPlatformsFunction
.
Nền tảng mục tiêu cho một mục tiêu đã định cấu hình được xác định bằng PlatformOptions.computeTargetPlatform()
. Đây là danh sách các nền tảng vì cuối cùng chúng tôi muốn hỗ trợ nhiều nền tảng mục tiêu, nhưng chưa triển khai.
Tập hợp chuỗi công cụ sẽ được sử dụng cho một mục tiêu đã định cấu hình được xác định bằng ToolchainResolutionFunction
. Đây là hàm của:
- Tập hợp các chuỗi công cụ đã đăng ký (trong tệp WORKSPACE và cấu hình)
- Nền tảng thực thi và nền tảng mục tiêu mong muốn (trong cấu hình)
- Tập hợp các loại chuỗi công cụ mà mục tiêu đã định cấu hình yêu cầu (trong
UnloadedToolchainContextKey)
- Tập hợp các đ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
), trongUnloadedToolchainContextKey
Kết quả là một UnloadedToolchainContext
, về cơ bản là một bản đồ từ loại chuỗi công cụ (được biểu thị dưới dạng một thực thể ToolchainTypeInfo
) đến nhãn của chuỗi công cụ đã chọn. Tệp này được gọi là "đã huỷ tải" vì không chứa các chuỗi công cụ mà chỉ chứa nhãn của các chuỗi công cụ đó.
Sau đó, các chuỗi công cụ thực sự được tải bằng ResolvedToolchainContext.load()
và được sử dụng trong quá trình triển khai mục tiêu đã định cấu hình đã yêu cầu các chuỗi công cụ đó.
Chúng tôi cũng có một hệ thống cũ dựa trên việc có một cấu hình "máy chủ" duy nhất và các cấu hình mục tiêu được biểu thị bằng nhiều cờ cấu hình, chẳng hạn như --cpu
. Chúng tôi đang từng bước chuyển sang hệ thống trên. Để xử lý 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, bạn chỉ muốn chỉ định một mục tiêu tương thích với một vài nền tảng. (Rất tiếc) Bazel có nhiều cơ chế để đạt được mục tiêu này:
- Các điều kiện ràng buộc theo quy tắc cụ thể
environment_group()
/environment()
- Các quy tắc ràng buộc của 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 quản lý việc này được gọi là constraints=
.
environment_group() và environment()
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" mà chúng có thể được tạo, trong đó "môi trường" là một thực thể của quy tắc environment()
.
Có nhiều cách để chỉ định môi trường được hỗ trợ cho một quy tắc:
- 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. - 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. - Thông qua các thuộc tính cấp gói
default_restricted_to=
vàdefault_compatible_with=
. - Thông qua thông số kỹ thuật mặc định trong quy tắc
environment_group()
. Mỗi môi trường thuộc về một nhóm các môi trường ngang hàng có liên quan theo chủ đề (chẳng hạn như "cấu trúc CPU", "phiên bản JDK" hoặc "hệ điều hành di động"). 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ínhrestricted_to=
/environment()
chỉ định khác. Một quy tắc không có các thuộc tính như vậy sẽ kế thừa tất cả các giá trị mặc định. - Thông qua 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 để kiểm thử tất cả quy tắc
*_test
mà không cần mỗi thực thể phải khai báo rõ ràng chức năng này.
environment()
được triển khai dưới dạng quy tắc thông thường, trong khi environment_group()
vừa là lớp con của Target
nhưng không phải là Rule
(EnvironmentGroup
) vừa là hàm có sẵn theo mặc định từ Starlark (StarlarkLibrary.environmentGroup()
) và cuối cùng tạo ra một mục tiêu cùng tên. Điều này nhằm 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ể hạn chế một bản dựng cho một môi trường nhất định bằng tuỳ chọn dòng lệnh --target_environment
.
Việc triển khai quy trình kiểm tra quy tắc ràng buộc nằm trong RuleContextConstraintSemantics
và TopLevelConstraintSemantics
.
Các quy tắc ràng buộc của nền tảng
Cách "chính thức" hiện tại để mô tả nền tảng mà một mục tiêu tương thích là sử dụng các quy tắc ràng buộc giống như dùng để mô tả chuỗi công cụ và nền tảng. Yêu cầu này đang được xem xét trong yêu cầu lấy #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 quy luật của Hyrum, mọi người sẽ dựa vào những hành vi mà bạn coi là chi tiết triển khai.
Bazel hỗ trợ việc này bằng cơ chế có tên là visibility (mức độ hiển thị): bạn có thể khai báo rằng chỉ có thể phụ thuộc vào một mục tiêu cụ thể bằng cách sử dụng thuộc tính visibility (mức độ hiển thị). Thuộc tính này có một chút đặc biệt vì mặc dù chứa danh sách nhãn, nhưng các nhãn này có thể mã hoá một mẫu trên tên gói thay vì con trỏ đến bất kỳ mục tiêu cụ thể nào. (Đúng vậy, đây là một lỗi thiết kế.)
Việc này được triển khai ở những vị trí sau:
- Giao diện
RuleVisibility
thể hiện nội dung khai báo chế độ hiển thị. Đó 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 gói được xác định trước), đến các gói trực tiếp (
//pkg:__pkg__
) hoặc các cây con của gói (//pkg:__subpackages__
). Điều này khác với cú pháp dòng lệnh sử dụng//pkg:*
hoặc//pkg/...
. - Các nhóm gói được triển khai dưới dạng mục tiêu riêng (
PackageGroup
) và mục tiêu được định cấu hình (PackageGroupConfiguredTarget
). Chúng ta có thể thay thế các mục tiêu này bằng các quy tắc đơn giản nếu muốn. Logic của chúng được triển khai với sự trợ giúp của:PackageSpecification
, tương ứng với một mẫu duy nhất như//pkg/...
;PackageGroupContents
, tương ứng với một thuộc tínhpackages
củapackage_group
; vàPackageSpecificationProvider
, tổng hợp trên mộtpackage_group
vàincludes
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 số nơi khác. - Quá trình kiểm tra thực tế được thực hiện trong
CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()
Tập hợp lồng nhau
Thông thường, một mục tiêu được định cấu hình sẽ tổng hợp một tập hợp các tệp từ các phần phụ thuộc của nó, thêm các tệp 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++ dùng cho một bản dựng
- Các tệp đối tượng đại diện cho phép đóng vi phạm của
cc_library
- Tập hợp các tệp .jar cần có trong đường dẫn lớp để một quy tắc Java có thể biên dịch hoặc chạy
- Tập hợp các tệp Python trong tập hợp đóng bắc cầu của một quy tắc Python
Nếu thực hiện việc này theo cách đơn giản bằng cách sử dụng, ví dụ: List
hoặc Set
, thì chúng ta sẽ có mức sử dụng bộ nhớ bậc hai: nếu có một chuỗi gồm N quy tắc và mỗi quy tắc thêm một tệp, thì chúng ta sẽ có 1+2+...+N thành phần của tập hợp.
Để giải quyết vấn đề này, chúng tôi đã đưa ra khái niệm về NestedSet
. Đây là một cấu trúc dữ liệu bao gồm các thực thể NestedSet
khác và một số thành viên của chính nó, từ đó tạo thành một biểu đồ không tuần hoàn có hướng của các tập hợp. Các tập hợp này không thể thay đổi và các thành phần của tập hợp có thể được lặp lại. Chúng ta xác định nhiều thứ tự lặp lại (NestedSet.Order
): thứ tự trước, thứ tự sau, thứ tự topo (một nút luôn nằm sau các nút cấp trên) và "không quan trọng, nhưng mỗi lần phải giống nhau".
Cấu trúc dữ liệu tương tự được gọi là depset
trong Starlark.
Cấu phần phần mềm và Hành động
Bản dựng thực tế bao gồm một tập hợp các lệnh cần đượ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
. Các hành động này được sắp xếp trong một biểu đồ hai phân đoạn, có hướng và không tuần hoàn được gọi là "biểu đồ hành động".
Cấu phần phần mềm có hai loại: cấu phần phần mềm nguồn (cấu phần phần mềm có sẵn trước khi Bazel bắt đầu thực thi) và cấu phần phần mềm phái sinh (cấu phần phần mềm cần được tạo). Bản thân cấu phần phần mềm phái sinh có thể có nhiều loại:
- **Cấu phần phần mềm thông thường. **Các tệp này được kiểm tra để đảm bảo mới nhất bằng cách tính toán tổng kiểm của chúng, với mtime làm lối tắt; chúng tôi không tính tổng kiểm của tệp nếu ctime của tệp đó không thay đổi.
- 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, chúng có thể là các đường liên kết tượng trưng lơ lửng. Thường được dùng trong trường hợp một tệp được đóng gói vào một loại tệp lưu trữ.
- Cấu phần phần mềm dạng cây. Đây không phải là các tệp đơn lẻ mà là cây thư mục. Các tệp này được kiểm tra 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
. - 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. Thuộc tính này chỉ dùng cho thông tin dấu bản dựng: chúng ta không muốn tạo lại bản dựng chỉ vì thời gian hiện tại đã thay đổi.
Không có lý do cơ bản nào khiến cấu phần phần mềm nguồn không thể là cấu phần phần mềm cây hoặc cấu phần phần mềm đường liên kết tượng trưng chưa được phân giải, chỉ là chúng ta chưa triển khai cấu phần phần mềm đó (mặc dù chúng ta nên tham chiếu thư mục nguồn trong tệp BUILD
là một trong số ít vấn đề không chính xác đã biết từ lâu với Bazel; chúng ta có một cách triển khai loại công việc này được bật bằng thuộc tính JVM BAZEL_TRACK_SOURCE_DIRECTORIES=1
)
Một loại Artifact
đáng chú ý là bên trung gian. Các giá trị này được biểu thị bằng các thực thể Artifact
là kết quả của MiddlemanAction
. Các hàm này được dùng để xử lý một số trường hợp đặc biệt:
- Các bên trung gian tổng hợp được dùng để nhóm các cấu phần phần mềm lại với nhau. Điều này là để nếu nhiều hành động sử dụng cùng một tập hợp lớn đầu vào, chúng ta sẽ không có N*M cạnh phần phụ thuộc, chỉ có N+M (các cạnh này đang được thay thế bằng các tập hợp lồng nhau)
- Việc lên lịch cho các phần phụ thuộc trung gian đảm bảo một hành động chạy trước một hành động khác.
Các công cụ này chủ yếu được dùng để tìm lỗi mã nguồn nhưng cũng dùng để biên dịch C++ (xem
CcCompilationContext.createMiddleman()
để biết nội dung giải thích) - Trình trung gian Runfiles được dùng để đảm bảo sự hiện diện của cây chạy tệp 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.
Bạn nên hiểu hành động là một lệnh cần chạy, môi trường cần thiết và tập hợp đầu ra mà lệnh đó tạo ra. Sau đây là các thành phần chính của nội dung mô tả một hành động:
- Dòng lệnh cần chạy
- Các cấu phần phần mềm đầu vào cần thiết
- Các biến môi trường cần thiết lập
- Chú 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 số trường hợp đặc biệt khác, chẳng hạn như ghi một tệp có nội dung mà Bazel biết. Đây là lớp con của AbstractAction
. Hầu hết các thao tác đều là SpawnAction
hoặc StarlarkAction
(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
, CppCompileAction
và CppLinkAction
).
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 biểu đồ hành động đến cạnh phần phụ thuộc Skyframe được mô tả trong ActionExecutionFunction.getInputDeps()
và Artifact.key()
, đồng thời có một số hoạt động tối ưu hoá để giảm thiểu số lượng cạnh Skyframe:
- Cấu phần phần mềm phái sinh không có
SkyValue
riêng. Thay vào đó,Artifact.getGeneratingActionKey()
được dùng để tìm hiểu khoá cho hành động tạo ra khoá đó - Các tập hợp lồng nhau có khoá Skyframe riêng.
Hành động được chia sẻ
Một số hành động được tạo bởi nhiều mục tiêu được định cấu hình; các quy tắc Starlark bị hạn chế hơn vì chúng chỉ được phép đặt các hành động phái sinh vào một thư mục do cấu hình và gói của chúng xác định (nhưng ngay cả như vậy, các quy tắc trong cùng một gói có thể xung đột), nhưng các quy tắc được triển khai trong Java có thể đặt các cấu phần phần mềm phái sinh ở bất cứ đâu.
Đây được coi là một tính năng không mong muốn, nhưng việc loại bỏ tính năng này rất khó vì tính năng này giúp tiết kiệm đáng kể thời gian thực thi khi, ví dụ: một tệp nguồn cần được xử lý theo cách nào đó và tệp đó được tham chiếu bằng nhiều quy tắc (handwave-handwave). Điều này sẽ làm tiêu hao một lượng RAM: mỗi thực thể của một thao tác dùng chung cần được lưu trữ riêng trong bộ nhớ.
Nếu hai hành động tạo ra cùng một tệp đầu ra, thì các hành động đó phải giống hệt nhau: có cùng dữ liệu đầu vào, cùng dữ liệu đầu ra và chạy cùng một dòng lệnh. Mối quan hệ tương đương này được triển khai trong Actions.canBeShared()
và được xác minh giữa các giai đoạn phân tích và thực thi bằng cách xem xét mọi Hành động.
Việc này được triển khai trong SkyframeActionExecutor.findAndStoreArtifactConflicts()
và là một trong số ít nơi trong Bazel yêu cầu chế độ xem "toàn cục" của bản dựng.
Giai đoạn thực thi
Đây là thời điểm Bazel thực sự bắt đầu chạy các hành động bản dựng, chẳng hạn như các lệnh tạo ra đầu ra.
Đ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
; nói một cách đơn giản, đó là filesToBuild
của các mục tiêu đã định cấu hình trên dòng lệnh và nội dung của một nhóm đầu ra đặc biệt nhằm mục đích rõ ràng là thể hiện "nếu mục tiêu này nằm trên dòng lệnh, hãy tạo các cấu phần phần mềm này".
Bước tiếp theo là tạo gốc thực thi. Vì Bazel có thể đọc các gói nguồn từ nhiều vị trí trong hệ thống tệp (--package_path
), nên cần cung cấp các thao tác được thực thi cục bộ bằng một cây nguồn đầy đủ. Việc này 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 đó. Một cách khác là truyền đường dẫn chính xác đến các lệnh (có tính đến --package_path
).
Điều này là không mong muốn vì:
- Thay đổi dòng lệnh hành động khi một gói được di chuyển từ mục nhập đường dẫn gói sang một mục nhập khác (thường xảy ra)
- Điều này dẫn đến các dòng lệnh khác nhau nếu một hành động được chạy từ xa so với khi 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 duyệt qua biểu đồ hành động (biểu đồ hai phân đoạn, có hướng, bao gồm các hành động và cấu phần phần mềm đầu vào và đầu ra của các hành động đó) và chạy các hành động.
Việc thực thi mỗi hành động được biểu thị bằng một thực thể của lớp SkyValue
ActionExecutionValue
.
Vì việc chạy một hành động tốn kém, nên chúng ta có một vài lớp lưu vào bộ nhớ đệm có thể được truy cập sau Skyframe:
ActionExecutionFunction.stateMap
chứa dữ liệu để giúp Skyframe khởi động lạiActionExecutionFunction
một cách tiết kiệm- Bộ nhớ đệm thao tác cục bộ chứa dữ liệu về trạng thái của hệ thống tệp
- Các hệ thống thực thi từ xa thường cũng chứa bộ nhớ đệm riêng
Bộ nhớ đệm thao tác cục bộ
Bộ nhớ đệm này là một lớp khác nằm sau Skyframe; ngay cả khi một hành động được thực thi lại trong Skyframe, hành động đó vẫn có thể là một lượt truy cập trong bộ nhớ đệm hành động cục bộ. Tệp này đại diện cho trạng thái của hệ thống tệp cục bộ và được chuyển đổi tuần tự sang ổ đĩa, nghĩa là khi khởi động một máy chủ Bazel mới, bạn có thể nhận được các lượt truy cập vào bộ nhớ đệm hành động cục bộ ngay cả khi biểu đồ Skyframe trống.
Bộ nhớ đệm này được kiểm tra các lượt truy cập bằng phương thức ActionCacheChecker.getTokenIfNeedToExecute()
.
Trái ngược với tên gọi, đây là một bản đồ từ đường dẫn của 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ả là:
- Tập hợp các tệp đầu vào và đầu ra cùng với tổng kiểm của các tệp đó
- "Khoá hành động" của nó thường là dòng lệnh đã được thực thi, nhưng nói chung, đại diện cho mọi thứ không được tổng kiểm của tệp đầu vào ghi lại (chẳng hạn như đối với
FileWriteAction
, đó là tổng kiểm của dữ liệu đã ghi)
Ngoài ra, còn có một "bộ nhớ đệm thao tác từ trên xuống" thử nghiệm cao vẫn đang trong quá trình phát triển, sử dụng hàm băm bắc cầu để tránh truy cập vào bộ nhớ đệm nhiều lần.
Khám phá dữ liệu đầu vào và cắt giảm dữ liệu đầu vào
Một số hành động phức tạp hơn so với việc chỉ có một tập hợp dữ liệu đầu vào. Các thay đổi đối với tập hợp dữ liệu đầu vào của một hành động có hai dạng:
- Một hành động có thể khám phá dữ liệu đầu vào mới trước khi thực thi hoặc quyết định rằng một số dữ liệu đầu vào thực sự không cần thiết. Ví dụ chuẩn là C++, trong đó tốt hơn là bạn nên dự đoán những tệp tiêu đề mà tệp C++ sử dụng từ phép đóng vi mô để chúng ta không cần gửi mọi tệp đến trình thực thi từ xa; do đó, chúng ta có thể chọn không đăng ký mọi tệp tiêu đề làm "đầu vào", mà quét tệp nguồn để tìm các tiêu đề được đưa vào một cách bắc cầu và chỉ đánh dấu các tệp tiêu đề đó là đầu vào được đề cập trong câu lệnh
#include
(chúng ta đánh giá quá cao để không cần triển khai trình xử lý trước C đầy đủ) Tuỳ chọn này hiện được kết nối cứng với "false" trong Bazel và chỉ được sử dụng tại Google. - Một hành động có thể nhận ra rằng một số tệp không được sử dụng trong quá trình thực thi. Trong C++, đây được gọi là "tệp .d": trình biên dịch cho biết tệp tiêu đề nào đã được sử dụng sau khi thực tế và để tránh sự xấu hổ khi có mức tăng dần kém hơn Make, Bazel sử dụng thực tế này. Phương thức này đưa ra số liệu ước tính tốt hơn so với trình quét bao gồm vì nó dựa vào trình biên dịch.
Các hành động này được triển khai bằng cách sử dụng các phương thức trên Hành động:
Action.discoverInputs()
được gọi. Hàm này sẽ trả về một tập hợp các Cấu phần phần mềm lồng nhau được xác định là bắt buộc. Đây phải là các cấu phần phần mềm nguồn để không có cạnh phần phụ thuộc nào trong biểu đồ hành động không có cạnh tương đương trong biểu đồ mục tiêu đã định cấu hình.- Thao tác này được thực thi bằng cách gọi
Action.execute()
. - Ở cuối
Action.execute()
, thao tác có thể gọiAction.updateInputs()
để cho Bazel biết rằng không phải tất cả dữ liệu đầu vào đều cần thiết. Điều này có thể dẫn đến các bản dựng tăng dần không chính xác nếu dữ liệu đầu vào đã sử dụng được báo cáo là không sử dụng.
Khi bộ nhớ đệm 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 việc khám phá dữ liệu đầu vào và việc cắt giảm trước đó.
Các thao tác 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 được sử dụng thông qua đối số unused_inputs_list=
của ctx.actions.run()
.
Nhiều cách để chạy hành động: Chiến lược/ActionContexts
Bạn có thể chạy một số thao tác theo nhiều cách. Ví dụ: một dòng lệnh có thể được thực thi cục bộ, cục bộ, nhưng trong nhiều loại hộp cát hoặc từ xa. Khái niệm thể hiện điều này được gọi là ActionContext
(hoặc Strategy
, vì chúng ta chỉ thực hiện được một nửa việc đổi tên thành công…)
Vòng đời của ngữ cảnh hành động như sau:
- Khi bắt đầu giai đoạn thực thi, các thực thể
BlazeModule
sẽ được hỏi về ngữ cảnh hành động mà chúng có. Điều này xảy ra trong hàm khởi tạo củaExecutionTool
. 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ủaActionContext
và giao diện mà ngữ cảnh hành động phải triển khai. - 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
ActionExecutionContext
vàBlazeExecutor
. - Hành động yêu cầu ngữ cảnh bằng cách sử dụng
ActionExecutionContext.getContext()
vàBlazeExecutor.getStrategy()
(thực sự chỉ có một cách để thực hiện việc này...)
Các chiến lược có thể tự do gọi các chiến lược khác để thực hiện công việc của mình; ví dụ: chiến lược này được sử dụng trong chiến lược động bắt đầu các hành động cả cục bộ và từ xa, sau đó sử dụng bất kỳ hành động nào kết thúc trước.
Một chiến lược đáng chú ý là chiến lược triển khai các quy trình worker liên tục (WorkerSpawnStrategy
). Ý tưởng là một số công cụ có thời gian khởi động lâu, do đó, bạn nên sử dụng lại các công cụ này giữa các thao tác thay vì bắt đầu lại một công cụ cho mỗi thao tác (Đây là vấn đề về độ chính xác tiềm ẩn, vì Bazel dựa vào lời hứa của quy trình worker rằng quy trình này không mang trạng thái có thể quan sát được giữa các yêu cầu riêng lẻ)
Nếu công cụ thay đổi, bạn cần khởi động lại quy trình worker. Việc một worker có thể được sử dụng lại hay không được xác định bằng cách tính toán tổng kiểm cho công cụ được sử dụng bằng WorkerFilesHash
. Phương thức này dựa vào việc biết dữ liệu đầu vào nào của hành động đại diện cho một phần của công cụ và dữ liệu đầu vào nào đại diện cho dữ liệu đầu vào; điều này do nhà sáng tạo của Hành động xác định: Spawn.getToolFiles()
và tệp chạy của Spawn
được tính là một phần của công cụ.
Thông tin khác về chiến lược (hoặc ngữ cảnh hành động!):
- Bạn có thể xem thông tin về nhiều chiến lược để chạy hành động tại đây.
- Thông tin về chiến lược động, chiến lược mà chúng ta chạy một thao tác cả trên máy và từ xa để xem thao tác nào kết thúc trước có sẵn tại đây.
- Thông tin về những vấn đề phức tạp của việc thực thi 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 song song nhiều thao tác. 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ộ.
Điều này được triển khai trong lớp ResourceManager
: mỗi hành động phải được chú thích bằng số liệu ước tính về tài nguyên cục bộ mà hành động đó cần ở dạng thực thể ResourceSet
(CPU và RAM). Sau đó, khi ngữ cảnh hành động thực hiện một thao tác cần có tài nguyên cục bộ, các ngữ cảnh này sẽ gọi ResourceManager.acquireResources()
và bị chặn cho đến khi có sẵn tài nguyên cần thiết.
Bạn có thể xem nội dung mô tả chi tiết hơn về việc quản lý tài nguyên cục bộ tại đây.
Cấu trúc của thư mục đầu ra
Mỗi thao tác cần có một vị trí riêng trong thư mục đầu ra để đặt đầu ra. Vị trí của các cấu phần phần mềm phái sinh thường như sau:
$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>
Tên của thư mục liên kết với một cấu hình cụ thể được xác định như thế nào? Có hai thuộc tính mong muốn xung đột với nhau:
- Nếu hai cấu hình có thể xuất hiện trong cùng một bản dựng, thì chúng phải có các thư mục khác nhau để cả hai có thể có phiên bản riêng của cùng một hành động; nếu không, nếu hai cấu hình không đồng ý về một hành động, chẳng hạn như dòng lệnh của một hành động tạo ra cùng một tệp đầu ra, thì Bazel sẽ không biết nên chọn hành động nào (một "xung đột hành động")
- Nếu hai cấu hình đại diện cho "gần như" cùng một nội dung, thì chúng phải có cùng tên để các hành động được thực thi trong một cấu hình có thể được sử dụng lại cho cấu hình còn lại nếu các dòng lệnh khớp nhau: ví dụ: các thay đổi đối với tuỳ chọn dòng lệnh cho trình biên dịch Java không được dẫn đến việc các hành động biên dịch C++ được chạy lại.
Cho đến nay, chúng tôi vẫn chưa đưa ra được cách giải quyết nguyên tắc cho vấn đề này, vấn đề này có những điểm tương đồng với vấn đề cắt bớt cấu hình. Bạn có thể xem thêm nội dung thảo luận về các tuỳ chọn tại đây. Các vấn đề chính là các quy tắc Starlark (tác giả thường không quen thuộc với Bazel) và các khía cạnh, thêm một phương diện khác vào không gian của những thứ có thể tạo ra "cùng một" tệp đầu ra.
Phương pháp hiện tại là phân đoạn đường dẫn cho cấu hình là <CPU>-<compilation mode>
với nhiều hậu tố được thêm vào để các chuyển đổi cấu hình được triển khai trong Java không dẫn đến xung đột hành động. Ngoài ra, một tổng kiểm của tập hợp các chuyển đổi cấu hình Starlark sẽ được thêm vào để người dùng không thể gây ra xung đột hành động. Nó còn rất nhiều điểm chưa hoàn thiện. Việc này được triển khai trong OutputDirectories.buildMnemonic()
và dựa vào từng mảnh cấu hình thêm phần riêng vào tên của thư mục đầu ra.
Thử nghiệm
Bazel có nhiều tính năng hỗ trợ chạy kiểm thử. API này hỗ trợ:
- Chạy kiểm thử từ xa (nếu có phần phụ trợ thực thi từ xa)
- Chạy song song nhiều lần kiểm thử (để loại bỏ lỗi hoặc thu thập dữ liệu về thời gian)
- Phân đoạn kiểm thử (phân tách các trường hợp kiểm thử trong cùng một kiểm thử trên nhiều quy trình để tăng tốc độ)
- Chạy lại các kiểm thử không ổn định
- Nhóm các bài kiểm thử thành bộ kiểm thử
Kiểm thử là các mục tiêu được định cấu hình thông thường có TestProvider, mô tả cách chạy kiểm thử:
- Các cấu phần phần mềm có 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
đã chuyển đổi tuần tự - Số lần chạy 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 kiểm thử (chẳng hạn như thời gian chờ kiểm thử)
Xác định kiểm thử nào sẽ chạy
Xác định kiểm thử nào sẽ chạy là một quá trình phức tạp.
Trước tiên, trong quá trình phân tích cú pháp mẫu mục tiêu, các bộ kiểm thử được mở rộng đệ quy. Việc mở rộng được triển khai trong TestsForTargetPatternFunction
. Một điều hơi đáng ngạc nhiên là nếu một bộ kiểm thử không khai báo kiểm thử nào, thì bộ kiểm thử đó sẽ tham chiếu đến mọi kiểm thử trong gói của bộ kiểm thử đó. Việc này được triển khai trong Package.beforeBuild()
bằng cách thêm một thuộc tính ngầm ẩn có tên là $implicit_tests
vào các quy tắc của bộ kiểm thử.
Sau đó, các chương trình kiểm thử sẽ được lọc theo kích thước, thẻ, thời gian chờ và ngôn ngữ theo các tuỳ chọn dòng lệnh. Phương thức này được triển khai trong TestFilter
và được gọi từ TargetPatternPhaseFunction.determineTests()
trong quá trình phân tích cú pháp mục tiêu, kết quả được đưa vào TargetPatternPhaseValue.getTestsToRunLabels()
. Lý do 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 đó, dữ liệu này được xử lý thêm trong BuildView.createResult()
: các mục tiêu có phân tích không thành công sẽ bị lọc ra và các kiểm thử được chia thành kiểm thử độc quyền và không độc quyền. Sau đó, tệp này được đưa vào AnalysisResult
. Đây là cách ExecutionTool
biết cần chạy kiểm thử nào.
Để mang lại sự minh bạch cho quy trình phức tạp này, toán tử truy vấn tests()
(được triển khai trong TestsFunction
) có sẵn để cho biết kiểm thử nào sẽ chạy khi một mục tiêu cụ thể được chỉ định trên dòng lệnh. Rất tiếc, đây là một quá trình triển khai lại, nên có thể sẽ khác với cách trên theo nhiều cách tinh tế.
Chạy kiểm thử
Cách chạy kiểm thử là yêu cầu cấu phần phần mềm trạng thái bộ nhớ đệm. Sau đó, 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 chương trình kiểm thử được chạy theo một giao thức phức tạp sử dụng các biến môi trường để cho các chương trình kiểm thử biết kết quả dự kiến. Bạn có thể xem nội dung mô tả chi tiết về những gì Bazel yêu cầu đối với kiểm thử và những gì kiểm thử có thể mong đợi từ Bazel tại đây. Ở mức đơn giản nhất, mã thoát là 0 có nghĩa là thành công, mọi giá trị khác có nghĩa là không thành công.
Ngoài tệp trạng thái bộ nhớ đệm, mỗi quy trình kiểm thử sẽ phát ra một số tệp khác. Các tệp này được đặt trong "thư mục nhật ký kiểm thử", là thư mục con có tên testlogs
của thư mục đầu ra của cấu hình mục tiêu:
test.xml
, một tệp XML kiểu JUnit 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 của bảng điều khiển của kiểm thử. stdout và stderr không được tách riêng.test.outputs
, "thư mục đầu ra chưa khai báo"; thư mục này được các chương trình kiểm thử sử dụng để xuất tệp ngoài những gì được in ra thiết bị đầu cuối.
Có 2 điều có thể xảy ra trong quá trình chạy chương trình kiểm thử mà bạn không thể thực hiện 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ụ như không chạy song song với các chương trình kiểm thử khác. Bạn có thể kích hoạt điều này bằng cách thêm tags=["exclusive"]
vào quy tắc kiểm thử hoặc chạy kiểm thử bằng --test_strategy=exclusive
. Mỗi 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 kiểu 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.
Bạn có thể xem kết quả của các chương trình kiểm thử đã thực thi trên bus sự kiện bằng cách quan sát nhiều sự kiện (chẳng hạn như TestAttempt
, TestResult
hoặc TestingCompleteEvent
). Các kết quả này được chuyển vào Giao thức sự kiện bản dựng và được AggregatingTestListener
phát vào bảng điều khiển.
Thu thập thông tin về mức độ phù hợp
Mức độ sử dụng được báo cáo bằng các bài kiểm thử ở định dạng LCOV trong các tệp bazel-testlogs/$PACKAGE/$TARGET/coverage.dat
.
Để thu thập mức độ sử dụng, mỗi lần thực thi kiểm thử được gói trong một tập lệnh có tên là collect_coverage.sh
.
Tập lệnh này thiết lập môi trường kiểm thử để cho phép thu thập mức độ sử dụng và xác định vị trí các 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 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 chèn collect_coverage.sh
do các chiến lược kiểm thử thực hiện và yêu cầu collect_coverage.sh
phải nằm trong dữ liệu đầu vào của kiểm thử. Việc này được thực hiện bằng thuộc tính ngầm :coverage_support
được phân giải thành giá trị của cờ cấu hình --coverage_support
(xem TestConfiguration.TestOptions.coverageSupport
)
Một số ngôn ngữ thực hiện đo lường ngoại tuyến, nghĩa là 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 độ phù hợp cơ sở. Đây là mức độ sử dụng của một thư viện, tệp nhị phân hoặc kiểm thử nếu không có mã nào trong đó được chạy. Vấn đề mà công cụ này giải quyết là nếu bạn muốn tính toán mức độ bao phủ 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 đó, chúng ta sẽ tạo một tệp mức độ sử dụng cho mọi tệp nhị phân chỉ chứa các tệp mà chúng ta thu thập mức độ sử dụng mà không có dòng nào được bao phủ. Tệp mức độ sử dụng cơ sở cho một mục tiêu nằm ở bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat
. Tệp này cũng được tạo cho các tệp nhị phân và thư viện ngoài các chương trình kiểm thử nếu bạn truyền cờ --nobuild_tests_only
đến Bazel.
Mức độ sử dụng cơ sở hiện đang bị lỗi.
Chúng tôi theo dõi hai nhóm tệp để thu thập mức độ sử dụng cho 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 thời gian chạy phạm vi trực tuyến, bạn có thể sử dụng tính năng này trong thời gian chạy để quyết định tệp nào sẽ được đo lường. Phương pháp này cũng được dùng để triển khai mức độ sử dụng cơ sở.
Tập hợp các tệp siêu dữ liệu đo lường là tập hợp các tệp bổ sung mà quy trình kiểm thử cần có để tạo các tệp LCOV mà Bazel yêu cầu. Trong thực tế, tệp này bao gồm các tệp dành riêng cho thời gian chạy; ví dụ: gcc phát ra các tệp .gcno trong quá trình biên dịch. Các hành động này được thêm vào tập hợp dữ liệu đầu vào của các hành động kiểm thử nếu chế độ phạm vi được bật.
Liệu phạm vi phủ sóng có đang được thu thập hay không sẽ được lưu trữ trong BuildConfiguration
. 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 các nhãn trong một phần phụ thuộc ngầm ẩn để có thể được ghi đè bằng chính sách gọi, cho phép các tệp này khác nhau giữa các phiên bản Bazel. Lý tưởng nhất là các điểm khác biệt này sẽ bị xoá và chúng tôi đã chuẩn hoá một trong số đó.
Chúng ta cũng tạo một "báo cáo mức độ sử dụng" để hợp nhất mức độ sử dụng được thu thập cho mọi chương trình kiểm thử trong một lệnh gọi Bazel. Việc này do CoverageReportActionFactory
xử lý và được gọi từ BuildView.createResult()
. Công cụ này có quyền truy cập vào các công cụ cần thiết bằng cách xem thuộc tính :coverage_report_generator
của kiểm thử đầu tiên được thực thi.
Công cụ truy vấn
Bazel có một ít ngôn ngữ dùng để đặt câu hỏi cho Bazel nhiều câu về các biểu đồ khác nhau. 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êubazel cquery
được dùng để điều tra biểu đồ mục tiêu đã định cấu hìnhbazel aquery
được dùng để điều tra biểu đồ hành động
Mỗi lớp trong số này được triển khai bằng cách tạo lớp con AbstractBlazeQueryEnvironment
.
Bạn có thể thực hiện các hàm truy vấn bổ sung bằng cách tạo lớp con QueryFunction
. Để cho phép truyền trực tuyến 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 của 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ể đã thay đổi hay chưa. 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. Giải pháp thông thường là sử dụng nhãn và đính kèm thông tin phức tạp vào quy tắc bằng nhãn đó. Đây không phải là giải pháp thỏa đáng và sẽ rất tuyệt nếu bạn có thể gỡ bỏ yêu cầu này.
Hệ thống mô-đun
Bạn có thể mở rộng Bazel bằng cách thêm các mô-đun vào đó. Mỗi mô-đun phải có lớp con BlazeModule
(tên này là di tích của quá trình phát triển Bazel khi trước đây được gọi là Blaze) và nhận thông tin về nhiều sự kiện trong quá trình thực thi lệnh.
Các 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 cho các hệ thống thực thi từ xa
- Lệnh mới
Tập hợp các điểm mở rộng mà BlazeModule
cung cấp có phần lộn xộn. Đừng lấy ví dụ 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 để BlazeModules giao tiếp với phần còn lại của Bazel là thông qua một bus sự kiện (EventBus
): một thực thể mới được tạo cho mỗi bản dựng, nhiều phần của Bazel có thể đăng sự kiện lên bus này và các mô-đun có thể đăng ký trình nghe cho các sự kiện mà chúng quan tâm. Ví dụ: những điều sau đây được biểu thị dưới dạng sự kiện:
- Danh sách các mục tiêu bản dựng cần tạo đã được xác định (
TargetParsingCompleteEvent
) - Các cấu hình cấp cao nhất đã được xác định (
BuildConfigurationEvent
) - Đã tạo một mục tiêu, thành công hay không (
TargetCompleteEvent
) - Đã chạy một kiểm thử (
TestAttempt
,TestSummary
)
Một số sự kiện trong số này được biểu thị bên ngoài Bazel trong Build Event Protocol (Giao thức sự kiện bản dựng) (chúng là BuildEvent
). Điều này không chỉ cho phép BlazeModule
mà còn cho phép các thành phần bên ngoài quy trình Bazel quan sát bản dựng. Bạn có thể truy cập các tệp này dưới dạng một tệp chứa thông báo giao thức hoặc Bazel có thể kết nối với một máy chủ (được gọi là Dịch vụ sự kiện bản dựng) để truyền trực tuyến sự kiện.
Việc này được triển khai trong các gói Java build.lib.buildeventservice
và build.lib.buildeventstream
.
Kho lưu trữ bên ngoài
Mặc dù ban đầu Bazel được thiết kế để sử dụng trong một monorepo (một cây nguồn duy nhất chứa mọi thứ cần thiết để xây dựng), nhưng Bazel hoạt động trong một thế giới mà điều này không nhất thiết phải đúng. "Kho lưu trữ bên ngoài" là một khái niệm trừu tượng dùng để kết nối hai thế giới này: chúng đại diện cho mã cần thiết cho bản dựng nhưng không có trong cây nguồn chính.
Tệp WORKSPACE
Tập hợp các kho lưu trữ bên ngoài được xác định bằng cách phân tích cú pháp tệp WORKSPACE. Ví dụ: một nội dung khai báo như sau:
local_repository(name="foo", path="/foo/bar")
Có kết quả trong kho lưu trữ có tên @foo
. Điều phức tạp ở đây là bạn có thể xác định các quy tắc mới về kho lưu trữ trong tệp Starlark, sau đó sử dụng các quy tắc này để tải mã Starlark mới, từ đó xác định các quy tắc mới 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 đoạn được biểu thị bằng WorkspaceFileKey.getIndex()
và tính toán WorkspaceFileFunction
cho đến khi chỉ mục X có nghĩa là đánh giá chỉ mục đó cho đến câu lệnh load()
thứ X.
Tìm nạp kho lưu trữ
Trước khi có thể cung cấp mã của kho lưu trữ cho Bazel, bạn cần phải tìm nạp mã đó. Điều này khiến Bazel tạo một thư mục trong $OUTPUT_BASE/external/<repository name>
.
Quá trình tìm nạp kho lưu trữ diễn ra theo các bước sau đây:
PackageLookupFunction
nhận ra rằng nó cần một kho lưu trữ và tạo mộtRepositoryName
dưới dạngSkyKey
, gọiRepositoryLoaderFunction
RepositoryLoaderFunction
chuyển tiếp yêu cầu đếnRepositoryDelegatorFunction
vì lý do không rõ ràng (mã này cho biết là để tránh tải lại các nội dung trong trường hợp khởi động lại Skyframe, nhưng đây không phải là lý do vững chắc)RepositoryDelegatorFunction
tìm ra quy tắc kho lưu trữ mà nó được yêu cầu tìm nạp bằng cách lặp lại các phần của tệp WORKSPACE cho đến khi tìm thấy kho lưu trữ được yêu cầu- 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:
- Có một bộ nhớ đệm cho các tệp đã tải xuống được khoá bằng tổng kiểm của tệp (
RepositoryCache
). Điều này yêu cầu tổng kiểm phải có trong tệp WORKSPACE, nhưng dù sao thì điều đó cũng tốt cho tính kín đáo. Mỗi thực thể máy chủ Bazel trên cùng một máy trạm đều chia sẻ thông tin này, bất kể chúng đang chạy trong không gian làm việc hay cơ sở đầu ra nào. - "Tệp điểm đá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 tổng kiểm không thay đổi, thì tổng kiểm đó sẽ không được tìm nạp lại. Việc này được triển khai trongRepositoryDelegatorFunction.DigestWriter
. - 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 doDownloadManager
triển khai .
Sau khi tải kho lưu trữ xuống, các cấu phần phần mềm trong kho lưu trữ đó sẽ được coi là cấu phần phần mềm nguồn. Điều này gây ra vấn đề vì Bazel thường kiểm tra tính mới nhất của các cấu phần phần mềm nguồn bằng cách gọi stat() trên các cấu phần phần mềm đó và các cấu phần phần mềm này cũng không hợp lệ khi định nghĩa của kho lưu trữ chứa các cấu phần phần mềm đó thay đổi. Do đó, 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 lưu trữ 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. Để loại kho lưu trữ bên ngoài này hoạt động, Bazel thực hiện hai việc:
- Cho phép người dùng chỉ định các thư mục con của không gian làm việc mà Bazel không được phép truy cập. Các tệp này được liệt kê trong một tệp có tên là
.bazelignore
và chức năng được triển khai trongBlacklistedPackagePrefixesFunction
. - Chúng ta mã hoá mối 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à không gian làm việc được xử lý trong
ManagedDirectoriesKnowledge
và xử lý cácFileStateValue
tham chiếu đến các thư mục đó theo cách tương tự như các thư mục cho kho lưu trữ bên ngoài thông thường.
Ánh xạ kho lưu trữ
Có thể xảy ra trường hợp nhiều kho lưu trữ muốn phụ thuộc vào cùng một kho lưu trữ, nhưng ở các phiên bản khác nhau (đây là một thực thể của "vấn đề phần phụ thuộc kim cương"). Ví dụ: nếu hai tệp nhị phân trong các kho lưu trữ riêng biệt trong bản dựng muốn phụ thuộc vào Guava, thì có 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 ánh xạ lại các nhãn kho lưu trữ bên ngoài để chuỗi @guava//
có thể tham chiếu đến một kho lưu trữ Guava (chẳng hạn như @guava1//
) trong kho lưu trữ của một tệp nhị phân và một kho lưu trữ Guava khác (chẳng hạn như @guava2//
) là kho lưu trữ của tệp nhị phân còn lại.
Ngoài ra, bạn cũng có thể sử dụng hàm này để kết hợp các viên kim cương. Nếu một kho lưu trữ phụ thuộc vào @guava1//
và một kho lưu trữ khác phụ thuộc vào @guava2//
, thì tính năng liên kết kho lưu trữ cho phép liên kết lại cả hai kho lưu trữ để sử dụng kho lưu trữ @guava//
chính tắc.
Việc liên kết được chỉ định trong tệp WORKSPACE làm thuộc tính repo_mapping
của các định nghĩa riêng lẻ trong kho lưu trữ. Sau đó, thành phần này sẽ xuất hiện trong Skyframe dưới dạng một thành phần của WorkspaceFileValue
, trong đó thành phần này được kết nối với:
Package.Builder.repositoryMapping
dùng để chuyển đổi các thuộc tính có giá trị nhãn của quy tắc trong gói bằngRuleClass.populateRuleAttributeValues()
Package.repositoryMapping
được dùng trong giai đoạn phân tích (để giải quyết những vấn đề như$(location)
không được phân tích cú pháp trong giai đoạn tải)BzlLoadFunction
để phân giải nhãn trong câu lệnh load()
Bit JNI
Máy chủ của Bazel chủ yếu được viết bằng Java. Trường hợp ngoại lệ là những phần mà Java không thể tự làm hoặc không thể tự làm khi chúng ta triển khai. Điều này chủ yếu giới hạn ở việc tương tác với hệ thống tệp, kiểm soát quy trình và nhiều thứ cấp thấp khác.
Mã C++ nằm trong src/main/native và các lớp Java có các phương thức gốc là:
NativePosixFiles
vàNativePosixFileSystem
ProcessUtils
WindowsFileOperations
vàWindowsFileProcesses
com.google.devtools.build.lib.platform
Kết quả xuất ra trên bảng điều khiển
Việc phát ra đầu ra của bảng điều khiển có vẻ như là một việc đơn giản, nhưng sự kết hợp của việc chạy nhiều quy trình (đôi khi từ xa), lưu vào bộ nhớ đệm chi tiết, mong muốn có đầu ra của thiết bị đầu cuối đẹp và đầy màu sắc cũng như có máy chủ chạy trong thời gian dài khiến việc này không hề đơn giản.
Ngay sau khi lệnh gọi RPC đến từ ứng dụng, 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 in trên bảng điều khiển đều phải đi qua các luồng này. Sau đó, các luồng này được chuyển giao cho BlazeCommandDispatcher.execExclusively()
.
Theo mặc định, kết quả được in bằng các chuỗi thoát ANSI. Khi không mong muốn (--color=no
), các thuộc tính này sẽ bị AnsiStrippingOutputStream
xoá bỏ. Ngoài ra, System.out
và System.err
cũng được chuyển hướng đến các luồng đầu ra này.
Điều này là để có thể in thông tin gỡ lỗi bằng cách sử dụng System.err.println()
và vẫn kết thúc trong đầu ra của thiết bị đầu cuối của ứng dụng (khác với đầu ra của máy chủ). Hãy cẩn thận nếu một quy trình tạo ra đầu ra nhị phân (chẳng hạn như bazel query --output=proto
), thì không có hoạt động kết hợp nào của stdout diễn ra.
Thông báo ngắn (lỗi, cảnh báo và các thông báo tương tự) được thể hiện thông qua giao diện EventHandler
. Lưu ý rằng các giá trị này khác với giá trị mà người dùng đăng lên EventBus
(điều này gây nhầm lẫn). Mỗi Event
có một EventKind
(lỗi, cảnh báo, thông tin và một số loại khác) và có thể có Location
(vị trí trong mã nguồn đã gây ra sự kiện).
Một số cách triển khai EventHandler
lưu trữ các sự kiện mà chúng nhận được. Phương thức này được dùng để phát lại thông tin cho giao diện người dùng do nhiều loại hoạt động xử lý được lưu vào bộ nhớ đệm gây ra, chẳng hạn như cảnh báo do mục tiêu được định cấu hình trong bộ nhớ đệm phát ra.
Một số EventHandler
cũng cho phép đăng các sự kiện cuối cùng sẽ tìm thấy đường đến xe buýt sự kiện (Event
thông thường _không_ xuất hiện ở đó). Đây là các cách triển khai ExtendedEventHandler
và mục đích chính của các cách này là phát lại các sự kiện EventBus
đã lưu vào bộ nhớ đệm. Các sự kiện EventBus
này đều triển khai Postable
, nhưng không phải mọi nội dung được đăng lên EventBus
đều triển khai giao diện này; chỉ những nội dung được ExtendedEventHandler
lưu vào bộ nhớ đệm (điều này sẽ rất tốt và hầu hết các nội dung đều thực hiện; tuy nhiên, điều này không được thực thi)
Đầu ra của dòng lệnh chủ yếu được phát qua UiEventHandler
. Thành phần này chịu trách nhiệm về tất cả định dạng đầu ra và báo cáo tiến trình mà Bazel thực hiện. Hàm này có hai đầu vào:
- Xe buýt sự kiện
- Luồng sự kiện được chuyển vào đó thông qua Reporter
Kết nối trực tiếp duy nhất mà cơ chế thực thi lệnh (ví dụ: phần còn lại của Bazel) có với luồng RPC đến máy khách là thông qua Reporter.getOutErr()
, cho phép truy cập trực tiếp vào các luồng này. Phương thức này chỉ được dùng khi một lệnh cần kết xuất một lượng lớn dữ liệu nhị phân có thể có (chẳng hạn như bazel query
).
Phân tích tài nguyên Bazel
Bazel có tốc độ nhanh. Bazel cũng chậm, vì các bản dựng có xu hướng phát triển cho đến khi đạt đến giới hạn chịu đựng. Vì lý do này, Bazel bao gồm một trình phân tích tài nguyên có thể được dùng để phân tích tài nguyên cho các bản dựng và chính Bazel. Lớp này được triển khai trong một lớp có tên là Profiler
. Tính năng này được bật theo mặc định, mặc dù nó 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ể.
Trình phân tích tài nguyên này sẽ tạo một hồ sơ ở định dạng trình phân tích tài nguyên Chrome; bạn nên xem hồ sơ này trong Chrome. Mô hình dữ liệu của nó là mô hình ngăn xếp tác vụ: người dùng có thể bắt đầu và kết thúc tác vụ và các tác vụ này phải được lồng ghép gọn gàng với nhau. Mỗi luồng Java sẽ có một ngăn xếp tác vụ riêng. 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 được khởi động và dừng tương ứng trong BlazeRuntime.initProfiler()
và BlazeRuntime.afterCommand()
, đồng thời cố gắng hoạt động trong thời gian dài nhất có thể để chúng ta có thể phân tích mọi thứ. Để thêm nội dung vào hồ sơ, hãy gọi Profiler.instance().profile()
. Phương thức này trả về một Closeable
, trong đó phần đóng biểu thị việc kết thúc tác vụ. Tốt nhất bạn nên sử dụng với câu lệnh try-with-resources.
Chúng ta cũng thực hiện việc phân tích bộ nhớ sơ khai trong MemoryProfiler
. Chế độ này cũng luôn bật và chủ yếu ghi lại kích thước vùng nhớ khối xếp tối đa và hành vi của GC.
Kiểm thử Bazel
Bazel có hai loại kiểm thử chính: những kiểm thử quan sát Bazel dưới dạng "hộp đen" và những kiểm thử chỉ chạy giai đoạn phân tích. Chúng 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.
Có hai loại kiểm thử tích hợp:
- Những lệnh được triển khai bằng một khung kiểm thử bash rất phức tạp trong
src/test/shell
- Những hàm được triển khai trong Java. Các lớp này được triển khai dưới dạng lớp con của
BuildIntegrationTestCase
BuildIntegrationTestCase
là khung kiểm thử tích hợp được ưu tiên vì khung này được trang bị đầy đủ cho hầu hết các trường hợp kiểm thử. Vì là một khung Java, nên khung này có khả năng gỡ lỗi và tích hợp liền mạch với nhiều công cụ phát triển phổ biến. Có nhiều ví dụ về lớp BuildIntegrationTestCase
trong kho lưu trữ Bazel.
Các 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ể sử dụng hệ thống tệp tạm để ghi tệp BUILD
, sau đó nhiều phương thức trợ giúp có thể yêu cầu các mục tiêu đã định cấu hình, thay đổi cấu hình và xác nhận nhiều thông tin về kết quả của quá trình phân tích.