6 puan yazan GN⁺ 2025-06-21 | 1 yorum | WhatsApp'ta paylaş
  • Makefile, dosya değişikliklerine göre yalnızca gerekli build adımlarını yeniden çalıştırarak, çoğunlukla büyük projelerde C/C++ derlemesini otomatikleştirmek için kullanılır
  • Make, hedefler ile bağımlılıkların dosya sistemi zaman damgalarını karşılaştırır; hedef yoksa veya bağımlılıklar daha yeniyse komutları çalıştırır
  • Temel kural targets: prerequisites ve tab ile girintilenmiş komutlardan oluşur; boşlukla girintileme make hatasına yol açabilir
  • Değişkenler, wildcard’lar, otomatik değişkenler, pattern rule’lar, koşullu ifadeler, fonksiyonlar, recursive make, include, .PHONY, .DELETE_ON_ERROR ile build akışı genişletilebilir
  • Cookbook örneği, src/ içindeki C/C++ kaynaklarını build/ çıktıları haline getirir ve -MMD -MP ile -include kullanarak otomatik bağımlılık yönetimi kurar

Makefile’a ne zaman ihtiyaç duyulur

  • Makefile, büyük programlarda hangi dosyaların yeniden derlenmesi gerektiğini belirlemek için kullanılır
  • Tutorial’ın ana örneği C/C++ dosyalarını derlemek üzerinedir
  • Dosya değişip değişmediğine göre bir dizi komut çalıştırılması gereken işlerde Make, derleme dışı amaçlarla da kullanılabilir
  • C/C++ için diğer build araçları olarak SCons, CMake, Bazel, Ninja tanıtılır
  • Java’da Ant, Maven, Gradle bulunur; Go, Rust, TypeScript gibi dillerin ise kendi build araçları vardır
  • Python, Ruby, saf JavaScript gibi yorumlanan dillerde dosya değiştiğinde yeniden derleme gerekmediğinden Makefile benzeri araçlar zorunlu değildir

Make’in temel çalışma modeli

  • Örnekler, make kurulu bir terminalde Makefile dosyası oluşturduktan sonra ilgili dizinde make çalıştırma şeklinde işler
  • Temel kural aşağıdaki biçimi izler
    • targets: prerequisites
    • Tab ile başlayan command
  • Hedef (target) genellikle bir dosya adıdır; komutlar bu hedefi oluşturmak için gereken adımlardır
  • Bağımlılıklar (prerequisites) komut çalışmadan önce var olması gereken dosyalardır; dependencies olarak da adlandırılır
  • Makefile’daki komut satırları mutlaka tab karakteri ile girintilenmelidir; boşluk kullanılırsa make başarısız olur

Hedefler ve bağımlılıklar arasında zaman karşılaştırması

  • make hello, hello hedefine bağlı komutları çalıştırır; ancak hello dosyası zaten varsa komutları çalıştırmaz
  • Hedef ile dosya adı doğrudan bağlantılıdır; genellikle hedefin komutu, hedefle aynı ada sahip dosyayı oluşturur
  • blah: blah.c kuralında derleme komutu yalnızca blah yoksa veya blah.c, blahdan daha yeniyse çalıştırılır
  • Make, değişiklik geçmişini doğrudan izlemez; heuristic olarak dosya sistemi zaman damgalarını kullanır
  • Bir dosyayı düzenledikten sonra değiştirilme zamanını geçmişe alırsanız Make, dosyanın değişmediğine yanlış karar verebilir

Varsayılan target ve clean

  • make komutuna hedef belirtilmezse Makefile’daki ilk hedef varsayılan hedef olarak çalıştırılır
  • all hedefi birden fazla hedefi tek seferde oluşturmak için kullanılır; ilk kural olarak konursa yalnızca make çalıştırmak tüm hedefleri build eder
  • clean genellikle build çıktılarını silen hedef olarak kullanılır, ancak Make’in özel bir reserved word’ü değildir
  • clean ilk hedef değilse ve başka bir hedefin bağımlılığı değilse çalışması için make clean gibi açıkça çağrılması gerekir
  • Gerçekte clean adlı bir dosya varsa clean hedefi çalışmayabilir; bu durum daha sonra .PHONY ile çözülür

Değişkenler ve string işleme

  • Make değişkenleri yalnızca string tutabilir; tutorial genellikle := kullanımını önerir
  • Değişken referansları $(name) veya ${name} biçimini kullanır
  • Tek tırnak ve çift tırnak Make’in kendisi için özel bir anlam taşımaz; değişkene giren karakterlerden ibarettir
  • Değişken tanımlama biçimleri temelde ikiye ayrılır
    • =: recursive değişkendir; kullanıldığında değişkenleri bulup genişletir
    • :=: simply expanded değişkendir; tanımlandığı anda yalnızca o zamana kadar tanımlanmış değerleri genişletir
  • ?= yalnızca henüz ayarlanmamış değişkenlere değer atar
  • += değişkene değer ekler
  • Tanımlanmamış değişkenler boş string olarak ele alınır
  • Satır sonundaki boşluklar kaldırılmaz; tek boşlukluk bir değişken oluşturmak için $(nullstring) gibi teknikler kullanılabilir

Wildcard’lar ve otomatik değişkenler

  • Make’te * ve % ikisi de wildcard olarak adlandırılır, ancak anlamları farklıdır
  • *, dosya sisteminde eşleşen dosya adlarını arar; doğrudan değişken tanımında kullanmak yerine $(wildcard *.o) gibi wildcard fonksiyonu içine almak daha güvenlidir
  • * hiçbir dosyayla eşleşmezse wildcard fonksiyonunun içinde olmadığı durumlarda olduğu gibi kalabilir
  • %, kurallarda ve belirli fonksiyonlarda kullanılır; eşleşen kısma stem denir
  • Sık kullanılan otomatik değişkenler şunlardır
    • $@: hedef adı
    • $?: hedeften daha yeni bağımlılıklar
    • $^: tüm bağımlılıklar
    • $<: ilk bağımlılık

Implicit rule’lar ve pattern rule’lar

  • Make, C derlemesi için implicit rule’lara sahiptir; bu sayede derleme komutu açıkça yazılmasa da build ilerleyebilir
  • Başlıca implicit rule’lar aşağıdaki biçimdedir
    • n.o, n.cden $(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@ biçiminde oluşturulur
    • n.o, n.cc veya n.cppden $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@ biçiminde oluşturulur
    • n, n.odan linker komutuyla oluşturulur
  • Implicit rule’larda önemli değişkenler CC, CXX, CFLAGS, CXXFLAGS, CPPFLAGS, LDFLAGS’tir
  • Static pattern rule’lar targets...: target-pattern: prereq-patterns ... biçimindedir; hedefte % ile eşleşen stem’i bağımlılık pattern’larına yerleştirir
  • Pattern rule’lar, %.o: %.c gibi hedefte % içerir ve kendi oluşturduğunuz implicit rule gibi kullanılabilir
  • Çift kolon kuralı ::, aynı hedef için birden fazla kural tanımlamayı sağlar ancak nadiren kullanılır

Komut çalıştırma ve hata işleme

  • Bir komutun başına @ eklerseniz komutun kendisi çıktılanmaz
  • make -s çalıştırmak, her satırın başına @ eklenmiş gibi davranır
  • Her komut satırı yeni bir shell’de çalıştırılıyormuş gibi etki eder; bu yüzden bir satırdaki cd .. sonraki satırı etkilemez
  • Aynı shell durumunu korumak için cd ..; echo \pwd`` gibi tek satırda yazmak veya backslash ile devam ettirmek gerekir
  • Varsayılan shell /bin/sh’dir; SHELL=/bin/bash gibi değiştirilebilir
  • Shell değişkenlerindeki $ işaretini Makefile içinde kullanmak için $$ kullanılır
  • Hata işleme, çalıştırma biçimine göre değişir
    • make -k: hata olsa bile mümkün olan diğer işleri çalıştırmaya devam eder
    • Komut başındaki -: ilgili komutun hatasını yok sayar
    • make -i: hata yok saymayı tüm komutlara uygular
  • ctrl+c ile Make’i durdurursanız Make, az önce oluşturduğu güncel hedefi siler

Recursive make ve ortam değişkenleri

  • Makefile içinden yeniden make çağırırken make yerine özel değişken $(MAKE) kullanılmalıdır
  • $(MAKE), make flag’lerini iletir ve flag’lerin etkisini uygun şekilde işler
  • Make başladığında, çalıştırma anında ayarlanmış ortam değişkenlerini otomatik olarak Make değişkenleri olarak oluşturur
  • export yönergesi, değişkenleri tüm recipe’lerin shell komut ortamına ayarlar
  • Alt make’lerde de değişken kullanmak için ilgili değişkeni export etmeniz gerekir
  • .EXPORT_ALL_VARIABLES kullanılarak tüm değişkenler export edilebilir
  • make clean run test gibi birden fazla hedef tek seferde belirtilirse clean, run, test sırasıyla çalıştırılır

Koşullu ifadeler ve fonksiyonlar

  • Koşullu ifadeler ifeq, ifneq, ifdef, ifndef, else, endif öğelerinden oluşur
  • ifdef, değişken referansını genişletmez; yalnızca değişkenin tanımlı olup olmadığını kontrol eder
  • $(MAKEFLAGS) ve findstring kullanılarak make -i gibi flag’lerin aktarılıp aktarılmadığı denetlenebilir
  • Make fonksiyonları çoğunlukla metin işleme için kullanılır ve $(fn, arguments) veya ${fn, arguments} biçiminde çağrılır
  • Başlıca fonksiyonlar şunlardır
    • subst: string değiştirme
    • patsubst: pattern’a uyan kelimeleri değiştirme
    • foreach: boşlukla ayrılmış kelime listesini dönüştürme
    • if: ilk argüman boş değilse ikinci argümanı, değilse üçüncü argümanı kullanır
    • call: değişkeni basit bir fonksiyon gibi çağırır ve $(1), $(2) gibi parametreler kullanır
    • shell: shell’i çağırır ancak satır sonlarını boşluğa çevirir
    • filter: listeden belirli pattern’lara uyan öğeleri seçer
    • filter-out: belirli pattern’lara uymayan öğeleri seçer

include, vpath, .PHONY, .DELETE_ON_ERROR

  • include filenames..., bir veya daha fazla başka Makefile’ın okunmasını sağlar
  • include, gcc’nin -M gibi seçenekleriyle oluşturulmuş Makefile’ları okumak için kullanışlıdır
  • vpath <pattern> <directories>, belirli bağımlılık dosyalarının aranacağı dizinleri belirtir
  • VPATH değişkeniyle benzer arama yolu davranışı global olarak belirtilebilir
  • Backslash \, uzun komutları birden fazla satıra bölmek için kullanılır
  • Bir hedefe .PHONY eklenirse Make, o hedefi aynı adlı dosyayla karıştırmaz
  • .DELETE_ON_ERROR, kural çalışırken bir komut 0 olmayan çıkış durumu döndürürse ilgili kuralın hedefini siler
  • .DELETE_ON_ERROR, PHONY gibi yalnızca hemen önündeki hedefe uygulanmaz; tüm hedeflere uygulanır

Makefile Cookbook örneği

  • Cookbook örneği, orta ölçekli projelere uygun bir Makefile şablonu sunar
  • src/ klasörüne C/C++ dosyaları konduğunda Makefile, derlenecekleri ve bağımlılıkları otomatik olarak belirler
  • Başlıca değişken yapısı şöyledir
    • TARGET_EXEC := final_program
    • BUILD_DIR := ./build
    • SRC_DIRS := ./src
  • SRCS, find ve $(shell ...) kullanarak *.cpp, *.c, *.s dosyalarını bulur
  • OBJS, her kaynak dosyanın başına BUILD_DIR ekleyip .o ekleyerek build çıktısı yolunu oluşturur
  • DEPS, .o uzantılarını .d ile değiştirerek bağımlılık Makefile’ları listesini oluşturur
  • INC_DIRS, src/ altındaki tüm dizinleri bulur; INC_FLAGS her dizinin başına -I öneki ekler
  • CPPFLAGS := $(INC_FLAGS) -MMD -MP, GCC’nin .d bağımlılık dosyaları oluşturmasını sağlayacak şekilde yapılandırır
  • C ve C++ kaynakları, ayrı pattern rule’larla build/ altında .o dosyaları oluşturur; gerekli dizinler mkdir -p $(dir $@) ile oluşturulur
  • clean, .PHONY olarak bildirilir ve rm -r $(BUILD_DIR) ile build dizinini siler
  • -include $(DEPS), .d dosyalarını dahil eder; ancak ilk build’de olduğu gibi dosyalar yokken oluşan hatayı bastırır

1 yorum

 
GN⁺ 2025-06-21
Hacker News yorumları
  • Pek bilinmeyen ama kullanışlı birkaç make bayrağı var. Çıktı senkronizasyonu, stdout/stderr çıktısını ancak hedef tamamlandıktan sonra yazdırarak paralel derlemelerde logların birbirine karışıp okunmasını zorlaştırma sorununu azaltır
    make --output-sync=recurse -j10
    Yoğun kullanılan çok kullanıcılı sistemlerde yalnızca -j iş sayısı yeterli olmayabilir; paralelliği yük ortalamasına göre de sınırlayabilirsiniz: make -j10 --load-average=10
    Hedef zamanlama sırasını rastgeleleştiren --shuffle da CI’da Makefile’daki eksik bağımlılıkları yakalamak için kullanışlıdır: make --shuffle # or --shuffle=seed/reverse

    • “Pek bilinmiyor” ise make yazarlarının seçenek listesini bir yerde derleyip programla birlikte dağıtması iyi olurdu. Kullanıcıların okuyabileceği bir metin dosyası ya da dizgi dili gibi bir biçim olsaydı bilgiye erişim çok daha iyi olurdu
    • En sık kullandığım seçenek -B; koşulsuz olarak her şeyi yeniden derlemeye zorlar
    • make -jnin makineleri birkaç kez DoS durumuna soktuğunu gördüğüm için bunu neredeyse bir bug gibi görüyorum
    • Yoğun kullanılan çok kullanıcılı bir sistemse bunu işletim sistemi zamanlayıcısı halledemiyor mu diye düşünüyorum
    • Kullanışlı olsa da taşınabilir değil. Dağıtılmayan kişisel oyuncak projeler dışında kullanmamak daha iyi
  • 1985 civarında Boston University Graphics lab’da Makefile ile animasyon için 3D renderer üretimi çalıştıran birini görmüştüm. Lisp tarafındandı; erken dönem prosedürel üretim ve 3D aktör sistemleriyle uğraşıyordu. Makefile toplamda yaklaşık 10 satırdı ve çok zarifti
    Yalnızca basit dosya tarihi bağımlılıklarıyla yüzlerce animasyon üretiyordu; Lisp her karenin 3D şeklini oluşturuyor, Make de kareleri üretiyordu
    1985 olduğundan, bugün 3D ve animasyonda doğal karşıladığımız şeylerin neredeyse hiçbiri yoktu; bu yüzden herkesi şaşırtmıştı. Daha sonra Iron Giant’ın 3D renderer’ını yazdı ve hatırladığım kadarıyla Coraline’da da kilit rol oynadı. Brian Gardner’dı

  • Make, kariyerimin başlarında öğrendiğim için gerçekten memnun olduğum araçlardan biri. Şu anda çok kullanmıyorum ama bildirimsel sistemlerin, emirsel sistemlere göre sahip olduğu gücü göstermişti
    Bu fikrin başka işlere de doğal biçimde genişlediğini fark ettim. Bu sitenin üst kısmındaki görsele bakınca yazarın da benzer hissettiği anlaşılıyor; geleneksel tarifler gibi betik tarzı komut sırası olarak yazmak yerine Makefile gibi bildirimsel bakınca yemek tariflerini daha iyi anlayabiliyorsunuz
    İlgili yazı: https://blog.gpkb.org/posts/cooking-with-make/
    Tarifleri her zaman Makefile gibi okunabilecek şekilde yazıp mutfağa götürüyorum; bunu bu şekilde dizen ya da gösteren biri var mı merak ediyorum. Yeni bir tarifi okurken betiği zihnimde Makefile’a dönüştürmek zorunda kalmamak epey zaman kazandırırdı

    • Bu yaklaşımın iyi yanı, kullanıcının fiilen bağımlılık grafiğini kendisinin yorumlayıp yürütücü hâline gelmesi ve böylece daha fazla kontrole sahip olması. Yemek örneğinde, bildirimsel yapı bir sonraki adımda ne yapılabileceğine dair seçeneklerin açık olduğu noktaları daha iyi gösteriyor; ayrıca bulaşık yıkamanın kolay olduğu sıra gibi Makefile’da resmen yazılı olmayan dış kısıtları da yansıtma özgürlüğü veriyor
      Elbette bunun bedeli, bağımlılık grafiğini bizzat çözmek zorunda kalmak. Sadece önceden serileştirilmiş sıralı adımları takip etmek istediğinizde daha fazla iş üstlenmiş oluyorsunuz
  • Yazıda çoğu tarifin .PHONY olarak işaretlenmediği söyleniyor ve bu, öğreticide özellikle ele almamanın nedeni gibi kullanılıyor; ama bu zayıf bir mazeret. Araçlar doğru kullanımı öğretmeli
    Ekipte make’i görev çalıştırıcı olarak kullandığımız için tüm tariflere .PHONY ekleyip bunu sürdürmem konusunda laf işitmiştim
    Clark Grubb’ın Makefile stil rehberi iyi: https://clarkgrubb.com/makefile-style-guide
    Bu stil rehberini kullanan var mı merak ediyorum. phony tarifleri tanımlandıkları noktada mı işaretliyorsunuz, yoksa dosyanın başında dev bir liste olarak mı tutuyorsunuz? Bunu zorunlu kılan bir linter olsa iyi olurdu

    • Okudum; genel olarak iyi bir belge. Yine de birkaç noktaya katılmıyorum
      -o pipefailı alışkanlıkla kullanmak pek iyi değil. Pipefail yararlı, ama insanların pipeline’larda en sık yaptığı işlerden biri olan grep ile çıktı filtrelemeyi bozuyor. Sadece gerekli tariflere tek tek koymak daha iyi
      Dosya olmayan hedefleri .PHONY olarak işaretlemek teknik olarak doğru, ama çoğu zaman gerekli değil. Hedef sayısı çoksa Makefile’a gereksiz laf kalabalığı ekliyor; bu yüzden bence yalnızca gerektiğinde eklemek daha iyi
      Birden fazla çıktı dosyası üreten tariflerde, eskiden desen kuralları uymadığında dummy dosya/bayrak dosyası standarttı; ancak GNU Make 4.3’ten itibaren grup hedefleri yerel olarak destekleniyor. Ubuntu 22.04 LTS’de de var: https://www.gnu.org/software/make/manual/html_node/Multiple-...
  • Son dönemde ilginç olan nokta, CMake’in C++20 modülleri kullanan projeler için Makefile’ın uygun olmadığını, Ninja’nın doğru seçenek olduğunu değerlendirmiş olması. https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules....
    Temelde hedef bağımlılıklarını statik olarak tanımlamanın çok zor ya da imkânsız olduğunu düşünüyor gibi. Artık bunu clang-scan-deps gibi araçlarla dinamik olarak ele alıyorlar: https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang...

    • Yanılıyorsam düzeltin, ama Makefile ile ilgili bu sınırlama tamamen CMake’in tercihi gibi görünüyor; ya da en azından Makefile üreticisine destek ekleyecek gönüllü eksikliğinin sonucu. Ninja’nın kendisi de C++ modüllerini desteklemiyor: https://github.com/ninja-build/ninja/issues/2457
      Aslında Ninja, tüm bağımlılıkların statik olarak tanımlanmasını gerektirdiği için bu açıdan sıradan Make’ten daha az yetenekli
    • Açıkçası modüller felakete yakın
  • Make’in büyük C kod tabanları için bir derleme aracı olarak yeri var. Ama çoğu zaman genel amaçlı, “proje başına görev çalıştırıcı” gibi ele alınıyor; buna pek uygun değil. Çünkü basit koşullu ifadeler bile zor
    Örneğin Terraform’u Make ile sarmalamaya yönelik iyi niyetli girişimleri birkaç kez gördüm, ama sonu iyi olmadı

    • Make genel amaçlı bir görev çalıştırıcı değildir. Daha çok doğrusal shell betiklerini bildirimsel bağımlılıklara dönüştürmenin genel bir yoludur. Shell için genel bir araçtır
    • Büyük C kod tabanları için derleme aracı olduğu sözü de bence artık doğru değil. Son 20 yılda daha sağlam ve iyi tanımlanmış derleme sistemleri geliştirildi; artık güncelleme zamanı
    • İyi bir genel amaçlı görev çalıştırıcı var mı? Düzenleme: “görev çalıştırıcı” ifadesini tamamen yanlış anlamışım gibi görünüyor
  • Makefile’ın dağınıklaştığı kısımların yerine geçebilecek harika ve modern bir araç olarak just var: https://github.com/casey/just

    • Make’in “kısa shell betikleri listesi” yönünün yerini alıyor, ama asıl yararlı kısmı olan “yalnızca yeniden çalıştırılması gereken kuralları çalıştırma” özelliğinin yerini almıyor
    • Başka seçenekler de var: Task(Go) https://github.com/go-task/task, Cake(C#) https://github.com/cake-build/cake, Rake(Ruby) https://github.com/ruby/rake
      Tamamen farklı bir kavram olarak, 8 ay önce HN’de ele alınan Makedown da var: https://news.ycombinator.com/item?id=41825344
    • Bu araçlar kendilerini make’e alternatif olarak konumlandırsa da, kişisel olarak tamamen farklı olduklarını ve karşılaştırma konusu bile olmadıklarını düşünüyorum. make’in odağı çıktı üretimi ve zaten üretilmiş şeyleri yeniden derlememektir; just ise bir komut çalıştırıcıdır
    • Make’i komut çalıştırıcı olarak kullanmanın başlıca avantajı, “her yerde” kurulu olan standart bir araç olmasıdır. Alternatif araçlar kullanımı daha iyi görünse de, ek araç kurulumunu haklı çıkaracak kadar büyük bir avantaj sunduklarını hiç hissetmedim
    • Task da başka bir alternatif. Ancak kişisel olarak yalnızca basit bir C hobi projesinde kullandım; ölçek büyüdüğünde ne kadar iyi dayanır söylemek zor: https://taskfile.dev/
  • Makefile harikadır ama fazla kaptırmamak iyi olur. Birkaç yıl önce saf bir GNU Make framework’ü yapmaya çalışırken aslında autoconf’u yeniden icat ettiğimi fark ettim; GNU autotools’un neden var olduğunu da o zaman anladım
    Makefile tuhaf biçimde Lisp benzeri bir Turing tarpit. GNU Make’te metaprogramlama özellikleri bile var; bu yüzden Makefile içinde kutsallık dışı bir sistemi metaprogramlamak istemeye direnmek zor. GNU Make’in bu kadar yaygın kurulu olması da cazibeyi artırıyor

  • Make alternatifi Task’in yaratıcısı ve bakımcılarından biriyim. 8 yılı aşkın süredir var ve gelişmeye devam ediyor; yeni bir şey arıyorsanız denemeniz iyi olabilir. Soruları da rahatça sorabilirsiniz
    https://taskfile.dev/
    https://github.com/go-task/task

  • tup’ı deneyen var mı merak ediyorum: https://gittup.org/tup/ex_dependencies.html
    Dosya sistemi erişimine dayanarak bağımlılıkları otomatik belirleyen bir derleme sistemi; bu yüzden herhangi bir derleyici ya da araçla birlikte kullanılabiliyor