Sistem Build Berbasis Tugas

Laporkan masalah Lihat sumber

Halaman ini membahas sistem build berbasis tugas, cara kerjanya, dan beberapa detail 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 pekerjaannya adalah tugas. Setiap tugas adalah skrip yang dapat mengeksekusi logika apa pun, 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. Sebagai ganti skrip shell, sebagian besar sistem build modern mengharuskan engineer untuk membuat file build yang menjelaskan cara menjalankan build.

Ambil contoh ini dari Panduan 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>

Buildfile ditulis dalam XML dan menentukan beberapa metadata sederhana tentang build bersama daftar tugas (tag <target> dalam XML). (Ant menggunakan kata target untuk mewakili tugas, dan menggunakan kata tugas untuk merujuk ke perintah.) Setiap tugas mengeksekusi daftar kemungkinan perintah yang ditentukan oleh Ant, yang di sini termasuk membuat dan menghapus direktori, menjalankan javac, dan membuat file JAR. Kumpulan perintah ini dapat diperluas dengan plugin yang disediakan pengguna untuk mencakup logika apa pun. Setiap tugas juga dapat menentukan tugas yang bergantung padanya melalui atribut dependen. 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 melakukan langkah-langkah berikut:

  1. Memuat file bernama build.xml di direktori saat ini dan mengurainya untuk membuat struktur grafik yang ditampilkan dalam 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. Mengeksekusi perintah yang ditentukan dalam tugas compile mengingat semua dependensi tugas tersebut telah dijalankan.
  7. Mengeksekusi 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 dihilangkan, buildfile dan skrip build sebenarnya tidak terlalu berbeda. Namun, kita telah mendapatkan banyak manfaat dengan melakukan ini. Kita dapat membuat file build baru di direktori lain dan menautkannya bersama-sama. Kita dapat dengan mudah menambahkan tugas baru yang bergantung pada tugas yang ada dengan cara yang arbitrer dan kompleks. Kita hanya perlu meneruskan nama tugas tunggal ke alat command line ant, dan menentukan semua hal yang perlu dijalankan.

Ant adalah software lama, yang awalnya dirilis pada tahun 2000. Alat lain seperti Maven dan Gradle telah mengalami peningkatan pada 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 ini yang lebih baru tetap sama: memungkinkan engineer menulis skrip build dengan prinsip 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 apa pun yang dapat Anda bayangkan dengannya. Namun, keunggulan itu memiliki kelemahan, dan sistem build berbasis tugas dapat menjadi sulit digunakan karena skrip build-nya menjadi lebih kompleks. Masalah pada sistem tersebut adalah sistem tersebut akhirnya memberikan terlalu banyak daya untuk engineer dan tidak cukup daya untuk sistem. Karena sistem tidak tahu apa yang dilakukan skrip, performa akan terpengaruh, karena harus sangat konservatif tentang cara menjadwalkan dan menjalankan langkah build. Dan tidak ada cara bagi sistem untuk mengonfirmasi bahwa setiap skrip melakukan apa yang harus dilakukan, sehingga skrip cenderung tumbuh dalam kerumitan dan akhirnya menjadi hal lain yang perlu di-debug.

Kesulitan memparalelkan langkah build

Workstation pengembangan modern cukup canggih, dengan beberapa core yang dapat menjalankan beberapa langkah build secara paralel. Namun, sistem berbasis tugas sering kali tidak dapat memparalelkan eksekusi tugas meskipun seharusnya dapat dilakukan. Misalkan tugas A bergantung pada tugas B dan C. Karena tugas B dan C tidak memiliki dependensi satu sama lain, apakah aman untuk menjalankannya secara bersamaan sehingga sistem dapat menyelesaikan tugas A dengan lebih cepat? Mungkin, jika tidak menyentuh resource yang sama. Namun, mungkin tidak—mungkin keduanya menggunakan file yang sama untuk melacak statusnya dan menjalankannya secara bersamaan yang menyebabkan konflik. Tidak ada cara umum untuk diketahui oleh sistem, jadi sistem harus menanggung risiko konflik ini (menyebabkan masalah build yang jarang terjadi tetapi sangat sulit di-debug), atau harus membatasi seluruh build agar berjalan di satu thread dalam satu proses. Hal ini dapat membuang banyak mesin developer yang canggih, dan sepenuhnya mengecualikan kemungkinan distribusi build di beberapa perangkat.

Kesulitan melakukan build inkremental

Sistem build yang baik memungkinkan engineer melakukan build inkremental yang andal sehingga perubahan kecil tidak memerlukan build ulang seluruh codebase dari awal. Hal ini sangat penting jika sistem build lambat dan tidak dapat mengparalelkan langkah build karena alasan yang disebutkan di atas. Namun, sayangnya, sistem build berbasis tugas juga mengalami kesulitan. Karena tugas dapat melakukan apa pun, secara umum tidak ada cara untuk memeriksa apakah tugas tersebut sudah dilakukan. Banyak tugas cukup mengambil satu set file sumber dan menjalankan compiler untuk membuat sekumpulan biner; oleh karena itu, tugas tidak perlu dijalankan ulang jika file sumber yang mendasarinya belum berubah. Namun tanpa informasi tambahan, sistem tidak dapat memastikannya mungkin—mungkin tugas mendownload file yang dapat berubah, atau mungkin menulis stempel waktu yang dapat berbeda pada setiap pengujian. Untuk menjamin ketepatan, sistem biasanya harus menjalankan kembali setiap tugas selama setiap build. Beberapa sistem build mencoba mengaktifkan build inkremental dengan mengizinkan engineer menentukan kondisi yang mengharuskan tugas dijalankan kembali. Terkadang hal ini dapat dilakukan, tetapi sering kali ini adalah masalah yang jauh lebih rumit daripada yang terlihat. Misalnya, dalam bahasa seperti C++ yang memungkinkan file disertakan langsung oleh file lain, Anda tidak dapat menentukan keseluruhan kumpulan file yang harus diperhatikan jika ada perubahan tanpa mengurai sumber input. Engineer sering kali akhirnya mengambil pintasan, dan pintasan ini dapat menyebabkan masalah yang jarang terjadi dan menyulitkan, sehingga hasil tugas digunakan kembali, padahal seharusnya tidak. Jika ini sering terjadi, engineer akan membiaskan diri untuk menjalankan clean sebelum setiap build untuk mendapatkan status baru, sehingga sepenuhnya tidak memiliki tujuan untuk membuat build inkremental sejak awal. Mencari tahu kapan tugas perlu dijalankan ulang ternyata sangat halus, dan merupakan pekerjaan yang lebih baik ditangani oleh mesin daripada manusia.

Kesulitan mengelola dan men-debug skrip

Terakhir, skrip build yang diberlakukan oleh sistem build berbasis tugas sering kali sulit untuk digunakan. Meskipun skrip ini sering kali tidak terlalu ketat, skrip build adalah kode seperti sistem yang dibuat, dan merupakan tempat yang mudah bagi bug untuk disembunyikan. Berikut beberapa contoh bug yang sangat umum 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 mengandalkannya, sehingga mereka mengubahnya untuk menghasilkan output di lokasi yang berbeda. Hal ini tidak dapat dideteksi sampai seseorang mencoba menjalankan tugas A dan mendapati 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 tidak perlu bergantung pada tugas C lagi, yang menyebabkan tugas A gagal meskipun tugas B tidak peduli dengan tugas C sama sekali.
  • Developer tugas baru secara tidak sengaja membuat asumsi tentang mesin yang menjalankan tugas, seperti lokasi alat atau nilai variabel lingkungan tertentu. Tugas ini berjalan di mesin mereka, tetapi gagal setiap kali developer lain mencobanya.
  • Tugas berisi komponen nondeterministik, seperti mendownload file dari internet atau menambahkan stempel waktu ke build. Kini, orang-orang mendapatkan hasil yang berpotensi berbeda setiap kali menjalankan build. Artinya, engineer tidak akan selalu dapat mereproduksi dan memperbaiki kegagalan atau kegagalan satu sama lain yang terjadi pada sistem build otomatis.
  • Tugas dengan beberapa dependensi dapat membuat kondisi race. Jika tugas A bergantung pada tugas B dan tugas C, serta tugas B dan C, keduanya akan memodifikasi file yang sama, tugas A akan mendapatkan hasil berbeda, bergantung pada tugas B dan C mana yang selesai lebih dulu.

Tidak ada cara umum untuk memecahkan masalah performa, ketepatan, atau pengelolaan ini dalam framework berbasis tugas yang diuraikan di sini. Selama engineer dapat menulis kode arbitrer yang berjalan selama build, sistem tidak akan memiliki cukup informasi untuk selalu dapat menjalankan build dengan cepat dan dengan benar. Untuk mengatasi masalah ini, kita perlu mengambil beberapa kemampuan dari tangan para engineer dan menyerahkannya kembali ke tangan sistem, serta mengkreasikan kembali peran sistem bukan sebagai tugas yang berjalan, melainkan sebagai artefak artefak.

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