Makefiles on IOOPM
Table of Contents
1 Kort introduktion till build management med Make1
När man utvecklar programsystem bestående av flera moduler, med källkodsfiler, headerfiler, objektkodsfiler och rutinbibliotek börjar det bli besvärligt att kompilera och länka. Det besvärliga består av två saker: dels blir kommandon långa och jobbiga att skriva, dels måste man komma ihåg vilka moduler som måste kompileras om när man har gjort ändringar i källkods- eller headerfiler.
Man kan få hjälp med båda dessa saker genom att använda ett
byggverktyg, t.ex. programmet make
. För att avända make
skapar
vi en fil med namnet makefile
eller Makefile
. I denna kan vi
deklarera olika “targets”, dvs. filer som skall skapas vid
kompilering (och länkning) samt beroenden mellan targets och
källkodsfiler, och olika targets. För varje target anger vi också
hur det skall kompileras. Makeverktyget är sedan intelligent nog
att (åtminstone för C, dock ej för Java) räkna vad som måste
kompileras om som resultat av en förändring. Du kompilerar nu
genom att skriva make
(alt make foo
för något namn på foo
)
och resten sköts automatiskt.
Detta är guld värt, eftersom det nu blir lätt att hela tiden kompilera och se till att du har ett körande program.
1.1 En minimal makefile
En minimal makefile
kan se ut på följande sätt. Säg att du t.ex.
har ett program prog.c
som man du kompilera och länka ihop med
en modul modul.o
som ligger i mappen /foo/bar/baz
.
prog: prog.c
gcc -g -Wall prog.c /foo/bar/Baz/modul.o -o prog
prog%3A%20prog.c%20%0A%20%20%20%20gcc%20-g%20-Wall%20prog.c%20%2Ffoo%2Fbar%2FBaz%2Fmodul.o%20-o%20prog%0A
På första raden anges normalt namnet på filen som skall
framställas (målfilen, dvs. target) med ett kolon efter och en
blankseparerad lista av namn på filer som behövs för
framställningen (som målfilen är beroende utav, dependencies).
Andra raden skall börja med ett tab-tecken och innehålla
därefter det kommando som skall utföras för att framställa
målfilen. Har man gjort en sådan fil så kompilerar och länkar du
programmet med kommandot make prog
.
1.2 Hur gör make
?
make
letar upp målet prog
i din makefile
och kontrollerar om
målfilen prog
finns i den aktuella katalogen och om den i så
fall är äldre än de filer den är beroende utav (i exemplet är
den endast beroende av prog.c
). Om prog
inte finns eller är
äldre än prog.c
utför make
kommandot på raden över. Om prog
finns och inte är äldre än prog.c
drar make
slutsatsen att
någon ny kompilering eller länkning inte behövs och skriver ut
meddelandet “`prog´ is up to date”2.
1.3 Ett mer komplicerat exempel
Exemplet ovan är lite väl enkelt: prog
är beroende av endast en
fil, vi har slagit ihop kompilering och länkning till ett enda
steg osv. För att illustrera hur make
kan användas med system
bestående av flera moduler får vi titta på ett större exempel.
Antag att vi skall bygga (kompilera och länka) ett system
bestående av källkods- och header-filer enligt följande figur:
Figure 1: C-filer med include-beroenden till header-filer
Systemet består av två moduler och ett huvudprogram. Varje modul
består av två filer: en headerfil och en källkodsfil. Pilarna i
figuren visar filberoenden: varje modul är beroende av sin
headerfil, modulB
är dessutom beroende av modulA
:s header-fil.
Huvudprogrammet är beroende av båda modulers header-filer.
Framställning av ett exekverbart program från dessa filer sker i
flera steg: först kompileras varje modul för sig och resulterar i
objektfilerna modulA.o
och modulB.o
. Därefter länkas
modulA.o
, modulB.o
och main.o
samman till ett exekverbart
program med något namn, t.ex. main
. I figuren nedan visas vilka
filer som behövs vid framställning av de olika objektmodulerna
resp. vid framställning av det exekverbara programmet: modulA.c
Figure 2: Beroendeträd för kompilering
En makefile för detta system kunde se ut så här:
main: main.o modulA.o modulB.o gcc -g main.o modulA.o modulB.o -o main main.o: main.c modulA.h modulB.h gcc -c -g -Wall main.c modulA.o: modulA.c modulA.h gcc -c -g -Wall modulA.c modulB.o: modulB.c modulA.h modulB.h gcc -c -g -Wall modulB.c
main%3A%20main.o%20modulA.o%20modulB.o%0A%20%20%20%20gcc%20-g%20main.o%20modulA.o%20modulB.o%20-o%20main%0A%0Amain.o%3A%20main.c%20modulA.h%20modulB.h%0A%20%20%20%20gcc%20-c%20-g%20-Wall%20main.c%0A%0AmodulA.o%3A%20modulA.c%20modulA.h%0A%20%20%20%20gcc%20-c%20-g%20-Wall%20modulA.c%0A%0AmodulB.o%3A%20modulB.c%20modulA.h%20modulB.h%0A%20%20%20%20gcc%20-c%20-g%20-Wall%20modulB.c%0A
Om alla källkods- och header-filer finns inmatade så kan nu det
exekverbara programmet main framställas med ett enda kommando:
make main
.
Som förut: tittar make
i makefile
:n och ser att main
beror
av main.o
, modulA.o
och modulB.o
. make
söker då upp dessa
i tur och ordning och kontrollerar deras datum i relation till
datumen för de filer som de i sin tur är framställda från.
Målfiler som inte finns eller är äldre än någon av sinan
källkodsfiler kompileras om genom att motsvarande kommando utförs.
Därefter kommer make
tillbaka till main
och utför kommandot
för att producera det slutgiltiga programmet.
För spårbarhet skriver make
ut de kommandon som utförs på
skärmen. Om något mål inte kan framställas (t.ex. därför att det
har blivit kompileringsfel vid kompilering) avbryts arbetet.
Låt säga att vi har kompilerat main
, testkört programmet och
upptäckt ett fel i modulB
. Vi åtgärdar det genom att ändra i
modulB.c
och sedan kör vi återigen make
. Programmet kör som
förut: den ser att main
är beroende av main.o
, modulA.o
och
modulB.o
. Den tittar då på main.o
och ser att denna är
beroende avf main.c
, modulA.h
och modulB.h
. Ingen av dessa
har ändrats efter att main.o
kompilerades så den kompileringen
görs inte. Samma gäller modulA.o
: den är inte äldre än de filer
den är beroende utav och alltså “up to date”. Men modulB.o
är
äldre än modulB.c
(som vi ändrade när vi fixade felet) och
alltså “out of date”. make
utför därför kompileringskommandot
för modulB.o
. Därefter kommer make
tillbaka till main
. Nu
har dock modulB.o
blivit yngre än main
(vi har ju precis byggt
den!) så make
utför länkningskommandot för att framställa main
på nytt.
Genom att använda make
på detta sätt behöver vi alltså inte
komma ihåg vilka omkompileringar som måste göras efter ändringar
av källkods- eller headerfiler, make gör vad som behöver göras med
ledning av makefile!
En och samma makefile kan med fördel användas för att kompilera
olika targets vid olika tilfällen3. Argumentet till
make
-kommandot är namnet på ett target. Vill du endast kompilera
om modulB.c
i exemplet ovan kan du skriva make modulB.o
.
make
söker då upp målet modulB.o
, undersöker vilka filer det
är beroende utav osv., och struntar i allt som inte behövs för att
framställa modulB.o
. Om man ger kommandot make
utan argument
använder make
det första målet i makefile
:n, alternativt om du
angivit ett target main
som då blir “default”.
2 GNU Make Examples
2.1 A First Simple Makefile
main: myprog C_COMPILER = gcc C_OPTIONS = -Wall -pedantic -g C_LINK_OPTIONS = -lm CUNIT_LINK = -lcunit %.o: %.c $(C_COMPILER) $(C_OPTIONS) $? -c myprog: file1.o file2.o file3.o $(C_COMPILER) $(C_LINK_OPTIONS) $? -o $@ myprog.final: file1.o file2.o file3.o # TODO: add e.g. optimisation flags, remove unnecessary linking, etc. clean: rm -f *.o myprog
main%3A%09myprog%0A%0AC_COMPILER%20%20%20%20%20%3D%20gcc%0AC_OPTIONS%20%20%20%20%20%20%3D%20-Wall%20-pedantic%20-g%0AC_LINK_OPTIONS%20%3D%20-lm%20%0ACUNIT_LINK%20%20%20%20%20%3D%20-lcunit%0A%0A%25.o%3A%09%25.c%0A%20%20%24%28C_COMPILER%29%20%24%28C_OPTIONS%29%20%24%3F%20-c%0A%0Amyprog%3A%09file1.o%20file2.o%20file3.o%0A%20%20%24%28C_COMPILER%29%20%24%28C_LINK_OPTIONS%29%20%24%3F%20-o%20%24%40%0A%0Amyprog.final%3A%09file1.o%20file2.o%20file3.o%0A%23%20TODO%3A%20add%20e.g.%20optimisation%20flags%2C%20remove%20unnecessary%20linking%2C%20etc.%0A%0Aclean%3A%0A%20%20rm%20-f%20%2A.o%20myprog%0A
Explanations:
%.o: %.c
is a target that matches any file.o
pattern and
translates into a dependency to file.c
.
$?
expands to the dependency(ies) for the current target, i.e.,
file.c
for file.o
and file1.o file2.o file3.o
for myprog
.
(Only those with a newer date.)
$@
expands to the name of the target, e.g. file.o
in the case
of the file.o
target, and myprog
in the case of the myprog
target.
It is common to use variables like C_COMPILER = gcc
and then
write $(C_COMPILER)
in the commands. If we wanted to change
compiler to clang, for example, we would simply need to change one
line, C_COMPILER = clang
and be good to go. Typical names for
compiler variable is CC
, CFLAGS
for options to the compiler,
and LDFLAGS
for linking flags. I choose more descriptive names
above, I hope.4
2.2 Make Test
In addition to the above, we add:
test1: mytests1.o file1.o $(C_COMPILER) $(C_LINK_OPTIONS) $(CUNIT_LINK) $? -o $@ test2: mytests2.o file2.o file3.o $(C_COMPILER) $(C_LINK_OPTIONS) $(CUNIT_LINK) $? -o $@ test: test1 test2 ./test1 ./test2
test1%3A%20mytests1.o%20file1.o%20%0A%20%20%24%28C_COMPILER%29%20%24%28C_LINK_OPTIONS%29%20%24%28CUNIT_LINK%29%20%24%3F%20-o%20%24%40%0A%0Atest2%3A%20mytests2.o%20file2.o%20file3.o%0A%20%20%24%28C_COMPILER%29%20%24%28C_LINK_OPTIONS%29%20%24%28CUNIT_LINK%29%20%24%3F%20-o%20%24%40%0A%0Atest%3A%20test1%20test2%0A%20%20.%2Ftest1%20%0A%20%20.%2Ftest2%0A
If we are using CUnit, it is likely that we only have one
compilation unit with all tests, as CUnit groups the tests in test
suites anyways. Also add test1
and test2
to be removed by clean
.
2.3 Make Memtest (Valgrind)
In addition to the above, we add:
memtest: test1 test2 myprog
valgrind --leak-check=full ./test1
valgrind --leak-check=full ./test2
valgrind --leak-check=full ./myprog
memtest%3A%20test1%20test2%20myprog%0A%20%20valgrind%20--leak-check%3Dfull%20.%2Ftest1%20%0A%20%20valgrind%20--leak-check%3Dfull%20.%2Ftest2%0A%20%20valgrind%20--leak-check%3Dfull%20.%2Fmyprog%0A
Questions about stuff on these pages? Use our Piazza forum.
Want to report a bug? Please place an issue here. Pull requests are graciously accepted (hint, hint).
Nerd fact: These pages are generated using org-mode in Emacs, a modified ReadTheOrg template, and a bunch of scripts.
Ended up here randomly? These are the pages for a one-semester course at 67% speed on imperative and object-oriented programming at the department of Information Technology at Uppsala University, ran by Tobias Wrigstad.
Footnotes:
touch prog.c
som ändrar på
datumet på prog.c
till nu eller make -B
.