CMake ile Çeşitli Hedeflere Derleme Yapmak

Programlama projelerindeki tek çıktı çalıştırılabilir dosya değildir. Bazen çalıştırılabilir dosyaların yanında, dinamik ve statik kütüphane dosyası biçiminde çıktı elde etmek de gerekebilir. Çok küçük projelerde bu gerekli olmasa bile, proje büyüdükçe kütüphane (library) oluşturma gereksinimi artar. Çünkü kodları kütüphane haline getirmek onların hem kullanımlarını hem de bakımlarını önemli ölçüde kolaylaştırır. CMake de kütüphane çıktılarını elde etme ve bu çıktıları çeşitli çalıştırılabilir dosyalara bağlama gibi özellikleri desteklemektedir.

Bir önceki yazımda CMake ile çalıştırılabilir dosyaları nasıl oluşturabileceğimizden bahsetmiştim. Bunun gibi CMake’ten çıktı olarak alabileceğimiz dosyalara CMake’de (ve pek çok Build sisteminde) hedef (target) adı verilmektedir. Bu yazıda çeşitli hedef türlerini inceleyeceğiz, ama ondan önce bir önceki yazıda bahsettiğimiz add_executable komutu ile ilgili bazı ayrıntılara değineceğiz.

Çalıştırılabilir Dosyaların Oluşturulması

Bu komutu aşağıdaki gibi kullandığımızda, CMake bunun o sisteme özel çalıştırılabilir bir dosya olduğunu varsayar:

add_executable(Uygulama main.cpp)

Ancak Uygulama isimli hedefimiz Apple platformlarında yer alan bir App Bundle veya Windows platformlarında yer alan bir Windows GUI çalıştırılabilir dosyası olabilir. add_executable komutu da bu gibi platforma özel ayrıntıların belirtilmesi için bazı anahtar kelimelerin kullanımına izin vermektedir. Bu komutun tam formu aslında şöyledir:

add_executable(<name> [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL]
[source1] [source2 …])

"<name>" kısmına hedefin ismini, "[sourceX]" kısımlarına ise kullanılan kaynak kod dosyalarının isimlerini yazacağımızı zaten biliyoruz. Kullanılabilen diğer 3 anahtar kelimeyi ise şu şekilde açıklayabiliriz:

  • WIN32: Bu anahtar kelime Widnows platformlarında konsol yerine bir GUI çalıştırılabilir dosyası oluşturulması gerektiğini beliritir. Diğer platformlarda bu anahtar kelimenin kullanılması görmezden gelinir. Bu anahtar kelime kullanıldığında, Build sistemi işlemi gerçekleştirebilmesi için main fonksiyonu yerine WinMain fonksiyonuna ihtiyaç duymaktadır.
  • MACOSX_BUNDLE: CMake ile herhangi bir Apple platformunda derleme yaparken (MacOS, iOS vb.) CMake’e bir App Bundle oluşturmasını söyleyen anahtar kelimedir. Apple olmayan platformlarda ise bu anahtar kelime görmezden gelinir. App Bundle aslında Apple sistemlerinde üst dizin ismi ".app" uzantısına sahip bir dizin hiyerarşisini belirtir.[1] Bu anahtar kelimenin kesin olarak etkisi, kullanılan Apple platformuna göre değişiklik gösterir. CMake ayrıca bu anahtar kelime ile "Info.plist" dosyasını da oluşturur.
  • EXCLUDE_FROM_ALL: Bir projede birden fazla hedef (target) tanımlanabilir. Fakat genellikle sadece bunlardan birkaçı derlenir. Derleme zamanında hangi hedefin derleneceği tam olarak belirtilmemişse, ALL hedefi derlenir ve bu hedef o anda CMake’te tanımlanan bütün hedefleri içerir (bu hedefin ismi platforma göre bazen ALL_BUILD gibi isimler alabilir). Bu anahtar kelimeyi kullanan hedefler ise ALL hedefine dahil edilmezler. Bu durumda bu çalıştırılabilir dosya, derleme komutunda açık bir şekilde belirtilmediği sürece derlenmez. Eğer bu dosyanın ALL altındaki herhangi bir hedef ile bir bağımlılığı varsa, bu dosya yine derlenebilir.

add_executable komutunun yine birkaç farklı formu bulunsa da bunlar bu yazının kapsamı dışında yer almaktadır. Şimdi bir örnek ile bu konuyu tamamlayalım. Diyelim ki Windows sisteminde Windows GUI çalıştırılabilir dosyası derlemek istiyoruz ve bu dosyanın isminin App1 olduğunu varsayıyoruz. Ayrıca bu çalıştırılabilir dosyayı ALL hedef listesinden çıkarmak istiyoruz. Projede sadece "main.cpp" dosyasının bulunduğunu varsayarsak, "CMakeLists.txt" dosyasının içeriği şuna benzer olacaktır:

cmake_minimum_required(VERSION 3.10)
project(App LANGUAGES CXX)
add_executable(App1 WIN32 EXCLUDE_FROM_ALL main.cpp)

Elbette "build" klasörünüze gelip şu komutları çalıştırdığınızda App1 isimli çalıştırılabilir bir dosya ile karşılaşmayacaksınız:

cmake ..
cmake --build .

Ancak ikinci komuta şunu eklediğinizde bu sefer App1 isimli çalıştırılabilir dosya ile karşılacaksınız:

cmake ..
cmake --build . --target App1

--target ile derlemek istediğiniz hedeflerin isimlerini girerek, sadece bu hedefleri derlemeniz mümkündür. Örneğin elimizde A, B ve C isimli add_executable komutu ile oluşturulan 3 farklı hedef olsun. Bu hedeflerden sadece A ve B hedeflerini derleyip C hedefini derlemek istemezsek, şuna benzer bir komut yazmamız gerekir:

cmake --build . --target A B

Bu komutu -t kısa seçeneğini kullanarak da yazmanız mümkündür:

cmake --build . -t A B

Statik ve Dinamik Kütüphanelerin Oluşturulması

Şimdi de CMake ile oluşturabileceğimiz diğer çıktılara bakalım. Bildiğiniz gibi bir C veya C++ kaynak kodunu derlediğinizde ortaya Object Code (Amaç Kod) adı verilen dosyalar çıkacaktır. Daha sonra bağlayıcı (linker) ile bu dosyalar birleştirilerek çalıştırılabilir dosyalar oluşturulmaktadır. Ancak bazen bazı Object Code dosyalarını bir araya getirerek bunları yeniden kullanılabilir bir yapıda tutmak isteriz. İşte Statik Kütüphane (Static Library) dediğimiz şey tam olarak budur. Daha açık anlatacak olursak, farklı kaynak kodlarda (veya başlık dosyalarında) yer alan fonksiyonların (veya sınıfların) derlenerek bir araya getirilmesi ve daha sonra çeşitli hedeflere bağlanarak yeniden kullanılmasını sağlayan dosyalara statik kütüphane dosyaları denilmektedir.

Statik kütüphaneler çalıştırılabilir dosyaya bağlama aşamasında doğrudan bağlandıkları için onun boyutunu arttırırlar. Ayrıca her bir uygulamada yapılan değişiklik için tekrar tekrar derlenmeleri gerekir. Bu nedenle bakımları ve farklı programlarda paylaşımları biraz zordur. Bir başka kütüphane türü olan Dinamik Kütüphane (Dynamic Library) veya diğer bir adıyla Paylaşılan Kütüphane (Shared Library) bu sorunu bir nevi çözer. Dinamik kütüphanelerde kodun bağlanması çalışma zamanında gerçekleşir. Böylelikle çalıştırılabilir dosyaların boyutu arttırılmamış olur. Ayrıca bu kütüphanelerde yapılan her bir değişiklik için bütün kodun tekrar tekrar derlenmesi gerekmediğinden, bu tip kütüphanelerin bakımı da oldukça kolaydır.

Aslında kütüphane konusu ayrı bir yazı konusudur. Ben burada bu konu ile ilgili çok fazla ayrıntıya girmeyeceğim. Gerektiğinde başka bir yazı ile bu iki kütüphane türü arasındaki ayrıntılı farkı anlatırım. Şimdi CMake ile bu kütüphanelerin ne ilgisi var onu anlatalım. CMake sadece çalıştırılabilir dosyalar üretmek için değil, statik ve dinamik kütüphaneler üretmek için de kullanılabilir. Ayrıca bu işlem oldukça kolaydır. CMake’te kütüphane hedefleri tanımlamak için add_library komutu kullanılır. Bu komut oluşturulacak her bir kütüphane için ayrı ayrı yazılmalıdır. Bu komutun normal kütüphaneler için olan formu şu şekildedir:

add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
[<source>…])

Normal kütüphaneler diyorum, çünkü CMake ile bu yazıda gördüklerimizin dışında çok çeşitli kütüphaneler (veya kütüphane benzeri yapılar) de oluşturabilmemiz mümkündür. Bu komutta "<name>" kısmına oluşturulacak kütüphanenin ismi yazılır. "[<source>…]" kısmına ise tıpkı add_executable komutunda olduğu gibi kaynak dosya isimleri yazılır. EXCLUDE_FROM_ALL kelimesinin kullanımı da add_executable komutundaki ile birebir aynı anlama gelir. Burada bizim karşımıza seçeneksel olarak kullanabileceğimiz 3 anahtar kelime çıkmaktadır (sadece bu form için geçerli). Bu anahtar kelimelerin anlamlarını şöyle verebiliriz (burada "<name>" ksımına targetName yazıldığını varsayarak örnekler vereceğim):

  • STATIC: Statik kütüphane veya bir diğer adıyla arşiv (archive) oluşturmak için kullanılan anahtar kelimedir. Windows’ta varsayılan olarak "targetName.lib" şeklinde çıktı verirken, UNIX türevi platformlarda "libtargetName.a" şeklinde çıktı vermektedir.
  • SHARED: Dinamik kütüphane oluşturmak için kullanılan anahtar kelimedir. Windows’ta "targetName.dll" çıktısını, Apple platformalarında "libtargetName.dylib" çıktısını, diğer UNIX türevi platformlarda ise "libtargetName.so" çıktısını verir.
  • MODULE: Bu da aslında bir tür dinamik kütüphane oluşturmayı belirtir. Ancak kütüphanenin doğrudan başka bir kütüphaneye veya çalıştırılabilir dosyaya bağlanması yerine, çalışma zamanında bağlama mantığı vardır. Bu nedenle modüller aslında uygulamayı kullanan kullanıcıların çalışma zamanında seçebilecekleri eklentileri (Plug-ins) veya keyfi bileşenleri tasarlamak için kullanılırlar. (Bu konu burada anlatılmayacaktır.)

Bu üç anahtar kelimeden yalnızca biri kullanılabilir veya hiçbiri kullanılmayabilir. Eğer herhangi biri kullanılmazsa, bu durumda oluşturulacak kütüphane türünü seçmek geliştiriciye bırakılır. Bu gibi durumlarda geliştirici derlenecek kütüphanelerin statik veya dinamik olmasını derleme zamanında belirtebilir. Bunu yapması için BUILD_SHARED_LIBS değişkenine değer ataması gerekir. Eğer bu değişkenin değeri true olursa dinamik kütüphane derleneceği, false olursa statik kütüphane derleneceği anlaşılır. Değişkenleri bir sonraki yazıda görecek olsak da cmake komutunda -D seçeneği kullanarak onları nasıl belirttiğimizi ve "=" ile onlara nasıl atama yapabileceğimizi kısaca şöyle gösterebiliriz:

cmake -DBUILD_SHARED_LIBS=TRUE ..

Şimdi add_library komutunun kullanımı ile ilgili birkaç örnek verip diğer konumuza geçelim (örneklerde UNIX türevi bir platformda işlem yaptığımız varsayılmıştır):

add_library(math STATIC my_math.c) # libmath.a çıktısını verir
add_library(integer SHARED integer1.c integer2.c) # libinteger.so çıktısını verir
add_library(stats STATIC EXCLUDE_FROM_ALL stats.cpp) # libstats.a çıktısını verir, ancak hedefi ALL hedefinden çıkarır
add_library(vision vision.c) # Çıktı programcının seçimine bağlıdır

Kütüphane Hedeflerini Başka Hedeflere Bağlamak

Buraya kadar kütüphane hedefleri tanımlamayı öğrendik. Peki onları çalıştırılabilir dosyalara veya başka kütüphanelere nasıl bağlayacağız? Kütüphaneler arasında olan veya çalıştırılabilir dosyalar ile kütüphaneler arasında olan ilişkiler başta oldukça basit görünür. Yani genellikle her şey bir kütüphanenin bir diğerine ihtiyaç duyması ve onu basitçe bağlamamız gibi düşünülebilir. Ancak gerçekte kütüphaneler arasındaki bağımlılık ilişkisi bu kadar basit değildir. Bağımlılığın ne türde olduğu da önemli bir konudur. CMake 3 çeşit bağımlılık türünü destekler. Şimdi bunları hızlıca inceleyelim:

  • PRIVATE: Bir A kütüphanesi ile B kütüphanesi arasında olan bu tür bir bağımlılık, B kütüphanesinin, A kütüphanesinde sadece iç implementasyonda kullanıldığını belirtir. Bu nedenle A kütüphanesini kullanan herhangi bir kütüphanenin B ile ilgili bir şey bilmesi gerekmez.
  • INTERFACE: Bir A kütüphanesi ile B kütüphanesi arasında olan bu tür bir bağımlılık, B kütüphanesinin, A kütüphanesinde sadece iç implementasyonda kullanılmadığı, ancak sadece arayüzünde kullanıldığını belirtir. Yani A kütüphanesini kullanan herhangi bir kütüphanenin, B kütüphanesini de ele alması gerekir.
  • PUBLIC: Bu tür bağımlılıkta ise B kütüphanesinin, A kütüphanesinde hem iç implementasyonda hem de arayüzde kullanıldığı belirtilir. Bunun anlamı A kütüphanesi B olmadan kullanılamaz ve A kütüphanesini kullanan bir başka kütüphane B kütüphanesine de bağımlı olur.

Kütüpaheneler arası bu ilişkiler CMake’te target_link_libraries komutu ile belirtilebilir. Bu komut iki hedef arasındaki bağımlılığın türü ile beraber belirtilmesini sağlar. Böylelikle CMake, hedefler arası bağlama işlemini gerçekleştirebilir. Bu komutun en yaygın kullanılan formu şöyledir:

target_link_libraries(<target>
<PRIVATE|PUBLIC|INTERFACE> <item>…
[<PRIVATE|PUBLIC|INTERFACE> <item>…]…)

"<target>" kısmına bağlama yapılacak hedef yazılr. Bu hedef adı daha önce add_executable ile belirtilen bir çalıştırılabilir dosya hedefi adı veya add_library ile belirtilen bir kütüphane hedefi adı olabilir. Daha sonra da bağımlılık türü belirtilip bağlanacak diğer hedef isimleri yazılır. Bağımlılık türleri zaten daha önce açıkladığımız isimlerden birini alabilir.

Bu komutun ne kadar esnek olduğunu göstermek için hemen bir örnek yapalım. Bu örnekte 5 kütüphane hedefi ve 1 de çalıştırılabilir dosya hedefi tanımlayıp 4 kütüphaneyi çeşitli bağımlılık türleri ile bir kütüphaneye bağlayalım. En son da bu kütüphaneyi çalıştırılabilir dosyaya bağlayalım. "CMakeLists.txt" dosyasındaki diğer ayrıntıları atlarsak kodumuz şöyle olacaktır:

add_library(manager manager.cpp)
add_library(algortihms algorithms.cpp)
add_library(math math.cpp)
add_library(stats stats.cpp)
add_library(ui ui.cpp)
add_executable(test main.cpp)
target_link_libraries(manager
    PUBLIC ui
    PRIVATE algortihms math stats
)
target_link_libraries(test PRIVATE manager)

Gördüğünüz gibi manager kütüphanesine ui kütüphanesini PUBLIC olarak, diğerlerini ise PRIVATE olarak bağladık. En son ise bu kütüphaneyi çalıştırılabilir dosya hedefine PRIVATE olarak bağladık. İşte CMake’in belki de avantajını en fazla gösterdiği yerlerden biri burasıdır. Normalde komut satırından bu tür bağlama işlemleri için satırlarca komut yazmanız gerekebilirken, CMake bu işi bu kadar basit ve açık bir şekilde halledebilmektedir.

Bazı projelerde kendi tanımladığınız hedeflere bağlama yapmak yerine sistemde olan veya başka kişilerce hazırlanmış kütüphaneleri, projenizde kullandığınız diğer kütüphanelere veya çalıştırılabilir dosyalara bağlamak istersiniz. Bu durumda target_link_libraries komutunun çok daha sade bir formunu kullanabilirsiniz. Örneğin sisteminizde bulunan, ancak standart arama yollarından birinde yer almayan "libtest.a" isimli statik kütüphaneyi App isimli bir çalıştırılabilir dosya hedefine şu şekilde tam yol (full path) göstererek bağlayabilirsiniz:

target_link_libraries(App /home/user12/libs/libtest.a)

Eğer sisteminizde standart dizinlerden birinde bulunan bir kütüphaneyi bağlamak isterseniz, sadece ismini yazmanız yeterli olur (bu durum bazı platformlarda değişiklik gösterebilir):

target_link_libraries(App pthread)

Gördüğünüz gibi CMake bütün bu işlemleri tek bir komut ile halledebilmektedir. Son olarak özellikle çoklu yapılandırmaya sahip platformlar için işinize yarayacak küçük bir bilgi ile yazımı bitirmeyi planlıyorum. Eğer target_link_libraries komutunda yazdığınız kütüphane hedeflerinin başına debug, optimized veya general gibi bir kelime yazarsanız, bu özel bir anlama gelecektir. debug kelimesi bu hedefin sadece Debug modunda, optimized kelimesi sadece Release modunda, general kelimesi ise her iki modda da bağlanması gerektiğini CMake’e anlatır. general kelimesi zaten hiçbir kelimenin kullanılmadığı varsayılan durum ile aynıdır. Diğer kelimeler ile ilgili şu örnekleri verebiliriz:

add_library(debuglib code_debug.cpp)
add_library(releaselib code_release.cpp)
add_executable(app main.cpp)
target_link_libraries(app debug debuglib)
target_link_libraries(app optimized releaselib)

Ancak bu kelimelerin kullanımı projenin temizliği açısından önerilmemektedir. İleride kullanacağımız daha yeni CMake özellikleri ile bu tip projeleri daha temiz bir şekilde hazırlamamız mümkündür. Ancak bazı CMake projelerinde bu kelimeleri görürseniz ne anlama geldiğini bilin.

Bununla birlikte bu yazımızın da sonuna geldik. Bu yazıda CMake’te çalıştırılabilir hedef dışındaki hedeflerden bazılarını görüp bunları nasıl tanımlayacağımızı açıkladık. Ayrıca statik ve dinamik kütüphaneleri çalıştırılabilir dosyalara ve diğer kütüphanelere nasıl bağlayabileceğimizi de görmüş olduk. Bunun yanında dış kütüphaneleri veya sistemde yer alan kütüphaneleri CMake ile nasıl kullanabileceğimizi gördük. Son olarak da genellikle eski CMake projelerinde karşınıza çıkabilecek olan bazı kelimeleri, bunların anlamlarını ve kullanımlarını gördük.

Referanslar

  1. https://en.wikipedia.org/wiki/Bundle_(macOS)#Application_bundles
4.3 3 votes
Article Rating
Subscribe
Bildir
guest

0 Yorum
Eskiler
En Yeniler Beğenilenler