make, cmake
make¶
Nagyobb rendszerek fordításakor segítség, ha a fordítás lépéseit, elemeit nem kell mindig egyesével megadnunk, hanem egy egyszerűbb eszköz segítségével definiálhatjuk az egyes lépéseket, ráadásul mindezt úgy, hogy a rendszer azon részei legyenek mindig újrafordítva, amelyben valamilyen változtatás történt a legutóbbi fordítás óta.
Ezt nyújtja nekünk a Makefile a C/C++ programok fordításához, amely gyakorlatilag egy szabálygyűjteményt kezel, amelyeket végrehajtva akár egy teljes fordítási folyamatot is le tudunk vezényelni.
Szabályok¶
-
Szabály (rule): célok (targets), előfeltételek (prerequisites) & leírások (recipes). Minden szabály több parancs végrehajtásából állhat, minden parancs külön sorba kell kerüljön.
-
Target: ami elkészül (általában egy fájl, amit a program generál), prereq: miből, recipe: hogyan.
-
Hívása:
make TARGET(amennyiben nincs TARGET, akkor az első szabály fog meghívódni). -
Adott leírás végrehajtódik, ha a cél még nem létezik, vagy ha valamelyik előfeltétel menet közben megváltozott.
-
clean: speciális target, hatása nem egycleannevű állomány elkészítése, hanem a megfelelő állományok törlése. -
Vigyázat!
Makefile-oknál a formátum elég régimódi: kötelezően tabot kell használni (8 space nem ugyanazt jelenti)
Példaként legyen adott egy hello.c állományunk:
1 2 3 4 5 6 | |
A fordításához szükséges Makefile
1 2 3 4 5 | |
Ebben a Makefile-ban egy hello és egy clean target van. Előbbi célja a hello bináris állomány előállítása, utóbbié ennek törlése. A hello target létrehozása a hello.c állomány meglététől függ. Ha még nincs hello binárisunk, akkor a make parancs kiadásával meghívódik ez a target, és előáll a bináris a gcc megfelelő hívásával. Amennyiben már lenne hello bináris, de annak az elkészítési ideje régebbi, mint a hello.c állomány -amitől függ a target végrehatjása - utolsó módosításai ideje, a make meghívásával újrafordítódik a bináris.
A clean targetet külön kell hívnunk, ha azt szeretnénk, hogy a bináris futtatása után az törlődjön:
1 2 3 | |
Ahogy nő a Makefile mérete, elképzelhető, hogy megváltozik annak default szabálya. Hogy ezt elkerüljük, általános elv, hogy alkalmazunk egy all targetet a Makefile elején, ebben rögzítve, hogy mely fájl/fájlok, avagy targetek elkészítése a végső cél.
Megjegyzés: a make parancs alapvetően a Makefile-t értelmezi, és annak tartalmától függ, mi is történik. Ha más fájlt szeretnénk a make paranccsal feldolgozni, használjuk a -f kapcsolót, majd adjuk meg a fájlnevet.
Implicit szabályok¶
A Makefile-okat alapvetően a C/C++ programok fordítására találták ki (mégha mást is meg lehet oldani velük), ezért vannak olyan ismeretei, amelyek a C/C++ programok fordítását segítik, és anélkül, hogy konkrétan megmondanánk, mit kell tenni, a make tudja azt. Ilyen implicit szabályok lehetnek:
n.oállomány automatikusan előáll egyn.cállományból a$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@paranccsal (azaz ha a target%.o, az előfeltétel%.c, akkor ki sem kell írni a parancsot)n.oállomány automatikusan előáll egyn.ccvagyn.cppállományból a$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@nállomány előáll azn.oautomatikus linkelésével a$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@parancsot futtatva
.PHONY¶
Ha nem szeretnénk, hogy a target nevének függvénye, illetve az, hogy létezik-e ilyen nevű állomány, vagy se legyen az előfeltétele az adott target lefuttatásának, akkor az adott targetet soroljuk fel a .PHONY target előfeltételeként:
1 2 3 | |
Általában a clean és all targeteket érdemes megadni ilyen módon, hiszen ezek bizonyos értelemben ál targetek, a nevük nem tükrözi a feladatukat, és végrehajtásuk hatására nem jön létre a nevükkel megegyező nevű állomány.
Változók¶
Elképzelhető, hogy egy-egy target ugyanattól, vagy ugyanazoktől az előfeltételektől függ. Például attól, hogy elkészültek-e az adott rendszer különböző moduljai, object állományai. Ilyenkor ahelyett, hogy mindig felsorolnánk ezeket, érdemes változókat használni, amelyeknél elegendő egyszer felsorolni a szükséges előfeltételeket, és utána erre a változóra hivatkozhatunk minden alkalommal.
1 2 3 4 5 6 7 8 9 | |
Széles körben használt, standardnak tekinthető változók:
- $(CC): a rendszer default fordítója
- $(CFLAGS): parancssori kapcsolók, amiket a fordítónak átadunk C fordításkor
- $(LDFLAGS): linkelés parancssori kapcsolói
- Az $(LDFLAGS) használata nem szükségszerűen fontos egy ilyen egyszerű példában, de jó gyakorlatként érdemes hosszászokni:
1 2 3 | |
Automatikus változók:
$@- a target neve$<- az első előfeltétel$^- az össszes előfeltétel$?- az összes előfeltétel, amely újabb, mint a target
Wildcardok:
-
% -
illeszkedésnél egy sztring egy vagy több karakterére illeszkedhet
- helyettesítéskor az erre illeszkedő karaktersorozatot tudjuk kicserélni
-
legtöbbször szabály definíciókban vagy néhány speciális függvénynél használjuk
-
* -
hasonló, mint az előbbi, de vigyázni kell vele, mert ha nem illeszkedik semmire, akkor marad a jelentése
* - ha mindenképp wildcardként szeretnénk kezelni, akkor használni kell a
wildcardfüggvényt$(wildcard *.o)
%.o: %.c önmagában egy-egy jó minta fordításhoz, ugyanakkor a header állományokban levő változtatásokat nem érzékeli. Manuálisan azt vezetni, hogy melyik forrás állomány melyik headertől függ (tranzitíven), nagy rendszerek esetében hibákat hordozhat magában.
- A fordítót meg lehet arra kérni, hogy gyűjtse össze az adott forrásban használt header állományokat, és azokat írja ki egy speciális
Makefileformátumban. Általában ennek a fájlnak a kiterjesztése.d, minden egyes forrásra egy külön ilyen fájl. includedirektíva: másolhat/beilleszthet tartalmakat másMakefile-okból (például ilyen.dállományokból. A lenti példában az ezzel a módszerrel megadott megoldás a függőségek kezelésére egy kicsit egyszerűbbé válik.
hello.c:
1 2 3 4 5 | |
hello.h:
1 2 3 4 5 6 | |
main.c:
1 2 3 4 5 6 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
Függvények¶
Már láttunk példát pár függvényre, amelyek meghívhatóak $(fn, arguments) vagy ${fn, arguments} alakban.
Néhány függvény a teljesség igénye nélkül:
$(subst from,to,text): sztring helyettesítés, ahol atextparaméterben előforduló összesfromkifejezésto-ra íródik.$(patsubst pattern,replacement,text): atextwhitespace-szel szeparált szavaiban apattern-re illeszkedő szavakat keres, amelyet lecserél areplacementáltal adott minta alapján.$(strip string): a sztringet határoló whitespace karaktereket eltávolítja$(sort list): alist-ben adott szavakat rendezi$(dir names): anames-ben felsorolt fájlok könyvtár részét adja vissza- ...
CMake¶
Nagyobb projektek esetén a Makefile kézi karbantartása könnyen átláthatatlanná válik, különösen ha több platformon vagy többféle konfigurációval szeretnénk fordítani a programot. Erre nyújt megoldást a CMake, amely egy platformfüggetlen build rendszer-generátor. A CMake nem fordít közvetlenül, hanem Makefile-t, ninja-t vagy egyéb build scriptet generál, amely aztán fordításra használható.
A CMake alapja egy konfigurációs fájl, amelyet mindig CMakeLists.txt néven kell elnevezni. Ebben határozzuk meg:
- a projekt nevét,
- a verziót,
- a fordítandó forrásokat,
- a fordítókapcsolókat,
- a szükséges könyvtárakat.
Példa CMakeLists.txt¶
Használjuk az előbbi hello.c, main.c és hello.h forrásállományokat a példához!
1 2 3 | |
CMake használata¶
Ha már rendelkezünk egy CMakeLists.txt fájllal, a következő lépésekkel tudunk fordítani:
1 2 3 4 | |
- A
cmake ..parancs legenerálja a Makefile-okat a build könyvtárban (természetesen ha aCMakeLists.txtelérési útvonala nem a gyökér könyvtár, akkor azt módosítani kell). - A
makeutána már használható a projekt lefordítására.
Előnyök¶
- Platformfüggetlenség: nem csak GNU Make-ot, hanem Visual Studio projektet, ninja-t, stb. is képes generálni.
- Könnyű konfiguráció többféle build-típusra (debug, release, stb.).
- Kódmodulok, könyvtárak és külső függőségek egyszerű kezelése.
- Automatikus header dependency kezelés (nincs szükség
.dfájlokra külön).
Továbbfejlesztés¶
A CMake sokkal többre képes:
- különböző build konfigurációk beállítása (
CMAKE_BUILD_TYPE), - tesztelési keretrendszerek integrációja,
- külső könyvtárak
find_package-gel történő kezelése, - telepítő készítése.