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

Trang này đề cập đến các hệ thống xây dựng dựa trên tác vụ, cách chúng hoạt động và một số chức năng có thể xảy ra với các hệ thống dựa trên tác vụ. Sau các tập lệnh shell, hệ thống xây dựng dựa trên tác vụ là sự phát triển logic tiếp theo của quá trình xây dựng.

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

Trong một hệ thống xây dựng dựa trên tác vụ, đơn vị công việc cơ bản là tác vụ. Mỗi nhiệm vụ là một tập lệnh có thể thực thi bất kỳ loại logic nào, còn nhiệm vụ chỉ định các nhiệm vụ khác 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 được sử dụng hiện nay (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 các kỹ sư tạo tệp bản dựng mô tả cách triển khai bản dựng.

Hãy xem ví dụ sau trong Hướng dẫn sử dụng Ant:

<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 tác vụ (các thẻ <target> trong XML). (Ant sử dụng từ mục tiêu để đại diện cho một tác vụ và sử dụng từ tác vụ để tham chiếu đến các lệnh.) Mỗi tác vụ sẽ thực thi một danh sách các lệnh có thể có do Ant xác định, trong đó bao gồm việc tạo và xoá các thư mục, chạy javac và tạo tệp JAR. Bộ lệnh này có thể được mở rộng bằng trình bổ trợ do người dùng cung cấp để xử lý bất kỳ loại logic nào. Mỗi tác vụ cũng có thể xác định các tác vụ mà nó phụ thuộc thông qua thuộc tính phần phụ thuộc. Các phần phụ thuộc này tạo thành một biểu đồ tuần hoàn, như trong Hình 1.

Biểu đồ acrylic thể hiện các phần phụ thuộc

Hình 1. Một biểu đồ tuần hoàn thể hiện các phần phụ thuộc

Người dùng tạo bản dựng bằng cách cung cấp các 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 vào thư mục hiện tại và phân tích cú pháp tệp đó để tạo cấu trúc biểu đồ như ở Hình 1.
  2. Tìm tác vụ tên là dist được cung cấp trên dòng lệnh và phát hiện ra rằng tác vụ đó có một phần phụ thuộc trong tác vụ tên là compile.
  3. Tìm tác vụ có tên compile và phát hiện tác vụ đó có phần phụ thuộc trong tác vụ tên là 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 nhiệm vụ init.
  6. Thực thi các lệnh được xác định trong tác vụ compile vì tất cả phần phụ thuộc của tác vụ đó đều đã được chạy.
  7. Thực thi các lệnh được xác định trong tác vụ dist vì tất cả phần phụ thuộc của tác vụ đó đều đã được chạy.

Tóm lại, mã được Ant thực thi khi chạy tác vụ dist tương đương với tập lệnh shell sau:

./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ự không quá khác nhau. Nhưng chúng tôi đã thu được rất nhiều từ 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 những tệp đó với nhau. Chúng ta có thể dễ dàng thêm tác vụ mới phụ thuộc vào tác vụ hiện có một cách tuỳ ý và phức tạp. Chúng ta chỉ cần truyền tên của một tác vụ duy nhất đến công cụ dòng lệnh ant và công cụ này sẽ xác định mọi thứ cần chạy.

Ant là một 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 xen kẽ và về cơ bản đã thay thế Ant 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 bên ngoài và một cú pháp gọn gàng hơn mà không cần XML. Nhưng bản chất của các hệ thống mới này không thay đổi: chúng cho phép các kỹ sư viết tập lệnh xây dựng theo nguyên tắc và mô-đun dưới dạng các tác vụ, đồng thời cung cấp công cụ để thực thi các tác vụ đó và quản lý các phần phụ thuộc giữa các tác vụ đó.

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

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

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

Khó thực hiện các bản dựng tăng dần

Một hệ thống xây dựng tốt sẽ cho phép các kỹ sư thực hiện các bản dựng tăng dần đáng tin cậy sao cho một thay đổi nhỏ không yêu cầu phải xây dựng lại toàn bộ cơ sở mã từ đầ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ể tải song song các bước xây dựng vì những lý do nêu trên. Nhưng không may, các hệ thống xây dựng dựa trên tác vụ cũng gặp khó khăn trong trường hợp này. Vì tác vụ có thể làm bất cứ điều gì, nên nhìn chung, không có cách nào để kiểm tra xem chúng đã được thực hiện hay chưa. Nhiều tác vụ chỉ đơn giản là lấy một tập hợp tệp nguồn và chạy trình biên dịch để tạo một tập hợp tệp nhị phân. Do đó, bạn không cần chạy lại các tệp đó nếu các tệp nguồn cơ bản không thay đổi. Tuy nhiên, nếu không có thông tin bổ sung, hệ thống không thể chắc chắn điều này, có thể tác vụ tải xuống một tệp có thể đã thay đổi hoặc ghi dấu thời gian có thể khác nhau trong mỗi lần chạy. Để đảm bảo độ 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. Một số 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 các kỹ sư chỉ định các điều kiện mà một tác vụ cần chạy lại. Đôi khi, việc này có thể khả thi, nhưng thường thì đây là một vấn đề phức tạp hơn nhiều so với vốn có. Ví dụ: trong các ngôn ngữ như C++ cho phép các tệp khác đưa trực tiếp vào tệp, bạn không thể xác định toàn bộ tập hợp tệp phải theo dõi 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 sử dụng lối tắt và các lối tắt này có thể gây ra các sự cố hiếm gặp và khó chịu khi kết quả tác vụ được sử dụng lại ngay cả khi lẽ ra không nên như vậy. 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 dữ liệu trước mọi bản dựng để có trạng thái mới, hoàn toàn loại bỏ mục đích phải tạo bản dựng tăng dần ngay từ đầu. Việc xác định được thời điểm cần chạy lại một tác vụ là việc tinh tế một cách đáng kinh ngạc và là công việc do công nghệ học máy xử lý hiệu quả hơn so với 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 tác vụ áp đặt thường rất khó sử dụng. Mặc dù chúng thường không được xem xét kỹ lưỡng hơn, nhưng các tập lệnh bản dựng cũng là mã giống như hệ thống đang được xây dựng và là nơi dễ dàng để lỗi ẩn giấu. Dưới đây là một số ví dụ về các lỗi thường gặp khi làm việc với hệ thống xây dựng dựa trên tác vụ:

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

Không có cách thức chung nào để giải quyết các vấn đề về hiệu suất, độ chính xác hoặc khả năng bảo trì trong khung dựa trên tác vụ được nêu ở đây. Vì vậy, miễn là các kỹ sư có thể viết mã tuỳ ý chạy trong quá trình tạo bản dựng, thì hệ thống sẽ 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 giải quyết vấn đề này từ tay các kỹ sư và đưa chúng lại vào tay hệ thống, đồng thời tái khái niệm vai trò của hệ thống, không phải là chạy tác vụ, mà là tạo 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, như Blaze và Bazel.