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