Yliopiston etusivulle Suomeksi På svenska In English
Helsingin yliopisto Tietojenkäsittelytieteen laitos
 

Tietojenkäsittelytieteen laitos

Make - ohjelmistojen ylläpitoon

Useista moduuleista koostuvat ohjelmat yleensä käännetään moduleittain erikseen, minkä jälkeen kaikki modulit linkitetään ajettavaksi ohjelmaksi. Olisi toki mahdollista aina kääntää kaikki uudelleen, tarvittavat komennotkin olisivat helpompia:

gcc -o ohjelma *.c

Valitettavasti koneet ovat liian hitaita, jotta tämä olisi käytännöllistä. Vaikka itse olisikin valmis odottamaan, monen käyttäjän koneessa kaikki käytetyt koneresurssit ovat pois muilta. Tahma ei liene aivan tuntematon käsite yliopistonkaan parissa.

Modulien erikseen kääntäminen on siis ainoa järkevä vaihtoehto. Sekään ei ole ongelmaton: on oltava erittäin tarkkana, että kaikki modulit todella käännetään uudestaan, kun niitä on muutettu, muuten ohjelmalla ei liene toivoakaan toimimisesta. On helppo nähdä, että jo muutaman modulin ohjelmalla tästä tylsästä ja mekaanisesta kirjanpitotyöstä tulee erittäin vaivalloista. Koska se on mekaanista, onkin järkevää automatisoida se. Tätä automatisointia varten on kehitetty ohjelma nimeltä make.

Make on laiskurin unelma: erittäin vähällä vaivalla pääsee huolehtimasta kääntäjälle annettavista komentoriveistä kokonaan, riittää kun antaa komennon make. (Jos tekee aliaksen m, ei tarvitse kirjoittaa niinkään paljoa.) Ainoa vaiva joka täytyy nähdä on maken opetteleminen ja makefileen kirjoittaminen aina tarvittaessa.

Esimerkki

Alla olevassa kuvassa on listattu tässä esimerkissä käytettävät tiedostot. Esimerkki on erittäin yksinkertainen, mutta kaikki oleellinen maken käytön opettelun kannalta on mukana.

/* main.c */        /* foo.c */           /* bar.c */
#include "foo.h"    #include <stdio.h>    #include <stdio.h>
#include "bar.h"    #include "foo.h"      #include "bar.h"
int main(void) {    void foo(void) {      void bar(void) {
    foo();          puts("foo");          puts("bar");
    bar();          }                     }
    return 0;
}

/* foo.h */             /* bar.h */
void foo(void);         void bar(void);

Tarvittavat komennot koko ohjelman kääntämiseksi ja linkkaamiseksi ovat siis seuraavat:

gcc -c main.c
gcc -c foo.c
gcc -c bar.c
gcc -o ohjelma main.o foo.o bar.o

Kun ohjelmaa muutetaan, pitää muuttuneet osat kääntää uudestaan. Eli jos muutamme tiedostoja main.c ja foo.c, on uudelleen käännettäessä annettava seuraavat komenneot:

gcc -c main.c
gcc -c foo.c
gcc -o ohjelma main.o foo.o bar.o

Tiedostoa bar.c ei siis käännetty uudestaan. Jos jotakin otsikkotiedostoa muutetaan, on käännettävä uudelleen kaikki ne lähdekooditiedostot joihin se sisällytetään. Eli jos tiedosta foo.h muutetaan, on annettava seuraavat komennot:

gcc -c main.c
gcc -c foo.c
gcc -o ohjelma main.o foo.o bar.o

Tiedostoa bar.c ei tälläkään kertaa käännetty. Tämä on tyypillistä ohjelman kehityksen aikana: vain muutamia tiedostoa muutetaan kerralla.

Jotta make pystyisi automatisoimaan tämän kääntämisen, on sille kerrottava mitkä tiedostot pitää kääntää ja miten. Nämä tiedot kirjataan tiedostoon Makefile, jonka perussyntaksi on luettelo sääntöjä. Kunkin säännön syntaksi on esitetty alla olevassa kuvassa. Säännössä lahde_1, ..., lahde_n ovat lähdetiedostot, eli kaikki ne tiedostot jotka tarvitaan käännöksen yhteydessä. kohde on se tiedosto joka syntyy käännöksen tuloksena (tai se kiinnostava tiedosto, mikäli niitä syntyy useita). Komennot sisennetään yhdellä tabulaattorimerkillä (ei siis välilyönneillä); suorittamalla ne syntyy kohdetiedosto.

kohde : lahde_1 lahde_2 ... lahde_n
        komento_1
        komento_2
        .
        .
        .
        komento_m

Sääntö suoritetaan siten, että verrataan kohdetiedostoa ja lähdetiedostoja ja mikäli kohdetiedostoa ei ole tai ainakin yhden lähdetiedoston aikaleima on uudempi kuin kohdetiedoston, niin kohdetiedosto on vanhentunut ja se päivitetään suorittamalla säännössä mainitut komennot. Lähdetiedostot voivat olla jonkin toisen säännön kohdetiedostoja (esimerkiksi objektitiedostot, kuten myöhemmin tulemme näkemään). Tämän takia suoritetaan ennen tiedostojen vertailua rekursiivisesti sama algoritmi kaikille lähdetiedostoille, jotka ovat jonkin toisen säännön kohdetiedostoja. Algoritmi on annettu alla olevassa kuvassa.

procedure M(sääntö S) 
  for f = jokainen lähdetiedosto do
      if on olemassa sääntö S', jossa f on kohteena then
          M(S')
  if kohdetta ei ole olemassa or jokin lähde on uudempi then 
      for k = jokainen komento do begin
           suorita komento k
              if komento k epäonnistui then begin
                  kirjoita virheilmoitus
                  lopeta ohjelma
              end
      end

Esimerkissämme Makefile:stä tulisi seuraavan kaltainen:

ohjelma: main.o foo.o bar.o
        gcc -o ohjelma main.o foo.o bar.o
main.o: main.c foo.h bar.h
        gcc -c main.c
foo.o: foo.c foo.h
        gcc -c foo.c
bar.o: bar.c bar.h
        gcc -c bar.c

Kirjoittamalla tälläinen Makefile, riittää siis aina vain sanoa make, kun ohjelma halutaan kääntää. Maken käyttäminen kannattaa jo muutaman käännöksen jälkeen.

Laiska ohjelmoija toisaalta huomaa, että yllä olevassa Makefile:ssä on toistoa, jota ehkä voisi hieman vähentää nostamalla maken älykkyyttä. Esimerkiksi käännöskomennot: eikö makelle voisi opettaa, että C-kieliset ohjelmat käännetään tietyllä tavalla, jolloin käännöskomennot voisi jättää pois. Maken tekijä oli laiska, joten seuraava tekee saman kuin edellinen:

CC = gcc
ohjelma: main.o foo.o bar.o
        gcc -o ohjelma main.o foo.o bar.o
main.o: main.c foo.h bar.h
foo.o: foo.c foo.h
bar.o: bar.c bar.h

Kolme käännöskomentoa on poistettu ja yksi mielenkiintoisen näköinen rivi on lisätty. Tuo alussa oleva rivi on makromäärittely. Makelle on älykkyyden noston yhteydessä kyllä kerrottu miten C-kielinen ohjelma käännetään, mutta jotta C-kääntäjää voisi vaihtaa, make on opetettu käyttämään makron CC arvona olevaa merkkijonoa C-kääntäjän nimenä. Oletusarvo on cc, joten koska haluamme käyttää GCC:tä, on makrolle annettava arvoksi gcc.

Laiska ohjelmoija vielä miettii, eikä noita objektitiedostojen sääntöjä voisi yksinkertaistaa, eli tarvitseeko jokaiselle objektitiedostolle erikseen kertoa, että sen rakentamiseen tarvitaan vastaava C-kielinen tiedosto? Maken tekijän laiskuus jo mainittiinkin; seuraava Makefile toimii:

CC = gcc
ohjelma: main.o foo.o bar.o
        gcc -o ohjelma main.o foo.o bar.o
main.o: foo.h bar.h
foo.o: foo.h
bar.o: bar.h

Maken ohjelmoija oli vieläpä älykäskin, joten make arvaa olemassaolevien tiedostojen perusteella, minkäkielisestä lähdekooditiedostosta objektitiedosto käännetään. C-kieltä ei siis ole koodattu maken sisään millään tavalla.

Tuo ylläoleva Makefile on suurinpiirtein niin minimaalinen kuin olla voi. Linkityskomentoa voi vähän rukata siistimmäksi, mutta valitettavasti makessa ei ole sisäänrakennettua linkityskomentoa, vaan sen joutuu kirjoittamaan joka kerran erikseen.

Linkityskomennon epäsiisteys johtuu siitä, että siihen on kovakoodattu GCC. Jos joskus joudutaan käyttämään jotakin toista C-kääntäjää, on se muutettava kahdessa paikkaa. Käyttämällä makron CC arvoa myös linkityskomennossa, päästään tästäkin vaivasta.

Samalla voidaan tehdä omakin makro, jolla voidaan listata objektitiedostot, jotta niitäkään ei tarvitsisi listata kahteen kertaan. Omia makroja voi tehdä vapaasti samalla tavalla kuin maken omille makroille voi antaa uusia arvoja. Makroja käytetään kuten alla.

CC = gcc
objs = main.o foo.o bar.o
ohjelma: $(objs)
        $(CC) -o ohjelma $(objs)
main.o: foo.h bar.h
foo.o: foo.h
bar.o: bar.h

Ylläolevissa esimerkeissä on iloisesti käytetty useita sääntöjä pohtimatta mistä make aloittaa niiden suorituksen. Yksinkertaisuuden vuoksi make suorittaa ensimmäisen säännön. Komentorivillä makea voi pyytää suorittamaan muita sääntöjä luettelemalla niiden kohdetiedostot. Eli jos halutaan vain päivittää foo.o ja bar.o, sanotaan

make foo.o bar.o

Mikäli komentorivillä pyydetään suorittamaan jokin sääntö, ensimmäistä sääntöä ei suoriteta ollenkaan (paitsi jos sitä komentorivillä pyydetään).

Tärkeitä makroja

Joitakin maken omia makroja on hyvä tuntea.

CC
C-kääntäjän nimi.
CFLAGS
C-kääntäjälle annettavat optiot
LDFLAGS
C-kääntäjälle annettavat lisäoptiot linkkauksen yhteydessä

Näitä makroja käytetään makeen sisään ohjelmoiduissa säännöissä ja silloin, kun niille on muuten tarvetta. Muitakin nimiä voi käyttää, mutta silloin ne eivät vaikuta sisäänrakennettuihin sääntöihin eivätkä ole yhtä selkeitä muille maken käyttäjille. Perinteitä on tässä asiassa syytä kunnioittaa.

Optiot

Make tunnistaa useita eri optioita. Eri versiot tunnistavat eri optioita, mutta seuraavassa esitellyt optiot toimivat kaikkialla samoin.

Optio -n käskee makea olemaan suorittamatta komentoja. Kaikki muu tehdään kuten tavallisesti, mutta komentoja ei suoriteta. Tämä on hyödyllistä Makefileä testattaessa ja esimerkiksi kun käyttää jonkun toisen tekemää Makefileä, jotta voi varmistua, ettei se tee mitään vahingollista.

Optio -s käske makea olemaan hiljaa, eli olemaan tulostamatta suoritettavia komentoja.

Vinkkejä

Säännössä esitelty kohdetiedostoa ei ole pakko rakentaa. Tästä on esimerkkinä seuraava sääntö:

clean:
        rm -f *.o core ohjelma

Tässä kohdetiedosto clean ei tarvitse mitään lähdetiedostoja, joten säännön komento suoritetaan vain, kun tiedostoa nimeltä clean ei ole olemassa. Koska komento ei luo tuollaista tiedostoa, se suoritetaan joka kerran, kun kohdetta clean yritetään rakentaa.

Tämä on perinteinen osa Makefileä, joka on mukana miltei jokaisessa Makefilessä. Sen avulla voi puhdistaa ylimääräiset tiedostot, esimerkiksi säästääkseen levytilaa. Tarkoitus on, että kaikki clean-säännön poistamat tiedostot voi tarvittaessa rakentaa uudestaan ajamalla make.

Toinen perinteinen sääntö on all:

all: ohjelma1 ohjelma2 ohjelma3

Tässä yritetään ensin päivittää lähdetiedostot ohjelma1, ohjelma2 ja ohjelma3 ja sen jälkeen päivittää kohdetiedosto all. Koska mitään komentoja all-tiedoston päivittämiseksi ei ole, säännön ainoa vaikutus on että lähdetiedostot päivitetään. Tälläinen sääntö on tapana kirjoittaa ensimmäiseksi säännöksi, jotta make automattisesti osaisi päivittää kaikki päivitettävät ohjelmat (tai muut tiedostot). Tiedostoa clean ei pidä lisätä all-säännön lähdetiedostoksi, muuten kaikki maken päivittämät tiedostot hävitetään joka kerran.

Makefile-malli

Seuraavassa on yksinkertaisen, mutta useimpiin kohtuullisen kokoisiin ohjelmiin riittävä Makefileen malli.

Muutettavat kohdat on merkitty merkkijonolla xxx. Tarvittavat muutokset on selvitetty kommenteissa.

# Lista kaikista käännettävistä ohjelmista
progs = xxx

# Jokaiselle ohjelmalle oma makro, joilla kerrotaan sen 
# objektitiedostot (tee uusi makro jokaiselle ohjelmalle)
xxxobjs = xxx.o

# Mukaan linkitettävät kirjastot
libs = -lm -lcurses -ltermcap

CC = gcc
CFLAGS = -Wall -ansi -pedantic -O -g
LDFLAGS = 

all: $(progs)

# Kopioi ja muuta nämä säännöt jokaiselle ohjelmalle
xxx: $(xxxobjs)
	$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(xxxobjs) $(libs)
$(xxxobjs): xxx.h

# Puhdista kaikki turha pois
clean:
	rm -f core *.o $(progs)
realclean: clean
	rm -f .depend

# Automaattisesti päivitettävät riippuvuudet
depend .depend:
	$(CC) $(CFLAGS) -MM *.c > .depend
include .depend

Mallin lopussa olevat rivit huolehtivat objektitiedostojen sääntöjen automaattisesta tuottamisesta. Optio -MM (toimii useimmilla UNIXin alla toimivilla C-kääntäjillä) käskee kääntäjää tuottamaan maken ymmärtämiä sääntöjä; kaikki otsikkotiedostot tulevat sääntöön automaattisesti mukaan. Nämä säännöt ohjataan tiedostoon .depend, mistä viimeinen rivi käy ne hakemassa. include toimii kuten C-kielen esikääntäjän #include.

Sääntö päivittää riippuvuudet automaattisesti, mikäli tiedostoa .depend ei ole olemassa, tai mikäli makea erikseen pyydetään päivittämään ne. Automaattinen päivitys ei toimi HP-UX:n omalla makella, mutta käyttämällä GNU-versiota gmake ongelmaa ei ole.

tietotekniikkaryhma@cs.helsinki.fi