Kihagyás

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 egy clean nevű á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
#include <stdio.h>

int main() {
  printf("Hello Make!\n");
  return 0;
}

A fordításához szükséges Makefile

1
2
3
4
5
hello: hello.c
    gcc hello.c -o hello

clean:
    rm -rf hello

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
make
./hello
make clean

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 egy n.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 egy n.cc vagy n.cpp állományból a $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@
  • n állomány előáll az n.o automatikus 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
.PHONY: clean all
clean: 
    rm hello $(objects)

Á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
all: hello 

objects = hello.o

hello: $(objects)
    gcc -o hello $(objects)

hello.o: hello.c 
    gcc -c hello.c

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
make CC=clang CFLAGS="-g -O1"
./hello
make clean

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 wildcard fü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 Makefile formátumban. Általában ennek a fájlnak a kiterjesztése .d, minden egyes forrásra egy külön ilyen fájl.
  • include direktíva: másolhat/beilleszthet tartalmakat más Makefile-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
#include <stdio.h>

void hello() {
  printf("Hello header prerequisites!\n");
}

hello.h:

1
2
3
4
5
6
#ifndef HELLO_H
#define HELLO_H

void hello();

#endif

main.c:

1
2
3
4
5
6
#include "hello.h"

int main() {
  hello();
  return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PROG:=hello
SRCS:=hello.c main.c
OBJS:=$(patsubst %.c,%.o,$(SRCS))
DEPS:=$(patsubst %.c,%.d,$(SRCS))

all: $(PROG)

$(OBJS): %.o: %.c
    $(CC) -c $(CPPFLAGS) $(CFLAGS) $< -o $@

$(DEPS): %.d: %.c
    echo $(DEPS)
    $(CC) -MM $(CPPFLAGS) $< -MF $@

$(PROG): $(OBJS)
    $(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@

clean:
    rm -rf $(PROG) $(OBJS) $(DEPS)

.PHONY: all clean

include $(DEPS)

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 a text paraméterben előforduló összes from kifejezés to-ra íródik.
  • $(patsubst pattern,replacement,text): a text whitespace-szel szeparált szavaiban a pattern-re illeszkedő szavakat keres, amelyet lecserél a replacement által adott minta alapján.
  • $(strip string): a sztringet határoló whitespace karaktereket eltávolítja
  • $(sort list): a list-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
project(Hello C)

add_executable(hello hello.c main.c)

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
mkdir build
cd build
cmake ..
make
  1. A cmake .. parancs legenerálja a Makefile-okat a build könyvtárban (természetesen ha a CMakeLists.txt elérési útvonala nem a gyökér könyvtár, akkor azt módosítani kell).
  2. A make utá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 .d fá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.