Maak je werk met make!

ArticleCategory: []

Software Development

AuthorImage:[Hier komt een kleine foto van jou]

[Wilbert Berendsen]

TranslationInfo:[Info over schrijver(s) en vertaler(s)]

original in nl Wilbert Berendsen

AboutTheAuthor:[een korte biografie over de auteur]

Wilbert Berendsen is professioneel musicus en Linux enthousiast. Hij heeft ooit eens intensief Z80 assembly gehackt en gebruikt nu Linux voor al zijn productiewerk. Voor de lol schrijft hij inleidende artikelen en hij onderhoudt een kleine website op http://www.xs4all.nl/~wbsoft/. Viva open source!

Abstract:[een kleine samenvatting/beschrijving van het artikel]

Dit artikel over make laat zien, hoe make werkt en toont aan dat het voor meer dan alleen software ontwikkeling kan ingezet worden.

ArticleIllustration:[Titel-illustratie van het Artikel]

[Illustratie]

ArticleBody:[Het eigenlijke artikel: zet hier teks en HTML codes]

Inleiding

Bijna iedereen die Linux gebruikt, is wel eens met het programma make in aanraking gekomen. Het doet zijn werk wanneer we een programma of de kernel willen bouwen vanaf de broncode, wanneer we pakketten willen installeren enz. '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.

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 zullen dan het doel-bestand 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  ?? ....

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, zal hij enkel de eerst gevonden target uitvoeren.

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' hebben we enkel gemaakt zodat hij de beide andere als prerequisite kan 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 manieren 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 worden 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 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

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.

Links naar meer informatie over make:

Veel plezier!