TEKNİK YAZI / Developer Tools ve Açık Kaynak

Makefile Nedir? Hedefler, Bağımlılıklar ve .PHONY

Makefile ile hedef, bağımlılık ve tarif kurallarını yazmak: artımlı derleme, header bağımlılıkları, .PHONY, Docker ve veri hattı örnekleri, Make'in sınırları.

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
  1. Hedef (target): Üretilecek dosya ya da çalıştırılacak eylemin adı.
  2. Bağımlılıklar (prerequisites): Hedefi üretmek için gereken dosyalar ya da başka hedefler.
  3. 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:

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:

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

Sınırlar

Make’in modeli basittir ve 1976’dan beri kullanılıyor; ama sınırları da vardı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.

Birincil kaynaklar