2.2. Daha Kompleks 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 2-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ı obje 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:

Bu bilgiler ışığında hemen bir örnek verelim. Uzantısı .cpp olan bir kaynak kodundan obje 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 obje 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şkenini helper.cpp'yi gösterir. Bu sayede helper.o dosyası üretilmiş olacaktır.

Şimdi aynı mantıkla obje dosyalarından çalıştırılabilir programımızı üretelim.


  .o:
      g++ $^ -o $@
  
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 obje dosyalara, obje dosyalarımızın ise hangi kaynak dosyalara 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 2-2. Soyut kuralların kullanıldığı Makefile


  CC = g++
  CFLAGS = -O2 -Wall -pedantic
  LIBS = -lnsl -lm
  INCLUDES = -I/usr/local/include/custom
  SERVER_OBJECTS = ortak.o server.o list.o que.o data.o hash.o
  CLIENT_OBJECTS = ortak.o client.o

  all: server client

  .SUFFIXES: .cpp .o

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

  .o:
      $(CC) $(CFLAGS) $(LIBS) $^ -o $@

  server: $(SERVER_OBJECTS)
  client: $(CLIENT_OBJECTS)
  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