Makefile Kullanımı
Önceki GNU Paket Yapılandırma Sistemi Sonraki
Makefile Kullanımı
Uygulama geliştirirken sıklıkla nesne dosyalarımızı yeniden ve yeniden oluşturmak zorunda kalırız. Yerine göre gcc, ld, ar vb. uygulamaları tekrar tekrar aynı parametrelere çağırırız. İşte make ugulaması, programların yeniden derlenme sürecini otomatik hale getirmek, sadece değişen kısımların yeniden derlenmesini sağlamak suretiyle zamandan kazanmak ve işlemleri her zaman otomatik olarak doğru sırada yapmak için tasarlanmıştır.
Temel Kurallar
make uygulaması çalıştırıldığında, bulunulan dizinde sırasıyla GNUmakefile, makefile ve Makefile dosyalarını arar. Alternatif olarak -f seçeneği ile Makefile olarak kullanacağınız dosyayı da belirlemeniz mümkün olsa da standartların dışına çıkmamakta fayda var. make neyi nasıl yapacağını bu dosyalardan öğrenecektir. Eğer bulunduğunuz dizinde bir Makefile yok ise aşağıdaki gibi bir çıktı alacaksınız demektir:
$ make
make: *** No targets specified and no makefile found.  Stop.
İpucu
Genel kabul görmüşlüğü ve göz alışkanlığı açısından dosya adı olarak alternatiflerin yerine Makefile kullanmanızı öneririm.
Bir Makefile aslında işlemlerin nasıl yapılacağını gösteren kural tanımlamalarından oluşmaktadır. Genel olarak dosyanın biçimi aşağıdaki gibidir:
hedef: bağımlılıklar
<TAB> komut
<TAB> komut
<TAB> ...
Diğer kurala geçmeden bir boş satır
...
Burada en sık yapacağımız hata <TAB> tuşuna basmayı unutmak olacaktır. Makefile dosyasını hazırladığınız metin düzenleyiciden kaynaklanan bir sorun da olabilir. En iyisi emacs kullanarak makefile-mode ile yazmaktır, böylece hata yapma olasılığınız oldukça azalacaktır.
Kurallar arasında bir satır boş bırakılması GNU make için zorunlu olmamakla birlikte bazı Unix sürümleriyle uyumluluk için boşluk bırakılması gereklidir.
İlk satırda hedef'in oluşturulmasında etkili olan, bağımlılık yaratan dosyalar birbirinden boşluk ile ayrılmış olarak tek satırda listelenir. Eğer bağımlılık kısmında yer alan dosyalardan en az birinin son değiştirilme tarihi, hedef'ten daha yeni ise, hedef yeniden oluşturulur. Diğer durumda hedefin yeniden oluşturulmasına gerek olmadığı anlaşılır, çünkü hedefin bağımlı olduğu dosyalarda son oluşturmadan sonra bir değişiklik olmamıştır. Sonraki satırlarda bağımlılık yaratan bu dosyalardan hedefin oluşturulabilmesi için gerekli komutlar yer alır. Şimdi basit bir örnek yapalım:
  test: test.c
      gcc -o test test.c
Bu örnekte hedef olarak test uygulaması derlenecektir. Uygulamanın bağımlı olduğu dosya test.c'dir. test.c dosyasında herhangi bir değişiklik olduğunda veya test silindiğinde, gcc -o test test.c komutu çalıştırılacak ve test yeniden oluşturulacaktır. Şimdi daha karışık bir örnek yapalım:
CC = gcc
CFLAGS = -O2 -Wall -pedantic
LIBS = -lm -lnsl

test: test.o
    $(CC) $(CFLAGS) $(LIBS) -o test test.o

test.o: test.c
    $(CC) $(CFLAGS) -c test.c

clean:
    rm -f test *.o

install: test
    cp test /usr/bin

İlk satırda CC değişkenine kullanacağımız derleyiciyi atıyoruz. Makefile dosyaları içerisinde bu şekilde değişken tanımlaması yapıp, değişkeni dosya içerisinde $(değişken) olarak kullanabiliriz. İkinci satırda ise derleyiciye vereceğimiz bazı seçenekleri CFLAGS değişkenine atıyoruz. Üçüncü satırda uygulamamızın kullandığı kütüphaneleri listeledik. Ardından ilk kuralımız geliyor. test uygulaması test.o dosyasına bağımlı olarak belirtilmiş ve test.o'dan test'in oluşturulabilmesi için gerekli komut hemen altında listelenmiştir. Değişkenlerin değerlerini yerine koyduğumuzda komutumuz gcc -O2 -Wall -pedantic -lm -lnsl -o test test.o şeklinde olacaktır.
İkinci kuralımız test.o'nun nasıl oluşturulacağını belirtmektedir. Aslında bu iki kural bir önceki örnekte olduğu gibi birleştirilebilir, ancak mantığı anlatabilmek için burada ikiye bölünmüştür. test.c dosyasında bir değişiklik olduğunda test.o dosyası hemen altında listelenen komutla yeniden oluşturulur: gcc -O2 -Wall -pedantic -c test.c
Üçüncü kuralımızda çalıştığımız dizinde nasıl temizlik yapacağımızı belirtiyoruz. make clean komutunu çalıştırdığımızda test dosyası ve .o ile biten nesne dosyaları silinecektir. Bir sonraki kuralımız ise install. Bu kuralda da test dosyasında bir değişme olduğunda cp test /usr/bin komutu ile dosyayı /usr/bin dizini altına kopyalıyoruz.
Makefile içerisindeki her bir kural make uygulamasına seçenek olarak verilebilir ve ayrıca işletilebilir. Yukarıdaki gibi bir Makefile dosyasına sahipsek make test.o komutuyla sadece test.o için verilen kuralın çalıştırılmasını sağlayabiliriz. Veya make install komutuyla sadece install kuralının çalışmasını sağlayabiliriz. Ancak install aynı zamanda test'e bağımlı olduğundan test'in kuralı da çalışır. Aynı şekilde test de test.o'ya bağlı olduğundan test.o kuralı da çalışacaktır. Komutu seçenek vermeden sadece make şeklinde çalıştırdığınızda ise Makefile dosyasını okur ve bulduğu ilk kuralı işler. Bizim örneğimizde ilk kural test olduğu için test dosyasının oluşturulabilmesi için gerekli işlemleri yapacaktır. Bu nedenle Makefile dosyalarında ilk kural çoğu zaman all: test install gibi olur. Böylece her defasında make xxx yazmak yerine sadece make yazarak hız kazanmış oluruz.
Bu örneği iyice anlamadan sonraki bölümlere devam etmeyiniz. make uygulamasının bu basit ama bir o kadar da güçlü mantığını tam olarak anladığınızda onu sadece kodunuzu derlemek için değil, çok farklı amaçlar için de kullanabileceğinizi göreceksiniz. Hemen bir örnek verelim, bir sanaldoku (web) uygulamanız var ve buradan isim:telefon şeklinde bir metin dosyasına giriş yapılıyor. Eğer bu metin dosyası değiştiğinde çalışacak şekilde bir kural tanımlarsanız, mesela metin dosyası her değiştiğinde bu dosyayı okuyup ayrıştırarak veritabanına kayıt edecek bir uygulamanın çalıştırılması sağlanabilir. Örneğimiz pek işe yarar bir şey olmadı ama eminim mantığı anlamışsınızdır.
Not
Aslında make için verilebilecek en iyi örneklerden bir tanesi de Debian sanalyöresidir. Debian sanalyöresi, tamamen statik HTML sayfalardan oluşur. Bu sayede yansılanması daha kolay hale gelir ve statik sayfalar sanaldoku sunucusuna çok az yük getirir. Ancak binlerce sayfadan oluşan Debian sanalyöresi, statik olmasına rağmen çok hızlı güncellenebilmektedir. Aynı zamanda yöreyi ziyaret ettiyseniz farketmiş olacağınız gibi, sanaldoku istemciniz dil ayarına göre sayfanın o dile çevirilmiş bir sürümü mevcut ise karşınıza o getirilmektedir. Tüm bu dinamiklik alt tarafta kullanılan, çoğunluğu wml, binlerce dosya tarafından sağlanmaktadır. Her 3-4 saatte bir CVS'de bulunan kaynak kodu çekilerek make ile wml dosyalarından HTML dosyaları üretilmekte, sayfalar arası aşamalar düzenlenmekte, farklı dillere çevirilen sayfalar kontrol edilmekte, bazı programlar ve betikler çalıştırılmaktadır. Kısaca özetlemek gerekirse böyle ama gerçekte tüm yörenin yeniden oluşturulması için gerçekten oldukça karmaşık işlemler yapılmaktadır. İlgilenenler http://www.debian.org/devel/website/desc adresine bakabilir.
Yukarıdaki Makefile örneğimize tekrar dönelim. make clean komutunu çalıştırdığımızda derleme sonrasında oluşan dosyalar silinmektedir. Peki, bulunduğumuz dizinde ismi clean olan bir dosya mevcut ise ne olur?
$ make clean
make: `clean' is up to date.
Gördüğünüz gibi clean adında bir dosya var olduğu ve clean için bağımlılık listesi olmadığından dolayı kuralın güncelliğini koruduğunu ve alttaki komutların çalıştırılmaması gerektiğini düşündü. İşte bu gibi durumlar için özel bir kural mevcuttur: .PHONY
Yukarıda anlatılan sorunu giderebilmek için Makefile dosyamızın içeriğine aşağıdaki kuralı da eklemeliyiz:
.PHONY: clean
Böylelikle make clean komutunun, bulunulan dizinde clean adında bir dosya olsa bile düzgün olarak çalışmasını sağlamış olduk.
Daha Karmaşık Makefile Dosyaları
Önceki bölümde temel olarak make kullanımı üzerinde durduk. Örnek bir Makefile hazırladık. Ancak tek bir kaynak dosyasından oluşturulan bir uygulama için make o kadar da yararlı bir şey değil. Zaten gerçekte de en küçük uygulama bile onlarca kaynak dosyadan oluşur. Şimdi böyle bir uygulama için Makefile hazırlayalım.
Örnek 7.1. Soyut kurallar kullanılmamış Makefile
CC = g++
CFLAGS = -O2 -Wall -pedantic
LIBS = -lnsl -lm
INCLUDES = -I/usr/local/include/custom

all: server client

server: ortak.o server.o list.o que.o \
            data.o hash.o
    $(CC) $(CFLAGS) $(LIBS) -o server ortak.o server.o \
            list.o que.o data.o hash.o

client: ortak.o client.o
    $(CC) $(CFLAGS) $(LIBS) -o client ortak.o client.o

ortak.o: ortak.cpp ortak.h
    $(CC) $(CFLAGS) $(INCLUDES) -c ortak.cpp

server.o: server.cpp server.h ortak.h
    $(CC) $(CFLAGS) $(INCLUDES) -c server.cpp

client.o: client.cpp client.h ortak.h
    $(CC) $(CFLAGS) $(INCLUDES) -c client.cpp

list.o: list.cpp list.h
    $(CC) $(CFLAGS) $(INCLUDES) -c list.cpp

que.o: que.cpp que.h
    $(CC) $(CFLAGS) $(INCLUDES) -c que.cpp

data.o: data.cpp data.h
    $(CC) $(CFLAGS) $(INCLUDES) -c data.cpp

hash.o: hash.cpp hash.h
    $(CC) $(CFLAGS) $(INCLUDES) -c hash.cpp

install: client server
    mkdir -p /usr/local/bin/test
    cp client /usr/local/bin/test
    cp server /usr/local/bin/test

uninstall:
    rm -rf /usr/local/bin/test

clean:
    rm -f *.o server client

.PHONY: clean
Kullandığımız derleyici, derleyici seçenekleri, kütüphaneler gibi değerleri değişkenlere atamakla neler kazandığımıza bir bakalım. Derleyici parametrelerini değiştirmeye karar verdiğimizde değişken kullanmıyor olsaydık 9 farklı yerde bu değişikliği el ile yapmak zorunda kalacaktır. Fakat şimdi ise sadece CFLAGS değişkeninin değerini değiştirmemiz yeterli olacaktır.
Ancak gene de yukarıdaki gibi bir Makefile yazmak uzun sürecek bir işlemdir. Eğer uygulamanız 60 cpp dosyasından oluşuyorsa ve 60 farklı nesne için tek tek kuralları yazmak zorunda kalıyorsanız bu hoş olmaz. Çünkü tüm .o dosyalarını üretebilmek için vereceğimiz komut aynı: $(CC) $(CFLAGS) $(INCLUDES) -c xxx.cpp. Oysa biz 60 defa bu komutu tekrar yazmak zorundayız. İşte bu noktada soyut kurallar (abstract rules) imdadımıza yetişir.
Bir soyut kural *.u1 uzantılı bir dosyadan nasıl *.u2 uzantılı bir dosyanın üretileceğini tanımlar. Genel olarak kullanımı aşağıdaki gibidir:
.u1.u2:
    komutlar
    komutlar
...
Burada u1 kaynak dosyanın uzantısı iken, u2 hedef dosyanın uzantısıdır. Bu tür kullanımda dikkat ederseniz bağımlılık tanımlamaları yer almamaktadır. Çünkü tanımladığımız soyut genel kural için bağımlılık belirtmek çok anlamlı değildir. Bunun yerine .u1 uzantılı bir dosyadan .u2 uzantılı dosya üretmede istisnai olarak farklı bağımlılıkları olan kurallar da ileride vereceğimiz örnekte olduğu gibi belirtilebilir.
Soyut kurallar tanımlarken aşağıdaki özel değişkenleri kullanmak gerekecektir:
  • $< Değiştiği zaman hedefin yeniden oluşturulması gereken bağımlılıkları gösterir.
  • [email protected] Hedefi temsil eder.
  • $^ Geçerli kural için tüm bağımlılıkları temsil eder.
Bu bilgiler ışığında hemen bir örnek verelim. Uzantısı .cpp olan bir kaynak kodundan nesne kodunu üretebilmek için aşağıdaki gibi bir kural tanımlayabiliriz:
.cpp.o:
    g++ -c $<
Şimdi biraz daha açıklık getirelim. Kaynak dosyamızın adı helper.cpp ve amacımız helper.o nesne dosyasını üretmek olsun. Yukarıdaki kural kaynak dosyamız için çalıştığında .cpp.o: satırı yüzünden helper.cpp oluşacak helper.o için bir bağımlılık durumunu alır. Bu nedenle $< değişkeni helper.cpp'yi gösterir. Bu sayede helper.o dosyası üretilmiş olacaktır.
Şimdi aynı mantıkla nesne dosyalarından çalıştırılabilir programımızı üretelim.
.o:
    g++ $^ -o [email protected]
Bu biraz daha karışık çünkü çalıştırılabilir dosyamızın uzantısı olmayacak. Eğer tek bir uzantı verilmiş ise bunun birinci uzantı olduğu ve ikincinin boş olduğu düşünülür.
Soyut kurallar tanımladığımızda yapmamız gereken iki işlem daha var. Bunlardan birincisi kullandığımız uzantıların neler olduğunu belirtmektir. Bu işlem için .SUFFIXES özel değişkeni kullanılır:
.SUFFIXES: .cpp .o
Diğer yapmamız gereken işlem ise üretilecek çalıştırılabilir dosyamızın hangi nesne dosyalarına, nesne dosyalarımızın ise hangi kaynak dosyalarına bağımlı olduğunu belirtmek olacaktır. İşin en güç tarafı budur. Her zaman doğru değerleri yazmak o kadar kolay olmayabilir. Bu noktada gcc derleyicisi -MM seçeneğiyle bize yardımcı olacaktır. Aşağıdaki ekran çıktısına bakalım:
$ g++ -MM -c server.cpp
server.o: server.cpp server.h ortak.h
$ 
Görüldüğü gibi server.o için gerekli Makefile kuralını bizim için hatasız olarak verdi. Tek yapmamız gereken bu satırları kopyalayıp Makefile içerisine yapıştırmaktır. Şimdi bölümün başında verdiğimiz Makefile dosyasını bu yöntemle yeniden yazalım:
Örnek 7.2. Soyut kuralların kullanıldığı Makefile
CC = g++
CFLAGS = -O2 -Wall -pedantic
LIBS = -lnsl -lm
INCLUDES = -I/usr/local/include/custom
SERVER_nesneCTS = ortak.o server.o list.o que.o data.o hash.o
CLIENT_nesneCTS = ortak.o client.o

all: server client

.SUFFIXES: .cpp .o

.cpp.o:
    $(CC) $(CFLAGS) $(INCLUDES) -c $<

.o:
    $(CC) $(CFLAGS) $(LIBS) $^ -o [email protected]

server: $(SERVER_nesneCTS)
client: $(CLIENT_nesneCTS)
ortak.o: ortak.cpp ortak.h
server.o: server.cpp server.h ortak.h
client.o: client.cpp client.h ortak.h
list.o: list.cpp list.h
que.o: que.cpp que.h
data.o: data.cpp data.h
hash.o: hash.cpp hash.h

install: client server
    mkdir -p /usr/local/bin/test
    cp client /usr/local/bin/test
    cp server /usr/local/bin/test

uninstall:
    rm -rf /usr/local/bin/test

clean:
    rm -f *.o server client

.PHONY: clean
Önceki Üst Ana Başlık Sonraki
Giriş Başlangıç Autoconf ve Automake Kullanımı
Bir Linux Kitaplığı Sayfası