Makefile adalah skrip build yang dibaca oleh program make. Makefile mendefinisikan target (file yang ingin dihasilkan), prerequisite (file yang harus ada sebelum target bisa dibuat), dan recipe (perintah shell untuk membuat target). Keunggulan utama Makefile dibanding script shell adalah incremental build — make hanya membangun ulang target yang prerequisite-nya berubah sejak target terakhir dibuat. Pada chapter ini kita akan menulis Makefile untuk proyek multi-file, memahami struktur rule, variable, dan automatic variable ($@, $<, $^), serta mengamati bagaimana make memilih target yang perlu dibangun ulang.

Struktur Dasar Makefile #

Berikut struktur proyek sederhana dengan dua file C dan satu header:

text
greet.h    -- deklarasi fungsi greet()
greet.c    -- implementasi greet()
main.c     -- program utama yang memanggil greet()

Makefile minimal untuk proyek ini:

makefile
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra
TARGET = app
OBJS = main.o greet.o

$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) $^ -o $@

main.o: main.c greet.h
	$(CC) $(CFLAGS) -c $< -o $@

greet.o: greet.c greet.h
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	rm -f $(OBJS) $(TARGET)

run: $(TARGET)
	./$(TARGET)

.PHONY: clean run

Jalankan make di direktori yang berisi file-file di atas:

bash
make

Output yang Dihasilkan:

text
gcc -std=c11 -Wall -Wextra -c main.c -o main.o
gcc -std=c11 -Wall -Wextra -c greet.c -o greet.o
gcc -std=c11 -Wall -Wextra main.o greet.o -o app

Mari kita telaah struktur Makefile baris per baris:

  1. Baris 1 (CC = gcc): variabel CC menyimpan nama compiler. Konvensi: pakai CC untuk C compiler, CXX untuk C++ compiler. make sudah punya variabel CC bawaan bernilai cc (alias ke gcc di kebanyakan distro), tetapi menetapkannya secara eksplisit memastikan konsistensi.
  2. Baris 2 (CFLAGS = -std=c11 -Wall -Wextra): variabel CFLAGS menyimpan flag compiler. make akan menambahkan $(CFLAGS) ke resep bawaan saat memakai implicit rules, tetapi kita menyebutnya eksplisit untuk kejelasan.
  3. Baris 3-4 (TARGET = app dan OBJS = main.o greet.o): variabel untuk menyimpan nama target dan daftar object file. Memakai variabel membuat Makefile lebih mudah diubah — bila kita menambah file baru, cukup tambahkan ke OBJS.
  4. Baris 6 ($(TARGET): $(OBJS)): rule dengan target app dan prerequisite main.o greet.o. make memeriksa apakah app lebih baru dari kedua .o; bila tidak, make menjalankan recipe di baris berikutnya.
  5. Baris 7 ( $(CC) $(CFLAGS) $^ -o $@): recipe — perintah shell untuk membuat target. Karakter pertama harus tab, bukan spasi — ini sering menjadi sumber frustrasi pemula. Recipe memakai automatic variable $^ (semua prerequisite) dan $@ (nama target).
  6. Baris 9 (main.o: main.c greet.h): rule untuk mengompilasi main.o. Prerequisite mencakup greet.h karena main.c meng-include-nya. Bila greet.h berubah, main.o akan dibangun ulang.
  7. Baris 10 ( $(CC) $(CFLAGS) -c $< -o $@): recipe memakai automatic variable $< (prerequisite pertama) alih-alih $^. Untuk kompilasi, kita hanya ingin file .c pertama, bukan header.
  8. Baris 15 (clean:): target tanpa prerequisite dan tanpa file output. Disebut phony target karena tidak menghasilkan file bernama clean. Dideklarasikan di .PHONY (baris 19) agar make tidak menganggap clean sebagai file.
  9. Baris 17 (run: $(TARGET)): target yang menjalankan executable setelah memastikan executable terbaru. Berguna untuk workflow make && make run.
  10. Baris 19 (.PHONY: clean run): deklarasi bahwa clean dan run bukan file target. Tanpa deklarasi ini, bila ada file bernama clean di direktori (jarang terjadi, tetapi mungkin), make clean akan menganggap target sudah up-to-date dan menolak menjalankan recipe.

Automatic Variables #

Tiga automatic variable yang paling sering dipakai:

VariableArti
$@Nama target
$<Prerequisite pertama
$^Semua prerequisite, dipisah spasi (dengan deduplikasi)
$?Prerequisite yang lebih baru dari target
$*Bagian target yang cocok dengan % di pattern rule

Mari kita lihat automatic variable beraksi. Makefile berikut memakai @echo untuk mencetak nilai setiap variable saat recipe dieksekusi:

makefile
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra

app: main.o greet.o
	@echo "=== Linking ==="
	@echo 'Target   ($$@):' $@
	@echo 'Sources ($$^):' $^
	@echo 'First    ($$<):' $<
	$(CC) $(CFLAGS) $^ -o $@

main.o: main.c greet.h
	$(CC) $(CFLAGS) -c $< -o $@

greet.o: greet.c greet.h
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	rm -f *.o app

.PHONY: clean
bash
make clean && make -f Makefile_auto

Output yang Dihasilkan:

text
rm -f *.o app
gcc -std=c11 -Wall -Wextra -c main.c -o main.o
gcc -std=c11 -Wall -Wextra -c greet.c -o greet.o
=== Linking ===
Target   ($@): app
Sources ($^): main.o greet.o
First    ($<): main.o
gcc -std=c11 -Wall -Wextra main.o greet.o -o app

Perhatikan beberapa hal penting dari output di atas:

  1. Baris Target ($@): app menunjukkan $@ bernilai app — nama target yang sedang dibuat.
  2. Baris Sources ($^): main.o greet.o menunjukkan $^ berisi kedua prerequisite dipisah spasi.
  3. Baris First ($<): main.o menunjukkan $< hanya berisi prerequisite pertama (main.o), bukan keduanya. Inilah sebabnya $< dipakai untuk kompilasi (kita hanya ingin file .c pertama), sementara $^ dipakai untuk linking (kita ingin semua .o).
  4. Karakter @ di awal @echo menyuruh make tidak menampilkan perintah echo itu sendiri — hanya outputnya. Tanpa @, baris echo "Target ($@):" app akan ikut tercetak.
  5. Konstruksi $$@ di dalam string single-quote '...' menampilkan literal $@ — $$ di Makefile menjadi $ saat diteruskan ke shell, dan shell tidak menafsirkan $@ di dalam single-quote.

Incremental Build #

Mari kita lihat bagaimana make hanya membangun ulang apa yang perlu:

bash
# Setelah make pertama
make

Output yang Dihasilkan:

text
make: 'app' is up to date.

make memeriksa timestamp app terhadap timestamp main.o dan greet.o. Karena app lebih baru dari keduanya, make tidak melakukan apa-apa. Sekarang sentuh greet.c untuk mensimulasikan perubahan:

bash
touch greet.c
make

Output yang Dihasilkan:

text
gcc -std=c11 -Wall -Wextra -c greet.c -o greet.o
gcc -std=c11 -Wall -Wextra main.o greet.o -o app

make mengompilasi ulang greet.o (karena greet.c berubah), lalu me-relink app. main.o tidak dibangun ulang karena main.c dan greet.h tidak berubah. Inilah kekuatan incremental build — proyek besar bisa di-build dalam hitungan detik, bukan menit.

Target Tambahan: clean dan run #

bash
make clean

Output yang Dihasilkan:

text
rm -f main.o greet.o app
bash
make run

Output yang Dihasilkan:

text
gcc -std=c11 -Wall -Wextra -c main.c -o main.o
gcc -std=c11 -Wall -Wextra -c greet.c -o greet.o
gcc -std=c11 -Wall -Wextra main.o greet.o -o app
./app
Halo Budi!
Halo Siti!

Target run bergantung pada $(TARGET), jadi make run akan membangun app bila belum ada atau sudah out-of-date, lalu menjalankannya. Workflow sehari-hari sering kali: make clean && make && make run.