Trang này trình bày về hệ thống xây dựng dựa trên cấu phần phần mềm và triết lý đằng sau việc tạo hệ thống này. Bazel là một hệ thống xây dựng dựa trên cấu phần phần mềm. Mặc dù hệ thống xây dựng dựa trên tác vụ là một bước tiến vượt trội so với tập lệnh xây dựng, nhưng hệ thống này lại trao quá nhiều quyền cho từng kỹ sư bằng cách cho phép họ xác định các tác vụ của riêng mình.
Hệ thống xây dựng dựa trên cấu phần phần mềm có một số ít tác vụ do hệ thống xác định
mà kỹ sư có thể định cấu hình theo cách hạn chế. Kỹ sư vẫn cho hệ thống biết
cần xây dựng gì, nhưng hệ thống xây dựng sẽ xác định cách xây dựng. Giống như
hệ thống xây dựng dựa trên tác vụ, hệ thống xây dựng dựa trên cấu phần phần mềm (chẳng hạn như Bazel) vẫn
có tệp bản dựng, nhưng nội dung của các tệp bản dựng đó rất khác nhau. Thay
vì là một tập hợp các lệnh bắt buộc trong ngôn ngữ tập lệnh hoàn chỉnh Turing
mô tả cách tạo ra kết quả đầu ra, các tệp bản dựng trong Bazel là một tệp kê khai khai báo
mô tả một tập hợp các cấu phần phần mềm cần xây dựng, các phần phụ thuộc của chúng và một
tập hợp các lựa chọn hạn chế ảnh hưởng đến cách xây dựng các cấu phần phần mềm đó. Khi kỹ sư chạy bazel
trên dòng lệnh, họ sẽ chỉ định một tập hợp các mục tiêu cần xây dựng (cần xây dựng gì) và
Bazel chịu trách nhiệm định cấu hình, chạy và lên lịch các bước biên dịch (cách xây dựng). Vì hệ thống xây dựng hiện có toàn quyền kiểm soát những công cụ
cần chạy khi nào, nên hệ thống này có thể đưa ra những đảm bảo mạnh mẽ hơn nhiều, giúp hệ thống hiệu quả hơn nhiều
trong khi vẫn đảm bảo tính chính xác.
Góc nhìn chức năng
Bạn có thể dễ dàng so sánh giữa hệ thống xây dựng dựa trên cấu phần phần mềm và lập trình hàm. Các ngôn ngữ lập trình bắt buộc truyền thống (chẳng hạn như Java, C và Python) chỉ định danh sách các câu lệnh cần thực thi lần lượt, giống như cách hệ thống xây dựng dựa trên tác vụ cho phép lập trình viên xác định một loạt các bước để thực thi. Ngược lại, các ngôn ngữ lập trình hàm (chẳng hạn như Haskell và ML) được cấu trúc giống như một loạt các phương trình toán học. Trong ngôn ngữ hàm, lập trình viên mô tả một phép tính cần thực hiện, nhưng để trình biên dịch xác định chi tiết về thời điểm và cách thực thi phép tính đó.
Điều này tương ứng với ý tưởng khai báo tệp kê khai trong hệ thống xây dựng dựa trên cấu phần phần mềm và cho phép hệ thống tìm ra cách thực thi bản dựng. Nhiều vấn đề không thể dễ dàng biểu thị bằng cách sử dụng lập trình hàm, nhưng những vấn đề có thể biểu thị sẽ được hưởng lợi rất nhiều từ việc này: ngôn ngữ thường có thể dễ dàng song song hoá các chương trình như vậy và đưa ra những đảm bảo mạnh mẽ về tính chính xác của chúng mà không thể thực hiện được trong ngôn ngữ bắt buộc. Các vấn đề dễ biểu thị nhất bằng cách sử dụng lập trình hàm là những vấn đề chỉ liên quan đến việc chuyển đổi một phần dữ liệu thành một phần dữ liệu khác bằng cách sử dụng một loạt các quy tắc hoặc hàm. Và đó chính xác là hệ thống xây dựng: toàn bộ hệ thống là một hàm toán học hiệu quả nhận các tệp nguồn (và các công cụ như trình biên dịch) làm dữ liệu đầu vào và tạo ra các tệp nhị phân làm dữ liệu đầu ra. Vì vậy, không có gì ngạc nhiên khi hệ thống này hoạt động hiệu quả khi dựa trên các nguyên tắc của lập trình hàm.
Tìm hiểu về hệ thống xây dựng dựa trên cấu phần phần mềm
Hệ thống xây dựng của Google, Blaze, là hệ thống xây dựng dựa trên cấu phần phần mềm đầu tiên. Bazel là phiên bản mã nguồn mở của Blaze.
Sau đây là giao diện của tệp bản dựng (thường có tên là BUILD) trong Bazel:
java_binary(
name = "MyBinary",
srcs = ["MyBinary.java"],
deps = [
":mylib",
],
)
java_library(
name = "mylib",
srcs = ["MyLibrary.java", "MyHelper.java"],
visibility = ["//java/com/example/myproduct:__subpackages__"],
deps = [
"//java/com/example/common",
"//java/com/example/myproduct/otherlib",
],
)
Trong Bazel, các tệp BUILD xác định mục tiêu – hai loại mục tiêu ở đây là
java_binary và java_library. Mỗi mục tiêu tương ứng với một cấu phần phần mềm mà
hệ thống có thể tạo: mục tiêu nhị phân tạo ra các tệp nhị phân có thể
thực thi trực tiếp và mục tiêu thư viện tạo ra các thư viện mà tệp nhị phân hoặc các thư viện khác có thể sử dụng. Mỗi mục tiêu có:
name: cách mục tiêu được tham chiếu trên dòng lệnh và bởi các mục tiêu khácsrcs: các tệp nguồn được biên dịch để tạo cấu phần phần mềm cho mục tiêudeps: các mục tiêu khác phải được xây dựng trước mục tiêu này và được liên kết vào mục tiêu này
Phần phụ thuộc có thể nằm trong cùng một gói (chẳng hạn như phần phụ thuộc của MyBinary vào :mylib) hoặc trên một gói khác trong cùng một hệ thống phân cấp nguồn (chẳng hạn như phần phụ thuộc của mylib vào //java/com/example/common).
Giống như hệ thống xây dựng dựa trên tác vụ, bạn thực hiện bản dựng bằng công cụ dòng lệnh của Bazel. Để xây dựng mục tiêu MyBinary, hãy chạy bazel build :MyBinary. Sau khi
nhập lệnh đó lần đầu tiên trong một kho lưu trữ sạch, Bazel:
- Phân tích cú pháp mọi tệp
BUILDtrong không gian làm việc để tạo biểu đồ phần phụ thuộc giữa các cấu phần phần mềm. - Sử dụng biểu đồ để xác định các phần phụ thuộc bắc cầu của
MyBinary; tức là mọi mục tiêu màMyBinaryphụ thuộc vào và mọi mục tiêu mà các mục tiêu đó phụ thuộc vào, một cách đệ quy. - Xây dựng từng phần phụ thuộc đó theo thứ tự. Bazel bắt đầu bằng cách xây dựng từng
mục tiêu không có phần phụ thuộc nào khác và theo dõi những phần phụ thuộc
vẫn cần được xây dựng cho từng mục tiêu. Ngay sau khi xây dựng xong tất cả các phần phụ thuộc của một mục tiêu, Bazel sẽ bắt đầu xây dựng mục tiêu đó. Quá trình này
tiếp tục cho đến khi mọi phần phụ thuộc bắc cầu của
MyBinaryđược xây dựng. - Xây dựng
MyBinaryđể tạo ra một tệp nhị phân thực thi cuối cùng liên kết tất cả các phần phụ thuộc được xây dựng ở bước 3.
Về cơ bản, có vẻ như những gì đang xảy ra ở đây không khác nhiều so với những gì đã xảy ra khi sử dụng hệ thống xây dựng dựa trên tác vụ. Thật vậy, kết quả cuối cùng là cùng một tệp nhị phân và quá trình tạo ra tệp nhị phân đó bao gồm việc phân tích một loạt các bước để tìm các phần phụ thuộc giữa chúng, sau đó chạy các bước đó theo thứ tự. Tuy nhiên, có những điểm khác biệt quan trọng. Điểm khác biệt đầu tiên xuất hiện ở bước 3: vì Bazel biết rằng mỗi mục tiêu chỉ tạo ra một thư viện Java, nên Bazel biết rằng tất cả những gì cần làm là chạy trình biên dịch Java thay vì một tập lệnh do người dùng tuỳ ý xác định, vì vậy, Bazel biết rằng việc chạy các bước này song song là an toàn. Điều này có thể giúp cải thiện hiệu suất theo cấp số nhân so với việc xây dựng các mục tiêu từng mục tiêu một trên máy nhiều lõi và chỉ có thể thực hiện được vì phương pháp dựa trên cấu phần phần mềm để hệ thống xây dựng chịu trách nhiệm về chiến lược thực thi của riêng mình để có thể đưa ra những đảm bảo mạnh mẽ hơn về tính song song.
Tuy nhiên, lợi ích không chỉ dừng lại ở tính song song. Điều tiếp theo mà phương pháp này
mang lại cho chúng ta sẽ trở nên rõ ràng khi nhà phát triển nhập bazel
build :MyBinary lần thứ hai mà không thực hiện bất kỳ thay đổi nào: Bazel thoát trong vòng chưa đầy
một giây với thông báo cho biết mục tiêu đã được cập nhật. Điều này có thể thực hiện được là do mô hình lập trình hàm mà chúng ta đã nói đến trước đó – Bazel biết rằng mỗi mục tiêu chỉ là kết quả của việc chạy trình biên dịch Java và Bazel biết rằng kết quả đầu ra từ trình biên dịch Java chỉ phụ thuộc vào dữ liệu đầu vào của nó, vì vậy, miễn là dữ liệu đầu vào không thay đổi, thì kết quả đầu ra có thể được sử dụng lại.
Và phân tích này hoạt động ở mọi cấp độ; nếu MyBinary.java thay đổi, Bazel biết
phải xây dựng lại MyBinary nhưng sử dụng lại mylib. Nếu một tệp nguồn cho
//java/com/example/common thay đổi, Bazel biết phải xây dựng lại thư viện đó,
mylib, và MyBinary, nhưng sử dụng lại //java/com/example/myproduct/otherlib.
Vì Bazel biết về các thuộc tính của các công cụ mà nó chạy ở mọi bước,
nên Bazel chỉ có thể xây dựng lại tập hợp cấu phần phần mềm tối thiểu mỗi lần trong khi
đảm bảo rằng nó sẽ không tạo ra các bản dựng lỗi thời.
Việc định hình lại quy trình xây dựng theo cấu phần phần mềm thay vì tác vụ là một điều tinh tế nhưng mạnh mẽ. Bằng cách giảm tính linh hoạt được hiển thị cho lập trình viên, hệ thống xây dựng có thể biết thêm về những gì đang được thực hiện ở mỗi bước của bản dựng. Hệ thống này có thể sử dụng kiến thức này để giúp bản dựng hiệu quả hơn nhiều bằng cách song song hoá các quy trình xây dựng và sử dụng lại kết quả đầu ra của chúng. Nhưng đây thực sự chỉ là bước đầu tiên và các khối xây dựng của tính song song và khả năng sử dụng lại này tạo thành cơ sở cho một hệ thống xây dựng phân tán và có khả năng mở rộng cao.
Các thủ thuật Bazel hữu ích khác
Hệ thống xây dựng dựa trên cấu phần phần mềm về cơ bản giải quyết các vấn đề về tính song song và khả năng sử dụng lại vốn có trong hệ thống xây dựng dựa trên tác vụ. Tuy nhiên, vẫn còn một số vấn đề đã xuất hiện trước đó mà chúng ta chưa giải quyết. Bazel có những cách thông minh để giải quyết từng vấn đề này và chúng ta nên thảo luận về chúng trước khi tiếp tục.
Công cụ dưới dạng phần phụ thuộc
Một vấn đề mà chúng ta gặp phải trước đó là các bản dựng phụ thuộc vào các công cụ được cài đặt trên máy của chúng ta và việc tái tạo các bản dựng trên các hệ thống có thể gặp khó khăn do các phiên bản hoặc vị trí công cụ khác nhau. Vấn đề trở nên khó khăn hơn khi dự án của bạn sử dụng các ngôn ngữ yêu cầu các công cụ khác nhau dựa trên nền tảng mà chúng đang được xây dựng hoặc biên dịch (chẳng hạn như Windows so với Linux), và mỗi nền tảng đó yêu cầu một tập hợp công cụ hơi khác nhau để thực hiện cùng một công việc.
Bazel giải quyết phần đầu tiên của vấn đề này bằng cách coi các công cụ là phần phụ thuộc của
từng mục tiêu. Mọi java_library trong không gian làm việc đều ngầm phụ thuộc vào trình biên dịch Java, mặc định là một trình biên dịch nổi tiếng. Bất cứ khi nào Bazel xây dựng a
java_library, Bazel sẽ kiểm tra để đảm bảo rằng trình biên dịch được chỉ định có sẵn
ở một vị trí đã biết. Giống như bất kỳ phần phụ thuộc nào khác, nếu trình biên dịch Java
thay đổi, thì mọi cấu phần phần mềm phụ thuộc vào trình biên dịch đó sẽ được xây dựng lại.
Bazel giải quyết phần thứ hai của vấn đề, tính độc lập của nền tảng, bằng cách thiết lập cấu hình bản dựng. Thay vì các mục tiêu phụ thuộc trực tiếp vào các công cụ của chúng, chúng phụ thuộc vào các loại cấu hình:
- Cấu hình máy chủ: xây dựng các công cụ chạy trong quá trình xây dựng
- Cấu hình mục tiêu: xây dựng tệp nhị phân mà bạn yêu cầu cuối cùng
Mở rộng hệ thống xây dựng
Bazel đi kèm với các mục tiêu cho một số ngôn ngữ lập trình phổ biến nhưng kỹ sư sẽ luôn muốn làm nhiều hơn – một phần lợi ích của hệ thống dựa trên tác vụ là tính linh hoạt trong việc hỗ trợ mọi loại quy trình xây dựng và sẽ tốt hơn nếu không từ bỏ điều đó trong hệ thống xây dựng dựa trên cấu phần phần mềm. May mắn thay, Bazel cho phép mở rộng các loại mục tiêu được hỗ trợ bằng cách thêm các quy tắc tuỳ chỉnh.
Để xác định một quy tắc trong Bazel, tác giả quy tắc sẽ khai báo dữ liệu đầu vào mà quy tắc
yêu cầu (dưới dạng các thuộc tính được truyền trong tệp BUILD) và tập hợp cố định
các kết quả đầu ra mà quy tắc tạo ra. Tác giả cũng xác định các hành động sẽ
được tạo bởi quy tắc đó. Mỗi hành động khai báo dữ liệu đầu vào và đầu ra,
chạy một tệp thực thi cụ thể hoặc ghi một chuỗi cụ thể vào một tệp và có thể được
kết nối với các hành động khác thông qua dữ liệu đầu vào và đầu ra của nó. Điều này có nghĩa là các hành động
là đơn vị có khả năng kết hợp ở cấp thấp nhất trong hệ thống xây dựng – một hành động có thể thực hiện
bất cứ điều gì mà nó muốn miễn là chỉ sử dụng dữ liệu đầu vào và đầu ra đã khai báo và
Bazel sẽ lo việc lên lịch các hành động và lưu kết quả vào bộ nhớ đệm khi thích hợp.
Hệ thống này không phải là hoàn hảo vì không có cách nào để ngăn nhà phát triển hành động làm điều gì đó như đưa quy trình không xác định vào hành động của họ. Nhưng điều này không xảy ra thường xuyên trong thực tế và việc đẩy các khả năng lạm dụng xuống cấp độ hành động sẽ làm giảm đáng kể cơ hội xảy ra lỗi. Các quy tắc hỗ trợ nhiều ngôn ngữ và công cụ phổ biến có sẵn rộng rãi trên mạng và hầu hết các dự án sẽ không bao giờ cần xác định các quy tắc của riêng mình. Ngay cả đối với những dự án cần xác định, định nghĩa quy tắc chỉ cần được xác định ở một vị trí trung tâm trong kho lưu trữ, nghĩa là hầu hết các kỹ sư sẽ có thể sử dụng các quy tắc đó mà không bao giờ phải lo lắng về việc triển khai.
Cách ly môi trường
Có vẻ như các hành động có thể gặp phải các vấn đề tương tự như các tác vụ trong các hệ thống khác – liệu có vẫn có thể viết các hành động vừa ghi vào cùng một tệp vừa xung đột với nhau không? Thực ra, Bazel khiến những xung đột này không thể xảy ra bằng cách sử dụng hộp cát. Trên các hệ thống được hỗ trợ, mọi hành động đều được cách ly khỏi mọi hành động khác thông qua hộp cát hệ thống tệp. Về cơ bản, mỗi hành động chỉ có thể xem một chế độ xem hạn chế của hệ thống tệp bao gồm dữ liệu đầu vào mà nó đã khai báo và mọi kết quả đầu ra mà nó đã tạo. Điều này được thực thi bởi các hệ thống như LXC trên Linux, cùng một công nghệ đằng sau Docker. Điều này có nghĩa là các hành động không thể xung đột với nhau vì chúng không thể đọc bất kỳ tệp nào mà chúng không khai báo và mọi tệp mà chúng ghi nhưng không khai báo sẽ bị loại bỏ khi hành động kết thúc. Bazel cũng sử dụng hộp cát để hạn chế các hành động giao tiếp qua mạng.
Tạo phần phụ thuộc bên ngoài xác định
Vẫn còn một vấn đề: hệ thống xây dựng thường cần tải xuống
các phần phụ thuộc (cho dù là công cụ hay thư viện) từ các nguồn bên ngoài thay vì
xây dựng trực tiếp. Bạn có thể thấy điều này trong ví dụ thông qua phần phụ thuộc
@com_google_common_guava_guava//jar, phần phụ thuộc này tải tệp JAR xuống
từ Maven.
Việc phụ thuộc vào các tệp bên ngoài không gian làm việc hiện tại là rủi ro. Các tệp đó có thể thay đổi bất cứ lúc nào, có khả năng yêu cầu hệ thống xây dựng liên tục kiểm tra xem chúng có mới hay không. Nếu một tệp từ xa thay đổi mà không có thay đổi tương ứng trong mã nguồn không gian làm việc, thì điều này cũng có thể dẫn đến các bản dựng không thể tái tạo – một bản dựng có thể hoạt động vào một ngày và không hoạt động vào ngày hôm sau mà không có lý do rõ ràng do thay đổi phần phụ thuộc không được chú ý. Cuối cùng, phần phụ thuộc bên ngoài có thể gây ra rủi ro bảo mật rất lớn khi thuộc sở hữu của bên thứ ba: nếu kẻ tấn công có thể xâm nhập vào máy chủ của bên thứ ba đó, chúng có thể thay thế tệp phần phụ thuộc bằng một tệp do chúng thiết kế, có khả năng cho phép chúng kiểm soát hoàn toàn môi trường xây dựng và kết quả đầu ra của môi trường đó.
Vấn đề cơ bản là chúng ta muốn hệ thống xây dựng nhận biết được các tệp này mà không cần kiểm tra chúng vào hệ thống kiểm soát nguồn. Việc cập nhật phần phụ thuộc phải là một lựa chọn có ý thức, nhưng lựa chọn đó phải được thực hiện một lần ở một vị trí trung tâm thay vì do từng kỹ sư quản lý hoặc tự động do hệ thống quản lý. Điều này là do ngay cả với mô hình "Live at Head", chúng ta vẫn muốn các bản dựng có tính xác định, điều này ngụ ý rằng nếu bạn kiểm tra một cam kết từ tuần trước, bạn sẽ thấy các phần phụ thuộc của mình như chúng đã từng chứ không phải như hiện tại.
Bazel và một số hệ thống xây dựng khác giải quyết vấn đề này bằng cách yêu cầu một tệp kê khai trên toàn không gian làm việc liệt kê hàm băm mật mã cho mọi phần phụ thuộc bên ngoài trong không gian làm việc. Hàm băm là một cách ngắn gọn để biểu thị duy nhất tệp mà không cần kiểm tra toàn bộ tệp vào hệ thống kiểm soát nguồn. Bất cứ khi nào một phần phụ thuộc bên ngoài mới được tham chiếu từ một không gian làm việc, hàm băm của phần phụ thuộc đó sẽ được thêm vào tệp kê khai, theo cách thủ công hoặc tự động. Khi Bazel chạy bản dựng, Bazel sẽ kiểm tra hàm băm thực tế của phần phụ thuộc được lưu vào bộ nhớ đệm so với hàm băm dự kiến được xác định trong tệp kê khai và chỉ tải lại tệp xuống nếu hàm băm khác nhau.
Nếu cấu phần phần mềm mà chúng ta tải xuống có hàm băm khác với hàm băm được khai báo trong tệp kê khai, thì bản dựng sẽ không thành công trừ phi hàm băm trong tệp kê khai được cập nhật. Bạn có thể thực hiện việc này tự động, nhưng thay đổi đó phải được phê duyệt và kiểm tra vào hệ thống kiểm soát nguồn trước khi bản dựng chấp nhận phần phụ thuộc mới. Điều này có nghĩa là luôn có bản ghi về thời điểm cập nhật phần phụ thuộc và phần phụ thuộc bên ngoài không thể thay đổi mà không có thay đổi tương ứng trong nguồn không gian làm việc. Điều này cũng có nghĩa là khi kiểm tra phiên bản cũ hơn của mã nguồn, bản dựng được đảm bảo sử dụng cùng các phần phụ thuộc mà bản dựng đã sử dụng tại thời điểm phiên bản đó được kiểm tra (nếu không, bản dựng sẽ không thành công nếu các phần phụ thuộc đó không còn nữa).
Tất nhiên, vẫn có thể xảy ra vấn đề nếu máy chủ từ xa không hoạt động hoặc bắt đầu cung cấp dữ liệu bị hỏng – điều này có thể khiến tất cả các bản dựng của bạn bắt đầu không thành công nếu bạn không có bản sao khác của phần phụ thuộc đó. Để tránh vấn đề này , đối với bất kỳ dự án không tầm thường nào, bạn nên sao chép tất cả các phần phụ thuộc của dự án đó vào các máy chủ hoặc dịch vụ mà bạn tin tưởng và kiểm soát. Nếu không, bạn sẽ luôn phải phụ thuộc vào bên thứ ba để có thể sử dụng hệ thống xây dựng, ngay cả khi các hàm băm đã kiểm tra đảm bảo tính bảo mật của hệ thống.