Maak je werk met Make!
Dit artikel is geschreven op 7/8 oktober 2000 door Wilbert Berendsen. (Many other translations available).
Inleiding
Bijna iedereen die Linux gebruikt, is wel eens met het programma
make
in aanraking gekomen. Het doet zijn werk wanneer wij een
programma of de kernel willen bouwen vanaf de broncode, wanneer wij pakketten
willen installeren etc. Make is dus een belangrijke tool voor het
ontwikkelen van software.
Maar make
kan nog veel meer!
In dit document zullen we zien dat make
een krachtige hulp kan
zijn bij dagelijks werk zoals het schrijven van artikelen, boeken of het
samenstellen van een leuke website.
Terloops zullen veel andere `unix handigheidjes' de revue passeren.
Aan het slot van dit verhaal nog enkele tips over hoe je make
verder nog kan gebruiken. Let wel: we hebben het af en toe over Linux, maar in
feite kun je make
op ieder besturingssysteem gebruiken.
Voorbeeld: Een website maken
We willen een website gaan maken, die door verschillende mensen wordt gebouwd. Jan zorgt voor een tweetal pagina's en houdt die ook bij, en Piet maakt de layout.
We hebben een simpel systeem nodig om de layout en de inhoud van elkaar te scheiden. Een krachtige manier is natuurlijk: de inhoud uit een database lezen, telkens als de pagina wordt opgevraagd. Dit is hoe bijvoorbeeld PHP en Microsoft Active Server Pages werken. Maar, we hebben slechts de mogelijkheid om simpele HTML (HyperText Markup Language) op te slaan. Bovendien verandert de inhoud in ons simpele geval niet zo vaak dat een database efficiënt is.
Wij gaan dus met een paar simpele commando's een site in elkaar smeden.
Opzet
Jan heeft enkele artikelen in zijn homedirectory staan:
aanbod.html
en index.html
. Beide staan de map
/home/jan/Docs/website/
. In index.html
staan wat nieuwtjes
en een welkom-bericht, in aanbod.html
staat het aanbod aan
weet-ik-wat aardappelen van die week.
Piet zorgt voor de layout en bouwt de website. Uiteindelijk worden de
pagina's in een directory /home/piet/public_html
geplaatst.
Een "template"
Piet heeft Jan meegedeeld dat hij zijn pagina's gewoon als simpele HTML mag schrijven, en hij zorgt er zelf voor dat Jan's pagina's in een passende layout worden gegoten.
Piet stopt bijvoorbeeld de kop van de site in header.html
en de
voet in footer.html
. header.html
kan er zo uitzien:
<html><!-- de header --> <head> <title>Piet en Jan producties</title> </head> <body bgcolor="white"> <table border="0" width="100%"><tr> <td bgcolor="#c040ff" valign="top"> Dit is onze website<br> Hier staat nog wat onzin.<br> We zijn heel interactief<br> dus dit is ons telefoonnummer:<br> <b>0123-456789</b> </td><td valign="top"> <!-- hier de inhoud -->
En dit is footer.html
:
<!-- de footer --> </td></tr></table> </body></html>
De unix commando's om dan van Jan's index.html de uiteindelijke pagina te bouwen zijn bijvoorbeeld:
cat header.html /home/jan/Docs/website/index.html echo -n '<hr>Laatst gewijzigd: ' date '+%A %e %B' cat footer.html
Zie de manual pages van deze commando's. De file die deze commando's tot resultaat hebben wordt dus naar de standaard uitvoer geleid, en die kunnen we simpel vangen in een file:
{ cat header.html /home/jan/Docs/website/index.html echo -n '<hr>Laatst gewijzigd: ' date '+%A %e %B' cat footer.html } > /home/piet/public_html/index.html
Hetzelfde kan ook met de andere file, aanbod.html
. Hiermee hebben
we in feite een klein script gebouwd, dat in staat is de website voor ons te
maken.
Echter, telkens dit commando invoeren is natuurlijk ondoenlijk. We kunnen er natuurlijk een uitvoerbaar shellscript van maken en dit telkens runnen als Jan zijn index heeft bijgewerkt. Maar ook als Piet besluit de header of footer aan te passen moet dit script gerund worden! En als Jan op de een of andere dag niets heeft veranderd hoeft het script ook niet te draaien. En we draaien niet voor niets Linux, dus we willen het slimmer kunnen doen (lees: vanzelf)!
Welnu, hier is het, dat make
om de hoek komt kijken.
Eerste ontmoeting met Make
De info-manual van GNU make is een fantastisch document. Echter het richt zich van meet af aan op gebruik in een programmeeromgeving. Vandaar dat ik hier een poging doe, de functie van make wat algemener aan te geven:
make bepaalt op grond van de time-stamp (de datum dus) van een doel-bestand, en de time-stamps van bron-bestanden of een reeks commando's uitgevoerd moet worden ja of nee.
Met andere woorden: Als één van de bron-bestanden, nodig voor het maken van het doel-bestand, nieuwer is dan het doel-bestand, dan wordt een bepaalde reeks commando's uitgevoerd. Die commando's hebben dan tot doel het doel-bestand te vernieuwen.
De doel-file is een `target', de bron-files zijn `prerequisites' (eerste
vereisten). De commando's worden dus uitgevoerd als een van de `prerequisites'
nieuwer is dan de `target' (of als de target niet bestaat). Zijn alle
prerequisites ouder dan of even oud als de target, dan worden de commando's
niet uitgevoerd, en beschouwt make
de target als up-to-date.
In de directory waarin we werken moet een bestand met de naam
Makefile
worden aangemaakt, dat deze informatie voor make
bevat. Als dat eenmaal gebeurd is hoeven we nog slechts `make
' in te
typen en de commando's nodig voor het bijwerken van de targets worden
automatisch uitgevoerd.
Make wordt aangeroepen met
make <target> ?<target>? ....
waarbij de target optioneel is (bij weglating wordt de eerste target in de Makefile gebruikt). Make kijkt altijd in de huidige directory voor de Makefile. Meer dan één target kan tegelijk worden opgegeven.
Syntax van de Makefile
De Makefile kan gewoon met een editor worden gemaakt en ziet er alsvolgt uit:
# Dit is een voorbeeld van een Makefile. # Commentaar mag achter hekjes (#) staan. target: prerequisites commando target: prerequisites commando # enz enz.
De target staat dus steeds vooraan, gevolgd door een dubbele punt (:) en de prerequisites die nodig zijn. Als er veel prerequisites zijn, kun je de regel eindigen met een backslash (\) en op de volgende regel verder gaan.
Op de volgende regel(s) staan één of meer commando's. Elke regel wordt door make gezien als een afzonderlijk commando. Wil je meerdere regels in één commando hebben, dan moet je de regels weer met een backslash (\) eindigen. Make zal de commando's dan verbinden door ze als het ware op 1 regel te zetten. Daarom moeten we in dat geval de commando's ook scheiden met een puntkomma (;), voor de shell die ze uitvoert.
Let op: de commando's moeten worden ingesprongen met een TAB, dus niet 8 spaties!
Make leest de Makefile, en bepaalt voor elke gevraagde target (beginnend bij de eerste) of de commando's moeten worden uitgevoerd. Elke target, samen met zijn prerequisites en commando's wordt een `rule' (regel) genoemd.
Wanneer make zonder argumenten wordt aangeroepen wordt alleen de eerste target die gevonden wordt, uitgevoerd.
Een Makefile voor ons voorbeeld
Voor ons voorbeeld zou de Makefile er dus zo uit kunnen zien:
# Deze Makefile bouwt de website van Piet en Jan, de aardappeleters. all: /home/piet/public_html/index.html /home/piet/public_html/aanbod.html /home/piet/public_html/index.html: header.html footer.html \ /home/jan/Docs/website/index.html { \ cat header.html /home/jan/Docs/website/index.html ;\ echo -n '<hr>Laatst gewijzigd: ' ;\ date '+%A %e %B' ;\ cat footer.html ;\ } > /home/piet/public_html/index.html /home/piet/public_html/aanbod.html: header.html footer.html \ /home/jan/Docs/website/aanbod.html { \ cat header.html /home/jan/Docs/website/index.html ;\ echo -n '<hr>Laatst gewijzigd: ' ;\ date '+%A %e %B' ;\ cat footer.html ;\ } > /home/piet/public_html/aanbod.html # the end
We hebben nu drie targets, `all' en de bestanden index.html
en
aanbod.html
van de website. De target `all' doet niets anders dan de
beide andere als prerequisite te hebben. Die worden dus beide getest. Omdat
`all' zelf niet de naam van een bestaande file is, wordt de target `all' dus
altijd uitgevoerd. (Later zullen we een elegantere manier zien waarmee targets
die geen file zijn kunnen worden gedefiniëerd.)
Als de header of footer werden gewijzigd, worden beide pagina's opnieuw gemaakt. Wijzigt Jan een van de pagina's, dan wordt alleen die pagina opnieuw gemaakt. Simpelweg `make' intypen is nu voldoende!
Het nadeel is natuurlijk, dat de Makefile nu niet bepaald overzichtelijk is. Gelukkig zijn er veel manieren om alles simpeler te maken!
Stroomlijnen van de Makefile
Variabelen
Dankzij variabelen kan de Makefile een stuk overzichtelijker worden gemaakt. Variabelen worden alsvolgt in de Makefile gedefinieerd:
variabele = waarde
We refereren naar een variabele met de uitdrukking $(variabele)
.
Als we hiermee de Makefile bewerken, ziet het er al vriendelijker uit:
# Deze Makefile bouwt de website van Piet en Jan, de aardappeleters. # Directory waar de website staat: TARGETDIR = /home/piet/public_html # Jan's directory: JANSDIR = /home/jan/Docs/website # Bestanden nodig voor de layout: LAYOUT = header.html footer.html all: $(TARGETDIR)/index.html $(TARGETDIR)/aanbod.html $(TARGETDIR)/index.html: $(LAYOUT) $(JANSDIR)/index.html { \ cat header.html $(JANSDIR)/index.html ;\ echo -n '<hr>Laatst gewijzigd: ' ;\ date '+%A %e %B' ;\ cat footer.html ;\ } > $(TARGETDIR)/index.html $(TARGETDIR)/aanbod.html: $(LAYOUT) $(JANSDIR)/aanbod.html { \ cat header.html $(JANSDIR)/index.html ;\ echo -n '<hr>Laatst gewijzigd: ' ;\ date '+%A %e %B' ;\ cat footer.html ;\ } > $(TARGETDIR)/aanbod.html # the end
Een goede gewoonte is het om hoofdletters te gebruiken voor variabelen. Het is nu ook veel eenvoudiger om bijvoorbeeld de doel-directory snel te wijzigen.
Je kunt als je dat wilt voor elk document een andere manier definiëren waarmee het in de goede layout wordt gezet. Maar wat als we nu vele documenten hebben die allemaal in dezelfde layout moeten? De Makefile zou wel erg lang worden, terwijl er veel herhalingen in zitten.
Ook dit kunnen we weer vereenvoudigen!
Pattern Rules
`Pattern Rules' maken het ons mogelijk om eenzelfde commandoreeks los te laten op allerlei targets.
Bij het gebruik van pattern rules verandert de syntax van een regel; er wordt een extra patroon veld ingevoegd:
meerdere targets: patroon : prerequisite prerequisite ... commando
Het patroon is een uitdrukking die op alle targets zou moeten kunnen passen, waarbij een procentteken wordt gebruikt om variabele gedeelten van een target-naam te vatten. Een voorbeeld:
/home/bla/target1.html /home/bla/target2.html: /home/bla/% : % commando's
Als make
dit leest, wordt het geëxpandeerd tot 2 regels, waarbij
het patroon bepaalt welk gedeelte van de target-naam in het procentteken wordt
gevat.
In het prerequisites-veld staat het procentteken voor het deel dat in het patroon door datzelfde procentteken gevat werd.
Make expandeert het bovenstaande dus aldus:
/home/bla/target1.html: target1.html commando's /home/bla/target2.html: target2.html commando's
Het procentteken in het patroon `/home/bla/%
' krijgt bij target
`/home/bla/target1.html
' dus de waarde `target1.html
',
waardoor de prerequisite `%
' expandeert tot
`target1.html
'.
Voor onze website komt een rule er nu alsvolgt uit te zien:
$(TARGETDIR)/index.html $(TARGETDIR)/aanbod.html: $(TARGETDIR)/% : $(JANSDIR)/% \ $(LAYOUT)
Nu zitten we nog met 1 probleem: Hoe kunnen we deze variabelen in de commando's gebruiken? De commando's waren immers ook voor de beide targets enigszins verschillend?
Automatische Variabelen
Make definiëert gelukkig zelf ook enige variabelen. Sommige daarvan worden automatische variabelen genoemd, en die bevatten tijdens de uitvoering van commando's (of liever vlak daarvoor) de waarde van de target en/of prerequisite.
De speciale variabele $<
wordt gebruikt om de eerste
prerequisite aan te geven, en de variabele $@
expandeert altijd tot
de huidige target.
Met behulp van deze variabelen kunnen we de complete rule dus als volgt `generaliseren':
$(TARGETDIR)/index.html $(TARGETDIR)/aanbod.html: $(TARGETDIR)/% : $(JANSDIR)/% \ $(LAYOUT) { \ cat header.html $< ;\ echo -n '<hr>Laatst gewijzigd: ' ;\ date '+%A %e %B' ;\ cat footer.html ;\ } > $@
Voilà! Deze ene regel werkt nu dus voor beide files!
Voor de volledigheid nu weer eens de volledige Makefile, met nog wat optimalisaties:
# Deze Makefile bouwt de website van Piet en Jan, de aardappeleters. # Directory waar de website staat: TARGETDIR = /home/piet/public_html # Jan's directory: JANSDIR = /home/jan/Docs/website # Bestanden nodig voor de layout: LAYOUT = header.html footer.html # Dit zijn de web pagina's: DOCS = $(TARGETDIR)/index.html $(TARGETDIR)/aanbod.html # beneden deze lijn niets veranderen;-) # ------------------------------------------------------------- all: $(DOCS) $(DOCS): $(TARGETDIR)/% : $(JANSDIR)/% $(LAYOUT) { \ cat header.html $< ;\ echo -n '<hr>Laatst gewijzigd: ' ;\ date '+%A %e %B' ;\ cat footer.html ;\ } > $@ # the end
Dit begint erop te lijken. Als er meerdere documenten bijkomen, kunnen die vrij eenvoudig in de Makefile (aan de DOCS variabele) worden toegevoegd, zonder al te veel typwerk.
Uiteindelijk moet zelfs degene die de Makefile onderhoudt gemakkelijk kunnen zien hoe het werkt, zonder telkens weer de werking ervan te moeten ontcijferen!
Laatste kleine optimalisaties
We zouden natuurlijk liever willen dat we de documenten gewoon konden noemen in DOCS, zonder steeds de hele directory erbij. Dat kan alsvolgt (we veranderen DOCS in het begin van de makefile in TEKSTEN):
TEKSTEN = index.html aanbod.html nogeenbestand.html # beneden deze lijn niets veranderen;-) # ------------------------------------------------------------- DOCS = $(addprefix $(TARGETDIR)/,$(TEKSTEN)) all: $(DOCS) # enz
Wat we hier zien, is een specialistische functie van make: In plaats van een variabele-naam kan tussen de haakjes ook een complete uitdrukking staan, waarmee tekst op allerlei wijze kan worden bewerkt.
Het speciale commando $(addprefix prefix,lijst)
voegt aan elk
element van de lijst een prefix toe, in het voorbeeld de inhoud van de
TARGETDIR variabele plus een slash (/).
De items in de lijst wordt door spaties gescheiden, vandaar dat het niet
echt een goed idee is om bestandsnamen waar spaties in voorkomen met
make
te bewerken.
Tot slot: in het begin zeiden we al dat de target `all' geen bestand creëert met de naam `all' (er zijn immers geen commando's in die regel) en daarom telkens wordt uitgevoerd. Maar wat als er toevallig een bestand in de directory staat met die naam, nieuwer dan de andere ...?
Er is een simpele manier om make
te vertellen dat een bepaalde
target altijd moet worden uitgevoerd en niet verwijst naar een file op de
harde schijf. Dit doen we door die target als `phony' (onecht) te markeren.
Dat gaat alsvolgt:
.PHONY: all
De hele Makefile ziet er nu alsvolgt uit:
# Deze Makefile bouwt de website van Piet en Jan, de aardappeleters. # Directory waar de website staat: TARGETDIR = /home/piet/public_html # Jan's directory: JANSDIR = /home/jan/Docs/website # Bestanden nodig voor de layout: LAYOUT = header.html footer.html # Dit zijn de namen van de web pagina's: TEKSTEN = index.html aanbod.html nogeenbestand.html # beneden deze lijn niets veranderen;-) # ------------------------------------------------------ DOCS = $(addprefix $(TARGETDIR)/,$(TEKSTEN)) .PHONY: all all: $(DOCS) $(DOCS): $(TARGETDIR)/% : $(JANSDIR)/% $(LAYOUT) { \ cat header.html $< ;\ echo -n '<hr>Laatst gewijzigd: ' ;\ date '+%A %e %B' ;\ cat footer.html ;\ } > $@ # the end
Die sla je op en vergeet je! Nu kun je met een simpele make, desnoods in je crontab, altijd je webpagina up-to-date houden, en de layout netjes van de inhoud scheiden!
Slotopmerkingen
Dit voorbeeld kan natuurlijk worden aangepast voor andere situaties.
Bijvoorbeeld de simpele manier waarop het document in elkaar wordt gezet is
niet waterdicht: Als Jan zijn artikelen per ongeluk met
</body></html>
eindigt, wordt (door de meeste browsers)
de door Piet gemaakte footer.html niet afgebeeld. Met grep, perl of tcl kunnen
we nog wat handiger titels uit Jans documenten overbrengen in de header van de
site.
Jan kan natuurlijk ook gewoon platte tekst schrijven, en met een sed commando worden dan alle wit-regels in <P> veranderd:
sed -e 's/^\s*$/<p>/g'
Daarnaast kan Jan natuurlijk ook zijn teksten in LyX schrijven, en gebruike men een programma als lyx2html om er html van te maken. Mogelijkheden te over!
Een andere template-constructie is natuurlijk ook mogelijk.
We hebben ook niet stil gestaan bij hoe eventuele plaatjes worden overgebracht (geschaald, geconverteerd of gecomprimeerd) naar de web directory. Dat kan natuurlijk ook automatisch!
In dit voorbeeld moet Piet leestoegang hebben in Jans website directory. Het interessante van deze scheiding van taken is, dat zij vertaald kan worden naar zeer grote organisaties! Piet kan zelfs aan de andere kant van de wereld inloggen, of zijn homedir over NFS mounten. De voorbeelden kunnen echter ook gebruikt worden voor werk door één gebruiker.
Het basisidee over hoe een Makefile werkt, en hoe simpel je dagelijks werk ineens wordt als je een goede Makefile hebt, is hopelijk een beetje duidelijk geworden!
Tips
Make als gebruikersinterface
Een verzameling bestanden kan ook functioneren als bron voor het op verschillende manieren samenstellen van een eindproduct of actie.
Door middel van `phony' targets (.PHONY: target
) kunnen eenvoudige
functies makkelijk worden gebundeld. Een voorbeeld hiervan is de configuratie
van de Linux kernel.
Met make menuconfig
wordt de configuratie gestart via een
interactief menu. Met make xconfig
wordt de configuratie gestart via
een Tcl/Tk interface onder X.
Deze beide targets hebben dus niets met de eigenlijke bouw van de kernel te maken, zij vormen gewoon een makkelijke interface naar de noodzakelijke functies (zoals configuratie van de kernel).
Een voorbeeld
Stel je voor: Je hebt een grote verzameling documenten, die je als één geheel bewaart, onderhoudt en distribueert. Je wil samenvattingen kunnen maken, in draft en verkleind kunnen printen, in topkwaliteit printen, publiceren naar het web etc. alles vanuit dezelfde bronbestanden (bijvoorbeeld LaTeX!).
Je zou dan een Makefile moeten maken met bijvoorbeeld de volgende PHONY targets:
- help:
- Print een eenvoudig overzicht van de in deze Makefile opgenomen functies (targets).
- print-draft:
- Print bijvoorbeeld een verkleinde versie van het document (met behulp van de ps-utils) naar een printer met een grove resolutie.
- print-full:
- Print het volledige document.
- ps:
- Exporteert het volledige document naar een PostScript file.
- report:
- Vat titel, abstract en summary van een document samen en mail dat naar iemand toe die de ontwikkeling van je werk moet volgen.
- html:
- Werkt de html-versie van je werk automatisch bij.
enz. enz. Zo kunnen complexe handelingen worden gedefiniëerd in een Makefile, en vervolgens aangeroepen via een simpel te onthouden interface.
Prerequisites kunnen ook weer target zijn
Bestanden die voor een bepaalde target als prerequisite genoemd worden, kunnen zelf ook weer target zijn voor een volgende bewerking.
Zo kun je bijvoorbeeld uit een tekstbestand HTML genereren, en die HTML weer in een completere layout zetten. Een voorbeeld:
TEMPLATE = layout1/Template1.txt /home/httpd/sales/sales.html: sales.html $(TEMPLATE) perl Scripts/BuildPage.pl -template $(TEMPLATE) $< > $@-new mv -f $@-new $@ sales.html: sales.txt aptconvert -toc $@ $<
Zie hoe de file ook wordt bijgewerkt als de Template1.txt was veranderd.
Commando echo, foutmeldingen en testen
Als je een commando met een `@' laat beginnen, wordt het niet afgebeeld door Make:
target: prerequisite @cc -o target prerequisite
Als je een commando met een `-' laat beginnen, wordt het Make proces niet afgebroken als dat commando een fout veroorzaakt (bijvoorbeeld probeert een niet bestaande file te wissen):
.PHONY: clean clean: -rm -r $(tempdir)
Als je wilt zien wat een bepaalde `make' aanroep doet, (bijvoorbeeld
make install
) maar je wilt per se niet dat het echt gedaan
wordt, gebruik dan de -n optie op de prompt:
wilbert@nutnix:~ > make -n install install -m 755 program /usr/local/bin install -m 644 program.1 /usr/local/man/man1 wilbert@nutnix:~ >
Substitutie van make-variabelen voorkomen
Als je het dollarteken ($) nodig hebt als onderdeel van bijvoorbeeld een filenaam of een shell commando, gebruik het dan dubbel ($$):
# A Makefile # Don't try this at home! :-) source = menu.txt help.txt target: $(source) for i in $(source) ;\ do \ if [ "$$i" = "menu.txt" ] ;\ then \ doThis $$i ;\ else \ doThat $$i ;\ fi ;\ done > target
Make zal, voordat het commando naar de shell wordt gestuurd om te worden uitgevoerd, netjes zijn eigen variabelen substitueren, en alle dubbele dollartekens door enkele vervangen.
Meer informatie
Meer informatie over de werking van make en alle mogelijkheden is te vinden in de `GNU Make Manual'. Op je Linux systeem kan die worden bekeken met:
info make
Natuurlijk kan je de GNU Make Manual ook bestuderen met de GNOME en KDE helpbrowsers of het handige programma tkinfo (zie mijn Killer Apps).
Links naar meer informatie over make
:
- Een recensie van Armijn Hemel over het boek `Managing your projects with make'.
- Een Google zoektocht op de term `make tutorial'.
Veel plezier!
© 2002 Wilbert Berendsen. Deze tekst is beschikbaar onder de GNU Free Documentation License.Eerste versie: 8 oktober 2000
Laatste update: di mei 7 13:14:16 CEST 2002