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:

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