En İyi Örneklerle Makefile Öğrenin
(makefiletutorial.com)- 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: prerequisitesve tab ile girintilenmiş komutlardan oluşur; boşlukla girintilememakehatası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_ERRORile build akışı genişletilebilir - Cookbook örneği,
src/içindeki C/C++ kaynaklarınıbuild/çıktıları haline getirir ve-MMD -MPile-includekullanarak 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,
makekurulu bir terminaldeMakefiledosyası oluşturduktan sonra ilgili dizindemakeç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
makebaşarısız olur
Hedefler ve bağımlılıklar arasında zaman karşılaştırması
make hello,hellohedefine bağlı komutları çalıştırır; ancakhellodosyası 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.ckuralında derleme komutu yalnızcablahyoksa veyablah.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
makekomutuna hedef belirtilmezse Makefile’daki ilk hedef varsayılan hedef olarak çalıştırılırallhedefi birden fazla hedefi tek seferde oluşturmak için kullanılır; ilk kural olarak konursa yalnızcamakeçalıştırmak tüm hedefleri build edercleangenellikle build çıktılarını silen hedef olarak kullanılır, ancak Make’in özel bir reserved word’ü değildircleanilk hedef değilse ve başka bir hedefin bağımlılığı değilse çalışması içinmake cleangibi açıkça çağrılması gerekir- Gerçekte
cleanadlı bir dosya varsacleanhedefi çalışmayabilir; bu durum daha sonra.PHONYile çö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şmezsewildcardfonksiyonunun 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şturulurn.o,n.ccveyan.cppden$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@biçiminde oluşturulurn,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: %.cgibi 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/bashgibi 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+cile 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
makeyerine ö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
exportyö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
exportetmeniz gerekir .EXPORT_ALL_VARIABLESkullanılarak tüm değişkenler export edilebilirmake clean run testgibi birden fazla hedef tek seferde belirtilirseclean,run,testsı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)vefindstringkullanılarakmake -igibi 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ştirmepatsubst: pattern’a uyan kelimeleri değiştirmeforeach: boşlukla ayrılmış kelime listesini dönüştürmeif: ilk argüman boş değilse ikinci argümanı, değilse üçüncü argümanı kullanırcall: değişkeni basit bir fonksiyon gibi çağırır ve$(1),$(2)gibi parametreler kullanırshell: shell’i çağırır ancak satır sonlarını boşluğa çevirirfilter: listeden belirli pattern’lara uyan öğeleri seçerfilter-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ğlarinclude,gcc’nin-Mgibi seçenekleriyle oluşturulmuş Makefile’ları okumak için kullanışlıdırvpath <pattern> <directories>, belirli bağımlılık dosyalarının aranacağı dizinleri belirtirVPATHdeğ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
.PHONYeklenirse 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,PHONYgibi 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_programBUILD_DIR := ./buildSRC_DIRS := ./src
SRCS,findve$(shell ...)kullanarak*.cpp,*.c,*.sdosyalarını bulurOBJS, her kaynak dosyanın başınaBUILD_DIRekleyip.oekleyerek build çıktısı yolunu oluştururDEPS,.ouzantılarını.dile değiştirerek bağımlılık Makefile’ları listesini oluştururINC_DIRS,src/altındaki tüm dizinleri bulur;INC_FLAGSher dizinin başına-Iöneki eklerCPPFLAGS := $(INC_FLAGS) -MMD -MP, GCC’nin.dbağı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.odosyaları oluşturur; gerekli dizinlermkdir -p $(dir $@)ile oluşturulur clean,.PHONYolarak bildirilir verm -r $(BUILD_DIR)ile build dizinini siler-include $(DEPS),.ddosyalarını dahil eder; ancak ilk build’de olduğu gibi dosyalar yokken oluşan hatayı bastırır
1 yorum
Hacker News yorumları
Pek bilinmeyen ama kullanışlı birkaç
makebayrağı 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ırmake --output-sync=recurse -j10Yoğun kullanılan çok kullanıcılı sistemlerde yalnızca
-jiş sayısı yeterli olmayabilir; paralelliği yük ortalamasına göre de sınırlayabilirsiniz:make -j10 --load-average=10Hedef zamanlama sırasını rastgeleleştiren
--shuffleda CI’da Makefile’daki eksik bağımlılıkları yakalamak için kullanışlıdır:make --shuffle # or --shuffle=seed/reverse-B; koşulsuz olarak her şeyi yeniden derlemeye zorlarmake -jnin makineleri birkaç kez DoS durumuna soktuğunu gördüğüm için bunu neredeyse bir bug gibi görüyorum1985 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ı
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
.PHONYolarak 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ı öğretmeliEkipte make’i görev çalıştırıcı olarak kullandığımız için tüm tariflere
.PHONYekleyip bunu sürdürmem konusunda laf işitmiştimClark 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
-o pipefailı alışkanlıkla kullanmak pek iyi değil. Pipefail yararlı, ama insanların pipeline’larda en sık yaptığı işlerden biri olangrepile çıktı filtrelemeyi bozuyor. Sadece gerekli tariflere tek tek koymak daha iyiDosya olmayan hedefleri
.PHONYolarak 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 iyiBirden 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-depsgibi araçlarla dinamik olarak ele alıyorlar: https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang...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
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ı
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
Tamamen farklı bir kavram olarak, 8 ay önce HN’de ele alınan Makedown da var: https://news.ycombinator.com/item?id=41825344
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