Hệ thống xây dựng dựa trên cấu phần phần mềm

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

Trang này đề cập đến các 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 các hệ thống này sáng tạo. Bazel là một hệ thống xây dựng dựa trên cấu phần phần mềm. Trong khi tạo bản dựng dựa trên nhiệm vụ các hệ thống là bước tốt hơn so với tập lệnh bản dựng, chúng cung cấp quá nhiều quyền để cho các kỹ sư riêng lẻ bằng cách cho phép họ xác định công việc của riêng mình.

Các hệ thống xây dựng dựa trên cấu phần phần mềm có một số ít nhiệm vụ do hệ thống xác định mà các kỹ sư có thể định cấu hình theo cách hạn chế. Các kỹ sư vẫn thông báo cho hệ thống nội dung cần xây dựng, nhưng hệ thống xây dựng xác định cách xây dựng ứng dụng đó. Giống như các hệ thống xây dựng dựa trên nhiệm vụ, các hệ thống xây dựng dựa trên cấu phần phần mềm (ví dụ: Bazel) vẫn có các tệp buildfile, nhưng nội dung của các tệp dựng đó rất khác nhau. Thay vào đó ngoài việc là một tập hợp các lệnh bắt buộc trong ngôn ngữ tập lệnh Turing-complete mô tả cách tạo đầu ra, các tệp bản dựng trong Bazel là một hàm khai báo tệp kê khai mô tả một tập hợp 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 giới hạn các tuỳ chọn ảnh hưởng đến cách xây dựng quảng cáo. Khi các kỹ sư chạy bazel trên dòng lệnh, chúng chỉ định một tập hợp mục tiêu cần tạo (cái gì) và Bazel chịu trách nhiệm định cấu hình, chạy và lên lịch biên dịch các bước (cách thức). Bởi vì hệ thống xây dựng hiện có toàn quyền kiểm soát những gì cụ thể để chạy vào thời điểm nào, nó có thể đảm bảo mạnh mẽ hơn nhiều cho phép chiến dịch ở xa hiệu quả hơn mà vẫn đảm bảo độ chính xác.

Góc nhìn chức năng

Dễ dàng so sánh giữa các hệ thống xây dựng dựa trên cấu phần phần mềm và chức năng lập trình. Ngôn ngữ lập trình truyền thống bắt buộc (chẳng hạn như Java, C và Python) chỉ định danh sách các câu lệnh cần được thực thi lần lượt, trong giống như cách mà hệ thống xây dựng dựa trên nhiệm 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ôn ngữ lập trình chức năng (chẳng hạn như Haskell và ML), theo nhưng có cấu trúc giống hơn một loạt phương trình toán học. Trong ngôn ngữ chức năng, lập trình viên mô tả một phép tính để thực hiện, nhưng để lại thông tin chi tiết về thời điểm và chính xác cách tính toán đó được thực thi cho trình biên dịch.

Điều này liên quan đến ý 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 bài toán không thể có thể dễ dàng được biểu thị bằng cách sử dụng lập trình chức năng, nhưng những chương trình có lợi ích rất nhiều từ nó: ngôn ngữ thường có thể song song một cách bình thường như vậy và đưa ra đảm bảo chắc chắn về tính chính xác của chương trình. không thể thực hiện được trong ngôn ngữ bắt buộc. Các vấn đề dễ nhất để thể hiện bằng lập trình chức năng là những chương trình đơn giản chỉ liên quan đến việc biến đổi một phần vào một 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. Đó chính xác là khái niệm hệ thống xây dựng: toàn bộ hệ thống thực sự là một hàm toán học lấy 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 tệp nhị phân làm đầu ra. Vì vậy, không có gì đáng ngạc nhiên khi việc xây dựng một bản dựng xoay quanh nguyên lý lập trình chức năng.

Tìm hiểu về các 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 sản xuất là phiên bản nguồn mở của Blaze.

Sau đây là giao diện của một 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 – có 2 loại mục tiêu ở đây là java_binaryjava_library. Mỗi mục tiêu tương ứng với một cấu phần phần mềm có thể do hệ thống tạo ra: các mục tiêu nhị phân tạo ra các tệp nhị phân có thể được thực thi trực tiếp và các mục tiêu thư viện sẽ tạo ra các thư viện có thể được tệp nhị phân hoặc các thư viện khác. 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 mục tiêu
  • srcs: các tệp nguồn cần được biên dịch để tạo cấu phần phần mềm cho mục tiêu
  • deps: các mục tiêu khác phải được tạo trước mục tiêu này và được liên kết với nó

Các phần phụ thuộc có thể nằm trong cùng một gói (chẳng hạn nhưMyBinary phần phụ thuộc trên :mylib) hoặc trên một gói khác trong cùng hệ phân cấp nguồn (chẳng hạn như phần phụ thuộc của mylib trên //java/com/example/common).

Giống như các hệ thống xây dựng dựa trên nhiệm vụ, bạn thực hiện các bản dựng bằng cách sử dụng dòng lệnh của Bazel . Để tạo mục tiêu MyBinary, bạn chạy bazel build :MyBinary. Sau nhập lệnh đó lần đầu tiên vào một kho lưu trữ sạch, Bazel:

  1. Phân tích cú pháp mọi tệp BUILD trong không gian làm việc để tạo biểu đồ cho các phần phụ thuộc trong số các cấu phần phần mềm.
  2. 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à MyBinary phụ thuộc vào và mọi mục tiêu mà các mục tiêu đó mục tiêu phụ thuộc vào, theo đệ quy.
  3. Tạo từng phần phụ thuộc đó theo thứ tự. Bazel bắt đầu bằng việc tạo ra mỗi mục tiêu không có phần phụ thuộc nào khác và theo dõi các phần phụ thuộc nào vẫn cần được xây dựng cho từng mục tiêu. Ngay khi tất cả đã tạo các phần phụ thuộc, Bazel bắt đầu tạo mục tiêu đó. Quy trình này sẽ tiếp tục cho đến khi mỗi phần phụ thuộc bắc cầu của MyBinary đều đã được tạo.
  4. Tạo MyBinary để tạo tệp nhị phân có thể thực thi cuối cùng liên kết trong tất cả các phần phụ thuộc được tạo ở bước 3.

Về cơ bản, có vẻ như những gì đang xảy ra ở đây không nhiều như vậy khác với những gì đã xảy ra khi sử dụng hệ thống xây dựng dựa trên nhiệm vụ. Thật vậy, kết quả cuối cùng có cùng tệp nhị phân và quy trình tạo kết quả liên quan phân tích một loạt các bước để tìm các phần phụ thuộc, 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. Mục đầ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 một thư viện Java, nên ứng dụng biết rằng tất cả những gì phải làm là chạy trình biên dịch Java thay vì một trình biên dịch tuỳ ý do người dùng xác định, để hệ thống biết rằng có thể chạy song song các bước này một cách an toàn. Điều này có thể dẫn đến cải thiện hiệu suất đáng kể so với khi xây dựng nhắm mục tiêu từng mục tiêu một trên máy đa lõi và chỉ khả thi vì phương pháp tiếp cận dựa trên cấu phần phần mềm để cho hệ thống xây dựng tự chịu trách nhiệm thực thi để có thể đưa ra đảm bảo chắc chắn hơn về tính song song.

Tuy nhiên, lợi ích của tính năng này không chỉ dừng lại ở tính song song. Điều tiếp theo phương pháp tiếp cận này 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 ít hơn hơn một giây kèm theo thông báo cho biết mục tiêu đã được cập nhật. Đây là do mô hình lập trình chức năng mà chúng ta đã đề cập trước đó – Baazel biết rằng mỗi mục tiêu chỉ là kết quả của việc chạy một Java trình biên dịch và biết rằng đầu ra từ trình biên dịch Java chỉ phụ thuộc vào đầu vào của nó, miễn là đầu vào không thay đổi thì có thể sử dụng lại đầu ra. Bản phân tích này hoạt động ở mọi cấp độ; nếu MyBinary.java thay đổi, Bazel biết để tạo 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 cần xây dựng lại thư viện đó, mylibMyBinary, nhưng sử dụng lại //java/com/example/myproduct/otherlib. Bởi vì Bazel biết về thuộc tính của các công cụ mà công cụ này chạy ở từng bước, chỉ có thể tạo 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 bản dựng sẽ không tạo ra các bản dựng lỗi thời.

Việc định lại khung hình của quá trình xây dựng về cấu phần phần mềm thay vì tác vụ là một cách tinh tế nhưng mạnh mẽ. Bằng cách giảm tính linh hoạt cho lập trình viên, hệ thống có thể biết thêm về những gì đang được thực hiện ở mỗi bước của quá trình tạo bản dựng. Chiến dịch này có thể vận dụng kiến thức này để tạo bản dựng hiệu quả hơn nhiều bằng cách tải song song bản dựng các quy trình và tái sử dụng đầu ra của chúng. Tuy nhiên, đây mới chỉ là bước đầu tiên và các khối dựng của tính song song và tái sử dụng này tạo thành cơ sở cho việc phân phối và có khả năng mở rộng cao.

Các thủ thuật Bazel tiện lợi khác

Các hệ thống xây dựng dựa trên cấu phần phần mềm sẽ giải quyết được vấn đề về cơ bản của tính song song và tái sử dụng vốn có trong các hệ thống xây dựng dựa trên nhiệm vụ. Nhưng vẫn còn đó một số vấn đề phát sinh trước đó mà chúng tôi chưa giải quyết. Bazel rất thông minh để giải quyết từng vấn đề. Chúng ta nên thảo luận trước khi tiếp tục.

Công cụ dưới dạng phần phụ thuộc

Một vấn đề chúng tôi đã 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 tôi và việc tái tạo các bản dựng qua nhiều hệ thống có thể khó khăn do các phiên bản công cụ hoặc vị trí khác nhau. Bài toá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 được tạo hoặc đượ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 bộ 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 để từng mục tiêu. Mọi java_library trong không gian làm việc ngầm ẩn phụ thuộc vào một Java Trình biên dịch mặc định là một trình biên dịch nổi tiếng. Bất cứ khi nào Bazel tạo java_library, hàm này sẽ kiểm tra để đảm bảo rằng trình biên dịch đã chỉ định có sẵn tại một vị trí xác định. Giống như mọi phần phụ thuộc khác, nếu trình biên dịch Java sẽ được tạo lại, mọi cấu phần phần mềm phụ thuộc vào cấu phần phần mềm đó.

Bazel giải quyết phần thứ hai của vấn đề, đó là sự độc lập của nền tảng, bằng cách đặt ra cấu hình bản dựng. Thay vì các mục tiêu tùy thuộc trực tiếp vào các công cụ của chúng, chúng phụ thuộc vào loại cấu hình:

  • Cấu hình máy chủ lưu trữ: tạo các công cụ chạy trong quá trình tạo bản dựng
  • Cấu hình mục tiêu: tạo tệp nhị phân mà bạn yêu cầu sau cùng

Mở rộng hệ thống xây dựng

Bazel đặt ra các mục tiêu cho một số ngôn ngữ lập trình phổ biến bên ngoài nhưng các kỹ sư sẽ luôn muốn làm nhiều việc hơn – một phần lợi ích của phương pháp dựa trên công việc hệ thống chính là sự linh hoạt trong việc hỗ trợ bất kỳ loại quy trình xây dựng nào và nó tốt hơn là không nên từ bỏ trong hệ thống xây dựng dựa trên cấu phần phần mềm. May mắn là Bazel cho phép mở rộng các loại mục tiêu được hỗ trợ lên thêm 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ạng thuộc tính được truyền trong tệp BUILD) và thuộc tính được khắc phục tập hợp đầ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 đều khai báo dữ liệu đầu vào và đầu ra của nó, 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 đầ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ị thành phần kết hợp cấp thấp nhất trong hệ thống xây dựng – một thao tác có thể thực hiện bất kỳ thứ gì nó muốn, miễn là nó chỉ sử dụng đầu vào và đầu ra đã khai báo, và Bazel đảm nhận việc lên lịch 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 hiệu quả vì không có cách nào để ngăn chặn nhà phát triển hành động làm một việc gì đó như đưa ra một quá trình không tất định như một phần của hành động của họ. Nhưng điều này không xảy ra rất thường xuyên trong thực tế và việc đẩy mạnh khả năng lạm dụng cho đến cấp hành động sẽ giảm đi đáng kể cơ hội lỗi. Quy tắc hỗ trợ nhiều ngôn ngữ và công cụ phổ biến được cung cấp rộng rãi trên mạng và hầu hết các dự án sẽ không bao giờ cần tự xác định quy tắc. Ngay cả đối với những quy tắc đó, bạn chỉ cần xác định định nghĩa quy tắc trong một vị trí trung tâm trong kho lưu trữ, tức 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 chúng.

Cô lập môi trường

Các thao tác có vẻ như có thể gặp phải vấn đề tương tự như các tác vụ trong các tác vụ khác hệ thống – không thể vẫn có thể ghi các hành động mà cả hai đều ghi vào cùng một hệ thống và xung đột với nhau? Thực ra thì Bazel là người tạo những video này xung đột không thể xảy ra bằng cách dùng hộp cát. Bật được hỗ trợ mọi hành động đều được tách biệt với mọi hành động khác thông qua hệ thống tệp hộp cát. Trên thực tế, 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 các đầu vào mà nó đã khai báo và mọi đầu ra mà nó có sản xuất. Việc này được thực thi bằng các hệ thống như LXC trên Linux, cùng công nghệ phía sau Docker. Điều này có nghĩa là các hành động không thể xung đột với nhau lý do khác vì họ không thể đọc bất kỳ tệp nào mà họ không khai báo, và các tệp mà họ ghi nhưng không khai báo sẽ bị xoá khi người dùng thực hiện thao tác kết thúc. Bazel cũng sử dụng hộp cát để hạn chế các thao tác giao tiếp qua mạng.

Xác định các phần phụ thuộc bên ngoài

Vẫn còn một vấn đề nữa: các hệ thống xây dựng thường cần phải 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ì trực tiếp tạo chúng. Bạn có thể xem điều này trong ví dụ qua Phần phụ thuộc @com_google_common_guava_guava//jar giúp 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 sẽ gây ra nhiều rủi ro. Những tệp đó có thể thay đổi bất cứ lúc nào, do đó có thể hệ thống xây dựng phải liên tục kiểm tra những thông tin đó có mới hay không. Nếu một tệp từ xa thay đổi mà không có sự thay đổi tương ứng trong mã nguồn Workspace, điều này cũng có thể dẫn đến các bản dựng không thể mô phỏng — một bản dựng có thể làm việc một ngày và không thành công vào ngày tiếp theo mà không có lý do rõ ràng do không có sự chú ý thay đổi về phần phụ thuộc. Cuối cùng, phần phụ thuộc bên ngoài có thể mang lại khả năng bảo mật rủi ro khi tài sản thuộc sở hữu của bên thứ ba: nếu kẻ tấn công có thể thâm nhập máy chủ của bên thứ ba đó, họ có thể thay thế tệp phụ thuộc bằng nội dung nào đó thiết kế riêng của họ, có thể cho họ toàn quyền kiểm soát phiên bản của bạn và đầ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 thức được những mà không phải kiểm tra chúng trong phần kiểm soát nguồn. Cập nhật phần phụ thuộc nên là một lựa chọn có ý thức, nhưng lựa chọn đó nên được đưa ra một lần ở một thay vì do từng kỹ sư riêng lẻ quản lý hoặc do hệ thống. Đó là vì ngay cả với mô hình "Trực tiếp", chúng tôi vẫn muốn có các bản dựng mang tính xác định, tức là nếu bạn kiểm tra một cam kết từ tuần, bạn sẽ thấy các phần phụ thuộc như trước đây thay vì hiện tại ngay bây giờ.

Bazel và một số hệ thống xây dựng khác giải quyết sự cố 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ê một hàm băm mật mã cho mọi bên ngoài phần phụ thuộc trong không gian làm việc. Hàm băm là một cách ngắn gọn để trình bày độc đáo mà không kiểm tra toàn bộ tệp trong kiểm soát nguồn. Bất cứ khi nào một khách hàng mới phần phụ thuộc bên ngoà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 đó là thêm vào tệp kê khai, theo cách thủ công hoặc tự động. Khi Bazel chạy một bản dựng này 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 dữ liệu dự kiến hàm băm được xác định trong tệp kê khai và tải lại tệp xuống chỉ khi 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 bạn cập nhật hàm băm trong tệp kê khai. Chiến dịch này có thể được thực hiện tự động, nhưng thay đổi đó phải được phê duyệt và kiểm tra kiểm soát nguồn trước khi bản dựng sẽ 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 một phần phụ thuộc và phần phụ thuộc không thể thay đổi nếu 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 xem phiên bản cũ hơn của mã nguồn, bản dựng đảm bảo sử dụng chính các phần phụ thuộc mà bản dựng đang sử dụng tại thời điểm đó thời điểm phiên bản đó được kiểm tra (nếu không, phiên bản sẽ không thành công nếu những phần phụ thuộc đó không sử dụng được nữa).

Tất nhiên, có thể vẫn gặp vấn đề nếu máy chủ từ xa không hoạt động hoặc bắt đầu phân phát 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 gặp lỗi nếu bạn không có bản sao khác của phần phụ thuộc đó. Để tránh tình trạng này đối với bất kỳ dự án không nhỏ nào, bạn nên phản chiếu tất cả các phần phụ thuộc lên máy chủ hoặc dịch vụ mà bạn tin cậy và kiểm soát. Nếu không, bạn sẽ luôn lợi dụng bên thứ ba để hệ thống xây dựng của bạn ngay cả khi hàm băm đăng ký đảm bảo tính bảo mật.