sayfa başı

10 Mart 2015 Salı

Makefile



    Adım Adım Linux Driver Yazıyoruz -2- başlıklı yazımın sonunda ilk kernel modülümüzü oluşturmuştuk. Bundan sonraki işlem modülü derlemek için Makefile oluşturmak. Fakat burada ezbere bir Makefile kullanarak Makefile konusunu es geçmek istemedim. Yeri gelmişken Makefile ile ilgili bir yazı hazırlamak istedim. Makefile ile ilgili bu yazım tamamen giriş düzeyinde. En azından Makefile ile ilgili ufak da olsa bir bilgimiz olsun.

Makefile

[Illustration]
 Makefile projemizdeki kaynak kodları hızlı ve kolayca derleme için kullandığımız bir dosyadır. Makefile dosyası işlemlerin nasıl yapılacağını gösteren kural tanımlamalarından oluşmaktadır. Makefile özel bir dosyadır ve ismi büyük harfle başmalıdır ve "Makefile" şeklinde olmalıdır.

1- Basit Düzeyde Makefile Oluşturma


hedef: bağımlılıklar
<TAB> komut
<TAB> komut
<TAB> ...

   Burada <TAB> kullanmak zorunludur. Bir satır boşluk bırakmak GNU make için zorunlu olmamakla birlikte bazı Unix sürümleriyle uyumluluk için boşluk bırakılması gereklidir. 
  İlk satırda hedefin oluşturulmasında etkili(bağımlı) dosyalar birbirinden boşluk ile ayrılarak sırasıyla yazılır. Etkili(bağımlı) dosyalar hedefin oluşturulmasında kullanılacak olan dosyalardır. Eğer bağımlı dosyalarda herhangi birinin değiştirilme tarihi hedefin tarihinden daha yeni ise hedef yeniden oluşturulur. Bu sınama ile hedefin tekrar tekrar oluşturulması engellenmiş olunur. hedef ve bağımlı dosyalar belirtildikten sonra bir alt starırda <TAB> ile başlanarak komutlar yazılır. 

keypad: keypad.c
      gcc keypad.c -o output_file_name

 Örnekte hedefe verilen isim keypad, bağımlı dosya keypad.c. Komut kısmında da bu hedefe yönelik kod yazılmıştır.(kodun zorunlu olan bir <TAB> içerinde yazıldığına dikkat ediniz.) 
Hedeflerin diğer bir özelliği de tek başlarına işletilebilir olmasıdır. Örneğin elimizde aşağıdaki gibi yazılmış bir makefile dosyası bulunsun:

dest1: file.c
      #commands
dest2: file.c
      #commands
dest3: file.o
      #dommands
  Konsoldan Makefile bulunduğu dizine geçtikten sonra “make dest1”, ya da “make dest2” gibi sadece o hedefi çalıştıracak komut verebiliriz. Eğer hiç bir hedef belirtmeden sadece make komutunu çalıştırırsak işlem ilk hedefi çalıştırır.     Genelde Makefile dosyalarında ilk hedefe diğer hedefleri bağlayabiliriz ve bu sayede tüm hedefler de çağrılmış olur. Bu yüzden ilk hedefe çoğunlukla “all” ismi verilir ve konsoldan bu hedef “make all” yerine sadece “make” yazılarak kısa bir yazımla çağrılmış olur. Makefile kurallarına ufak bir uygulama üzerinden anlatalı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

 Dosyanın başında yaptığımız ilk işlemler atama işlemleridir. Makefile içinde bu şekilde atama işlemi yapabiliriz. Kullanımı ise koddan da görüleceği gibi $(DEGISKEN).  Makefile yazılırken kullanılacak derleyici, derleyiciye ait komutlar ve kütüphane komutları bu şekilde tanımlanır. Bu sayede tek bir noktadan kontrol oluşturulur. 

 Makefile dosyasında çoğunlukla clean ve install hedefleri bulunur. Neredeyse her makefile içinde bu amaçlı çalışan hedefler bulunmaktadır. Hedef ismi yaygın olarak “clean” olarak kullanılsa da makefile yazan kişi istediği ismi verebileceği gibi bu hedef altında başka yardımcı kodları da koşturabilir. Örneğin obje dosyalarının silinmesi yanında derleme sonrası oluşan çalıştırılabilir çıktıyı ya da .so uzantılı dosyaları da sildirebilir. Burada yazanın proje içindeki gereksinimine göre işlem yapması gerekir. Ayrıca versiyon kontrol programları ile proje commit edilmeden önce bu komut kullanılarak repostory bulunmaması gereken dosyaları da bu şekilde temizlediğimizi göz önüne almak gerekir. Tüm bu isteklere cevap verecek bir “clean” hedefi yazmak gerekir.
 Örnek makefile dosyasında clean komutu için bağımlılık listesi olmadığına dikkat çekmek gerekir. Çünkü bağımlı listedeki belirtilen dosyada herhangi bir değişiklik olduğunda çağrılan hedef çalışır. Fakat biz clean hedefine her seferinde koşulsuz olarak çalıştırmak istediğimizden bağımlılık listesi clean komutu için kullanılmaz.

 Clean komutunu kullanırken şu hata durum da oluşabilir: proje dosyaları içince “clean” adında bir dosya var olması durumunda siz clean komutunu çalıştırmak isterseniz bunu yapamazsınız. make clean komutuna karşılık “make: `clean' is up to date.” cevabını alırsınız. İşte bu hataya karşı “.PHONY: clean” komutunu makefile dosyanıza eklemeniz gerekir. Aslında bu komutu tüm bağımlılık listesi olmayan hedefler için kullanmalıyız.
 “install” hedefi ise projenin derlenmesi sonucunda oluşan sonuç dosyanın nereye kopyalanması gerektiğini gösterir. Bu adım da bir makefile dosyasında bulunması gerekir. Çünkü kullanıcı sonuç dosyasının nerede oluşacağını bilemez. Ayrıca bu hedefin hangi dizine kopyalama yapacağı konsola çıktı olarak da verilmelidir.

2-Orta Düzeyde Makefile

 Basit Düzeyde Makefile başlığı altında verilen makefile dosyası uygulamada kullanılmayacak kadar basit düzeyde. Zaten uygulamalarda projelerde yüzlerce hatta binlerce dosya bulunabilir. Peki bu büyüklükteki projeler için nasıl bir makefile yazmak gerekir.

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
 Yukarıdaki makefile soyut kurallar kullanılmadan yazılmıştır. Proje sayıda dosyadan oluşmasına rağmen makefile yazımı yorucu ve zaman alıcı bir görünüm sunmaktadır ki bu basit projenini 50 dosyadan oluştuğunu düşünürsek 50 adet dosyanın derlenmesini tek tek bildirmek gerekecektir. Bu durum hem zaman kaybı hemde hatalara açıktır. Orta düzeyeki projelerde bile yukarıdaki örnek biçiminde makefile yazmaktan kaçınmalıyız.
 Bu fikirle hareket edilirse çözüm olarak soyut kurallar (abstract rules) imdadımıza yetişir. Bu soyut kurallar bize örneğin tüm .bet uzantılı dosyalardan pratik ve hatasız bir biçimde nasıl .son uzantılı dosyalar oluşturulacak sorununa  yardımcı olur.
.bet.son:
    komutlar
    komutlar
   ...
 Yukarıdaki özet kullanım .bet kaynak dosya .son hedef dosyayı gösterir ve dikkat edilirse bir bağımlılık listesi kullanılmadığı dikkati çeker. Daha kapsamlı bir örneğe geçmeden önce burada bilmemiz gereken bazı değişkenler var.

·      $< Değiştiği zaman hedefin yeniden oluşturulması gereken bağımlılıkları gösterir.
·      $@ Hedefi temsil eder.
·      $^ Geçerli kural için tüm bağımlılıkları temsil eder.
 Bir örnek ile bu değişlenlerin nasıl kullanıldığına bakalım.
.c.o:
       $(CC) -o $@ -c $< $(LIBS)
       @echo ">> $<: compiled $@ created"

all: $(OBJECT_FILE)
        
       gcc -o $(OUTPUT_FILE) $^ $(LIBS)
       @echo "Link done"

 .c.o hedef yazımı yukarıdaki .bet ile .son karşılık gelir yani kaynak dosyaların uzantısı .c hedef dosyanın uzantısı .o olmalı bildrimini yapmış olduk.
 $(CC) -o $@ -c $< $(LIBS) bu satırda kaynak dosyadan hedef dosyatı oluşturacak komutu işleriz. $@ ve $< anlarmları kapalı da olsa aslında basit bir ifadesi vardır. $@ burada oluşturulacak hedef dosyanın ismini içerirken $< ise kaynak dosyanın ismini içerir. Zaten komut bir .c(source) dosyasından .o(obje) dosyasının oluşmasını sağlıyor. Bunun açık yazılmış hali şu şekilde olurdu:
$(CC) -o mayfile.o -c mayfile.c $(LIBS). Peki neden bu şekilde a
çık değilde yukarıdaki gibi kapalı yazım yaptık. Çok sayıda dosyadan oluşan projelerde kapalı yazım ile dosyaların tek tek derlenme komutunu biz değil bu kapalı yazım yapar. Yani işin güzel tarafı bu kapalı yazım ile projede ne kadar .c(source) dosyası var ise o kadar .o(obje) dosyasını tek bir satır komut ile oluşturmuş oluruz.
İşin tam olarak nasıl bu kadar basitçe yapıldığını anlamak için  all: hedefini incelememiz gerekir. Çünkü .c.o ile all hedefleri birbirleriyle yardımlaşarak çalışırlar. All hedefine ve altında işlenen komutlara bir bakalım. 
all: $(OBJECT_FILE)
       $(CC) -o $(OUTPUT_FILE) $^ $(LIBS)
       @echo "Link done"

 Kodu inceleyenin ilk dikkatini çekmesi gereken nokta all hedefinin bir bağımlığının olması. All hedefi $(OBJECT_FILE) bağımlıdır. Yani all hedefinin altında yazılmış olan $(CC) -o $(OUTPUT_FILE) $^ $(LIBS) komutunun işlenebilmesi için $(OBJECT_FILE) ile belirtilen dosyalardan en az birinin değişmiş olması gerekir. OBJECT_FILE bir değişiklik ya da eksikli olduğunda ilk olarak bu ihtiyaçlar giderilir.
 OBJECT_FILE tanımlaması makefile dosyasının başında yapılmalıdır. Örneğin şu şekilde olabilir:
OBJECT_FILE = test.o test2.o test3.o test4.o test5.o. Bu durumda all hedefi aslında şu şekilde yazılmış olur: all: test.o test2.o test3.o test4.o test5.o.
 Artık all hedefi ile .c.o hedeflerinin yardımlaşarak çalışmasını daha basit bir şekilde açıklayabiliriz. All hedefi bağımlılık listesindeki her bir elemanı kontrol eder, eğer bir bağımlılığı bulamaz ya da güncel değil ise o bağımlılığın oluşması için .c.o hedefine çağrı yapar. .c.o hedefide üstüne düşen görevi yaparak all hedefinin ihtiyacı olan dosyayı üretir. İşin pratikliğini şu örnek ile daha da iyi anllayabiliriz: all hedefine bağlı dosyaların hepsinin değiştiğini ya da silindiğini var sayarsak tüm dosyalar sırasıyla otomatik bir şekilde oluşturulacaktır. İşte bu durum makefile yazana büyük bir kolaylık sağlar. Farkettiyseniz projemizde kullandığımız dosya isimleri sadece tek bir noktada geçiyor;
OBJECT_FILE = test.o test2.o test3.o test4.o test5.o. Art
ık projeye eklenen her bir dosyanın ismini bu listeye eklemek yeter.
 Aşağıdaki örnek kod ile projenizdeki tüm dosyları derleyip obje dosyası oluşturabilir ve daha sonrasında link ederek çıktı dosyanızı almayı sağlayabilirsiniz.

CC = gcc
CFLAGS = -O2 -Wall
LIBS = -lm -lnsl
OBJECT_FILE = test.o test2.o test3.o test4.o test5.o

OUTPUT_FILE_NAME = pinkFloyd

.PHONY: clean
.SUFFIXES: .c .o

.c.o:
       @$(CC) -o $@ -c $< $(LIBS)
       @echo "   -[CC]                     $< compiled"


all: $(OBJECT_FILE)
        
       @$(CC) -o $(OUTPUT_FILE_NAME) $^ $(LIBS)
       @echo "   ---------------------------------------------------------------------"
       @echo "   |-- Linking is successful. Output file name: $(OUTPUT_FILE) was created --|"
       @echo "   ---------------------------------------------------------------------"

       @##cp $(OUTPUT_FILE_NAME) /home/zafer/Desktop/
       @#@echo "output file $(OUTPUT_FILE) copyed to Desktop"

clean:
       @rm -f $(OUTPUT_FILE_NAME) *.o
       @echo "      ---------------------------------------------------------"
       @echo "      |-- All object file and outputfile: $(OUTPUT_FILE) deleted --|"
       @echo "      ---------------------------------------------------------\n"

Not: .SUFFIXES: .c .o ile kullanılacak uzantılar tanımlanır.

Çalıştırılan kodun terminalde görünmemesini istiyorsanız ilgili kodun başına @ koymanız yeterli. Bunu yapmaz iseniz çalıştrılacak komut ilk olarak komut satırına yazılır daha sonra çalışır. Bu da komut satırının kirlenmesine sebep olur ve ekranı takip etmeyi zorlaştırır.

3-İleri Düzeyde Makefile

 İleri düzeyde makefile yazabilmek için yukarıda kullandığımız komutlardan fazlasını bilmek gerekir. Örneğin bir önceki örnekte clean hedefinde kullandığımız
   rm -f $(OUTPUT_FILE_NAME) *.o komutu gerçek uygulamalarda yetersiz kalacaktır. Sizinde görebileceğiniz gibi silme işlemi sabit bir dizinde -içinde olduğumuz dizin- sadece obje dosyalarını silmeye yöneliktir.
 Gerçek uygulamalarda projeler birden fazla hatta çok sayıda klasörlerden oluşur. Doğal olarak da obje dosyalarının tek bir klasörde bulunmasını beklemek anlamsız olur. Yapılması gereken bu düzene uyum sağlayarak tüm klasörleri dolaşıp derleyicinin oluşturduğu obje dosyalarını silmektir.
 Tüm klasörleri dolaşıp içlerindeki obje dosyasını silmek göze çok zahmetli görünsede bu işi find komutu ile yapabiliriz. Bu komut istenilen öğeyi komutun çağrıldığı dizinden başlayarak aramaya başlar. İstenilen ögeyi bulduktan sonra tek yapmamız gereken bunu rm komutuna parametre olarak geçmektir. Hatta istenirse hangi dizinde silme yapıldığını  komut terminaline de yazdırarak kullanıcıyı bilgilendirmiş oluruz. Tüm bu anlatılanlara uyan örnek kullanım aşağıdaki gibi olur.
       @find -name '*.o' -exec rm -f {} \; -exec echo " >> "{} "    Deleted" \;
       @rm -f $(OUTPUT_FILE) *.o
       @echo "\n    ---------------------------------------------------------"
       @echo "    |-- All object file and outputfile: $(OUTPUT_FILE) deleted --|"
       @echo "    ---------------------------------------------------------\n"


 Ana konumuzu kaçırmamak için Makefile konusunu şimdilik burada sonlandırıyorum. İleri düzeyde makefile konusuna daha sonra devam edeceğim.

    Faydalanacağını düşündüğünüz kişilere iletin, eksik ya da yanlış olduğunu düşündüğünüz yerleri belirtin.


Şimdi Yazmaya Devam .... 


Hiç yorum yok:

Yorum Gönder

Son Ütücü