Hệ thống xây dựng dựa trên nhiệm vụ

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 nhiệm vụ, cách hoạt động và một số các chức năng có thể xảy ra với các hệ thống dựa trên nhiệm vụ. Sau tập lệnh shell, hệ thống xây dựng dựa trên nhiệm vụ là sự phát triển logic tiếp theo của hoạt động xây dựng.

Tìm hiểu về các hệ thống xây dựng dựa trên nhiệm vụ

Trong hệ thống xây dựng dựa trên nhiệm vụ, đơn vị công việc cơ bản là nhiệm vụ. Một tác vụ là một tập lệnh có thể thực thi bất kỳ loại logic nào, còn tác vụ sẽ chỉ định tác vụ dưới dạng phần phụ thuộc phải chạy trước chúng. Hầu hết các hệ thống xây dựng chính đang được sử dụng hiện nay, chẳng hạn như Ant, Maven, Gradle, Grunt và Rake, đều dựa trên nhiệm vụ. Thay vì tập lệnh shell, hầu hết các hệ thống xây dựng hiện đại đều yêu cầu kỹ sư tạo tệp bản dựng mô tả cách thực hiện quá trình tạo bản dựng.

Hãy lấy ví dụ này từ Hướng dẫn sử dụng Kiến trúc:

<project name="MyProject" default="dist" basedir=".">
   <description>
     simple example build file
   </description>
   <!-- set global properties for this build -->
   <property name="src" location="src"/>
   <property name="build" location="build"/>
   <property name="dist" location="dist"/>

   <target name="init">
     <!-- Create the time stamp -->
     <tstamp/>
     <!-- Create the build directory structure used by compile -->
     <mkdir dir="${build}"/>
   </target>
   <target name="compile" depends="init"
       description="compile the source">
     <!-- Compile the Java code from ${src} into ${build} -->
     <javac srcdir="${src}" destdir="${build}"/>
   </target>
   <target name="dist" depends="compile"
       description="generate the distribution">
     <!-- Create the distribution directory -->
     <mkdir dir="${dist}/lib"/>
     <!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
     <jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
   </target>
   <target name="clean"
       description="clean up">
     <!-- Delete the ${build} and ${dist} directory trees -->
     <delete dir="${build}"/>
     <delete dir="${dist}"/>
   </target>
</project>

Tệp buildfile được viết bằng XML và xác định một số siêu dữ liệu đơn giản về bản dựng cùng với danh sách các tác vụ (thẻ <target> trong XML). (Kiến sử dụng từ target để biểu thị một công việc và sử dụng từ công việc để chỉ lệnh.) Mỗi nhiệm vụ sẽ thực thi một danh sách các lệnh khả thi do Ant xác định, Thao tác này bao gồm việc tạo và xoá các thư mục, chạy javac và tạo một tệp JAR. Tập hợp các lệnh này có thể được mở rộng bằng phương thức do người dùng cung cấp trình bổ trợ để bao gồm mọi loại logic. Mỗi công việc cũng có thể xác định các công việc mà nó phụ thuộc vào thông qua thuộc tính phụ thuộc. Các phần phụ thuộc này tạo thành một đồ thị không chu trình, như được minh họa trong Hình 1.

Biểu đồ acrylic cho thấy các phần phụ thuộc

Hình 1. Một đồ thị không chu trình thể hiện các phần phụ thuộc

Người dùng thực hiện bản dựng bằng cách cung cấp nhiệm vụ cho công cụ dòng lệnh của Ant. Ví dụ: khi người dùng nhập ant dist, Ant sẽ thực hiện các bước sau:

  1. Tải một tệp có tên build.xml trong thư mục hiện tại và phân tích cú pháp tệp đó thành tạo cấu trúc biểu đồ như trong Hình 1.
  2. Tìm tác vụ có tên dist đã được cung cấp trên dòng lệnh và phát hiện ra rằng nó có một phần phụ thuộc vào tác vụ có tên là compile.
  3. Tìm tác vụ có tên compile và phát hiện tác vụ này có một phần phụ thuộc trên tác vụ có tên init.
  4. Tìm tác vụ có tên init và phát hiện tác vụ không có phần phụ thuộc.
  5. Thực thi các lệnh được xác định trong tác vụ init.
  6. Thực thi các lệnh được xác định trong tác vụ compile dựa trên tất cả các lệnh đó các phần phụ thuộc của tác vụ đã được chạy.
  7. Thực thi các lệnh được xác định trong tác vụ dist dựa trên tất cả các lệnh đó các phần phụ thuộc của tác vụ đã được chạy.

Cuối cùng, mã do Ant thực thi khi chạy tác vụ dist sẽ tương đương vào tập lệnh shell sau đây:

./createTimestamp.sh
mkdir build/
javac src/* -d build/
mkdir -p dist/lib/
jar cf dist/lib/MyProject-$(date --iso-8601).jar build/*

Khi cú pháp bị xoá, tệp bản dựng và tập lệnh bản dựng thực sự sẽ bị xoá không quá khác biệt. Nhưng chúng tôi đã thu được rất nhiều lợi ích nhờ việc này. Chúng ta có thể tạo các tệp bản dựng mới trong các thư mục khác và liên kết chúng với nhau. Chúng tôi có thể dễ dàng thêm các công việc mới phụ thuộc vào các công việc hiện có theo cách tuỳ ý và phức tạp. T4 chỉ cần chuyển tên của một công việc vào công cụ dòng lệnh ant và công cụ này xác định mọi thứ cần chạy.

Ant là một phần mềm phần mềm cũ, được phát hành lần đầu vào năm 2000. Các công cụ khác như Maven và Gradle đã cải thiện Ant trong những năm qua và về cơ bản đã thay thế nó bằng cách thêm các tính năng như tự động quản lý các phần phụ thuộc và cú pháp rõ ràng hơn mà không cần XML. Tuy nhiên, bản chất của những quảng cáo này hệ thống vẫn giữ nguyên: chúng cho phép kỹ sư viết tập lệnh bản dựng theo theo nguyên tắc và theo mô-đun thành các nhiệm vụ, đồng thời cung cấp các công cụ để thực hiện các nhiệm vụ đó cũng như quản lý các phần phụ thuộc.

Mặt đen tối của hệ thống xây dựng dựa trên nhiệm vụ

Bởi vì về cơ bản, những công cụ này cho phép kỹ sư xác định bất kỳ tập lệnh nào là một nhiệm vụ, nên họ cực kỳ mạnh mẽ, cho phép bạn làm gần như mọi thứ bạn có thể tưởng tượng với họ. Tuy nhiên, sức mạnh đó đi kèm với nhiều hạn chế và các hệ thống xây dựng dựa trên nhiệm vụ có thể trở nên khó xử lý khi các tập lệnh bản dựng ngày càng phức tạp. Chiến lược phát hành đĩa đơn vấn đề với những hệ thống như vậy là chúng thực sự cung cấp quá nhiều năng lượng để kỹ sư và không đủ điện cho hệ thống. Vì hệ thống không biết những gì kịch bản đang làm, hiệu suất bị ảnh hưởng, vì nó phải rất thận trọng về cách lên lịch và thực thi các bước xây dựng. Và không có cách nào để hệ thống để xác nhận rằng mỗi tập lệnh đang hoạt động đúng cách, vì vậy, các tập lệnh có xu hướng và trở thành một việc khác cần được gỡ lỗi.

Khó khăn trong việc song song hoá các bước tạo bản dựng

Các máy trạm phát triển hiện đại khá mạnh mẽ, với nhiều lõi được có thể thực thi song song một số bước xây dựng. Tuy nhiên, các hệ thống dựa trên nhiệm vụ thường không thể thực thi song song quá trình thực thi tác vụ, ngay cả khi có vẻ như chúng đáng ra có thể. Giả sử công việc A phụ thuộc vào công việc B và C. Vì nhiệm vụ B và C không phụ thuộc lẫn nhau, liệu có an toàn khi chạy đồng thời hai định dạng này không để hệ thống có thể chuyển sang tác vụ A nhanh hơn? Có thể, nếu họ không chạm vào bất kỳ của cùng một tài nguyên. Nhưng có lẽ không cần. Cả hai đều sử dụng cùng một tệp để theo dõi trạng thái của chúng và chạy chúng cùng lúc sẽ gây ra xung đột. Không có nói chung để hệ thống biết, để hệ thống phải gặp rủi ro về những xung đột này (dẫn đến các vấn đề hiếm gặp nhưng rất khó gỡ lỗi bản dựng) hoặc phải hạn chế toàn bộ bản dựng để chạy trên một luồng trong một quy trình. Đây có thể là sự lãng phí lớn của một cỗ máy phát triển mạnh mẽ và nó hoàn toàn sẽ loại trừ khả năng phân phối bản dựng trên nhiều máy.

Khó tạo bản dựng tăng dần

Một hệ thống xây dựng hiệu quả cho phép kỹ sư tạo các bản dựng tăng dần đáng tin cậy, chẳng hạn như một thay đổi nhỏ không yêu cầu xây dựng lại toàn bộ cơ sở mã đầu. Điều này đặc biệt quan trọng nếu hệ thống xây dựng bị chậm và không thể song song hoá các bước xây dựng vì những lý do nêu trên. Nhưng rất tiếc, các hệ thống xây dựng dựa trên nhiệm vụ cũng gặp khó khăn ở đây. Vì công việc có thể làm bất cứ việc gì, nói chung không có cách nào để kiểm tra xem việc đó đã được thực hiện hay chưa. Nhiều việc cần làm chỉ cần lấy một tập hợp các tệp nguồn và chạy một trình biên dịch để tạo một tập hợp nhị phân; do đó, các tệp này không cần chạy lại nếu các tệp nguồn cơ bản chưa thay đổi. Tuy nhiên, nếu không có thông tin bổ sung thì hệ thống sẽ không thể nói như vậy chắc chắn – có thể nhiệm vụ sẽ tải xuống một tệp có thể đã thay đổi hoặc có thể ghi dấu thời gian có thể khác nhau trong mỗi lần chạy. Để đảm bảo tính chính xác, hệ thống thường phải chạy lại mọi tác vụ trong mỗi bản dựng. Hơi nhiều hệ thống xây dựng cố gắng hỗ trợ các bản dựng tăng dần bằng cách cho phép kỹ sư chỉ định điều kiện mà trong đó một tác vụ cần được chạy lại. Đôi khi điều này khả thi, nhưng thường là một vấn đề phức tạp hơn nhiều so với những gì bạn nghĩ ra. Ví dụ: trong các ngôn ngữ giống như C++ cho phép các tệp được đưa trực tiếp vào tệp khác, không thể xác định toàn bộ tập hợp tệp phải được theo dõi để biết các thay đổi mà không cần phân tích cú pháp nguồn đầu vào. Các kỹ sư thường nhanh chóng rút lại những lối tắt và những lối tắt này có thể dẫn đến các vấn đề hiếm gặp và khó chịu khi kết quả tác vụ là tái sử dụng ngay cả khi không nên. Khi điều này xảy ra thường xuyên, các kỹ sư sẽ có thói quen chạy sạch trước mỗi bản dựng để có trạng thái mới, đánh bại hoàn toàn mục đích của việc có một bản dựng tăng dần trong phiên bản đầu tiên địa điểm. Việc xác định thời điểm cần chạy lại một tác vụ là việc rất khó và khó thực hiện công việc do máy móc xử lý tốt hơn con người.

Khó duy trì và gỡ lỗi tập lệnh

Cuối cùng, các tập lệnh bản dựng do các hệ thống xây dựng dựa trên nhiệm vụ áp đặt thường chỉ khó sử dụng. Mặc dù chúng thường nhận được ít sự giám sát hơn, nhưng các tập lệnh bản dựng là những đoạn mã giống như hệ thống đang được xây dựng và là nơi dễ dàng để lỗi ẩn đi. Sau đây là một số ví dụ về lỗi rất phổ biến khi làm việc với hệ thống xây dựng dựa trên nhiệm vụ:

  • Tác vụ A phụ thuộc vào nhiệm vụ B để tạo một tệp cụ thể làm đầu ra. Chủ sở hữu của nhiệm vụ B không nhận ra rằng các nhiệm vụ khác dựa vào nhiệm vụ đó, vì vậy họ thay đổi nó thành sản xuất ở một vị trí khác. Không phát hiện được điều này cho đến khi ai đó cố gắng chạy tác vụ A nhưng không thành công.
  • Nhiệm vụ A phụ thuộc vào nhiệm vụ B, phụ thuộc vào nhiệm vụ C, tức là tạo một tệp cụ thể dưới dạng đầu ra mà tác vụ A cần. Chủ sở hữu của việc cần làm B quyết định rằng nó không cần phụ thuộc vào tác vụ C nữa, điều này dẫn đến nhiệm vụ A sẽ không thành công mặc dù nhiệm vụ B không quan tâm đến nhiệm vụ C!
  • Nhà phát triển của một công việc mới vô tình đưa ra giả định về máy chạy tác vụ, chẳng hạn như vị trí của công cụ hoặc giá trị của các biến môi trường cụ thể. Nhiệm vụ này hoạt động trên máy của họ nhưng không thành công bất cứ khi nào một nhà phát triển khác dùng thử.
  • Một công việc chứa thành phần không xác định, chẳng hạn như tải tệp xuống từ Internet hoặc thêm dấu thời gian vào bản dựng. Giờ đây, mọi người sẽ có thể có kết quả khác nhau mỗi khi chúng chạy bản dựng, tức là không phải lúc nào các kỹ sư cũng có thể tái tạo và khắc phục thất bại của nhau hoặc lỗi xảy ra trên hệ thống xây dựng tự động.
  • Những tác vụ có nhiều phần phụ thuộc có thể tạo ra điều kiện tranh đấu. Nếu tác vụ A phụ thuộc vào cả công việc B và công việc C, và công việc B và C đều sửa đổi như nhau thì công việc A sẽ nhận được kết quả khác tuỳ thuộc vào công việc nào trong số các công việc B và C kết thúc trước tiên.

Không có cách chung nào để giải quyết những vấn đề về hiệu suất, độ chính xác hoặc các vấn đề về khả năng bảo trì trong khung dựa trên nhiệm vụ được nêu ở đây. Rất lâu vì các kỹ sư có thể viết mã tuỳ ý chạy trong quá trình tạo bản dựng, nên hệ thống không thể có đủ thông tin để luôn có thể chạy các bản dựng một cách nhanh chóng và chính xác. Để giải quyết vấn đề này, chúng ta cần dùng sức mạnh của và giao dữ liệu đó lại cho hệ thống, đồng thời định hình lại vai trò của hệ thống không phải là các tác vụ đang chạy mà là tạo ra cấu phần phần mềm.

Phương pháp này dẫn đến việc tạo ra các hệ thống xây dựng dựa trên cấu phần phần mềm, chẳng hạn như Blaze và Bazel.