Tài liệu này mô tả cơ sở mã và cách cấu trúc 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 Bazel nằm trên GitHub tại github.com/bazelbuild/bazel. Đây không phải là "nguồn đáng tin cậy"; mà là nguồn lấy từ cây nguồn nội bộ của Google, 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 quy trình máy chủ và nằm trong RAM giữa các bản dựng. Điều này cho phép Bazel duy trì trạng thái giữa các bản dựng.
Đây là lý do tại sao dòng lệnh Bazel có 2 loại tuỳ chọn: khởi động và lệnh. Trong một dòng lệnh như sau:
bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar
Một số tuỳ chọn (--host_jvm_args=
) 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 thực thể máy chủ có một cây nguồn được 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 thực thể máy chủ đang hoạt động. 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, đồng thời cũng là một tệp .zip hợp lệ.
Khi bạn nhập bazel
, tệp thực thi ELF ở trên được triển khai trong C++ ("ứng dụng") sẽ có quyền kiểm soát. Phương thức này thiết lập một quy trình máy chủ thích hợp bằng cách làm theo các bước sau:
- Kiểm tra xem tệp đó đã tự giải nén hay chưa. Nếu không, ứng dụng sẽ thực hiện việc đó. Đây là nơi bắt nguồn việc triển khai máy chủ.
- Kiểm tra xem có phiên bản máy chủ nào đang hoạt động hay không: phiên bản đó đang chạy, có các tuỳ chọn khởi động phù hợp và sử dụng đúng thư mục không gian làm việc. API này tìm máy chủ đang chạy bằng cách xem thư mục
$OUTPUT_BASE/server
, nơi có tệp khoá với cổng mà máy chủ đang theo dõi. - Nếu cần, hãy chấm dứt quy trình máy chủ cũ
- Khởi động một quy trình máy chủ mới nếu cần
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ế 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ì 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 được liên kết với một không gian làm việc cụ thể ghi dữ liệu vào. Mỗi cơ sở đầu ra có tối đa một thực thể máy chủ Bazel có thể chạy bất cứ lúc nào. 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 được tìm nạp tại
$OUTPUT_BASE/external
. - Thư mục gốc thực thi, một thư mục chứa các đường liên kết tượng trưng đến tất cả mã nguồn cho bản dựng hiện tại. Tệp này nằm ở
$OUTPUT_BASE/execroot
. Trong quá trình dựng, thư mục đang hoạt động là$EXECROOT/<name of main repository>
. Chúng tôi dự định thay đổi giá trị này thành$EXECROOT
, mặc dù đây là kế hoạch dài hạn vì đây là thay đổi không tương thích. - Tệp được tạo trong quá trình tạo bản dựng.
Quy trình thực thi một lệnh
Sau khi máy chủ Bazel có quyền kiểm soát và đượ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 xe buýt 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 (một biểu đồ không tuần hoàn có hướng của các lệnh cần 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
, dùng để nhóm các tuỳ chọn dòng lệnh có liên quan với nhau. Ví dụ:
- Các tuỳ chọn liên quan đến một ngôn ngữ lập trình (
CppOptions
hoặcJavaOptions
). Đây phải 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ố được tích hợp sẵn vào Bazel (
CommonCommandOptions
) - Từ chú giải
@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 của Starlark cũng có thể xác định các tuỳ chọn riêng (xem tại đây)
Mỗi tuỳ chọn (ngoại trừ các tuỳ chọn do Starlark xác định) là một biến thành 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 cho giá trị của tuỳ chọn dòng lệnh thường là một loại đơn giản (chuỗi, số nguyên, Boolean, nhãn, v.v.). Tuy nhiên, chúng tôi cũng hỗ trợ các tuỳ chọn 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 trên đó được gọi là "không gian làm việc" và được cấu trúc thành kho lưu trữ, gói và quy tắc.
Kho lưu trữ
"Kho lưu trữ" là 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 rộng trên nhiều kho lưu trữ. Kho lưu trữ mà Bazel được gọi là "kho lưu trữ chính", các kho lưu trữ khác được gọi là "kho lưu trữ bên ngoài".
Kho lưu trữ được đánh dấu bằng một tệp 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.
Tệp này thường dùng để lấy các kho lưu trữ cần thiết mà kho lưu trữ được tham chiếu rõ ràng (chúng tôi gọi đây là "mẫu deps.bzl
")
Mã của 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 thể làm thay đổi các gói khác. 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.
Việc đá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
. 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 phần chỉ dùng để phân tích cú pháp tệp WORKSPACE và các thành phần này không phù hợp với các gói thực. Đây là một lỗi thiết kế vì các đối tượng mô tả gói thông thường không được chứa các trường mô tả nội dung khác. bao gồm:
- Các mối liên kết kho lưu trữ
- Chuỗi công cụ đã đăng ký
- Các nền tảng thực thi đã đăng ký
Lý tưởng nhất là bạn nên tách biệt 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 đáp ứng 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: các tệp là đầ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 tạo bản dựng đều là mục tiêu; dữ liệu đầu ra của Bazel thường không có nhãn liên kết.
- 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. - Các chuyển đổi cấu hình và khía cạnh đính kèm vào từng 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 tôi thường dùng "Quy tắc" để chỉ mục tiêu do 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 sâu sắc; chỉ có thể tiếp cận các đối tượng bất biến từ chúng. 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.
Từ đó, mọi thứ được tính toán trong Skyframe (chẳng hạn như các mục tiêu đã định cấu hình) cũng phải ở dạng bất biến.
Cách thuận tiện nhất để quan sát biểu đồ Skyframe là chạy bazel dump
--skyframe=deps
. 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 tăng dần của Skyframe hoạt động ở độ chi tiết như 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ữ chuyên biệt theo miền mà mọi người sử dụng để định cấu hình và mở rộng Bazel. Thư viện này được xem là một tập hợp con bị hạn chế của Python, có ít loại hơn nhiều, có nhiều hạn chế hơn đối với luồng điều khiển và quan trọng nhất là đảm bảo khả năng bất biến mạnh mẽ để cho phép đọc đồng thời. Ngôn ngữ này không hoàn chỉnh theo Turing, điều này khiến một số (nhưng không phải tất cả) người dùng không muốn cố gắng hoàn thành các nhiệm vụ lập trình chung trong ngôn ngữ này.
Starlark được triển khai trong gói net.starlark.java
.
Ứng dụng này cũng có cách triển khai Go độc lập
tại đây. 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 các quy tắc mới được xác định. Mã Starlark chạy trong ngữ cảnh này chỉ có quyền truy cập vào nội dung của chính tệpBUILD
và các tệp.bzl
do tệp này tải. - Định nghĩa 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 bạn 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 mà Bazel đang chạy và tiếp cận bên ngoài không gian làm việc.
Các phương ngữ có sẵn cho tệp BUILD
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 lớp này là "mục tiêu đã định cấu hình", tức là một cặp (mục tiêu, cấu hình) khá hợp lý.
Giai đoạn này được gọi là "giai đoạn tải/phân tích" vì có thể được chia thành hai phần riêng biệt, từng được tuần tự hoá nhưng hiện có thể trùng lặp theo thời gian:
- Tải gói, tức là chuyển các tệp
BUILD
thành các đối tượngPackage
đại diện cho các tệp đó - Phân tích các mục tiêu đã định cấu hình, tức là chạy việc triển khai các quy tắc để tạo biểu đồ hành động
Bạn phải phân tích từng mục tiêu đã đị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 bản phân tích của một mục tiêu đã định cấu hình là:
- Cấu hình. ("cách" tạo quy tắc đó; ví dụ: nền tảng mục tiêu nhưng cũng bao gồm các tuỳ chọn dòng lệnh mà người dùng muốn được chuyển đến trình biên dịch C++)
- Các phần phụ thuộc trực tiếp. Trình cung cấp thông tin bắc cầu của họ có sẵn cho quy tắc đang được phân tích. Các tệp này được gọi như vậy vì 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 các quy tắc, dữ liệu này bao gồm các thuộc tính tương ứng, thường là những gì quan trọng.
- Triển khai mục tiêu đã định cấu hình. Đối với các quy tắc, thuộc tính này có thể trong Starlark hoặc trong Java. Tất cả mục tiêu đượ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 này mạnh mẽ hơn, nhưng đồng thời cũng dễ dàng thực hiện các thao tác Bad ThingsTM hơn, chẳng hạn như viết mã có độ phức tạp về thời gian hoặc không gian là bậc hai (hoặc tệ hơn), để gây ra sự cố máy chủ Bazel với ngoại lệ Java hoặc vi phạm các giá trị bất biến (chẳng hạn như vô tình sửa đổi thực thể Options
hoặc làm cho mục tiêu đã định cấu hình có thể thay đổi)
Thuật toán xác định các phần phụ thuộc trực tiếp của mộ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, trong thực tế, BuildOptions
được gói bằng BuildConfiguration
để cung cấp thêm nhiều 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 có thay đổi, bạn cần phân tích lại bản dựng.
Điều này dẫn đến các điểm bất thường như phải phân tích lại toàn bộ bản dựng, chẳng hạn như số lượng lượt chạy kiểm thử được yêu cầu thay đổi, mặc dù điều đó chỉ ảnh hưởng đến các mục tiêu kiểm thử (chúng tôi có kế hoạch "cắt bỏ" cấu hình để tình trạng này chưa sẵn sàng nhưng chưa sẵn sàng).
Khi 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()
. Điều này vừa giúp tránh sai sót (chẳng hạn như quy tắc Python sử dụng mảnh Java) vừa hỗ trợ việc cắt bớt cấu hình để nếu các tuỳ chọn Python thay đổi, thì bạn không cần phân tích lại mục tiêu C++.
Cấu hình của một quy tắc không nhất thiết phải giống với cấu hình của quy tắc "mẹ". Quá trình thay đổi cấu hình trong một cạnh phầ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 quá trình chuyển đổi này được chỉ định trong
Attribute.Builder.cfg()
và là các hàm từRule
(nơi diễn ra quá trình chuyển đổi) vàBuildOptions
(cấu hình ban đầu) đế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
.
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 tạo bản dựng và phần phụ thuộc đó phải được tạo trong cấu trúc thực thi
- Để 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 trung gian
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ên của các lớp này có chứa từ "transitive" (mang tính bắc cầu) là vì đây thường là một số loại cuộn lên của phép đóng bắc cầu của một mục tiêu đã định cấu hình.
Thường thì có mối tương ứng 1:1 giữa 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 lệ là DefaultInfo
, là sự kết hợp của FileProvider
, FilesToRunProvider
và RunfilesProvider
vì API đó được coi là giống Starlark hơn là bản chuyển tự trực tiếp của API Java).
Khoá của họ là một trong những điều 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 trình cung cấp này là một 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 được định cấu hình bằng Starlark được tạo thông qua StarlarkRuleConfiguredTargetUtil.buildRule()
.
Nhà máy mục tiêu đã định cấu hình phải sử dụng RuleConfiguredTargetBuilder
để tạo giá trị trả về. 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.
- Nhóm đầu ra của chúng. Đâ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 qua khái niệm "runfile". "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ể. 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 nhập cũ trong cây tệp chạy, các mục nhập này cũng cần được trình bày.
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 hiển thị 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ác mục tiêu 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 tệp chạy đầ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 phần phụ thuộc, bạn có thể sử dụng trình trung gian runfiles để biểu thị tất cả các cạnh này.
- Đối số dòng lệnh để chạy tệp nhị phân mà đối tượng
RunfilesSupport
đại diện cho tệp chạy.
Khía cạnh
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". 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 nên biết về bất kỳ ngôn ngữ cụ thể nào, nhưng việc xây dựng phương thức triển khai thông báo vùng đệm giao thức ("đơn vị cơ bản" của vùng đệm giao thức) trong bất kỳ ngôn ngữ lập trình nào cũng phải được kết hợp với quy tắc proto_library
để nếu hai mục tiêu trong cùng một ngôn ngữ phụ thuộc vào cùng một vùng đệm giao thức thì chỉ được tạo một lần.
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 số 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. Thuộc tính này tương đồng vớiRuleClass
.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, đây là một bản đồ chuỗi đến chuỗi. Một ví dụ điển hình về lý do tại sao vùng đệm giao thức lại hữu ích là: nếu một ngôn ngữ có nhiều API, thì thông tin về API nào cần tạo vùng đệm giao thức sẽ đượ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 sẽ truyền xuống biểu đồ phần phụ thuộc. Lớp này bao gồm lớp khung hình, định nghĩa và các tham số của lớp đó.RuleAspect
là hàm xác định những khía cạnh mà một quy tắc cụ thể sẽ áp dụng. Đây là hàmRule
->Aspect
.
Một vấn đề phức tạp không mong muố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 khía cạnh thu thập đường dẫn lớp cho IDE Java có thể muốn biết về tất cả tệp .jar trên đường dẫn lớp, nhưng một số tệp 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 tôi có "từ điển" về các giá trị và chế độ cài đặt ràng buộc thường dùng nhất trong kho lưu trữ @platforms
.
Khái niệm chuỗi công cụ xuất phát từ thực tế là tuỳ thuộc vào nền tảng mà bản dựng đang chạy và nền tảng được nhắm đế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 điều kiện ràng buộc thực thi và mục tiêu mà một chuỗi công cụ hỗ trợ, đồng thời cho biết loại chuỗi công cụ (chẳng hạn như C++ hoặc Java) (loại sau được biểu thị bằng quy tắctoolchain_type()
) - Một quy tắc dành riêng cho ngôn ngữ mô tả chuỗi công cụ thực tế (chẳng hạn như
cc_toolchain()
)
Chúng 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.
Nền tảng thực thi được chỉ định 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à một chức năng:
- 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 quy tắc 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
) 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ý các trường hợp mọi người dựa vào các giá trị cấu hình cũ, chúng tôi đã triển khai các ánh xạ nền tảng để dịch giữa các cờ cũ và các quy tắc ràng buộc nền tảng kiểu mới.
Mã của chúng nằm trong PlatformMappingFunction
và sử dụng một "ngôn ngữ nhỏ" không phải Starlark.
Giới hạn
Đôi khi, một người muốn chỉ định một mục tiêu là chỉ tương thích với một số nền tảng. (Rất tiếc) Bazel có nhiều cơ chế để đạt được mục tiêu này:
- Quy tắc ràng buộc theo quy tắc
environment_group()
/environment()
- Các quy tắc ràng buộc của nền tảng
Các quy tắc ràng buộc dành riêng cho 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 trong quá trình ngừng hoạt động và không có trong Bazel, nhưng mã nguồn có thể chứa các tệp tham chiếu đến quy tắc này. Thuộc tính chi phối quá trình chuyển đổi này có tên là constraints=
.
môi trường_group() và môi trường()
Các quy tắc này là một cơ chế cũ và không được sử dụng rộng rãi.
Tất cả quy tắc bản dựng đều có thể khai báo "môi trường" có thể xây dựng các quy tắc đó, 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 thông số kỹ thuật trực tiếp nhất; thông số này khai báo chính xác tập hợp môi trường mà quy tắc hỗ trợ cho nhóm này. - Thông qua thuộc tính
compatible_with=
. Thao tác này khai báo các môi trường mà 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"). Định nghĩa của một nhóm môi trường bao gồm môi trường nào trong số này sẽ được hỗ trợ theo chế độ "mặc định" nếu các thuộc tínhrestricted_to=
/environment()
không 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 một lớp quy tắc mặc định. Thao tác này sẽ ghi đè các giá trị mặc định chung cho tất cả các thực thể của lớp quy tắc đã cho. 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 như quy tắc thông thường, trong khi environment_group()
vừa là lớp con của Target
nhưng không phải của Rule
(EnvironmentGroup
) vừa là một hàm có sẵn theo mặc định từ Starlark (StarlarkLibrary.environmentGroup()
), mà cuối cùng sẽ tạo ra một mục tiêu cùng tên. Điều này 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
.
Quy trình kiểm tra điều kiện ràng buộc diễn ra 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 kéo #10945.
Chế độ hiển thị
Nếu làm việc trên một cơ sở mã lớn với nhiều nhà phát triển (như tại Google), bạn nên chú ý để ngăn những người khác tuỳ ý phụ thuộc vào mã của bạn. Nếu không, theo luật của Hyrum, mọi người sẽ dựa vào những hành vi mà bạn cho là chi tiết triển khai.
Bazel hỗ trợ việc này 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 hơi đặc biệt vì mặc dù chứa danh sách nhãn, nhưng các nhãn này có thể mã hoá một mẫu trên tên gói thay vì con trỏ đến bất kỳ mục tiêu cụ thể nào. (Đúng 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ị. Giá trị này có thể là một hằng số (hoàn toàn công khai hoặc hoàn toàn riêng tư) hoặc một danh sách nhãn. - Nhãn có thể 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 của nó. - 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 tệp từ các phần phụ thuộc, thêm các tệp của riêng mục tiêu đó và gói tập hợp tổng hợp vào một trình cung cấp thông tin bắc cầu để các mục tiêu được định cấu hình phụ thuộc vào mục tiêu đó cũng có thể làm như vậy. 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 chúng ta làm việc này theo cách đơn giản, chẳng hạn như dùng List
hoặc Set
, thì chúng ta sẽ sử dụng bộ nhớ bậc bốn: nếu có một chuỗi N quy tắc và mỗi quy tắc thêm một tệp, thì chúng ta sẽ có 1+2+...+N thành viên bộ sưu tập.
Để giải quyết vấn đề này, chúng tôi đã đưa ra khái niệm về NestedSet
. Đâ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 này có tên 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 chạy để tạo ra kết quả mà người dùng muốn. Các lệnh được biểu thị dưới dạng thực thể của lớp Action
và các tệp được biểu thị dưới dạng thực thể của lớp Artifact
. Chúng được sắp xếp trong một đồ thị hai phần, có hướng, không chu trình được gọi là "biểu đồ hành động".
Cấu phần phần mềm có 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). Các cấu phần phần mềm phát 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 tệp này được kiểm tra xem có mới nhất hay không 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, đây có thể là các đường liên kết tượng trưng bị treo. 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 để đảm bảo mới nhất bằng cách kiểm tra tập hợp các tệp trong đó và nội dung của các tệp đó. 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 việc tạo lại. Mã này được dùng riêng cho thông tin về dấu bản dựng: chúng ta không muốn tạo lại chỉ vì thời gian hiện tại đã thay đổi.
Không có lý do cơ bản nào khiến cấu phần phần mềm nguồn không thể là cấu phần phần mềm cây hoặc cấu phần phần mềm liên kết tượng trưng chưa được phân giải, chỉ là chúng tôi chưa triển khai (mặc dù chúng tôi nên – tham chiếu đến thư mục nguồn trong tệp BUILD
là một trong số ít các vấn đề không chính xác thường xuyên xảy ra với Bazel; chúng tôi đã triển khai loại công việc được kích hoạt bằng thuộc tính BAZEL_TRACK_SOURCE_DIRECTORIES=1
JVM)
Một loại Artifact
đáng chú ý là bên trung gian. Các giá trị này được biểu thị bằng các thực thể Artifact
là kết quả của MiddlemanAction
. Chúng được dùng cho một số trường hợp đặc biệt:
- 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 tệp chạy được dùng để đảm bảo sự hiện diện của cây tệp 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 mà cây tệp chạy tham chiếu.
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 vài trường hợp đặc biệt khác, chẳng hạn như viết một tệp có nội dung mà Bazel biết. Đây là lớp con của AbstractAction
. Hầu hết các thao tác đều là SpawnAction
hoặc StarlarkAction
(tương tự, chúng không được là các lớp riêng biệt), mặc dù Java và C++ có các loại thao tác riêng (JavaCompileAction
, 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 khái niệm, việc thực thi một hành động được biểu thị dưới dạng lệnh gọi của 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 sai, nhưng việc loại bỏ tính năng này thực sự rất khó vì sẽ tiết kiệm đáng kể thời gian thực thi, chẳng hạn như khi một tệp nguồn cần được xử lý theo cách nào đó và tệp đó được tham chiếu theo nhiều quy tắc (sóng tay). Đ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.
Việc đầu tiên mà Bazel thực hiện sau giai đoạn phân tích là xác định những Cấu phần phần mềm cần được 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 thư mục gốc thực thi. Vì Bazel có thể đọc các gói nguồn từ nhiều vị trí trong hệ thống tệp (--package_path
), nên 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 chạy cục bộ
- 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 (xem xét sự khác biệt giữa các đường dẫn lớp Java và đường dẫn bao gồm C++)
- 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 hành động 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 để tìm các lượt truy cập bằng phương thức
ActionCacheChecker.getTokenIfNeedToExecute()
.
Trái với tên gọi, đây là một bản đồ từ đường dẫn của một cấu phần phần mềm phái sinh đến hành động đã phát 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 nhóm thông tin đầu vào. Các thay đổi đối với tập hợp dữ liệu đầu vào của một hành động có 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 này là C++, trong đó tốt hơn nên dự đoán có cơ sở về tệp tiêu đề mà tệp C++ sử dụng từ đóng chuyển tiếp để chúng tôi không chú ý đến việc gửi mọi tệp đến các trình thực thi từ xa; do đó, chúng tôi có lựa chọn không đăng ký mọi tệp tiêu đề dưới dạng "đầu vào", mà chỉ quét tệp nguồn cho các câu lệnh chuyển tiếp được đưa vào và chỉ đánh dấu các tệp tiêu đề đó là đầu vào được đề cập quá mức trong
#include
. - 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++, tệp này được gọi là "tệp .d": trình biên dịch cho biết tệp tiêu đề nào được dùng sau khi thực tế, và để tránh bối rối khi có mức độ gia tăng kém hơn so với Make, Bazel sử dụng thực tế này. 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.
Những thao tác này được triển khai bằng phương thức trên Action:
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ấu phần phần mềm nguồn để không có cạnh phần phụ thuộc nào trong biểu đồ hành động không có cạnh tương đương trong biểu đồ mục tiêu được định cấu hình.- 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 cần dữ liệu đầu vào nào. Điều này có thể dẫn đến các bản dựng 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 hành động trả về một lượt truy cập trên một thực thể Hành động mới (chẳng hạn như được tạo sau khi khởi động lại máy chủ), Bazel sẽ tự gọi updateInputs()
để tập hợp đầu vào phản ánh kết quả của việc khám phá và cắt bớt đầu vào đã thực hiện trước đó.
Các thao tác Starlark có thể sử dụng cơ sở này để khai báo một số dữ liệu đầu vào là không sử dụng bằng cách sử dụng đối số unused_inputs_list=
của ctx.actions.run()
.
Nhiều cách để chạy hành động: Chiến lược/ActionContexts
Bạn có thể chạy một số thao tác theo nhiều cách. Ví dụ: bạn có thể thực thi dòng lệnh tại cục bộ, tại 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 có thể sử dụng lại một worker hay không được xác định bằng cách tính toán tổng kiểm tra cho công cụ được dùng bằng WorkerFilesHash
. Phương 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.
- Bạn có thể xem thông tin về các vấn đề phức tạp khi thực thi hành động trên thiết bị tại đây.
Trình quản lý tài nguyên cục bộ
Bazel có thể chạy song song nhiều thao tác. Số lượng hành động cục bộ nên chạy song song khác nhau tuỳ theo hành động: hành động càng cần nhiều tài nguyên thì càng ít thực thể chạy cùng lúc để tránh quá tải 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ể xảy ra trong cùng một bản dựng, thì chúng phải có các thư mục khác nhau để cả hai có thể có phiên bản riêng của cùng một hành động; nếu không, nếu hai cấu hình không thống nhất về nhau, chẳng hạn như dòng lệnh của một hành động tạo ra cùng một tệp đầu ra, thì Bazel sẽ không biết nên chọn hành động nào ("xung đột hành động")
- 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 nội dung thảo luận dài hơn về các lựa 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ó kết quả bản dựng trong quá trình chạy kiểm thử. Đâ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 mà kiểm thử sẽ đượ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. Tiện ích 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
để kiểm thử 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. Tính năng này được triển khai trong TestFilter
và được gọi từ TargetPatternPhaseFunction.determineTests()
trong quá trình phân tích cú pháp mục tiêu và kết quả được đưa vào TargetPatternPhaseValue.getTestsToRunLabels()
. Lý do bạn không thể định cấu hình các thuộc tính quy tắc có thể được lọc là vì việc này xảy ra trước giai đoạn phân tích, do đó, cấu hình không có sẵn.
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.
Để tăng cường tính minh bạch cho quy trình phức tạp này, toán tử truy vấn tests()
(được triển khai trong TestsFunction
) có sẵn để cho biết các hoạt động 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 đó, thao tác này sẽ 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, tuỳ chọn này sẽ chạy kiểm thử theo cách được yêu cầu.
Các bài kiểm thử được chạy theo một giao thức chi tiết sử dụng các biến môi trường để cho quy trình kiểm thử biết những gì được mong đợi từ các biến đó. Bạn có thể xem nội dung mô tả chi tiết về những 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ử", đây là thư mục con có tên là testlogs
của thư mục đầu ra của cấu hình mục tiêu:
test.xml
, một tệp XML kiểu JUnit nêu chi tiết các trường hợp kiểm thử riêng lẻ trong phân đoạn kiểm thửtest.log
, đầu ra 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, trong đó đầu ra của thiết bị đầu cuối được kết xuất khi thao tác hoàn tất, người dùng có thể yêu cầu truyền phát đầu ra của các chương trình kiểm thử để họ nhận đượ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ằng tuỳ chọn dòng lệnh --test_output=streamed
và ngụ ý việc thực thi kiểm thử độc quyền để kết quả của các kiểm thử khác nhau không bị xen kẽ.
Việc này được triển khai trong lớp StreamedTestOutput
có tên phù hợp và hoạt động bằng cách thăm dò các thay đổi đối với tệp test.log
của kiểm thử được đề cập, đồng thời kết xuất các byte mới vào thiết bị đầu cuối nơi quy tắc Bazel.
Kết quả của các bài kiểm thử đã thực thi sẽ có trên bus sự kiện bằng cách quan sát các sự kiện khác nhau (chẳng hạn như TestAttempt
, TestResult
hoặc TestingCompleteEvent
). Các sự kiện này được kết xuất vào Giao thức sự kiện bản dựng và được AggregatingTestListener
phát tới bảng điều khiển.
Thu thập 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) thời gian chạy mức độ sử dụng ghi tệp mức độ sử dụng. Sau đó, công cụ này sẽ chạy kiểm thử. Bản thân một chương trình kiểm thử có thể chạy nhiều quy trình con và bao gồm các phần được viết bằng nhiều ngôn ngữ lập trình khác nhau (với thời gian chạy thu thập phạm vi 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ử. Điều này có thêm nhờ thuộc tính ngầm ẩn :coverage_support
. Thuộc tính này được phân giải thành giá trị của cờ cấu hình --coverage_support
(xem TestConfiguration.TestOptions.coverageSupport
)
Một số ngôn ngữ thực hiện đo lường ngoại tuyến, nghĩa là đo lường phạm vi sử dụng được thêm vào thời gian biên dịch (chẳng hạn như C++) và một số ngôn ngữ khác thực hiện đo lường trực tuyến, nghĩa là đo lường phạm vi sử dụng được thêm vào thời gian thực thi.
Một khái niệm cốt lõi khác là mức độ sử dụng cơ sở. Đây là 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 phạm vi kiểm thử cho một tệp nhị phân thì việc hợp nhất mức độ bao phủ của tất cả các kiểm thử là chưa đủ vì có thể có mã trong tệp nhị phân không được liên kết với bất kỳ kiểm thử nào. Do đó, 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.
Phạm vi 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 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. Trên thực tế, quá trình này bao gồm các tệp dành riêng cho thời gian chạy; ví dụ: gcc phát ra các tệp .gcno trong quá trình biên dịch. Các 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.
Việc liệu phạm vi có được thu thập hay không có được lưu trữ trong BuildConfiguration
hay không. Cách này rất tiện lợi vì đây là cách dễ dàng để thay đổi hành động kiểm thử và biểu đồ hành động phụ thuộc vào bit này, nhưng cũng có nghĩa là nếu bit này được lật, tất cả các mục tiêu cần được phân tích lại (một số ngôn ngữ, chẳng hạn như C++ yêu cầu các tuỳ chọn trình biên dịch khác nhau để phát hành mã có thể thu thập mức độ sử dụng, giúp giảm thiểu phần nào vấn đề này, vì dù sao cũng cần phân tích lại).
Các tệp hỗ trợ mức độ sử dụng phụ thuộc vào 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. Tốt nhất là nên loại bỏ những khác biệt này và chúng tôi đã chuẩn hoá một trong các điểm khác biệt đó.
Chúng 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 ngôn ngữ nhỏ dùng để hỏi về nhiều thứ liên quan đến các biểu đồ. Các loại truy vấn sau đượ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 kết quả truy vấn, thay vì thu thập kết quả truy vấn vào một số cấu trúc dữ liệu, query2.engine.Callback
sẽ được truyền đến QueryFunction
. Phương thức này sẽ gọi kết quả truy vấn mà nó 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 tế của một số định dạng đầu ra truy vấn (chắc chắn là proto) là Bazel cần phát _tất cả _thông tin mà quá trình tải gói cung cấp để người dùng có thể so sánh đầu ra và xác định xem một mục tiêu cụ thể có thay đổi hay không. Do đó, các giá trị thuộc tính cần phải có thể chuyển đổi tuần tự. Đó là lý do tại sao chỉ có một số ít loại thuộc tính 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 phân lớp con BlazeModule
(tên này là một bản sao của lịch sử Bazel khi trước đây được gọi là Blaze) và nhận thông tin về các sự kiện khác nhau trong quá trình thực thi lệnh.
Các lớp này chủ yếu được dùng để triển khai nhiều phần chức năng "không phải cốt lõi" mà chỉ một số phiên bản Bazel (chẳng hạn như phiên bản chúng tôi sử dụng tại Google) cần đế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 sử dụng ví dụ này làm ví dụ về các nguyên tắc thiết kế tốt.
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 nội dung 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 thử nghiệm (
TestAttempt
,TestSummary
)
Một số sự kiện trong số này được biểu thị bên ngoài Bazel trong 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. Các API này có thể truy cập được dưới dạng tệp chứa thông báo giao thức hoặc Bazel có thể kết nối với một máy chủ (gọi là Build Event Service) để truyền trực tuyến các sự kiện.
Việc này được triển khai trong các gói Java build.lib.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ụ: 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 phân đoạn được biểu thị bằng WorkspaceFileKey.getIndex()
và tính toán WorkspaceFileFunction
cho đến khi chỉ mục X có nghĩa là đánh giá chỉ mục đó cho đến câu lệnh load()
thứ X.
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:
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. - Một "tệp điểm đánh dấu" được ghi cho mỗi kho lưu trữ trong
$OUTPUT_BASE/external
chứa tổng kiểm của quy tắc được dùng để tìm nạp tệp đó. Nếu máy chủ Bazel khởi động lại nhưng tổng kiểm không thay đổi, thì 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 để truy vấn 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 một vấn đề vì Bazel thường kiểm tra tính cập nhật của các cấu phần phần mềm nguồn bằng cách gọi stat() trên các cấu phần phần mềm đó. Các cấu phần phần mềm này cũng không hợp lệ khi định nghĩa về kho lưu trữ mà chúng đang thay đổi. Do đó, FileStateValue
của cấu phần phần mềm trong kho lưu trữ bên ngoài cần phải phụ thuộc vào kho lưu trữ bên ngoài. Việc này do ExternalFilesHelper
xử lý.
Thư mục được quản lý
Đôi khi, các kho lưu trữ bên ngoài cần sửa đổi các tệp trong thư mục gốc của không gian làm việc (chẳng hạn như trình quản lý gói 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 của Bazel 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 Bazel sửa đổi và cho phép các gói tham chiếu đến mọi thư mục trong thư mục 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 việc nhiều kho lưu trữ muốn phụ thuộc vào cùng một kho lưu trữ, nhưng trong các phiên bản khác nhau (đây là một ví dụ của "vấn đề 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ả hai tệp nhị phân đó đều sẽ tham chiếu đến Guava bằng các nhãn bắt đầu bằng @guava//
và dự kiến đó sẽ 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ể dùng thẻ này để tham gia 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 dưới dạng thuộc tính repo_mapping
của các định nghĩa kho lưu trữ riêng lẻ. 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 các 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 tôi triển khai. Điều này chủ yếu giới hạn ở việc tương tác với hệ thống tệp, kiểm soát quy trình và nhiều thứ 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, hai thực thể RpcOutputStream
sẽ được tạo (dành cho stdout và stderr) để chuyển tiếp dữ liệu được in vào các thực thể đó đến ứng dụng. Sau đó, các dòng này được gói trong OutErr
(một cặp (stdout, stderr)). Mọi nội dung cần được in trên bảng điều khiển đều trải qua các luồng này. Sau đó, 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 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.v.) được biểu thị 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 đầu ra chủ yếu được phát thông qua UiEventHandler
, chịu trách nhiệm về tất cả định dạng đầu ra ưa thích và báo cáo tiến trình mà Bazel thực hiện. Hàm này có 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 ngăn xếp tác vụ là: một ngăn xếp tác vụ có thể bắt đầu tác vụ và kết thúc tác vụ, đồng thời các tác vụ này phải được lồng ghép gọn gàng vào nhau. Mỗi luồng Java sẽ có một ngăn xếp tác vụ riêng. VIỆC CẦN LÀM: Cách hoạt động của tính năng này với các thao tác và kiểu truyền tiếp tục như thế nào?
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.
Thử nghiệm 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 ta gọi loại kiểm thử trước là "kiểm thử tích hợp" và loại kiểm thử sau là "kiểm thử đơn vị", mặc dù chúng giống như kiểm thử tích hợp nhưng ít tích hợp hơn. Chúng ta cũng có một số kiểm thử đơn vị thực tế, khi cần thiết.
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 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.