Makefile ja ympäristömuuttujat

Makefilet ovat varsin kätevä tapa automatisoida erilaisia prosesseja. Monesti myös tulee tarvetta käyttää erilaisia ympäristömuuttujia Makefileissä. Niiden käytössä kuitenkin piilee iso riski, joka voi pahimmillaan maksaa kaikki datasi. Esittelen tässä joitakin keinoja, joilla voit varmistaa, että ympäristömuuttujat todella ovat määriteltyjä.

Otetaan ensi yksi motivoiva ja havainnollistava esimerkki, millä tavalla asiat voivat mennä ikävällä tavalla pieleen vääränlaisten oletusten vuoksi.

Esimerkki. Skripti generoi automaattisesti asioita build-hakemistoon, joka on jonkinlaisen juurihakemiston alihakemisto. Esimerkiksi:

APP_ROOT=/opt/app
APP_BUILD_DIR=build

Makefilessä voi sitten olla make clean, joka huolehtii build-hakemiston poistamisesta.

(... muuta koodia ...)

clean:
    rm -rf ${APP_ROOT}/${APP_BUILD_DIR}

(... muuta koodia ...)

Skripti näyttää sopivan viattomalta. Tästähän näkee heti että clean-target poistaa tiedostot hakemistosta /opt/app/build:

rm -rf /opt/app/build

Mutta mitä tapahtuu, jos ympäristömuuttujia APP_ROOT ja APP_BUILD_DIR ei ole määritelty? Harmittoman näköinen komento saakin aivan uusia piirteitä:

rm -rf /

Yllätys oli suuri, kun pääkäyttäjän oikeuksilla skripti ajettiin ilman että ympäristömuuttujia oli määritelty. Teknisesti, clean-target tuhosi kyllä build hakemiston mutta siinä vaiheessa kun joku huomasi, että komento ottaa liian kauan, oli skripti kerennyt jo tuhota puolet kiintolevyn sisällöstä.

Esimerkki mukaili erästä lukemaani artikkelia, missä suurinpiirtien näin oli päässyt käymään. Tätä voi itsekin kokeilla, hieman vaarattomalla komennolla tosin:

clean:
    @echo "Poistetaan hakemisto ${APP_ROOT}/${APP_BUILD_DIR}"
make clean
Poistetaan hakemisto /

Ei mennyt ihan nappiin! Tästä ei sysadminille kiitosta tulisi. Jos ympäristömuuttujat asettaa, komento toimii sillä tavalla kuin pitääkin.

APP_ROOT=/opt/app APP_BUILD_DIR=build make clean
Poistetaan hakemisto /opt/app/build

Tarinan opetus on, että automatisoiduissa prosesseissa ei kannata luottaa siihen, että skriptissä käytettävät ympäristömuuttujat ovat asetettuja. Riippuu siitä, mitä on tekemässä, minkäasteista tuhoa vääränlaiset oletukset aiheuttavat. Voidaanko tehdä paremmin? Kyllä voidaan. Ympäristömuuttujat nimittäin voidaan tarkastaa, ja mikäli niitä ei ole asetettu, poistutaan skriptistä turvallisesti.

Yksi tapa on määritellä if-ehto Makefile-tiedoston alkuun. Ehdolla tarkastetaan, onko ympäristömuuttuja asetettu, ja heitetään virhe, mikäli näin ei ole. Modifikaatio olisi siis seuraava:

ifeq ($(APP_ROOT),)
  $(error APP_ROOT is not set)
endif

ifeq ($(APP_BUILD_DIR),)
  $(error APP_BUILD_DIR is not set)
endif

clean:
    @echo "Poistetaan hakemisto ${APP_ROOT}/${APP_BUILD_DIR}"

Nyt tämä toimii kuten pitääkin. Kutsutaan ensin ympäristömuuttujia:

make clean
Makefile:2: *** APP_ROOT is not set.  Stop.

Asetetaan vain toinen ympäristömuuttuja:

APP_ROOT=/opt/app make clean
Makefile:6: *** APP_BUILD_DIR is not set.  Stop.

Kun asetetaan molemmat ympäristömuuttujat, komento ajetaan:

APP_ROOT=/opt/app APP_BUILD_DIR=build make clean
Poistetaan hakemisto /opt/app/build

Aina ei ole mahdollista tai järkevää vaatia, että kaikki ympäristömuuttujat ovat määriteltyjä, mikäli niitä käytetään vain jossakin tietyssä targetissa. Ympäristömuuttujat voidaan tarkastaa test-komennolla, joka palauttaa vivulla -n "<tekstiä> arvon 0, mikäli tekstiä löytyy, ja arvon 1, mikäli tekstiä ei lyödy.

test -n "tekstiä"
echo "Paluukoodi: $?"
test -n ""
echo "Paluukoodi: $?"
Paluukoodi: 0
Paluukoodi: 1

Tällä tavalla toteutettuna Makefilen clean-target on:

clean:
    test -n "${APP_ROOT}" # APP_ROOT
    test -n "${APP_BUILD_DIR}" # APP_BUILD_DIR
    @echo "Poistetaan hakemisto ${APP_ROOT}/${APP_BUILD_DIR}"
make clean
test -n "" # APP_ROOT
make: *** [Makefile:10: clean] Error 1
APP_ROOT=/opt/app make clean
test -n "/opt/app" # APP_ROOT
test -n "" # APP_BUILD_DIR
make: *** [Makefile:11: clean] Error 1
APP_ROOT=/opt/app APP_BUILD_DIR=build make clean
test -n "/opt/app" # APP_ROOT
test -n "build" # APP_BUILD_DIR
Poistetaan hakemisto /opt/app/build

Tässä vielä etuna on se, että ympäristömuuttujat tulostuvat prosessin aikana. Mikäli se ei ole tarpeellista, ne voi prefiksata @-merkillä.

Tätä lähestymistapaa voi parantaa muutamilla eri tavoilla. Ensinnäkin, ympäristömuuttujien vaatimuksista voi tehdä omat reseptit, jolloin niitä voi lisätä useampaan kohteeseen riippuvuutena:

require-APP_ROOT:
    @ if test "$(APP_ROOT)" = ""; then \
        echo "Ympäristömuuttuja APP_ROOT ei määritelty!"; \
        exit 1; \
    fi

require-APP_BUILD_DIR:
    @ if test "$(APP_BUILD_DIR)" = ""; then \
        echo "Ympäristömuuttuja APP_BUILD_DIR ei määritelty!"; \
        exit 1; \
    fi

build: require-APP_ROOT require-APP_BUILD_DIR
    @echo "Buildataan projekti hakemistoon ${APP_ROOT}/${APP_BUILD_DIR}"

clean: require-APP_ROOT require-APP_BUILD_DIR
    @echo "Poistetaan hakemisto ${APP_ROOT}/${APP_BUILD_DIR}"
make clean
Ympäristömuuttuja APP_ROOT ei määritelty!
make: *** [Makefile:10: require-APP_ROOT] Error 1
APP_ROOT=/opt/app make clean
Ympäristömuuttuja APP_BUILD_DIR ei määritelty!
make: *** [Makefile:8: require-APP_BUILD_DIR] Error 1
APP_ROOT=/opt/app APP_BUILD_DIR=build make clean
Poistetaan hakemisto /opt/app/build

Makefile toimii, mutta siinä on toistoa. Käyttämällä Makefilen syntaksiin kuuluvia automaattisia muuttujia voidaan tarpeeton toisto poistaa.

require-%:
    @ if test "$(${*})" = ""; then \
        echo "Ympäristömuuttuja $* ei määritelty!"; \
        exit 1; \
    fi

build: require-APP_ROOT require-APP_BUILD_DIR
    @echo "Buildataan projekti hakemistoon ${APP_ROOT}/${APP_BUILD_DIR}"

clean: require-APP_ROOT require-APP_BUILD_DIR
    @echo "Poistetaan hakemisto ${APP_ROOT}/${APP_BUILD_DIR}"

Yhteenveto

Prosesseja automatisoivissa skripteissä ympäristömuuttujan puuttuminen voi johtaa ennalta-arvaamattomiin ja katastrofaalisiin virheisiin. Mikäli ympäristömuuttujaa ei ole määritelty skriptin sisällä, pitäisi sen olemassaolo aina erikseen tarkastaa. Ympäristömuuttujien tarkastuksen voi tehdä monella eri tavalla. Tärkeintä on, että sen tekee jollakin tavalla.