Trang này trình bày về hệ thống xây dựng dựa trên tác vụ, cách hoạt động của hệ thống này và một số vấn đề phức tạp có thể xảy ra với hệ thống dựa trên tác vụ. Sau tập lệnh shell, hệ thống xây dựng dựa trên tác vụ là bước phát triển hợp lý tiếp theo của quy 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 hệ thống bản dựng dựa trên tác vụ, đơn vị công việc cơ bản là tác vụ. Mỗi tác vụ là một tập lệnh có thể thực thi bất kỳ loại logic nào và các tác vụ chỉ định các tác vụ khác làm 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 tác vụ. Thay vì tập lệnh shell, hầu hết các hệ thống bản dựng hiện đại đều yêu cầu kỹ sư tạo các tệp bản dựng mô tả cách thực hiện bản dựng.
Hãy xem ví dụ này 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 bản dựng đượ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). (Ant dùng từ target (mục tiêu) để biểu thị một task (tác vụ) và dùng từ task để chỉ commands (lệnh).) Mỗi tác vụ 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á thư mục, chạy javac
và tạo tệp JAR. Người dùng có thể mở rộng bộ lệnh này bằng các trình bổ trợ do người dùng cung cấp để bao gồm mọi loại logic. Mỗi tác vụ cũng có thể xác định các tác vụ mà nó phụ thuộc vào thông qua thuộc tính depends. Các phần phụ thuộc này tạo thành một đồ thị không chu trình, như trong Hình 1.
Hình 1. Biểu đồ không có chu trình cho thấy các phần phụ thuộc
Người dùng thực hiện các bản dựng bằng cách cung cấp các tác 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:
- 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 đó để tạo cấu trúc biểu đồ như trong Hình 1. - Tìm kiếm tác vụ có tên
dist
được cung cấp trên dòng lệnh và phát hiện thấy tác vụ này có một phần phụ thuộc vào tác vụ có têncompile
. - Tìm kiếm tác vụ có tên
compile
và phát hiện thấy tác vụ này có một phần phụ thuộc vào tác vụ có têninit
. - Tìm kiếm tác vụ có tên
init
và phát hiện thấy tác vụ này không có phần phụ thuộc. - Thực thi các lệnh được xác định trong tác vụ
init
. - Thực thi các lệnh được xác định trong tác vụ
compile
, giả sử tất cả các phần phụ thuộc của tác vụ đó đã được chạy. - Thực thi các lệnh được xác định trong tác vụ
dist
, giả sử tất cả 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
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ị loại bỏ, tệp bản dựng và tập lệnh bản dựng thực sự không khác nhau nhiều. Nhưng chúng tôi đã đạt được nhiều thành quả 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 ta có thể dễ dàng thêm các tác vụ mới phụ thuộc vào các tác vụ hiện có theo những 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 qua 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à cú pháp rõ ràng hơn mà không có XML. Tuy nhiên, bản chất của những hệ thống mới hơn này vẫn không thay đổi: chúng cho phép các kỹ sư viết tập lệnh bản dựng theo cách có nguyên tắc và theo mô-đun dưới dạng các tác vụ, đồng thời cung cấp các công cụ để thực thi những tác vụ đó và quản lý các phần phụ thuộc giữa chúng.
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 dưới dạng một tác vụ, nên chúng cực kỳ mạnh mẽ, cho phép bạn làm hầu hết mọi thứ bạn có thể tưởng tượng với chúng. Nhưng sức mạnh đó cũng đi kèm với những hạn chế, và các hệ thống bản dựng dựa trên tác vụ có thể trở nên khó sử dụng khi tập lệnh bản dựng của chúng ngày càng phức tạp hơn. Vấn đề với những hệ thống như vậy là chúng thực sự trao quá nhiều quyền cho các kỹ sư và không đủ quyền cho hệ thống. Vì hệ thống không biết 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ập lịch và thực thi các bước xây dựng. Hệ thống không có cách nào để xác nhận rằng mỗi tập lệnh đang thực hiện đúng chức năng của nó, vì vậy, các tập lệnh có xu hướng tăng độ phức tạp và cuối cùng trở thành một thứ khác cần gỡ lỗi.
Độ khó của 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 có hiệu suất khá cao, với nhiều lõi có khả năng thực thi song song một số bước xây dựng. Nhưng các hệ thống dựa trên tác vụ thường không thể song song hoá việc thực thi tác vụ ngay cả khi có vẻ như chúng có thể. Giả sử rằng tác vụ A phụ thuộc vào các tác vụ B và C. Vì các tác vụ B và C không phụ thuộc vào nhau, nên có an toàn khi chạy chúng cùng lúc để hệ thống có thể nhanh chóng chuyển sang tác vụ A không? Có thể, nếu chúng không chạm vào bất kỳ tài nguyên nào giống nhau. Nhưng có thể không – có lẽ cả hai đều sử dụng cùng một tệp để theo dõi trạng thái của chúng và việc chạy chúng cùng lúc sẽ 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 phải chấp nhận rủi ro xảy ra những xung đột này (dẫn đến các vấn đề về bản dựng hiếm gặp nhưng rất khó gỡ lỗi), hoặc hệ thống 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 duy nhất. Điều này có thể gây lãng phí rất lớn cho một máy phát triển mạnh mẽ và 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 cho phép các kỹ sư thực hiện các bản dựng gia tăng đáng tin cậy, chẳng hạn như một thay đổi nhỏ không yêu cầu toàn bộ cơ sở mã được xây dựng lại từ đầu. Điều này đặc biệt quan trọng nếu hệ thống xây dựng 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 thật 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 ở đây. Vì các tác vụ có thể làm mọi việc, nên nói 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ỉ cần lấy một nhóm tệp nguồn và chạy một trình biên dịch để tạo một nhóm tệp nhị phân; do đó, chúng không cần chạy lại nếu các tệp nguồn cơ bản không thay đổi. Nhưng nếu không có thêm thông tin, hệ thống không thể chắc chắn về điều này – có thể tác vụ tải xuống một tệp có thể đã thay đổi hoặc có thể tác vụ ghi một dấu thời gian có thể khác nhau ở 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. Một số hệ thống bản dựng cố gắng bật các bản dựng gia tăng bằng cách cho phép kỹ sư chỉ định các điều kiện mà theo đó một tác vụ cần được chạy lại. Đôi khi, việc này có thể thực hiện được, nhưng thường thì đây là một vấn đề phức tạp hơn nhiều so với vẻ bề ngoài. Ví dụ: trong các ngôn ngữ như C++ cho phép các tệp được đưa trực tiếp vào các tệp khác, bạn không thể xác định toàn bộ tập hợp các tệp phải được theo dõi để phát hiện các thay đổi mà không cần phân tích cú pháp các nguồn đầu vào. Các kỹ sư thường chọn cách làm tắt và những cách làm tắt này có thể dẫn đến các vấn đề hiếm gặp và gây khó chịu khi kết quả của một tác vụ được dùng lại 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 lệnh clean trước mỗi bản dựng để có trạng thái mới, hoàn toàn đánh bại mục đích có bản dựng gia tăng ngay từ đầu. Việc xác định thời điểm cần chạy lại một tác vụ là một việc khá tinh tế và máy móc sẽ xử lý tốt hơn con người.
Khó khăn trong việc 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 hệ thống bản dựng dựa trên tác vụ áp đặt thường rất khó sử dụng. Mặc dù thường ít được xem xét kỹ lưỡng, nhưng tập lệnh xây 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. Sau đây là một số ví dụ về các lỗi rất thường gặp khi làm việc với hệ thống bản dựng dựa trên tác vụ:
- Tác vụ A phụ thuộc vào tác vụ B để tạo ra một tệp cụ thể làm đầu ra. Chủ sở hữu của việc cần làm B không nhận ra rằng các việc cần làm khác phụ thuộc vào việc này, vì vậy, họ thay đổi việc cần làm B để tạo ra kết quả ở một vị trí khác. Bạn không thể phát hiện ra điều này cho đến khi có người cố gắng chạy tác vụ A và nhận thấy tác vụ đó không thành công.
- Việc cần làm A phụ thuộc vào việc cần làm B, việc cần làm B phụ thuộc vào việc cần làm C. Việc cần làm C tạo ra một tệp cụ thể làm đầu ra mà việc cần làm A cần. Chủ sở hữu của tác vụ B quyết định rằng tác vụ này không cần phụ thuộc vào tác vụ C nữa, điều này khiến tác vụ A không thành công mặc dù tác vụ B hoàn toàn không quan tâm đến tác 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ụ 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 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 người có thể nhận được kết quả khác nhau mỗi khi chạy bản dựng, nghĩa là các kỹ sư sẽ không phải lúc nào cũng có thể tái tạo và khắc phục lỗi của nhau hoặc lỗi 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 tình huống tương tranh. Nếu tác vụ A phụ thuộc vào cả tác vụ B và tác vụ C, đồng thời cả tác vụ B và C đều sửa đổi cùng một tệp, thì tác vụ A sẽ nhận được kết quả khác nhau tuỳ thuộc vào tác vụ nào trong số B và C hoàn thành trước.
Không có cách nào để giải quyết các vấn đề về hiệu suất, tính chính xác hoặc khả năng duy trì trong khuôn khổ dựa trên nhiệm vụ được trình bày ở đâ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, hệ thống không thể có đủ thông tin để luôn có thể chạy 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ảm bớt quyền hạn của các kỹ sư và trao lại quyền hạn đó cho hệ thống, đồng thời tái định hình vai trò của hệ thống không phải là chạy các tác vụ mà là tạo ra các cấu phần phần mềm.
Cách tiếp cận 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.