Saat proyek C tumbuh dari satu file menjadi belasan file, struktur direktori menjadi penentu apakah proyek tetap mudah dipelihara atau berubah jadi tumpukan file yang membingungkan. Struktur direktori yang baik memisahkan kode sumber dari header, artefak build dari kode sumber, dan program utama dari program uji. Pada Chapter ini kita akan merancang struktur direktori standar untuk proyek C, menulis Makefile sederhana yang mengelola kompilasi, dan mencoba perintah make yang sudah teruji.

Pola Direktori Standar #

Pola yang paling sering dipakai pada proyek C berukuran kecil hingga menengah adalah pembagian menjadi empat direktori:

  • src/ — file sumber .c untuk program utama.
  • include/ — header .h buatan kita, dipakai oleh beberapa file .c.
  • build/ — tempat menyimpan object file dan executable akhir.
  • tests/ — program uji otomatis yang memeriksa perilaku fungsi-fungsi di src/.

Struktur lengkap terlihat seperti ini:

text
proyek-c/
├── Makefile
├── include/
│   └── util.h
├── src/
│   ├── main.c
│   └── util.c
├── build/
└── tests/

Mari kita buat struktur ini dengan satu perintah mkdir:

bash
mkdir -p proyek-c/{src,include,build,tests}
cd proyek-c
find . -type d | sort

Output yang Dihasilkan:

text
.
./build
./include
./src
./tests

Isi File Header dan Sumber #

Mari kita buat file sederhana yang menunjukkan interaksi antar direktori. File header include/util.h berisi deklarasi fungsi:

c
/* include/util.h */
#ifndef UTIL_H
#define UTIL_H

int tambah(int a, int b);

#endif

File implementasi src/util.c menyertakan header tersebut:

c
/* src/util.c */
#include "util.h"

int tambah(int a, int b) {
    return a + b;
}

File src/main.c adalah titik masuk program yang memakai fungsi tambah:

c
/* src/main.c */
#include <stdio.h>
#include "util.h"

int main(void) {
    printf("1 + 2 = %d\n", tambah(1, 2));
    return 0;
}
  • Tanda kutip ganda ("util.h") menyuruh preprocessor mencari header pertama di direktori yang sama dengan file .c, lalu di direktori yang ditentukan flag -I.
  • Tanda kurung siku (<stdio.h>) menyuruh preprocessor mencari header hanya di direktori sistem, bukan di direktori proyek.
  • Pemakaian tanda yang salah masih bisa berfungsi jika kebetulan ada header dengan nama yang sama, tetapi ini adalah praktik buruk yang sebaiknya dihindari.

Makefile Sederhana #

Makefile adalah file konfigurasi untuk make yang mengotomatiskan kompilasi. Berikut Makefile minimal yang sesuai dengan struktur direktori di atas:

makefile
CC      = gcc
CFLAGS  = -std=c11 -Wall -Wextra -Iinclude
SRCDIR  = src
BUILD   = build
SOURCES = $(wildcard $(SRCDIR)/*.c)
OBJECTS = $(patsubst $(SRCDIR)/%.c,$(BUILD)/%.o,$(SOURCES))
TARGET  = $(BUILD)/program

all: $(TARGET)

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

$(BUILD)/%.o: $(SRCDIR)/%.c | $(BUILD)
        $(CC) $(CFLAGS) -c $< -o $@

$(BUILD):
        mkdir -p $(BUILD)

clean:
        rm -rf $(BUILD)

.PHONY: all clean

Mari kita telaah baris-baris Makefile di atas:

  1. Baris 1 (CC = gcc): variabel CC menyimpan nama compiler; bisa diganti ke clang tanpa mengubah baris lain.
  2. Baris 2 (CFLAGS = -std=c11 -Wall -Wextra -Iinclude): flag kompilasi; -Iinclude memberitahu compiler untuk mencari header di direktori include/.
  3. Baris 4 (SOURCES = $(wildcard $(SRCDIR)/*.c)): fungsi wildcard mengumpulkan semua file .c di src/ tanpa harus menulis nama file satu per satu.
  4. Baris 5 (OBJECTS = $(patsubst ...) ): fungsi patsubst mengganti pola src/%.c menjadi build/%.o, sehingga daftar object file dihasilkan secara otomatis.
  5. Baris 7 (all: $(TARGET)): target all adalah default target; menjalankan make tanpa argumen akan membangun target ini.
  6. Baris 9 ($(TARGET): $(OBJECTS)): executable bergantung pada semua object file; bila ada object file berubah, linking diulang.
  7. Baris 10 ($(CC) $(CFLAGS) -o $@ $^): variabel otomatis $@ menyimpan nama target dan $^ menyimpan daftar prerequisite; perintah ini memanggil linker.
  8. Baris 12 ($(BUILD)/%.o: $(SRCDIR)/%.c | $(BUILD)): pattern rule; setiap object file bergantung pada file .c dengan nama yang sama dan pada direktori build/ (prerequisite order-only).
  9. Baris 16 ($(BUILD):): target yang membuat direktori build/ bila belum ada.
  10. Baris 19 (clean:): target yang menghapus direktori build/ beserta seluruh isinya.

Karakter tab di awal baris perintah Makefile bersifat wajib. Bila editor mengganti tab dengan spasi, make akan memberi pesan galat missing separator. Selain itu, pola order-only prerequisite (| $(BUILD)) memastikan direktori build/ dibuat sebelum kompilasi, namun perubahan timestamp direktori tidak memicu kompilasi ulang.

Menjalankan make #

Setelah Makefile tersimpan di root proyek, kita bisa membangun seluruh proyek dengan satu perintah:

bash
make

Output yang Dihasilkan:

text
gcc -std=c11 -Wall -Wextra -Iinclude -c src/main.c -o build/main.o
gcc -std=c11 -Wall -Wextra -Iinclude -c src/util.c -o build/util.o
gcc -std=c11 -Wall -Wextra -Iinclude -o build/program build/main.o build/util.o

Periksa hasil build:

bash
ls -l build
./build/program

Output yang Dihasilkan:

text
total 24
-rw-r--r-- 1 z z 1448 Oct 13 03:48 main.o
-rwxrwxr-x 1 z z 16016 Oct 13 03:48 program
-rw-r--r-- 1 z z 1104 Oct 13 03:48 util.o
1 + 2 = 3

Mari kita telaah baris-baris output di atas:

  1. make pertama: mengompilasi src/main.c menjadi build/main.o, src/util.c menjadi build/util.o, lalu meng-link keduanya menjadi build/program.
  2. ls -l build: menampilkan tiga file — dua object file dan satu executable. Karakter x pada izin program menandakan file tersebut dapat dieksekusi.
  3. ./build/program: menjalankan executable; program mencetak 1 + 2 = 3 sesuai ekspektasi.

Jalankan make sekali lagi tanpa mengubah file:

bash
make

Output yang Dihasilkan:

text
make: Nothing to be done for 'all'.

Pesan ini menunjukkan make sudah pintar — object file lebih baru daripada kode sumber, jadi tidak ada yang perlu dikompilasi ulang.

Perintah make clean menghapus seluruh artefak build:

bash
make clean
ls build 2>&1 | head -3

Output yang Dihasilkan:

text
rm -rf build
ls: cannot access 'build': No such file or directory
  • Selalu tambahkan build/ ke file .gitignore agar artefak build tidak ikut ter-commit ke version control.
  • Jangan menyimpan file sumber .c di dalam build/. Direktori ini boleh dihapus kapan saja; bila ada file sumber tersangkut di dalamnya, kita bisa kehilangan kode.
  • Bila proyek berkembang lebih besar (puluhan file, banyak target, dependensi pustaka eksternal), beralih ke CMake atau Meson yang lebih ekspresif daripada Makefile murni.