Setiap proyek punya ritual perintah yang sama: aktifkan virtualenv, install dependensi, jalankan linter, jalankan test, baru boleh commit. Saya pernah mengetik empat-lima perintah itu setiap kali pindah mesin atau clone repo baru, sampai akhirnya capek sendiri. Solusinya bukan README yang makin panjang, tapi satu file kecil bernama Makefile yang menyimpan semua perintah itu dan memanggilnya cukup dengan make test atau make deploy.
Make sebenarnya lahir buat kompilasi C/C++ di tahun 1970-an, tapi sekarang dipakai jauh lebih luas: ngatur build frontend, bungkus perintah Docker, sampai standarisasi task di mesin lokal. Kamu tidak perlu paham C sama sekali. Kalau kamu masih bolak-balik ngetik docker compose up -d dan pytest -v manual, artikel ini buat kamu.
Struktur Dasar: Target, Prerequisite, dan Recipe
File bernama Makefile berisi kumpulan rule. Setiap rule punya tiga bagian: target (nama perintah), prerequisite (file atau target lain yang harus ada lebih dulu), dan recipe (perintah shell yang dieksekusi). Bentuknya begini:
target: prerequisite
recipe baris satu
recipe baris dua
Jalankan dengan make target. Kalau kamu ketik make tanpa argumen, make menjalankan target pertama di file. Satu aturan yang paling sering bikin orang frustrasi: recipe wajib diawali tab, bukan spasi.
Recipe di Makefile harus pakai tab, bukan spasi. Kalau editor kamu otomatis mengubah tab jadi spasi, matikan dulu untuk file Makefile. Error Makefile:2: *** missing separator. Stop. hampir selalu karena spasi ini.
Contoh Pertama: Dua Target Sederhana
Buat file Makefile di folder kosong, lalu isi dengan ini:
hello:
echo "Halo dari Makefile"
clean:
rm -f *.tmp
Simpan, lalu di terminal jalankan make hello. Kamu akan lihat make menampilkan perintah yang dijalankan dulu, baru hasilnya. Kalau tidak mau perintahnya ikut tercetak, awali recipe dengan @:
hello:
@echo "Halo tanpa menampilkan perintahnya"
Variabel: Jangan Tulis Ulang Path yang Sama
Begitu proyek membesar, kamu akan mengetik path .venv/bin/pytest berulang-ulang. Variabel menyelesaikannya. Ada beberapa cara assignment, tapi tiga yang paling sering dipakai:
VENV := .venv # dievaluasi sekali
PIP = $(VENV)/bin/pip # dievaluasi setiap dipakai
MODE ?= development # dipakai hanya jika belum di-set
Perbedaan = dan := baru terasa kalau nilai variabelnya berubah saat diproses. Untuk pemakaian sehari-hari, := (simply expanded) lebih aman dan hasilnya lebih mudah ditebak. ?= berguna buat memberi default yang bisa di-override lewat command line, misalnya make MODE=production build.
Make juga punya variabel otomatis yang menghemat ketikan. Tiga yang paling sering kamu temui: $@ adalah nama target, $< adalah prerequisite pertama, dan $^ adalah seluruh prerequisite. Contoh praktisnya:
result.txt: data.csv
./process.py $< > $@ # $< = data.csv, $@ = result.txt
bundle.js: a.js b.js c.js
cat $^ > $@ # $^ = a.js b.js c.js
.PHONY: Obat untuk Target yang Bukan File
Make menganggap setiap target adalah nama file. Selama tidak ada file bernama sama dengan targetmu, semuanya jalan normal. Masalah muncul kalau suatu hari ada file bernama test di folder itu — maka make test akan bilang 'test' is up to date dan tidak melakukan apa-apa. Solusinya: deklarasikan target sebagai phony.
.PHONY: test clean lint
test:
pytest -v
clean:
rm -rf __pycache__ .pytest_cache
Target yang masuk daftar .PHONY selalu dijalankan, apa pun kondisi file di folder. Kebiasaan yang bagus: daftarkan semua target yang tidak menghasilkan file — test, lint, clean, deploy — ke .PHONY sekaligus.
Makefile Nyata untuk Proyek Python
Ini Makefile yang saya pakai di proyek Python. Dia ngurus virtualenv, instalasi, lint, format, dan test — semua tinggal satu kata. Kalau belum kenal pytest, baca dulu panduan pytest dari nol.
PYTHON := python3
VENV := .venv
PIP := $(VENV)/bin/pip
PYTEST := $(VENV)/bin/pytest
RUFF := $(VENV)/bin/ruff
.DEFAULT_GOAL := help
.PHONY: help install lint format test run clean
help: ## Tampilkan daftar perintah yang tersedia
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | sort
install: $(VENV)/bin/activate ## Buat venv dan install dependensi
$(PIP) install -r requirements.txt
$(VENV)/bin/activate:
$(PYTHON) -m venv $(VENV)
lint: install ## Jalankan linter
$(RUFF) check .
format: install ## Format kode sesuai aturan
$(RUFF) format .
test: install ## Jalankan seluruh test
$(PYTEST) -v
run: install ## Jalankan aplikasi
$(VENV)/bin/python app.py
clean: ## Hapus artefak build dan cache
rm -rf $(VENV) __pycache__ .pytest_cache .ruff_cache
Perhatikan target $(VENV)/bin/activate. Dia punya recipe sendiri, jadi make hanya membuat virtualenv kalau filenya belum ada — ini memanfaatkan prerequisite untuk menghindari kerja berulang. Semua target praktis bergantung ke install, jadi make test otomatis menyiapkan environment lebih dulu. Dan help dijadikan default lewat .DEFAULT_GOAL, sehingga make polos langsung menampilkan daftar perintah.
Trik komentar ## setelah nama target bikin make (tanpa argumen) menampilkan dokumentasi otomatis. Setiap nambah target baru, cukup tambah komentar ## dan target itu langsung muncul di make help.
Untuk lint dan format saya pakai Ruff, linter yang jauh lebih cepat dari flake8. Kalau penasaran kenapa Ruff menggantikan kombinasi flake8 + black + isort, ada tutorial lengkap Ruff di blog ini. Kamu bebas mengganti baris lint dan format dengan perintah apa pun yang proyekmu pakai.
Membungkus Docker Compose
Kalau proyekmu pakai Docker, Makefile jadi shortcut yang nyaman. Daripada ngetik docker compose up -d --build setiap kali, simpan jadi make up. Ini juga menolong rekan yang baru join: mereka cukup baca make help, tidak perlu hafal semua flag docker compose. Kalau baru mulai dengan Docker, cek dulu tutorial Docker untuk pemula.
.PHONY: up down build logs ps restart
up: ## Jalankan semua service (detached)
docker compose up -d
down: ## Hentikan dan hapus container
docker compose down
build: ## Build ulang image
docker compose build
logs: ## Ikuti log semua service
docker compose logs -f
ps: ## Lihat status container
docker compose ps
restart: ## Restart cepat: down lalu up
docker compose down && docker compose up -d
Menjalankan Task Paralel dengan -j
Kalau Makefile-mu punya beberapa target yang tidak saling bergantung, jalankan barengan dengan flag -j. Misalnya lint dan test yang bisa jalan bersamaan:
make -j4 lint test
Make akan menjalankan hingga 4 job sekaligus selama dependency memungkinkan. Dua flag lain yang wajib kamu tahu: make -n untuk dry-run (lihat perintah apa saja yang akan dijalankan tanpa mengeksekusi), dan make -B untuk memaksa semua target dijalankan ulang.
Kapan Make Tidak Perlu
Make adalah task runner lokal. Dia tidak menggantikan CI/CD. Kalau kamu butuh pipeline yang jalan di cloud, paralel lintas banyak runner, atau trigger otomatis setiap push, GitHub Actions atau GitLab CI jauh lebih tepat. Pola yang umum dipakai: Makefile buat kerjaan lokal yang seragam di semua mesin, lalu file workflow CI tinggal memanggil make lint dan make test. Keduanya saling melengkapi.
Mulai dari yang kecil: buat Makefile dengan tiga target saja — install, test, clean. Pakai di proyek yang sedang kamu kerjakan minggu ini. Seminggu kemudian kamu akan heran kenapa baru sekarang memakainya.