Makefile, make aracının okuduğu ve hangi dosyanın hangi girdilerden, hangi komutlarla üretileceğini tanımlayan metin dosyasıdır. Make bu tanımdan bir bağımlılık grafiği kurar ve yalnız girdileri değişmiş hedefleri yeniden üretir. Araç C ve C++ projelerinin derlenmesi için doğdu, ama tekrarlanan komutları ortak bir arayüzde toplamak için de yaygın olarak kullanılır.
Aşağıdaki örnekler GNU Make içindir. Kural yapısı, Make’in neyi ne zaman yeniden ürettiği, başlık dosyası (header) bağımlılıkları, .PHONY, Docker ve veri hattı gibi derleme dışı kullanımlar ve Make’in sınırları sırayla ele alınıyor.
Make hangi sorunu çözer?
Onlarca kaynak dosyası olan bir projede her değişiklikten sonra her şeyi yeniden derlemek zaman kaybıdır; hangi dosyanın yeniden derlenmesi gerektiğini elle takip etmek ise hataya açıktır. Make bu kararı dosyaların değişiklik zamanına bakarak verir: bir hedef, girdilerinden herhangi birinden daha eskiyse ya da hiç yoksa, tarifi çalıştırılır. Bu davranışa artımlı derleme (incremental build) denir.
Kural yapısı: hedef, bağımlılık ve tarif
Makefile’ın yapı taşı kuraldır (rule). Her kural üç parçadan oluşur:
hedef: bağımlılık1 bağımlılık2
tarif komutu
- Hedef (target): Üretilecek dosya ya da çalıştırılacak eylemin adı.
- Bağımlılıklar (prerequisites): Hedefi üretmek için gereken dosyalar ya da başka hedefler.
- Tarif (recipe): Hedefi üreten kabuk komutları.
Tarif satırları TAB karakteriyle başlamalıdır; boşlukla başlayan bir tarif satırı missing separator hatası verir. GNU Make 3.82 ve sonrasında .RECIPEPREFIX değişkeniyle bu karakter değiştirilebilir (örneğin .RECIPEPREFIX = >), ama varsayılan hâlâ TAB’dır ve çoğu proje varsayılanı kullanır. macOS’ta Xcode Command Line Tools ile gelen make GNU Make 3.81 olduğundan bu değişkeni desteklemez. Editörünüzün Makefile’larda TAB’ı boşluğa çevirmediğinden emin olun.
Basit bir C projesi
Aşağıdaki Makefile üç kaynak dosyasını derleyip bağlar. Derleyici bayraklarının ayrıntısı için sistem programlama yazısındaki GCC uyarı seçeneklerine bakılabilir.
CC := gcc
CFLAGS := -std=c17 -Wall -Wextra -O2
CPPFLAGS := -MMD -MP
TARGET := myapp
OBJS := main.o utils.o network.o
# İlk hedef varsayılan hedeftir: "make" tek başına bunu üretir
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
# Desen kuralı: her .c dosyasından aynı adlı .o dosyası
%.o: %.c
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
# Derleyicinin ürettiği başlık dosyası bağımlılıkları
-include $(OBJS:.o=.d)
.PHONY: clean
clean:
rm -f $(TARGET) $(OBJS) $(OBJS:.o=.d)
Otomatik değişkenler tarifleri kısaltır: $@ hedefin adı, $< ilk bağımlılık, $^ tüm bağımlılıklardır.
clean hedefi rm -f ile dosya siler. Listeye yalnız derleme çıktılarını yazın; kaynak dosyası, * gibi geniş bir joker ya da boş kalabilecek bir değişkenle oluşturulan yol eklemeyin.
Başlık dosyası bağımlılıkları: -MMD ve -MP
Make yalnız Makefile’da yazılı bağımlılıkları bilir. %.o: %.c kuralı .c dosyasına bakar; main.c dosyasının içe aktardığı utils.h başlık dosyası değiştiğinde main.o yeniden derlenmez ve program eski header ile derlenmiş nesne dosyalarıyla bağlanır. “Make yalnız değişeni yeniden derler” cümlesi ancak bağımlılıklar eksiksiz tanımlıysa doğrudur.
Yukarıdaki örnekte bu sorunu GCC çözer:
-MMD, derleme sırasında her nesne dosyası için kullanıcı header’larını listeleyen bir.ddosyası yazar (sistem header’ları dahil edilmez).-MP, her header için boş bir sahte hedef ekler; bir header silindiğinde Make’in “No rule to make target” hatası vermesini önler.-include $(OBJS:.o=.d)bu dosyaları Makefile’a dahil eder. Baştaki-, ilk derlemede henüz var olmayan.ddosyaları için hata verilmemesini sağlar.
Make nasıl karar verir?
Make, istenen hedeften başlayarak bağımlılıkları özyinelemeli olarak çözer. A, B’ye; B de C’ye bağlıysa önce C, sonra B, en son A üretilir. Her adımda hedefin değişiklik zamanı bağımlılıklarınkiyle karşılaştırılır; güncel olan hedefin tarifi atlanır.
make -j4, bağımlılık grafiği izin verdiğinde dört işi paralel yürütür. Kazanç; hedeflerin gerçekten bağımsız olmasına, I/O yüküne ve tariflerin paralel çalışmaya güvenli olmasına bağlıdır. Eksik tanımlanmış bir bağımlılık seri derlemede fark edilmeyip paralel derlemede rastgele hatalara yol açabilir.
.PHONY: dosya olmayan hedefler
clean, test, deploy gibi hedefler bir dosya üretmez, bir eylem çalıştırır. Make ise her hedefi varsayılan olarak dosya adı kabul eder. Dizinde clean ya da build adında bir dosya veya dizin varsa ve bağımlılığı yoksa Make o hedefi güncel sayar ve tarifi çalıştırmaz.
.PHONY listesine yazılan hedefler dosya sisteminde aranmaz; çağrıldıklarında tarifleri her seferinde çalışır:
# Önceki örneğin devamı: TARGET orada tanımlı
.PHONY: test deploy
test:
./run_tests.sh
deploy: $(TARGET)
rsync -av $(TARGET) server:/opt/myapp/
Değişkenler, koşullar ve fonksiyonlar
GNU Make’te ifeq/ifdef ile koşul, wildcard, patsubst, foreach gibi yerleşik fonksiyonlarla dosya listesi ve metin işleme yapılabilir. Yine de Make bir programlama dili gibi kullanılmaya uygun değildir. Döngü ve karmaşık mantık gerektiren adımları bir kabuk betiğine ya da Python betiğine taşıyıp Makefile’dan çağırmak hem okunabilirliği hem de hata ayıklamayı kolaylaştırır.
Her tarif satırının ayrı bir kabukta çalıştığını da unutmayın: bir satırdaki cd build sonraki satırı etkilemez. Aynı kabukta çalışması gereken komutları && ile tek satırda birleştirin ya da GNU Make 3.82 ile gelen .ONESHELL özel hedefini kullanın.
Derleme dışı kullanım alanları
Docker iş akışı
Make, uzun Docker komutlarını kısa ve paylaşılabilir hedefler hâline getirmek için de kullanılır. İmaj adını tek bir değişkende tutmak, etiketleme ile gönderme arasındaki tutarsızlığı önler:
IMAGE := registry.example.com/myapp:latest
.PHONY: build run push
build:
docker build -t $(IMAGE) .
run:
docker run --rm -p 8080:8080 $(IMAGE)
push: build
docker push $(IMAGE)
Bu hedefler dosya üretmediği için .PHONY olarak işaretlenmiştir; aksi hâlde projede build/ dizini olduğunda make build hiçbir şey yapmaz.
Veri hattı
Make’in artımlı çalışması veri işleme adımlarında da işe yarar: yalnız yeni ya da değişmiş ham dosyalar yeniden işlenir.
# Tarif yarıda hata verirse yarım kalmış hedef dosyasını sil
.DELETE_ON_ERROR:
RAW := $(wildcard raw/*.csv)
PROCESSED := $(patsubst raw/%.csv,processed/%.csv,$(RAW))
report.pdf: $(PROCESSED)
python generate_report.py $^ -o $@
processed/%.csv: raw/%.csv
@mkdir -p $(@D)
python clean_data.py $< > $@
Burada dikkat edilecek üç nokta vardır:
report.pdf: processed/*.csvgibi bir joker, temiz bir çalışmadaprocessed/boş olduğu için hiçbir dosyayla eşleşmez. GNU Make eşleşmeyen jokeri olduğu gibi bırakır veprocessed/*.csvifadesini tek bir dosya adı gibi ele alır. Desen kuralı yoksa sonuç “No rule to make target” hatasıdır; yukarıdaki gibi bir desen kuralı varken GNU Make 3.81 ile yapılan denemede adı gerçekten*.csvolan tek bir dosya üretildi ve yalnız ilk ham dosya işlendi. Hedef listesini var olan ham dosyalardanwildcardvepatsubstile türetmek bu sorunu ortadan kaldırır.> $@yönlendirmesi hedef dosyayı her çalıştırmada üzerine yazar. Betik yarıda hata verirse geride yarım bir dosya kalır ve bir sonraki çalıştırmada güncel sayılabilir..DELETE_ON_ERROR:bu durumda hedefi siler.$(@D)hedefin dizinidir;mkdir -pilk çalıştırmadaprocessed/dizinini oluşturur.
Böyle bir hattı düzenli aralıklarla çalıştırmak için make report.pdf komutu bir cron job olarak tanımlanabilir; her çalıştırmada yalnız yeni ya da değişmiş ham dosyalar işlenir.
Make her sistemde kurulu mu?
Hayır. Make Unix dünyasının eski bir aracıdır ama varsayılan kurulumun parçası olması garanti değildir. Minimal Debian ve Ubuntu kurulumlarında, Alpine tabanlı ve çoğu Docker imajında make yoktur; Debian/Ubuntu’da make ya da build-essential paketiyle, Alpine’de make paketiyle kurulur. macOS’ta Xcode Command Line Tools ile gelir. Windows’ta yerleşik değildir; WSL ya da MSYS2 gibi bir ortam üzerinden kullanılır. Ayrıca BSD sistemlerdeki make GNU Make değildir; GNU’ya özgü söz dizimi kullanan bir Makefile orada gmake ile çalıştırılmalıdır.
Alternatifler
- npm scripts: JavaScript projelerinde komutları adlandırmak için yeterlidir, ama dosya tabanlı bağımlılık takibi yapmaz.
- Gradle ve Maven: JVM ekosisteminin derleme araçlarıdır; bağımlılık indirme, test ve paketleme adımlarını kendi model ve eklentileriyle yönetir.
- CMake: Kendisi derleme yapmaz; Makefile ya da Ninja dosyası üreten bir derleme sistemi üreticisidir. Platformlar arası C ve C++ projelerinde Make’in üst katmanı olarak kullanılır.
- Bazel: Büyük ve çok dilli depolar için tasarlanmış, tekrarlanabilir derlemeye odaklanan bir derleme sistemidir; öğrenme maliyeti yüksektir.
- just: Bir derleme sistemi değil, görev çalıştırıcıdır (command runner). Dosya zamanlarını karşılaştırmaz, artımlı derleme yapmaz; buna karşılık
.PHONYgerektirmez ve tarif söz dizimi daha sadedir. Make’i yalnız komut kısayolu olarak kullanan projeler için uygun bir seçenektir.
Sınırlar
Make’in modeli basittir ve 1976’dan beri kullanılıyor; ama sınırları da vardır:
- Söz dizimi: TAB zorunluluğu, boşluk içeren dosya adlarının zor yönetilmesi ve satır satır ayrı kabukta çalışma ilk kullanımda hataya yol açar.
- Taşınabilirlik: Tarifler kabuk komutlarıdır;
rm,mkdir -pgibi komutlar Windows’un yerel kabuğunda çalışmaz. GNU Make ile BSD make arasında da söz dizimi farkları vardır. - Zaman damgasına dayanma: Karar dosya içeriğine değil değişiklik zamanına göre verilir. Saat farkı olan ağ dosya sistemleri ya da zaman damgasını koruyan kopyalama işlemleri yanlış “güncel” kararlarına yol açabilir.
- Karmaşık mantık: Koşul ve fonksiyonlar büyüdükçe Makefile’ın okunması ve hata ayıklaması zorlaşır.
Sonuç
Make; dosya üreten adımların birbirine bağlı olduğu, artımlı derlemenin zaman kazandırdığı C/C++ projelerinde ve veri hatlarında hâlâ sade ve etkili bir araçtır; bağımlılıkları eksiksiz tanımlamak (-MMD -MP) ve eylem hedeflerini .PHONY ile işaretlemek doğru çalışmasının ön koşuludur. Yalnız komut kısayolu gerekiyorsa just gibi bir görev çalıştırıcısı, platformlar arası büyük C++ projelerinde CMake, JVM projelerinde Gradle veya Maven daha uygun olabilir.