6 poin oleh GN⁺ 2025-06-21 | 1 komentar | Bagikan ke WhatsApp
  • Makefile terutama digunakan untuk mengotomatiskan kompilasi C/C++ pada proyek besar dengan menjalankan ulang hanya langkah build yang diperlukan berdasarkan perubahan file
  • Make membandingkan timestamp filesystem dari target dan dependensi, lalu menjalankan perintah hanya saat target tidak ada atau dependensinya lebih baru
  • Aturan dasar terdiri dari targets: prerequisites dan perintah yang diindentasi dengan tab; indentasi spasi dapat menyebabkan make gagal
  • Alur build dapat diperluas dengan variabel, wildcard, variabel otomatis, aturan pola, kondisional, fungsi, recursive make, include, .PHONY, dan .DELETE_ON_ERROR
  • Contoh Cookbook membuat sumber C/C++ di src/ menjadi artefak di build/, serta menyusun manajemen dependensi otomatis dengan -MMD -MP dan -include

Situasi saat Makefile dibutuhkan

  • Makefile digunakan dalam program besar untuk menentukan file mana yang perlu dikompilasi ulang
  • Contoh utama dalam tutorial ini adalah kompilasi file C/C++
  • Make juga dapat digunakan di luar kompilasi untuk tugas yang perlu menjalankan serangkaian perintah berdasarkan apakah file berubah atau tidak
  • Untuk C/C++, alat build lain yang diperkenalkan adalah SCons, CMake, Bazel, dan Ninja
  • Java memiliki Ant, Maven, dan Gradle, sementara bahasa seperti Go, Rust, dan TypeScript memiliki alat build sendiri
  • Bahasa interpreted seperti Python, Ruby, dan JavaScript murni tidak perlu dikompilasi ulang saat file berubah, sehingga alat seperti Makefile tidak bersifat wajib

Model eksekusi dasar Make

  • Contoh berjalan dengan membuat file Makefile di terminal yang sudah terpasang make, lalu menjalankan make di direktori tersebut
  • Aturan dasar mengikuti bentuk berikut
    • targets: prerequisites
    • command yang diawali dengan tab
  • Target biasanya berupa nama file, dan perintah adalah langkah untuk membuat target tersebut
  • Prerequisites adalah file yang harus ada sebelum perintah dijalankan, dan juga disebut dependencies
  • Baris perintah dalam Makefile harus diindentasi dengan karakter tab; jika memakai spasi, make akan gagal

Perbandingan waktu antara target dan dependensi

  • make hello menjalankan perintah yang terhubung ke target hello, tetapi jika file hello sudah ada, perintah tidak dijalankan
  • Target dan nama file terhubung langsung; umumnya perintah pada target membuat file dengan nama yang sama dengan target
  • Dalam aturan blah: blah.c, perintah kompilasi hanya dijalankan ketika blah tidak ada atau blah.c lebih baru daripada blah
  • Make tidak melacak riwayat perubahan secara langsung, melainkan menggunakan timestamp filesystem sebagai heuristik
  • Jika Anda mengubah file lalu mengubah waktu modifikasinya ke masa lalu, Make dapat keliru menganggap file tersebut tidak berubah

Target default dan clean

  • Jika target tidak ditentukan pada make, target pertama dalam Makefile dijalankan sebagai target default
  • Target all digunakan untuk membuat beberapa target sekaligus; jika diletakkan sebagai aturan pertama, seluruh target akan dibuild hanya dengan menjalankan make
  • clean biasanya digunakan sebagai target untuk menghapus artefak build, tetapi bukan kata khusus yang direservasi oleh Make
  • Jika clean bukan target pertama dan bukan dependensi dari target lain, ia harus dipanggil secara eksplisit seperti make clean agar dijalankan
  • Jika file bernama clean benar-benar ada, target clean mungkin tidak dijalankan; hal ini diselesaikan nanti dengan .PHONY

Variabel dan pemrosesan string

  • Variabel Make hanya dapat berisi string, dan tutorial ini umumnya merekomendasikan penggunaan :=
  • Referensi variabel menggunakan format $(name) atau ${name}
  • Tanda kutip tunggal dan ganda tidak memiliki makna khusus di Make itu sendiri; keduanya hanya karakter yang masuk ke variabel
  • Cara mendefinisikan variabel secara garis besar ada dua
    • =: variabel rekursif, yang mencari dan mengekspansi variabel saat digunakan
    • :=: variabel dengan ekspansi sederhana, yang hanya mengekspansi nilai yang sudah didefinisikan pada saat definisi
  • ?= hanya menetapkan nilai pada variabel yang belum disetel
  • += menambahkan nilai ke variabel
  • Variabel yang tidak didefinisikan diperlakukan sebagai string kosong
  • Spasi di akhir baris tidak dihapus, dan untuk membuat variabel spasi satu karakter dapat digunakan teknik seperti $(nullstring)

Wildcard dan variabel otomatis

  • Di Make, * dan % sama-sama disebut wildcard, tetapi maknanya berbeda
  • * mencari nama file yang cocok di filesystem; daripada dipakai langsung dalam definisi variabel, lebih aman membungkusnya dengan fungsi wildcard seperti $(wildcard *.o)
  • Jika * tidak cocok dengan file apa pun, di luar fungsi wildcard ia dapat tetap tersisa apa adanya
  • % digunakan dalam aturan dan fungsi tertentu, dan bagian yang cocok disebut stem
  • Variabel otomatis yang sering digunakan adalah sebagai berikut
    • $@: nama target
    • $?: dependensi yang lebih baru daripada target
    • $^: semua dependensi
    • $<: dependensi pertama

Aturan implisit dan aturan pola

  • Make memiliki aturan implisit untuk kompilasi C, sehingga build dapat berjalan tanpa menulis perintah kompilasi secara eksplisit
  • Aturan implisit yang umum berbentuk sebagai berikut
    • n.o dibuat dari n.c dalam bentuk $(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@
    • n.o dibuat dari n.cc atau n.cpp dalam bentuk $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@
    • n dibuat dari n.o dengan perintah linker
  • Variabel penting dalam aturan implisit adalah CC, CXX, CFLAGS, CXXFLAGS, CPPFLAGS, dan LDFLAGS
  • Aturan pola statis berbentuk targets...: target-pattern: prereq-patterns ..., dan memasukkan stem yang cocok dengan % pada target ke pola dependensi
  • Aturan pola menyertakan % pada target seperti %.o: %.c, dan dapat digunakan seperti aturan implisit buatan sendiri
  • Aturan double-colon :: dapat mendefinisikan beberapa aturan untuk target yang sama, tetapi jarang digunakan

Eksekusi perintah dan penanganan error

  • Jika @ ditambahkan di depan perintah, perintah itu sendiri tidak ditampilkan
  • Menjalankan make -s berperilaku seolah setiap baris diawali dengan @
  • Setiap baris perintah efektif dijalankan di shell baru, sehingga cd .. pada satu baris tidak memengaruhi baris berikutnya
  • Untuk mempertahankan status shell yang sama, tulis dalam satu baris seperti cd ..; echo \pwd`` atau sambungkan dengan backslash
  • Shell default adalah /bin/sh, dan dapat diganti seperti SHELL=/bin/bash
  • Untuk memakai $ milik variabel shell di dalam Makefile, gunakan $$
  • Penanganan error berbeda tergantung cara menjalankannya
    • make -k: tetap menjalankan tugas lain yang memungkinkan meskipun ada error
    • - di depan perintah: mengabaikan error pada perintah tersebut
    • make -i: menerapkan pengabaian error pada semua perintah
  • Jika Make dihentikan dengan ctrl+c, Make menghapus target terbaru yang baru saja dibuat

Recursive make dan variabel lingkungan

  • Saat memanggil make lagi di dalam Makefile, gunakan variabel khusus $(MAKE) alih-alih make
  • $(MAKE) meneruskan flag make dan menangani pengaruh flag dengan tepat
  • Saat Make dimulai, variabel lingkungan yang disetel pada saat eksekusi otomatis dibuat sebagai variabel Make
  • Direktif export menyetel variabel ke lingkungan perintah shell untuk semua recipe
  • Agar variabel juga dipakai di make turunan, variabel tersebut harus di-export
  • .EXPORT_ALL_VARIABLES dapat digunakan untuk mengekspor semua variabel
  • Jika beberapa target ditentukan sekaligus seperti make clean run test, target dijalankan dalam urutan clean, run, test

Kondisional dan fungsi

  • Kondisional terdiri dari ifeq, ifneq, ifdef, ifndef, else, dan endif
  • ifdef tidak mengekspansi referensi variabel, hanya memeriksa apakah variabel sudah didefinisikan
  • $(MAKEFLAGS) dan findstring dapat digunakan untuk memeriksa apakah flag seperti make -i diteruskan
  • Fungsi Make terutama digunakan untuk pemrosesan teks, dan dipanggil dalam bentuk $(fn, arguments) atau ${fn, arguments}
  • Fungsi utama adalah sebagai berikut
    • subst: substitusi string
    • patsubst: substitusi kata yang cocok dengan pola
    • foreach: transformasi daftar kata yang dipisahkan spasi
    • if: menggunakan argumen kedua jika argumen pertama tidak kosong, jika tidak menggunakan argumen ketiga
    • call: memanggil variabel seperti fungsi dasar dan menggunakan parameter seperti $(1), $(2)
    • shell: memanggil shell, tetapi mengubah newline menjadi spasi
    • filter: memilih elemen dalam daftar yang cocok dengan pola tertentu
    • filter-out: memilih elemen yang tidak cocok dengan pola tertentu

include, vpath, .PHONY, .DELETE_ON_ERROR

  • include filenames... membuat satu atau beberapa Makefile lain dibaca
  • include berguna saat membaca Makefile yang dibuat dengan opsi seperti -M milik gcc
  • vpath <pattern> <directories> menentukan direktori untuk mencari file dependensi tertentu
  • Variabel VPATH dapat menentukan perilaku jalur pencarian serupa secara global
  • Backslash \ digunakan untuk membagi perintah panjang menjadi beberapa baris
  • Jika .PHONY ditambahkan ke target, Make tidak akan keliru menganggap target tersebut sebagai file dengan nama yang sama
  • .DELETE_ON_ERROR menghapus target dari aturan tersebut jika perintah mengembalikan status keluar bukan 0 saat aturan dijalankan
  • .DELETE_ON_ERROR tidak hanya berlaku pada target tepat sebelumnya seperti PHONY, melainkan pada semua target

Contoh Makefile Cookbook

  • Contoh Cookbook menyediakan template Makefile yang sesuai untuk proyek skala menengah
  • Jika file C/C++ dimasukkan ke folder src/, Makefile otomatis menentukan target kompilasi dan dependensinya
  • Konfigurasi variabel utama adalah sebagai berikut
    • TARGET_EXEC := final_program
    • BUILD_DIR := ./build
    • SRC_DIRS := ./src
  • SRCS menggunakan find dan $(shell ...) untuk mencari file *.cpp, *.c, dan *.s
  • OBJS menambahkan BUILD_DIR di depan setiap file sumber dan menambahkan .o untuk membuat jalur artefak build
  • DEPS mengubah .o menjadi .d untuk membuat daftar Makefile dependensi
  • INC_DIRS mencari semua direktori di bawah src/, dan INC_FLAGS menambahkan prefiks -I ke setiap direktori
  • CPPFLAGS := $(INC_FLAGS) -MMD -MP mengonfigurasi GCC agar membuat file dependensi .d
  • Sumber C dan C++ masing-masing membuat file .o di bawah build/ dengan aturan pola terpisah, dan direktori yang diperlukan dibuat dengan mkdir -p $(dir $@)
  • clean dideklarasikan sebagai .PHONY dan menghapus direktori build dengan rm -r $(BUILD_DIR)
  • -include $(DEPS) menyertakan file .d, tetapi menekan error yang muncul saat file belum ada seperti pada build pertama

1 komentar

 
GN⁺ 2025-06-21
Komentar Hacker News
  • Ada beberapa flag make yang kurang dikenal tetapi berguna. Sinkronisasi output membuat stdout/stderr baru ditampilkan setelah target selesai, sehingga mengurangi masalah log yang tercampur dan sulit dibaca saat build paralel
    make --output-sync=recurse -j10
    Pada sistem multi-pengguna yang sibuk, jumlah job -j saja mungkin tidak cukup, jadi paralelisme juga bisa dibatasi berdasarkan load average: make -j10 --load-average=10
    --shuffle, yang mengacak urutan penjadwalan target, juga berguna di CI untuk menemukan dependensi yang hilang di Makefile: make --shuffle # or --shuffle=seed/reverse

    • Kalau memang “kurang dikenal”, alangkah baiknya para penulis make menyusun daftar opsi di suatu tempat dan mendistribusikannya bersama program. Kalau berbentuk file teks yang bisa dibaca pengguna atau bahasa typesetting, akses terhadap pengetahuan akan jauh lebih baik
    • Yang paling sering dipakai adalah -B, opsi yang memaksa semuanya di-build ulang tanpa syarat
    • Saya pernah melihat make -j beberapa kali membuat mesin berada dalam kondisi DoS, jadi saya menganggapnya hampir seperti bug
    • Kalau ini sistem multi-pengguna yang sibuk, bukankah scheduler sistem operasi seharusnya bisa menanganinya?
    • Meskipun berguna, ini tidak portabel. Sebaiknya jangan dipakai di luar proyek mainan pribadi yang tidak didistribusikan
  • Sekitar tahun 1985 di Boston University Graphics lab, saya melihat seseorang memakai Makefile untuk menggerakkan pembuatan renderer 3D untuk animasi. Ia orang Lisp dan sedang menangani generasi prosedural awal serta sistem aktor 3D, dan Makefile-nya sangat elegan, total sekitar 10 baris
    Hanya dengan dependensi tanggal file sederhana, ia membuat ratusan animasi; Lisp membuat bentuk 3D untuk tiap frame, lalu Make menghasilkan frame-nya
    Karena itu tahun 1985, masa ketika hampir tidak ada hal-hal yang sekarang kita anggap biasa dalam 3D dan animasi, semua orang dibuat takjub. Setelah itu, seingat saya, ia menulis renderer 3D untuk Iron Giant dan juga berperan penting di Coraline. Namanya Brian Gardner

  • Make adalah salah satu alat yang saya sangat syukuri telah saya pelajari di awal karier. Sekarang saya tidak banyak memakainya, tetapi ia menunjukkan kekuatan sistem deklaratif dibanding sistem imperatif
    Saya menyadari bahwa ide ini dapat diperluas secara alami ke pekerjaan lain. Melihat gambar di bagian atas situs ini, penulisnya tampaknya merasakan hal serupa: resep masakan bisa dipahami lebih baik jika dilihat secara deklaratif seperti Makefile, daripada sebagai urutan perintah bergaya skrip seperti resep tradisional
    Tulisan terkait: https://blog.gpkb.org/posts/cooking-with-make/
    Saya selalu menulis resep agar bisa dibaca seperti Makefile dan membawanya ke dapur; saya penasaran apakah ada yang pernah menata huruf atau menampilkannya seperti ini. Saat membaca resep baru, sepertinya akan sangat menghemat waktu kalau tidak perlu mengubah skrip menjadi Makefile di kepala

    • Hal bagus dari pendekatan ini adalah pengguna pada dasarnya menafsirkan sendiri graf dependensi dan menjadi eksekutor, sehingga punya lebih banyak kendali. Dalam contoh memasak, struktur deklaratif lebih jelas menunjukkan titik-titik ketika pilihan tentang apa yang akan dilakukan berikutnya masih terbuka, dan memberi kebebasan untuk mencerminkan batasan eksternal yang tidak ditulis secara formal di Makefile, misalnya urutan yang memudahkan mencuci peralatan
      Tentu saja biayanya adalah harus menyelesaikan sendiri graf dependensi. Kalau hanya ingin mengikuti langkah berurutan yang sudah diserialisasi sebelumnya, pekerjaan yang ditanggung jadi lebih banyak
  • Artikel itu mengatakan kebanyakan resep tidak ditandai sebagai .PHONY, dan memakai itu seolah sebagai alasan untuk tidak membahasnya dalam tutorial, tetapi itu alasan yang lemah. Alat harus mengajarkan cara penggunaan yang benar
    Di tim, karena kami memakai make sebagai runner tugas, saya pernah dikritik karena menambahkan dan memelihara .PHONY untuk semua resep
    Panduan gaya Makefile dari Clark Grubb bagus: https://clarkgrubb.com/makefile-style-guide
    Saya penasaran apakah ada yang memakai panduan gaya ini. Saya juga penasaran apakah resep phony biasanya ditandai di titik deklarasinya, atau diletakkan sebagai daftar besar di bagian atas file. Akan bagus kalau ada linter yang memaksakan ini

    • Saya sudah membacanya, dan secara umum itu dokumen yang bagus. Namun ada beberapa hal yang tidak saya setujui
      Menggunakan -o pipefail secara kebiasaan bukan ide bagus. Pipefail berguna, tetapi ia merusak pemfilteran output dengan grep, hal yang paling sering dilakukan orang dalam pipeline. Lebih baik ditambahkan hanya pada resep yang membutuhkannya
      Menandai target yang bukan file sebagai .PHONY memang secara ketat benar, tetapi biasanya tidak diperlukan. Jika targetnya banyak, itu menambah verbosity yang tidak perlu pada Makefile, jadi menurut saya lebih baik ditambahkan hanya saat diperlukan
      Pada resep yang menghasilkan beberapa file output, dulu file dummy/flag merupakan standar ketika aturan pola tidak cocok, tetapi mulai GNU Make 4.3, target grup didukung secara native. Ini juga sudah tersedia di Ubuntu 22.04 LTS: https://www.gnu.org/software/make/manual/html_node/Multiple-...
  • Hal yang menarik belakangan ini adalah CMake menilai bahwa Makefile tidak cocok untuk proyek yang memakai modul C++20, dan Ninja-lah yang tepat. https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules....
    Pada dasarnya, tampaknya mereka menganggap mendefinisikan dependensi target secara statis terlalu sulit atau mustahil. Sekarang ini ditangani secara dinamis dengan alat seperti clang-scan-deps: https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang...

    • Koreksi saya kalau salah, tetapi batasan terkait Makefile ini tampaknya sepenuhnya merupakan pilihan CMake, atau setidaknya akibat kurangnya relawan untuk menambahkan dukungan pada generator Makefile. Ninja sendiri juga tidak mendukung modul C++: https://github.com/ninja-build/ninja/issues/2457
      Bahkan, karena Ninja mengharuskan semua dependensi didefinisikan secara statis, dalam hal ini fiturnya lebih sedikit daripada Make biasa
    • Sejujurnya, modul hampir seperti bencana
  • Make memang punya tempat sebagai alat build untuk codebase C berukuran besar. Namun sering kali ia diperlakukan seperti “task runner per proyek” serbaguna, dan untuk itu kurang cocok. Bahkan kondisi sederhana pun sulit
    Misalnya, saya beberapa kali melihat upaya beritikad baik untuk membungkus Terraform dengan Make, tetapi akhirnya tidak berjalan baik

    • Make bukan task runner serbaguna. Ia lebih seperti cara umum untuk mengubah skrip shell linear menjadi dependensi deklaratif. Alat umum untuk shell
    • Menurut saya, menyebutnya alat build untuk codebase C besar juga sudah tidak tepat lagi. Selama 20 tahun terakhir sudah dibuat sistem build yang lebih kokoh dan terdefinisi dengan baik, jadi sudah waktunya diperbarui
    • Apakah ada task runner serbaguna yang bagus? Edit: sepertinya saya benar-benar salah memahami istilah “task runner”
  • Ada alat modern yang bagus bernama just untuk menggantikan bagian Makefile yang cenderung berantakan: https://github.com/casey/just

    • Ia menggantikan sisi Make sebagai “daftar skrip shell pendek”, tetapi tidak menggantikan bagian yang benar-benar berguna, yaitu menjalankan “hanya aturan yang perlu dijalankan ulang”
    • Ada pilihan lain juga: Task(Go) https://github.com/go-task/task, Cake(C#) https://github.com/cake-build/cake, Rake(Ruby) https://github.com/ruby/rake
      Sebagai konsep yang sepenuhnya berbeda, ada juga Makedown yang dibahas di HN 8 bulan lalu: https://news.ycombinator.com/item?id=41825344
    • Meski alat-alat ini memosisikan diri sebagai alternatif make, secara pribadi saya menganggapnya sepenuhnya berbeda dan bukan objek perbandingan. make berfokus pada pembuatan artefak dan tidak membangun ulang sesuatu yang sudah dibuat, sedangkan just adalah pelaksana perintah
    • Keunggulan utama memakai Make sebagai pelaksana perintah adalah bahwa ia merupakan alat standar yang terpasang “di mana-mana”. Walaupun alat pengganti tampak lebih enak dipakai, saya belum pernah merasa manfaatnya cukup besar untuk membenarkan pemasangan alat tambahan
    • Task juga alternatif lain. Namun secara pribadi saya hanya pernah memakainya untuk proyek hobi C yang sederhana, jadi sulit mengatakan apakah ia tetap baik saat skalanya membesar: https://taskfile.dev/
  • Makefile itu bagus, tetapi sebaiknya jangan terlalu tenggelam di dalamnya. Beberapa tahun lalu saya mencoba membuat framework GNU Make murni, lalu menyadari bahwa pada dasarnya saya sedang menciptakan ulang autoconf; saat itulah saya memahami mengapa GNU autotools dibuat
    Makefile secara aneh merupakan Turing tarpit yang mirip Lisp. GNU Make bahkan punya fitur metaprogramming, sehingga sulit menahan dorongan untuk memetaprogramkan sistem yang tidak suci di dalam Makefile. Fakta bahwa GNU Make terpasang sangat luas juga makin menggoda

  • Saya adalah pembuat sekaligus salah satu maintainer Task, alternatif untuk Make. Proyek ini sudah ada lebih dari 8 tahun dan terus berkembang, jadi kalau Anda mencari sesuatu yang baru, silakan coba. Jangan ragu bertanya
    https://taskfile.dev/
    https://github.com/go-task/task

  • Saya penasaran apakah ada yang pernah memakai tup: https://gittup.org/tup/ex_dependencies.html
    Ini adalah sistem build yang secara otomatis menentukan dependensi berdasarkan akses file system, sehingga bisa dipakai bersama compiler atau alat apa pun