Sistem Build Berbasis Tugas

Laporkan masalah Lihat sumber

Halaman ini membahas sistem build berbasis tugas, cara kerjanya, dan beberapa detail yang dapat terjadi dengan 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 kerja dasar adalah tugas. Setiap tugas adalah skrip yang dapat menjalankan logika apa pun, dan tugas menentukan tugas lain sebagai dependensi yang harus dijalankan sebelumnya. Sebagian besar sistem build utama yang digunakan saat ini, seperti Ant, Maven, Gradle, Grunt, dan Rake, adalah sistem berbasis tugas. Sebagai ganti skrip shell, sebagian besar sistem build modern mengharuskan engineer membuat file build yang menjelaskan cara menjalankan build.

Ambil 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>

Buildfile ditulis dalam XML dan menentukan beberapa metadata sederhana tentang build beserta daftar tugas (tag <target> dalam XML). (Ant menggunakan kata target untuk mewakili task, dan menggunakan kata task untuk merujuk pada perintah.) Setiap tugas mengeksekusi daftar kemungkinan perintah yang ditentukan oleh Ant, yang di sini meliputi membuat dan menghapus direktori, menjalankan javac, dan membuat file JAR. Kumpulan perintah ini dapat diperluas oleh plugin yang disediakan pengguna untuk mencakup logika apa pun. Setiap tugas juga dapat menentukan tugas yang menjadi dependensinya 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 menjalankan build dengan memberikan tugas ke alat command line Ant. Misalnya, saat pengguna mengetik ant dist, Ant akan mengambil 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 pada 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 dieksekusi 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 hal dengan melakukan ini. Kita dapat membuat buildfile 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 satu tugas ke alat command line ant, dan alat ini akan menentukan semua yang perlu dijalankan.

Ant adalah perangkat lunak lawas, yang awalnya dirilis pada tahun 2000. Alat lain seperti Maven dan Gradle telah menjadi lebih baik di Ant selama bertahun-tahun, dan pada dasarnya menggantikannya dengan menambahkan fitur seperti pengelolaan otomatis dependensi eksternal dan sintaksis yang lebih rapi tanpa XML. Namun, sifat dari sistem yang lebih baru ini tetap sama: sistem ini memungkinkan engineer untuk menulis skrip build dengan cara yang prinsipal dan modular sebagai tugas, serta menyediakan alat untuk menjalankan tugas-tugas tersebut dan mengelola dependensi di antara tugas-tugas tersebut.

Sisi gelap dari sistem build berbasis tugas

Karena pada dasarnya alat ini memungkinkan engineer menentukan skrip sebagai tugas, alat ini sangat canggih, sehingga Anda dapat melakukan hampir semua hal yang dapat Anda bayangkan dengan alat tersebut. Namun, keunggulan tersebut memiliki kelemahan, dan sistem build berbasis tugas dapat menjadi sulit untuk digunakan karena skrip build-nya menjadi lebih kompleks. Masalah dengan sistem seperti ini adalah sistem tersebut sebenarnya memberikan terlalu banyak daya ke engineer dan tidak cukup daya ke sistem. Karena sistem tidak mengetahui apa yang dilakukan skrip, performa akan menurun karena harus sangat konservatif dalam menjadwalkan dan menjalankan langkah-langkah build. Selain itu, tidak ada cara bagi sistem untuk mengonfirmasi bahwa setiap skrip berfungsi sebagaimana mestinya, sehingga skrip cenderung berkembang secara kompleks dan berakhir menjadi hal lain yang memerlukan proses debug.

Kesulitan untuk memparalelkan langkah build

Workstation pengembangan modern cukup andal, dengan beberapa core yang mampu 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 lebih cepat membuka tugas A? Mungkin, jika mereka tidak menggunakan sumber daya yang sama. Namun, mungkin tidak—mungkin keduanya menggunakan file yang sama untuk melacak statusnya dan menjalankannya secara bersamaan akan menyebabkan konflik. Secara umum, tidak ada cara yang dapat diketahui sistem, jadi sistem harus mempertaruhkan konflik ini (yang 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 menjadi pemborosan besar mesin developer yang kuat, dan sepenuhnya mengesampingkan kemungkinan mendistribusikan build ke beberapa mesin.

Kesulitan dalam menjalankan build inkremental

Sistem build yang baik memungkinkan engineer melakukan build inkremental yang andal sehingga perubahan kecil tidak mengharuskan seluruh codebase dibuat ulang dari awal. Hal ini sangat penting jika sistem build lambat dan tidak dapat paralelkan langkah-langkah build karena alasan yang disebutkan di atas. Namun sayangnya, sistem build berbasis tugas juga kesulitan di sini. Karena tugas dapat melakukan apa saja, secara umum tidak ada cara untuk memeriksa apakah tugas tersebut sudah selesai. Banyak tugas yang hanya mengambil satu set file sumber dan menjalankan compiler untuk membuat kumpulan biner; dengan demikian, tugas tersebut tidak perlu dijalankan ulang jika file sumber yang mendasarinya tidak berubah. Namun tanpa informasi tambahan, sistem tidak dapat memastikannya —mungkin tugas mendownload file yang telah berubah, atau mungkin menulis stempel waktu yang mungkin berbeda setiap kali dijalankan. Untuk menjamin kebenaran, sistem biasanya harus menjalankan ulang setiap tugas selama setiap build. Beberapa sistem build mencoba mengaktifkan build inkremental dengan mengizinkan engineer untuk menentukan kondisi saat tugas perlu dijalankan kembali. Terkadang hal ini bisa dilakukan, tetapi sering kali itu menjadi masalah yang jauh lebih rumit daripada yang terlihat. Misalnya, dalam bahasa seperti C++ yang memungkinkan file disertakan secara langsung oleh file lain, seluruh kumpulan file yang harus dipantau untuk perubahan tidak dapat ditentukan tanpa mengurai sumber input. Engineer sering kali mengambil pintasan, dan pintasan ini dapat menyebabkan masalah yang jarang terjadi dan menjengkelkan karena hasil tugas digunakan kembali meskipun seharusnya tidak. Jika hal ini sering terjadi, engineer akan terbiasa menjalankan pembersihan sebelum setiap build untuk mendapatkan status baru, sehingga benar-benar menggagalkan tujuan memiliki build inkremental sejak awal. Mencari tahu kapan tugas perlu dijalankan ulang sangat halus, dan merupakan pekerjaan yang lebih baik ditangani oleh mesin daripada manusia.

Kesulitan dalam mengelola dan melakukan debug skrip

Terakhir, skrip build yang diberlakukan oleh sistem build berbasis tugas sering kali sulit untuk digunakan. Meskipun sering kali tidak diawasi, skrip build merupakan kode seperti sistem yang sedang dibangun, dan merupakan tempat yang mudah bagi bug untuk menyembunyikan bug. Berikut beberapa contoh bug yang sangat umum saat menggunakan sistem build berbasis tugas:

  • Tugas A bergantung pada tugas B untuk menghasilkan file tertentu sebagai {i>output<i}. Pemilik tugas B tidak menyadari bahwa tugas lain bergantung pada tugas itu, jadi dia mengubahnya untuk menghasilkan output di lokasi yang berbeda. Ini tidak dapat dideteksi sampai 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, sehingga 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 tersebut, seperti lokasi alat atau nilai variabel lingkungan tertentu. Tugas ini berjalan di komputer mereka, tetapi gagal setiap kali developer lain mencobanya.
  • Tugas berisi komponen yang tidak deterministik, seperti mendownload file dari internet atau menambahkan stempel waktu ke build. Sekarang, pengguna mendapatkan kemungkinan hasil yang berbeda setiap kali mereka menjalankan build, yang berarti bahwa 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 memodifikasi file yang sama, tugas A mendapatkan hasil yang berbeda, bergantung pada tugas B dan C yang selesai terlebih dahulu.

Tidak ada cara umum untuk menyelesaikan masalah performa, ketepatan, atau pengelolaan ini dalam framework berbasis tugas yang dijabarkan di sini. Selama insinyur dapat menulis kode arbitrer yang berjalan selama proses build, sistem tidak akan memiliki cukup informasi untuk selalu dapat menjalankan build dengan cepat dan benar. Untuk mengatasi masalah ini, kita perlu mengambil alih daya dari tangan insinyur dan mengembalikannya ke tangan sistem dan mengkonsep ulang peran sistem, bukan sebagai tugas yang berjalan, melainkan sebagai memproduksi artefak.

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