Sistem Build Berbasis Tugas

Halaman ini membahas sistem build berbasis tugas, cara kerjanya, dan beberapa komplikasi yang dapat terjadi pada sistem berbasis tugas. Setelah skrip shell, sistem build berbasis tugas adalah evolusi logis berikutnya dari proses build.

Memahami sistem build berbasis tugas

Dalam sistem build berbasis tugas, unit dasar pekerjaan adalah tugas. Setiap tugas adalah skrip yang dapat menjalankan segala jenis logika, dan tugas menentukan tugas lain sebagai dependensi yang harus dijalankan sebelum tugas tersebut. Sebagian besar sistem build utama yang digunakan saat ini, seperti Ant, Maven, Gradle, Grunt, dan Rake, berbasis tugas. Daripada skrip shell, sebagian besar sistem build modern mengharuskan engineer membuat file build yang menjelaskan cara melakukan build.

Lihat contoh ini dari manual 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>

File build ditulis dalam XML dan menentukan beberapa metadata sederhana tentang build beserta daftar tugas (tag <target> dalam XML). (Ant menggunakan kata target untuk mewakili tugas, dan menggunakan kata tugas untuk merujuk ke perintah.) Setiap tugas menjalankan daftar perintah yang mungkin ditentukan oleh Ant, yang di sini mencakup pembuatan dan penghapusan direktori, menjalankan javac, dan membuat file JAR. Kumpulan perintah ini dapat diperluas dengan plugin yang disediakan pengguna untuk mencakup segala jenis logika. Setiap tugas juga dapat menentukan tugas yang menjadi dependensinya melalui atribut depends. Dependensi ini membentuk grafik asiklik, seperti yang terlihat pada Gambar 1.

Grafik akrilik yang menunjukkan dependensi

Gambar 1. Grafik asiklik yang menunjukkan dependensi

Pengguna melakukan build dengan memberikan tugas ke alat command line Ant. Misalnya, saat pengguna mengetik ant dist, Ant akan melakukan langkah-langkah berikut:

  1. Memuat file bernama build.xml di direktori saat ini dan mengurainya untuk membuat struktur grafik yang ditampilkan pada Gambar 1.
  2. Mencari tugas bernama dist yang disediakan di command line dan menemukan bahwa tugas tersebut memiliki dependensi pada tugas bernama compile.
  3. Mencari tugas bernama compile dan menemukan bahwa tugas tersebut memiliki dependensi pada tugas bernama init.
  4. Mencari tugas bernama init dan menemukan bahwa tugas tersebut tidak memiliki dependensi.
  5. Menjalankan perintah yang ditentukan dalam tugas init.
  6. Menjalankan perintah yang ditentukan dalam tugas compile mengingat semua dependensi tugas tersebut telah dijalankan.
  7. Menjalankan perintah yang ditentukan dalam tugas dist mengingat semua dependensi tugas tersebut telah dijalankan.

Pada akhirnya, kode yang dijalankan oleh Ant saat menjalankan tugas dist setara dengan skrip shell berikut:

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

Jika sintaksis dihapus, file build dan skrip build sebenarnya tidak terlalu berbeda. Namun, kita telah mendapatkan banyak hal dengan melakukan hal ini. Kita dapat membuat file build baru di direktori lain dan menautkannya. Kita dapat dengan mudah menambahkan tugas baru yang bergantung pada tugas yang ada dengan cara yang arbitrer dan kompleks. Kita hanya perlu meneruskan nama satu tugas ke alat command line ant, dan alat tersebut akan menentukan semua yang perlu dijalankan.

Ant adalah software lama, yang awalnya dirilis pada tahun 2000. Alat lain seperti Maven dan Gradle telah meningkatkan Ant dalam beberapa tahun terakhir dan pada dasarnya menggantinya dengan menambahkan fitur seperti pengelolaan otomatis dependensi eksternal dan sintaksis yang lebih bersih tanpa XML. Namun, sifat sistem yang lebih baru ini tetap sama: sistem ini memungkinkan engineer menulis skrip build dengan cara yang berprinsip dan modular sebagai tugas serta menyediakan alat untuk menjalankan tugas tersebut dan mengelola dependensi di antara tugas tersebut.

Sisi gelap sistem build berbasis tugas

Karena alat ini pada dasarnya memungkinkan engineer menentukan skrip apa pun sebagai tugas, alat ini sangat canggih, sehingga Anda dapat melakukan hampir semua hal yang dapat Anda bayangkan. Namun, kemampuan tersebut memiliki kekurangan, dan sistem build berbasis tugas dapat menjadi sulit digunakan karena skrip build-nya menjadi lebih kompleks. Masalah dengan sistem tersebut adalah sistem tersebut sebenarnya memberikan terlalu banyak kemampuan kepada engineer dan tidak cukup kemampuan kepada sistem. Karena sistem tidak tahu apa yang dilakukan skrip, performa akan terpengaruh, karena sistem harus sangat konservatif dalam cara menjadwalkan dan menjalankan langkah-langkah build. Selain itu, sistem tidak dapat mengonfirmasi bahwa setiap skrip melakukan apa yang seharusnya dilakukan, sehingga skrip cenderung menjadi lebih kompleks dan akhirnya menjadi hal lain yang perlu di-debug.

Kesulitan melakukan langkah-langkah build secara paralel

Komputer pengembangan modern cukup canggih, dengan beberapa core yang mampu menjalankan beberapa langkah build secara paralel. Namun, sistem berbasis tugas sering kali tidak dapat melakukan eksekusi tugas secara paralel meskipun tampaknya dapat melakukannya. Misalkan tugas A bergantung pada tugas B dan C. Karena tugas B dan C tidak memiliki dependensi satu sama lain, apakah aman untuk menjalankan tugas tersebut secara bersamaan sehingga sistem dapat lebih cepat menjalankan tugas A? Mungkin, jika tugas tersebut tidak menggunakan resource yang sama. Namun, mungkin tidak—mungkin keduanya menggunakan file yang sama untuk melacak statusnya dan menjalankan tugas tersebut secara bersamaan akan menyebabkan konflik. Secara umum, sistem tidak dapat mengetahui hal ini, sehingga sistem harus mengambil risiko konflik ini (yang menyebabkan masalah build yang jarang terjadi tetapi sangat sulit di-debug), atau sistem harus membatasi seluruh build agar berjalan pada satu thread dalam satu proses. Hal ini dapat menyebabkan pemborosan besar pada komputer developer yang canggih, dan sepenuhnya mengesampingkan kemungkinan mendistribusikan build ke beberapa komputer.

Kesulitan melakukan build inkremental

Sistem build yang baik memungkinkan engineer melakukan build inkremental yang andal sehingga perubahan kecil tidak mengharuskan seluruh codebase dibangun ulang dari awal. Hal ini sangat penting jika sistem build lambat dan tidak dapat melakukan langkah-langkah build secara paralel karena alasan yang disebutkan di atas. Namun, sayangnya, sistem build berbasis tugas juga mengalami kesulitan di sini. Karena tugas dapat melakukan apa saja, secara umum tidak ada cara untuk memeriksa apakah tugas tersebut telah selesai. Banyak tugas hanya mengambil sekumpulan file sumber dan menjalankan compiler untuk membuat sekumpulan biner; dengan demikian, tugas tersebut tidak perlu dijalankan ulang jika file sumber yang mendasarinya tidak berubah. Namun, tanpa informasi tambahan, sistem tidak dapat mengatakan hal ini dengan pasti—mungkin tugas tersebut mendownload file yang mungkin telah berubah, atau mungkin tugas tersebut menulis stempel waktu yang mungkin berbeda pada setiap proses. Untuk menjamin kebenaran, sistem biasanya harus menjalankan ulang setiap tugas selama setiap build. Beberapa sistem build mencoba mengaktifkan build inkremental dengan mengizinkan engineer menentukan kondisi saat tugas perlu dijalankan ulang. Terkadang hal ini dapat dilakukan, tetapi sering kali masalahnya jauh lebih rumit daripada yang terlihat. Misalnya, dalam bahasa seperti C++ yang memungkinkan file disertakan langsung oleh file lain, tidak mungkin menentukan seluruh kumpulan file yang harus dipantau perubahannya tanpa mengurai sumber input. Engineer sering kali mengambil jalan pintas, dan jalan pintas ini dapat menyebabkan masalah yang jarang terjadi dan membuat frustrasi saat hasil tugas digunakan kembali meskipun seharusnya tidak. Jika hal ini sering terjadi, engineer akan terbiasa menjalankan clean sebelum setiap build untuk mendapatkan status baru, sehingga sepenuhnya mengalahkan tujuan memiliki build inkremental. Menentukan kapan tugas perlu dijalankan ulang sangat sulit, dan merupakan tugas yang lebih baik ditangani oleh mesin daripada manusia.

Kesulitan memelihara dan men-debug skrip

Terakhir, skrip build yang diterapkan oleh sistem build berbasis tugas sering kali sulit digunakan. Meskipun sering kali kurang diperhatikan, skrip build adalah kode seperti sistem yang sedang dibangun, dan merupakan tempat yang mudah bagi bug untuk bersembunyi. Berikut adalah beberapa contoh bug yang sangat umum terjadi saat menggunakan sistem build berbasis tugas:

  • Tugas A bergantung pada tugas B untuk menghasilkan file tertentu sebagai output. Pemilik tugas B tidak menyadari bahwa tugas lain bergantung padanya, sehingga mereka mengubahnya untuk menghasilkan output di lokasi yang berbeda. Hal ini tidak dapat dideteksi hingga seseorang mencoba menjalankan tugas A dan menemukan bahwa tugas tersebut gagal.
  • Tugas A bergantung pada tugas B, yang bergantung pada tugas C, yang menghasilkan file tertentu sebagai output yang diperlukan oleh tugas A. Pemilik tugas B memutuskan bahwa tugas tersebut tidak perlu lagi bergantung pada tugas C, yang menyebabkan tugas A gagal meskipun tugas B tidak peduli dengan tugas C sama sekali.
  • Developer tugas baru secara tidak sengaja membuat asumsi tentang komputer yang menjalankan tugas, seperti lokasi alat atau nilai variabel lingkungan tertentu. Tugas tersebut berfungsi di komputernya, tetapi gagal setiap kali developer lain mencobanya.
  • Tugas berisi komponen nondeterministik, seperti mendownload file dari internet atau menambahkan stempel waktu ke build. Sekarang, orang-orang berpotensi mendapatkan hasil yang berbeda setiap kali mereka menjalankan build, yang berarti bahwa engineer tidak akan selalu dapat mereproduksi dan memperbaiki kegagalan satu sama lain atau kegagalan yang terjadi pada sistem build otomatis.
  • Tugas dengan beberapa dependensi dapat membuat kondisi race. Jika tugas A bergantung pada tugas B dan tugas C, dan tugas B dan C memodifikasi file yang sama, tugas A akan mendapatkan hasil yang berbeda bergantung pada tugas B atau C yang selesai terlebih dahulu.

Tidak ada cara umum untuk memecahkan masalah performa, kebenaran, atau pemeliharaan ini dalam framework berbasis tugas yang dijelaskan di sini. Selama engineer dapat menulis kode arbitrer yang berjalan selama build, sistem tidak dapat memiliki informasi yang cukup untuk selalu dapat menjalankan build dengan cepat dan benar. Untuk mengatasi masalah ini, kita perlu mengambil beberapa kemampuan dari engineer dan memberikannya kembali kepada sistem serta mengonseptualisasikan kembali peran sistem bukan sebagai tugas yang berjalan, tetapi sebagai artefak yang dihasilkan.

Pendekatan ini menyebabkan pembuatan sistem build berbasis artefak, seperti Blaze dan Bazel.