FLUGin logo

Finnish Linux User Group FLUG ry


Linux-ohjelmointi

Tämä dokumentti on tarkoitettu ensisijaisesti niille, joilla on jo jonkinlainen käsitys C- tai C++-ohjelmoinnista, ja jotka ovat tutustumassa Linux-ohjelmointiin, mutta toivon, että artikkeli antaisi jotain myös kokeneemmille käyttäjille.

Tässä artikkelissa käydään aluksi läpi yksinkertaisen C-ohjelman sekä vastaavan C++-ohjelman tekeminen GNU/Linux-ympäristössä. Artikkelissa ei keskitytä C- tai C++-ohjelmointiin sinällään, vaan siihen, millä välineillä ohjelmointi GNU-ympäristössä tapahtuu ja millä työkaluilla tätä ohjelmointiprojektia hallitaan.

Artikkelissa ei sinällään ole paljoakaan erityisesti Linux-riippuvaista materiaalia. Samat ohjeet pätevät pitkälti muihinkin Unixeihin, joissa käytetään vapaita GNU-työkaluohjelmia.

Dokumentissa on käytetty tyyliarkkeja (style sheets) tekstin muotoiluun. Suosittelen tyyliarkkeja tukevaa selainta, jotta koodinpätkät näkyisivät selvempinä.

© Jukka Suomela 1998-1999. Ks. kopiointi.

Viimeisimmät olennaiset muutokset tehty 1999-10-12. Ks. historia.


Sisällysluettelo


Kulttuurishokki?

DOS/Windows-maailmasta tulevalle koodaajalle Linux-ohjelmointi voi tuntua aluksi kovin vieraalta. Standardi C-kieli on sama, mutta kaikki työkalut voivat poiketa kovastikin tutuista ohjelmointivälineistä.

Kun Windows-maailmassa trendi on kohti integroituja hiiriohjattuja kehitysympäristöjä, Linuxissa käytetään Unix-perinteiden mukaisesti tehokkaita komentorivityökaluja, jotka kukin hoitavat oman työnsä hyvin. Samoin esimerkiksi Windowsista tutut tavat toteuttaa graafisia käyttöliittymiä eivät välttämättä taivukaan suoraan Linux-maailmaan.

Kulttuurishokki voi olla suuri, mutta suosittelen, että uhraat hiukan vapaa-aikaasi Linux-ohjelmointiin paneutumiseen. Vaikket koskaan alkaisikaan toden teolla koodaamaan Linux-sovelluksia, ei oman näkökulman avartaminen varmastikaan ole haitaksi.

Linux-ympäristössä on ohjelmoijan kannalta muutamia merkittäviä etuja. Yksi merkittävimpiä on se, että lähes kaikkien Linux-pakettien mukana tulee suoraan kaikki tarvittavat ohjelmointivälineet. FSF:n ideologioiden mukainen ohjelmistojen vapaus on toteutunut erittäin hyvin Linux-järjestelmissä. Paitsi että lähes kaikki ohjelmat ovat vapaasti kopioitavissa, niihin saa myös hyvin usein lähdekoodin, joten muiden tekemiin ohjelmiin tutustuminen onnistuu helposti. Lisäksi alusta on vakaa, eikä nyrjähdä heti ensimmäisestä pieleenmenneestä kokeilusta.

Tässä artikkelissa oletetaan, että Linux on sinulle edes jollain tavalla tuttu. Jos olet aivan aloittelija, kannattaa sinun varmasti ensin tutustua hiukan Linuxin perusteisiin ja peruskäyttöön.


Esimerkkiprojekti

Tutustumme ensin käytettäviin välineisiin yksinkertaisen maailmaatervehtivän esimerkkiohjelman avulla. Esimerkkiohjelma on listattu tämän tekstin lopussa. Ohjelmasta on sekä C- että C++-versio ja tutustumme rinnan ohjelmointiin molemmilla välineillä.


Perustoimet

Unixin perustyökaluja ei opeteta tässä sen tarkemmin. Ohessa kuitenkin pikainen kertaus tärkeimmistä:

mv tiedosto uusitiedosto siirrä tai uudelleennimeä tiedosto
cp tiedosto uusitiedosto kopioi tiedosto
rm tiedosto poista tiedosto
mkdir hakemisto luo hakemisto
rmdir hakemisto poista hakemisto
cd hakemisto siirry hakemistoon
ls näytä hakemistolistaus

Jokaiselle uudelle projektille kannattaa luoda aina oma hakemisto. Hakemistot ovat halpoja, niiden kanssa on turha pihtailla.

Luodaan nyt siis kotihakemistoon uusi hakemisto "hw" (mkdir hw), siirrytään sinne (cd hw) ja luodaan tuohon hakemistoon alihakemistot "hello" ja "world" (mkdir hello world).

Opasteet

man komento näytä komennon manuaalisivu
info komento näytä komennon info-sivu

Manuaalisivut ovat perinteinen tapa toteuttaa komentojen opasteet Unix-maailmassa. Manuaaleista löytyy tietoa paitsi komennoista, myös esimerkiksi monista kirjastofunktioista - kokeile esimerkiksi komentoa man fgets.

Info-sivut ovat lähinnä GNU-maailman kummajaisia. Info-dokumentit eroavat man-sivuista käyttäjän kannalta lähinnä siinä, että info-sivut ovat hypertekstiä (voivat sisältää linkkejä aivan kuten weppisivutkin) kun taas man-sivuilla ei varsinaisia linkkejä ole.


Editointi

emacs

Etenkin ohjelmoijien keskuudessa emacs on varmasti kaikkein suosituin editori. Emacs on erittäin monipuolinen (ja samalla myös valitettavan raskas) editori. Emacsin näppäintoiminnot eivät välttämättä aukea aivan heti, mutta kärsivällisyys palkitsee. Kun emacsin peruskäytön on kerran opetellut, se auttaa takuulla niin ohjelmoinnissa kuin muissakin editointitarpeissa.

Komennolla emacs helloworld.c emacs käynnistyy ja avaa komentorivillä kerrotun tiedoston muokattavaksi. Emacs on normaali kokoruutueditori. Näppäilyllä C-x C-s (eli painat Control-näppäimen kanssa ensin x:ää ja sitten s:ää) voit tallentaa tiedoston ja näppäilyllä C-x C-c sulkea emacsin.

Yksi emacsin lukuisista toiminnoista on info-lukija, jolla voi selailla info-sivuja samaan tapaan kuin info-komennollakin. Tämä toiminto käynnistyy näppäilemällä C-h i (siis painat Control-näppäimen kanssa h:ta, vapautata Control-näppäimen ja painat sitten i:tä). Info-sivuilta löytyy myös emacsin omat opasteet.

Emacs tarjoaa lohtua myös Windows-maailman graafisiin editoreihin ja hiiren käyttöön tottuneille. X-ympäristössä emacs avautuu omaan ikkunaansa ja tarjoaa käyttäjälle mm. hiirellä käytettävät valikot, tekstin maalaamisen hiirellä ja monia muita toimintoja.

Myös XEmacsia kannattanee kokeilla. XEmacs on GNU Emacsin (eli sen "tavallisen" emacsin) pohjalta kehitetty, pitkälti samankaltainen editori. XEmacs tarjoaa mm. hiukan aloittelijaystävällisemmän tavan muokata editorin asetuksia. Se, kumpi kaksikosta on parempi, jääkööt jokaisen itsensä ratkaistavaksi... XEmacs käynnistyy komennolla xemacs.

Lisätietoja emacsin käytöstä löytyy esimerkiksi TKK:n Emacs-oppaasta.

vi

Toinen koulukunta emacs-käyttäjien ohella on vi-käyttäjät. Jos emacs tuntuu aluksi hiukan kryptiseltä, vi on takuulla sitä. Vi on kuitenkin tehokas osaavissa käsissä, ja vi löytyy lähes miltä tahansa Unix-koneelta, jonka eteen ikinä joudut.

Linuxin mukana tulee tyypillisesti erittäin monipuolinen ja kätevä vi:n versio, vim. Allekirjoittanut on entinen emacsoija ja nykyinen vim-käännynnäinen. Molemmilla maailmoilla on etunsa, mutta vimin keveys ajoi minun kohdallani voiton.

Jos kiinnostus heräsi (tai jos onnistuit käynnistämään uteliaisuuttasi vi:n, etkä keksi, miten pääset siitä pois...), käy tutustumassa Vi Lovers Home Page:en.

Eikö ole mitään yksinkertaisempaa?

Tyypillisen Linux-paketin mukana tulee toki paljon muitakin editoreita. Esimerkkejä näistä ovat pico, jed ja joe.

Näistä erityisesti pico on hyvin aloittelijaystävällinen, mutta vakavampaan käyttöön esimerkiksi ohjelmoijan editorina siitä ei käytännössä ole. Näitä muitakin vaihtoehtoja kannattaa silti kokeilla, jos aloittelevan Linux-ohjelmoijan ura uhkaa kompastua emacsin ja vi:n kummallisuuksiin.

Lopulta editorin valinnan ratkaisee kuitenkin omat mieltymykset. Kannattaa kuitenkin muistaa, että opetteluun uhratut tunnit tulevat nopeasti takaisin, kunhan on oppinut hyödyntämään monipuolisen editorin tehokkaita toimintoja.


Kääntäminen ja linkkaus

Perinteisesti ohjelmien kääntäminen lähdekoodista ajettavaksi ohjelmaksi suoritetaan kahdessa vaiheessa:

  1. Käännetään yksittäiset lähdekoodit objektitiedostoiksi.
  2. Linkitetään objektitiedostot sekä valmiit kirjastot ajettavaksi ohjelmaksi.

Seuraavassa tutustutaan siihen, mitä näissä vaiheissa tapahtuu ja miten se käytännössä tehdään GNU/Linux-ympäristössä.

GNU-projektin C-kääntäjä gcc

GNU-projektin C-kääntäjä, gcc, on ylivoimaisesti yleisin C-kääntäjä Linux-ympäristössä.

gcc sisältää sekä C- että C++-kääntäjän. Se, käännetäänkö C- vai C++-ohjelmaa, riippuu kääntämiskomennosta ja tiedoston päätteestä. Varminta on noudattaa seuraavia sääntöjä:

  • C-kieliset tiedostot nimetään tyyliin abc.c ja ne käännetään komennolla gcc.
  • C++-kieliset tiedostot nimetään tyyliin abc.cc ja ne käännetään komennolla g++.

helloworld:in kääntäminen ja linkkaus

Tutkitaan seuraavaksi vaiheittain, kuinka saamme ensin käännettyä lähdekoodit objektitiedostoiksi ja sen jälkeen linkitettyä ne.

Käännetään lähdekoodit objektitiedostoiksi
~/hw$ gcc -c helloworld.c
~/hw$ cd hello
~/hw/hello$ gcc -c hello.c
~/hw/hello$ cd ..
~/hw$ cd world
~/hw/world$ gcc -c world.c
~/hw/world$ cd ..
~/hw$ g++ -c helloworld.cc
~/hw$ cd hello
~/hw/hello$ g++ -c hello.cc
~/hw/hello$ cd ..
~/hw$ cd world
~/hw/world$ g++ -c world.cc
~/hw/world$ cd ..
Katsotaan, että objektitiedostot ovat olemassa
~/hw$ ls *.o
helloworld.o
~/hw$ ls hello/*.o
hello/hello.o
~/hw$ ls world/*.o
world/world.o
Linkitetään objektitiedostot
~/hw$ gcc helloworld.o hello/hello.o world/world.o -o helloworld ~/hw$ g++ helloworld.o hello/hello.o world/world.o -o helloworld
Koeajo
~/hw$ ./helloworld
Hello, world!

Mitä siis teimme?

Käännösvaiheessa käytettiin -c-vipua. Tämä kertoo gcc:lle, että haluamme ainoastaan kääntää ohjelman objektitiedostoksi, emme linkittää. Käännösvaiheessa gcc tulostaa virheilmoituksia, jos se havaitsee C-koodissa syntaksivirheitä - virhetilanteessa yksinkertaisesti korjaat virheellisen lähdekoodin ja yrität käännöstä uudelleen.

Kuten hakemistolistauksesta näimme, käännöksen tuloksena syntyi kustakin lähdekooditiedostosta abc.c (tai abc.cc) yksi objektitiedosto, joka on nimetty automaattisesti tyyliin abc.o. Objektitiedosto sisältää kaikki vastaavassa lähdekooditiedostossa määritellyt funktiot ja muuttujat konekielelle käännettynä.

Nyt siis esimerkiksi tiedosto helloworld.o sisältää main-funktion määrittelyn. Samassa objektitiedostossa on puolestaan viittaukset funktioihin hello ja world. Vastaavasti tiedosto hello/hello.o sisältää hello-funktion määrittelyn ja edelleen viittauksen funktioon printf.

Nyt pitäisi saada palapeli koottua. Tähän käytetään linkkeriä, joka, kuten jo mainittua, löytyy myös gcc:stä. Linkityskomennossa yksinkertaisesti kerrotaan, mitkä objektitiedostot linkitetään yhteen. -o helloworld-valitsin ei ole välttämätön, sillä ainoastaan kerrotaan, että tuloksena syntyvä ohjelma tallennetaan tiedostoon helloworld eikä tiedostoon a.out, mikä on oletusarvo.

Linkkeri luo ensimmäisen itse tehdyn Linux-ohjelmasi ja voit ajaa sen kuten minkä tahansa muunkin ohjelman komennolla ./helloworld. (./ alussa on yleensä tarpeen, koska normaalisti hakupolussa ei ole nykyistä hakemistoa, eikä käyttöjärjestelmä tällöin myöskään etsi ajettavaa ohjelmaa nykyisestä hakemistosta.)

Ohjelma toimii loistavasti ja tervehtii koko maailmaa. Nautimme hetken kansainvälisestä hurmoksesta.

Salaisuus

Ok, myönnetään, gcc osaa kyllä tehdä itse suoraan sekä käännöksen että linkityksen. Voit kokeilla komentoa gcc helloworld.c hello/hello.c world/world.c -o helloworld tai g++ helloworld.cc hello/hello.cc world/world.cc -o helloworld.

Sisäisesti gcc tekee tällöinkin aivan samoin kuin edellä on esitetty. Tämä ainoastaan säästää kirjoitusvaivaa käyttäjältä.

Tietenkään ei ole myöskään mitään pakkoa jakaa ohjelmaa useisiin pieniin lähdetiedostoihin. Kaiken voisi kyllä pitää yhdessä mammuttimaisessa tiedostossa.

Pienten, erikseen käännettävien lähdekoodien käyttö sekä käännöksen ja linkkauksen ajatuksen sisäistäminen on joka tapauksessa erittäin hyödyllistä:

  1. Ohjelma on joka tapauksessa järkevää kirjoittaa modulaariseksi. Yhden ison möhkäleen hallinta on vaikeaa. Ohjelmakoodista tulee helposti hallitsematonta spagettia, ellei modulaarisuuteen kiinnitetä lainkaan huomiota.
  2. Kun muutat yhtä lähdekoodia, ei ole useinkaan mitään pakkoa kääntää uusiksi koko projektia, vaan riittää, että käännät vain sen yhden modulin uusiksi ja pelkästään linkität ohjelman uudelleen. Isoissa projekteissa koko ohjelman kääntäminen voi viedä nopealtakin koneelta tunteja, kun yhden tiedoston käännös vie vain sekunteja. Ero voi siis olla todella tuntuva, vaikkei sitä helloworldissä välttämättä huomaakaan.
  3. Ymmärtämällä kääntäjän ja linkkerin eron, ymmärrät myös, mitkä ovat kääntäjän ja mitkä linkkerin virheilmoituksia ja osaat paremmin arvata, mistä ne johtuvat.
  4. Ymmärrät huomattavasti enemmän siitä, mitä käännöksen yhteydessä oikeasti tapahtuu.

Jäljempänä kirjastojen käsittelyn yhteydessä tutustumme lisää siihen, miten voit modularisoida ohjelmaasi edelleen.

Suosittelen lämpimästi seuraavaa kokeilua:

  • Lähdetään tilanteesta, jossa meillä on helloworld jo valmiiksi käännettynä ja linkitettynä.
  • Haluamme kokeilla, miltä ohjelma näyttäisi, jos sanan "Hello" vaihtaisi sanaksi "Hi". Käydään tekemässä muutos tideostoon hello/hello.c.
  • Nyt riittää, että käännämme pelkästään tuon yhden tiedoston uusiksi (gcc -c hello.c). Muita C-kielisiä lähdekoodeja ei tarvitse kääntää uudelleen.
  • Linkitetään valmiit palaset (gcc helloworld.o hello/hello.o world/world.o -o helloworld) ja ihmetellään, kuinka upealta uusi ohjelmamme näyttääkään.

Palaamme siis motivoituneina tylsän teoriapuolen kimppuun. Tongimme tarkemmin, mitä linkityksessä tapahtuukaan.

Pintaraapaisu linkityksen ja kirjastojen sielunelämään

Linkkeri käy läpi jokaisen objektitiedoston, linkittää ne yhteen ja selvittelee objektien väliset viittaukset. helloworld.o-tiedostoa ihmetellessään linkkeri huomaa viittauksen symboliin hello. Tämä ongelma ratkeaa helposti, symboli löytyy toisesta objektitiedostosta, hello/hello.o, joka myös linkitetään mukaan.

Entä sitten printf? Tuotahan ei löydy mistään objektitiedostosta. Kyse on kuitenkin standardista C-kielen funktiosta ja niinpä funktio löytyy C-kirjastosta (/lib/libc.*). gcc linkittää aina oletuksena C-kirjaston mukaan. Niinpä ei meidän tarvitse erikseen murehtia C-kielen vakiofunktioiden löytymisestä.

C++-ohjelmia g++-komennolla käännettäessä mukaan tulevat myös automaattisesti tarvittavat C++-vakiokirjastot.

Dynaaminen linkitys

C-kirjasto linkitetään useimmissa Linux-järjestelmissä ohjelman mukaan itseasiassa dynaamisesti. Tämä tarkoittaa käytännössä sitä, että C-kirjastosta ei liitetä mitään osia itse käännettyyn ohjelmaan, vaan ohjelmaa käynnistettäessä ladataan automaattisesti tarvittava kirjasto.

Monissa Linux-järjestelmissä voit komennolla ldd helloworld tutkia, onko ohjelma linkitetty dynaamisesti ja jos on, mitä kirjastoja dynaamisesti ladataan. Tyypillisesti listauksessa esiintyy jokin tämänkaltainen rivi: libc.so.6 => /lib/libc.so.6, joka yksinkertaisesti tarkoittaa sitä, että ohjelmaa ladattaessa ladataan libc.so-kirjaston versio 6, ja tässä järjestelmässä se löytyy paikasta /lib/libc.so.6.

Jos koneellasi on sekä kirjasto libxyz.a että kirjasto libxyz.so.3.2.1, ensimmäinen on staattisesti linkitettävä ja jälkimmäinen on dynaamisesti linkitettävä. Linkkeri linkittää oletuksena dynaamisesti, jos dynaaminen kirjasto löytyy ja dynaaminen linkitys on kyseisessä järjestelmässä tuettu.

Vaikkei kirjastoa linkitettäisikään dynaamisesti, ei tarvitse pelätä, että koko C-kirjasto liitettäisiin oman ohjelmaasi mukaan. Kirjastosta linkitetään vain tarpeelliset osat.

Muita kirjastoja

Pian kuitenkin huomaat, ettei pelkkä vakio C-kirjasto riitä pitkälle. Jo osa tavallisista C-funktioista on peruskirjaston ulkopuolella. Matemaattiset funktiot eivät löydykään libc:stä, vaan erillisestä matematiikkakirjastosta libm:stä. Tällöin on linkityskomennon yhteydessä kerrottava, että tuokin kirjasto pitää ottaa mukaan. Tämä tehdään vipusella -lm, jossa m viittaa kirjaston nimeen ilman lib-etuliitettä ja .a- tai .so.*-päätettä.

Vastaavasti curses-kirjastoa käytettäessä joudut kertomaan -lncurses-valitsimella, että haluat linkittää mukaan libncurses.*-kirjaston.

Mutta entä se #include?

Tärkeää on ymmärtää, että C-kielisessä lähdekoodissa oleva rivi #include <stdio.h> tarkoittaa ainoastaan sitä, että ohjelmaa käännettäessä käydään lukemassa mukaan tiedosto /usr/include/stdio.h. Tuolta tiedostosta löytyy mm. printf-funktion prototyyppi, joka kertoo, että tuollainen funktio on olemassa ja kertoo funktion parametrien tyypit sekä paluuarvon. Tuossa header-tiedostosta ei ole kerrottu sitä, miten funktio on toteutettu. Funktion toteutus löytyy edellämainitusta C-kirjastosta, joka tulee mukaan vasta linkitysvaiheessa.

Tilanne on aivan sama kuin meidän oman ohjelmamme hello-funktion kanssa. hello/hello.h-headerissa on kerrottu tuon funktion prototyyppi. Näin pääohjelmaa kääntäessään C-kääntäjä tietää, että tuonniminen funktio on todellakin olemassa ja että se ei ota mitään argumentteja. Itse funktion toteutus sen sijaan on hello/hello.o-tiedostossa (joka puolestaan käännettiin hello/hello.c-lähdekoodista).

Esimerkiksi pääohjelman helloworld.c voi kääntää objektitiedostoksi, vaikkei hello/hello.o-tiedostoa olisi oikeasti olemassakaan. Sen sijaan linkittäminen kokonaiseksi ohjelmaksi ei onnistu, elleivät kaikki palaset loksahda paikoilleen. Jokaisen funktion ja muuttujan on löydyttävä jostain mukaan linkitettävästä objektitiedostosta tai kirjastosta.

Aivan samoin, jos ohjelmoit cursesilla, sinun on #include:n avulla kerrottava kääntäjälle, mistä löytyvät funktioiden prototyypit, mutta sen lisäksi on kerrottava linkkerille, mistä kirjastosta löytyvät itse funktiot.

Omien kirjastojen luominen

Kirjasto on yksinkertaisesti vain kokoelma objektitiedostoja tietyllä tavalla organisoituna.

Voit myös rakentaa omia kirjastojasi. Tällöin sinun ei tarvitse joka kerta valita mukaan linkitettävissä kaikkia yksittäisiä objektitiedostoja, vaan voit yksinkertaisesti valita kokonaisen kirjaston linkitettäväksi mukaan.

Kirjastojen kohdalla erityisen mukavaa on se, että niiden avulla on helppo hyödyntää kerran kirjoitettuja ohjelman osia muissa projekteissa (aivan kuten kerran kirjoitettua curses-kirjastoa on helppo hyödyntää kaikissa tekstiruudun käsittelyä tarvitsevissa projekteissa).

Erittäin keinotekoinen esimerkki

Oletetaanpa, että olet esimerkkiprojektissamme kehittänyt hello-hakemistossa olevaa tervehdyksenpuolikkaan tulostavaa funktiota pitemmälle. hello-funktioon on tullut kutsuja muihin funktioihin - joku funktio voisi esimerkiksi tutkia, ajetaanko ohjelmaa lätäkön toisella puolella ja sen perusteella tulostettaisiin sitten Hello-tervehdyksen sijasta Hi. Lopulta ei ole enää järkevää pitää kaikkia funktioita samassa lähdetiedostossa ja niinpä jaamme tiedoston useampaan osaan. Esimerkiksi tiedostossa hello.c olisi edelleen tuo hello-funktio, tiedostossa american.c olisi funktio are_we_american ja tiedostossa strings.c olisivat sitten nuo eri tervehdykset.

Huomaamme, että olemme kasanneet erittäin hyödyllisen ohjelmanpalesen, joka tulostaa lokalisoidun tervehdyksen alun. Tuotahan olisi mukava käyttää jossain toisessakin projektissa, vaikkapa siinä seuraavassa hellouser-ohjelmassa, joka ei tervehdikään enää koko maailmaa vaan käyttäjää.

Riesana on kuitenkin se, että nyt joutuisimme tuossa toisessakin projektissa ottamaan mukaan kaikki tähän liittyvät objektitiedostot: hello.o, american.o ja strings.o.

Lisäksi hallittavuus kärsii: Kun seuraavan kerran laajennamme tuota hello-funktiota ja jaamme toteutusta taas uusiin tiedostoihin, ei riitäkään, että pelkästään käännämme kaikki tähän liittyvät lähdekooditiedostot uusiksi ja linkitämme pääohjelmat (helloworld ja hellouser) kuten ennen. Nythän pääohjelmien linkityksessä pitäisi komentoriville lisäillä aina vain uusia objektitiedostoja.

Ratkaisu

Kaunis ratkaisu ongelmaan olisi tehdä kaikista hello-hakemiston objektitiedostoista yksi kirjasto, libhello.a.

Edut ovat ilmeiset. Koko ohjelmaa kääntäessämme riittää, että otamme mukaan tuon libhello.a-kirjaston, siinä kaikki.

hello-funktiota voi kehittää vapaasti ja sen voi jakaa millä tavalla tahansa objektitiedostoihin. Lopullisen ohjelman kääntämiseksi riittää, että otetaan mukaan tuo uusi kirjasto.

Isossa projektissa voitaisiin jättää esimerkiksi hello-kirjaston kehittäminen kokonaan yhdelle henkilölle. Hän koostaisi kirjaston haluamallaan tavalla palasista ja muut projektin kimpussa työskentelevät ainoastaan hyödyntäisivät tuota kirjastoa.

Rajapinta - funktioiden prototyypit sekä mahdolliset vakiot, tyypit, structit ja C++:n tapauksessa luokat - esiteltäisiin esimerkiksi hello.h-otsikkotiedostossa ja itse toteutus olisi koottu yhteen kirjastoon, libhello.a:han. Käyttö olisi siis aivan yhtä helppoa kuin vaikkapa curses-kirjaston tapauksessa. Lähdekoodiin otetaan mukaan header-tiedosto tarvittaessa (#include hello/hello.h) ja lopullista ohjelmaa linkitettäessä mukaan linkitetään kirjasto libhello.a.

Tärkeää on huomata, että vaikka hello-kirjastoa kehitettäisiin, ei muita projektin osia tarvitsisi kääntää uudestaan eikä niitä varsinkaan tarvitsisi millään tavalla muuttaa. Riittäisi, että muut objektitiedostot ja kirjastot vain linkitetään uudestaan uuden kirjaston kanssa.

ar, ranlib ja nm

Oman kirjaston tekeminen voi tuntua oudolta, mutta seuraavilla kahdella komennolla pärjää GNU-ympäristössä:

~/hw/hello$ ar cru libhello.a hello.o america.o strings.o
~/hw/hello$ ranlib libhello.a

Ensimmäinen komento kokoaa kirjaston, jälkimmäinen luo kirjastoon linkitystä nopeuttavan hakemiston.

Lopputuloksena syntyy libhello.a-kirjasto, jonka voi linkittää vaikkapa helloworld-ohjelmamme mukaan. Komennolla nm -s libhello.a voit ihmetellä, mitä kirjasto on syönyt. Listasta näkee, mitä symboleja kirjasto sisältää missäkin objektitiedostossa ja toisaalta, mihin symboleihin mistäkin objektitiedostosta viitataan (esimerkiksi printf).

Lisätietoja ar, ranlib ja nm -ohjelmista sekä näiden valitsimista kannattaa tonkia ohjelmien man- ja info-sivuilta.

Oman kirjaston käyttö

Linkittämisen voisi tehdä aivan samalla tavalla kuin aiemmin kerrottiin muiden kirjastojen kohdalla, yksinkertaisesti lisäämällä -lhello kääntäjän komentoriville. Tämä kuitenkin vaatisi, että oma kirjastomme libhello.a löytyisi niistä hakemistoista, joista linkkeri kirjastoja yleensä etsii (esim. /lib ja /usr/lib). Voimme kuitenkin kertoa kääntäjälle, että kirjastoja etsitään myös tuosta hello-hakemistosta -L-vipusella. Koko linkitysrivi on siis:

~/hw$ gcc -Lhello -o helloworld helloworld.o world/world.o -lhello

Toinen tapa on yksinkertaisesti kirjoittaa kirjasto muiden objektitiedostojen sekaan:

~/hw$ gcc -o helloworld helloworld.o world/world.o hello/libhello.a

Kuten aiemmin todettiin, kirjastosta linkitetään mukaan vain tarpeelliset osat. Käytännössä linkkerit ovat aina sen verran älykkäitä, että linkittävät mukaan kustakin kirjastosta vain tarvittavat objektitiedostot. Tämänkin vuoksi hello-kirjaston koostaminen erillisistä objektitiedostoista on järkevää: jos esimerkiksi jossain ohjelmassasi tarvitset hello-kirjastosta ainoastaan are_we_american-funktiota, ei ohjelman mukaan linkitetä hello-kirjastosta muuta kuin america.o (sekä ne objektitiedostot, joita tuolla olevat funktiot käyttävät).


Helpotusta projektin rakentamiseen

Olemme edellä käyneet joukon komentoja, joiden avulla voidaan kääntää lähdekoodeista objektitiedostoja, koota objektitiedostoja kirjastoiksi ja linkittää objektitiedostoista ja kirjastoista edelleen lopullisia, ajettavia ohjelmia.

Kaikkien käännöskomentojen kirjoittelu käsin on tietenkin ikävää puuhaa. Onneksi tähän ei ole tarvetta. Tutustumme seuraavaksi erittäin hyödylliseen ohjelmaan make:en, jonka avulla projektienhallinta helpottuu huomattavasti.

make

Riippuvuudet

Esimerkkiprojektissamme optimaalinen tapa kääntää koko ohjelma on seuraava:

  • Tutkitaan, tarvitseeko joku objektitiedosto kääntää uudestaan. Jos tarvitsee, tehdään käännös lähdetiedostosta objektitiedostoksi.
  • Jos käytetään kirjastoja, tutkitaan, tarvitseeko joku kirjasto rakentaa uudelleen. Jos tarvitsee, rakennetaan kirjasto objektitiedostoista.
  • Lopuksi tutkitaan, tarvitseeko itse ohjelma linkittää uudelleen. Jos tarvitsee, tehdään linkitys.

Jokaisessa vaiheessa tutkiminen on hyvin suoraviivaista. Esimerkiksi objektitiedosto hello/hello.o käännetään tiedostosta hello/hello.c. Lähdekoodissa on puolestaan #include:lla otettu mukaan myös tiedostot hello/hello.h sekä helloworld.h. Muutos missä tahansa noista kolmesta lähdetiedostosta vaatii siis objektitiedoston uudelleenkääntämisen. Sanotaankin, että kohde hello/hello.o riippuu tiedostoista hello/hello.c, hello/hello.h ja helloworld.h.

Vastaavasti esimerkiksi linkitysvaiheessa ohjelma on linkitettävä uudelleen, jos joku objektitiedostoista on muuttunut. Niinpä valmis ohjelma, helloworld, riippuu tiedostoista helloworld.o, hello/hello.o ja world/world.o.

Seuraavassa on listattu kaikki (C-kielisen) esimerkkiprojektin kohteet sekä mistä lähdetiedostoista ne riippuvat:

  • helloworld: helloworld.o hello/hello.o world/world.o
  • helloworld.o: helloworld.c helloworld.h
  • hello/hello.o: hello/hello.c hello/hello.h helloworld.h
  • world/world.o: world/world.c world/world.h helloworld.h

Kun riippuvuudet on listattu, on jo helppo automatisoida projektin rakentaminen. Logiikka on yksinkertainen:

  • Jos kohdetiedosto puuttuu, se tietenkin rakennetaan uudestaan.
  • Jos joku lähdetiedosto on uudempi kuin kohdetiedosto, rakennetaan kohdetiedosto uudestaan.

On tärkeää ymmärtää, että jos kohdetiedosto a riippuu lähdetiedostoista b ja c, tarkoittaa se sitä, että tiedosto a voidaan milloin tahansa luoda tyhjästä käyttämällä pelkästään tiedostoja b ja c pohjana. Se, että tiedoston b luomiseen voidaan tarvita muita tiedostoja, kerrotaan tiedoston b riippuvuuksissa.

Makefile

Kun riippuvuudet on kasassa, voidaan alkaa miettiä sitä, millä komennoilla kohdetiedostot saadaan luotua lähdetiedostoista. Tässä tapauksessa riippuvuudet ja komennot ovat seuraavia:

  • helloworld: helloworld.o hello/hello.o world/world.o
    • gcc -o helloworld helloworld.o hello/hello.o world/world.o
  • helloworld.o: helloworld.c helloworld.h
    • gcc -c helloworld.c
  • hello/hello.o: hello/hello.c hello/hello.h helloworld.h
    • cd hello; gcc -c hello.c
  • world/world.o: world/world.c world/world.h helloworld.h
    • cd world; gcc -c world.c

Kun edelläkuvattu lista kirjoitetaan määrättyyn muotoon, saadaan make-ohjelman ymmärtämä Makefile:

helloworld: helloworld.o hello/hello.o world/world.o
        gcc -o helloworld helloworld.o hello/hello.o world/world.o

helloworld.o: helloworld.c helloworld.h
        gcc -c helloworld.c

hello/hello.o: hello/hello.c hello/hello.h helloworld.h
        cd hello; gcc -c hello.c

world/world.o: world/world.c world/world.h helloworld.h
        cd world; gcc -c world.c 

On erittäin tärkeää huomata, että Makefile:n rakenteen tulee olla täsmälleen oikea:

kohde : lähde-1 lähde-2 rivinvaihto
tabulaattori komento, jolla lähdetiedostoista rakennetaan kohdetiedosto

Huomaa, että sisennys on Makefile:ssä osa syntaksia ja sisennys on tehtävä nimenomaan tabulaattorilla, ei välilyönneillä.

make:n käyttö

make-ohjelman käyttö on äärimmäisen yksinkertaista. Kun Makefile on tehty, riittää, että käynnistämme ohjelman komennolla make:

~/hw$ make
make: `helloworld' is up to date.

make toimii seuraavasti:

  • Avataan nykyisestä hakemistosta tiedosto Makefile ja aletaan lukea sitä.
  • Etsitään tiedostosta ensimmäinen kohde.
  • Rakennetaan kohde.

Rakentaminen puolestaan tapahtuu seuraavasti:

  • Katsotaan, mistä lähteistä kohde riippuu.
  • Rakennetaan kaikki lähteet.
  • Tutkitaan, onko kohde olemassa ja onko se uudempi kuin lähteet.
    • Jos ei, katsotaan Makefile:stä, kuinka kohde luodaan ja suoritetaan kyseinen komento.

Huomaa, että rakentaminen on rekursiivista: yhden kohteen luomiseen tarvitaan tietyt lähdetiedostot, niiden luomiseen puolestaan toiset lähdetiedostot jne.

Esimerkkitapauksessa make tekee siis seuraavat asiat:

  • Ensimmäinen kohde on helloworld.
  • Rakennetaan kohde helloworld:
    • Kohde riippuu lähteistä helloworld.o, hello/hello.o ja world/world.o.
    • Rakennetaan kohde helloworld.o:
      • Tämä kohde riippuu lähteistä helloworld.c ja helloworld.h
      • Rakennetaan kohde helloworld.c:
        • Huomataan, ettei tätä kohdetta löydy Makefile:stä, niinpä se ei riipu mistään muista tiedostoista.
      • Rakennetaan kohde helloworld.h, sama tilanne kuin edellä.
      • Tutkitaan, onko kohde helloworld.o olemassa ja onko se uudempi kuin lähdetiedostot helloworld.c ja helloworld.h.
        • Jos kohde puuttui tai lähdetiedostot olivat uudempia, kohde on luotava komennolla gcc -c helloworld.c.
    • Rakennetaan kohde hello/hello.o kuten edellä.
    • Rakennetaan kohde hello/hello.o kuten edellä.
    • Tutkitaan, onko kohde helloworld olemassa ja onko se uudempi kuin lähdetiedostot helloworld.o, hello/hello.o ja world/world.o.
      • Jos kohde puuttui tai lähdetiedostot olivat uudempia, kohde on luotava komennolla gcc -o helloworld helloworld.o hello/hello.o world/world.o

Jos mitään ei tarvinnut tehdä, make tulostaa ilmoituksen make: `helloworld' is up to date. Muutoin make tulostaa kaikki komennot, mitä se suorittaa.

Kokeile! Muuta esimerkiksi tiedosto hello/hello.c hiukan ja aja tämän jälkeen make uudestaan. Nyt tulostuksen pitäisi näyttää suunnilleen tältä:

~/hw$ make
cd hello; gcc -c hello.c
gcc -o helloworld helloworld.o hello/hello.o world/world.o

make suoritti siis ainoastaan tarvittavan käännöksen ja linkityksen. Jos nyt käynnistät make:n heti uudelleen, tulostuksen pitäisi näyttää jälleen tältä:

~/hw$ make
make: `helloworld' is up to date.

make huomasi, että kohde kaikkine riippuvuuksineen on kunnossa, eikä siis tehnyt mitään.

Edut make:n käytössä ovat ilmeisiä. Riittää, että luot kerran Makefile:n ja tämän jälkeen joka kerta yhdellä make-komennolla saat rakennettua koko ohjelman kuntoon.

Kohteet

Voit komentorivillä kertoa, minkä kohteen haluat rakentaa. Hyvin usein tätä hyödynnetään esimerkiksi ohjelmien asennuksessa: Makefile:een luodaan kohde install, jossa on komentoina kyseisen ohjelman asentamiseen tarvittavat komennot. Näin ohjelman voi asentaa yksinkertaisesti komennolla make install.

Toinen esimekki on kohde clean, joka tyypillisesti poistaa kaikki objektitiedostot, käännöstulokset, core-tiedostot ja vastaavat. Lisää seuraavat rivit Makefile:n loppuun ja kokeile:

clean:
        rm -f *.o */*.o helloworld core 

Muuttujat

Makefileissä ei koskaan kannata "kovakoodata" komentojen nimiä, parametreja ja vastaavia. Sen sijaan tulee käyttää muuttujia:

CC = gcc
CFLAGS = -Wall
LDFLAGS =
RM = rm -f

helloworld: helloworld.o hello/hello.o world/world.o
	$(CC) $(LDFLAGS) -o helloworld helloworld.o hello/hello.o world/world.o

helloworld.o: helloworld.c helloworld.h
	$(CC) -c $(CPPFLAGS) $(CFLAGS) helloworld.c

hello/hello.o: hello/hello.c hello/hello.h helloworld.h
	cd hello; $(CC) -c $(CPPFLAGS) $(CFLAGS) hello.c

world/world.o: world/world.c world/world.h helloworld.h
	cd world; $(CC) -c $(CPPFLAGS) $(CFLAGS) world.c

clean:
	$(RM) *.o */*.o helloworld core 

Etuja muuttujien käytössä on lukuisia, tärkeimpänä se, että ohjelmaa käännettäessä voidaan vaihtaa käytettävät komennot ja vipuset helposti suoraan komentoriviltä. Kokeile esimerkiksi komentoa make CFLAGS=-O2.

GNU-projektin make määrittelee itse suuren joukon muuttujia, myös edellämainitut CC, CFLAGS, LDFLAGS ja RM, joten noiden oletusarvojen antaminen itse ei ole aivan välttämätöntä. Täydellinen lista muuttujista löytyy make:n info-sivuilta.

Näiden lisäksi voit luonnollisesti itsekin määritellä mitä tahansa muuttujia. Omat muuttujat kannattaa kirjoittaa pienillä kirjaimilla erotuksena LDFLAGS:n kaltaisista make:n itsensä määrittelemistä ja tunnistamista muuttujista.

Implisiittiset säännöt

Ennenkuin ryntäät kirjoittamaan nerokkaita omia muuttujia, kannattaa tutustua myös GNU make:n tarjoamiin implisiittisiin sääntöihin. Jos Makefile:n kohteelle ei ole määritelty mitään sääntöä, make käyttää omia oletusarvojaan.

Esimerkiksi kohdetiedosto abc.o rakennetaan lähdetiedostosta abc.c implisiittisellä säännöllä $(CC) -c $(CPPFLAGS) $(CFLAGS) abc.c -o abc.o. Jälleen info-sivuihin tutustuminen kannattaa.

Niinpä tälle projektille riittää GNU-ympäristössä jopa näin yksinkertainen Makefile:

helloworld: helloworld.o hello/hello.o world/world.o
helloworld.o: helloworld.c helloworld.h
hello/hello.o: hello/hello.c hello/hello.h helloworld.h
world/world.o: world/world.c world/world.h helloworld.h 

Luetellaan siis pelkät lähteet ja kohteet, make tekee loput. Yksinkertaista.

Rekursiiviset Makefile:t

Aina, kun projektiin tulee lisää tiedostoja, joudut luonnollisesti päivittämään Makefile:ä. Kun projekti paisuu suuremmaksi, kannattaa harkita myös Makefile:jen jakamista omiin alihakemistoihin.

Tällöin päähakemiston Makefile vastaa ainoastaan koko ohjelman linkittämisestä. Kussakin alihakemistossa on oma Makefile, jossa on ohjeet kyseisen modulin, kirjaston tms. rakentamisesta.

Erityisen hyödyllistä tällainen jako on silloin, kun samaa projektia työstää useampi henkilö: yhden kirjaston rakentaja voi itse huolehtia myös oman Makefile:n luomisesta ja päivittämisestä omassa alihakemistossaan ja päätason Makefile:ä ei tarvitse muuttaa lainkaan.

Seuraavassa on esitetty hyvin yksinkertainen esimerkki rekursiivisten Makefile:jen käytöstä esimerkkiprojektissamme:

Makefile:

# Määritellään muuttuja, jota käytetään myöhemmin.

OBJS = helloworld.o hello/hello.o world/world.o

# Oletuskohteena all, joka riippuu kolmesta lähteestä. Aluksi
# alihakemistot, viimeisenä varsinainen ohjelma.

all: module-hello module-world helloworld

# helloworld on pääohjelma, joka linkitetään objektitiedostoista.

helloworld: $(OBJS)
	gcc -o helloworld $(OBJS)

# Päätason Makefile:n vastuulla on ainoastaan pääohjelman luonti.

helloworld.o: helloworld.c helloworld.h
	gcc -c helloworld.c

# Kaikki muu hoidetaan alihakemistoissa.

# Ensin alihakemisto hello.

# Koska kohteella module-hello ei ole määritelty mitään riippuvuuksia,
# se luodaan joka kerta. Niinpä joka kerta, kun luodaan kohde all
# luodaan, luodaan myös module-hello.

# Tässä Makefile:ssä ei tarvitse välittää lainkaan siitä, miten
# hello/hello.o oikeasti luodaan, tässä ainoastaan siirrytään
# alihakemistoon ja kutsutaan siellä make:a. Tiedostossa hello/Makefile
# kerrotaan sitten, mitä kaikkea hello/hello.o:n luomiseen oikeasti
# tarvitaan.

# Muuttuja MAKE viittaa yksinkertaisesti make-ohjelmaan.
# Info-dokumenteista löytyy tarkempi selvitys muuttujasta.

module-hello:
	cd hello && $(MAKE)

# Ja sama temppu myös alihakemistolle world.

module-world:
	cd world && $(MAKE)

# Kerrotaan, että kohteet module-hello ja module-world eivät ole
# oikeasti mitään tiedostoja...

.PHONY: module-hello module-world
    

hello/Makefile:

hello.o: hello.c hello.h ../helloworld.h
	gcc -c hello.c
    

world/Makefile:

world.o: world.c world.h ../helloworld.h
	gcc -c world.c
    

Nyt jos esimerkiksi world/world.o vaatii yhden uuden include-tiedoston, riittää, että korjataan riippuvuudet world/Makefile:ssä. Päätason Makefile:een ei tarvitse koskea lainkaan:

world/Makefile:

world.o: world.c world.h ../helloworld.h uusi_tiedosto.h
	gcc -c world.c
    

Rekursiiviset Makefile:t kannattaa yrittää pääpiirteissään ymmärtää, sillä ainakin isoihin projekteihin tutustuessaan niihin törmää usein. Lisäksi niitä käytetään yleisesti jäljempänä esiteltävän automake-työkalun kanssa.

Rekursio voi osoittautua hyödylliseksi ja käteväksi omissa laajemmissa projekteissa, mutta sen edut eivät suinkaan ole yksikäsitteisiä. Peter Millerin teksti Recursive Make Considered Harmful voi olla terveellistä luettavaa. Tekstistä löytyy esimerkkejä muista vaihtoehdoista.

Lisätietoja

Lisää make:n käytöstä, hyvä esimerkki Makefile:stä sekä monia hyödyllisiä vihjeitä voit lukea Lars Wirzeniuksen make-ohjeesta. Info-dokumenteista löytyy tarvittaessa lisätietoja.

make on monipuolinen työkalu, jonka avulla voi hoitaa hyvinkin mutkikkaita asioita ja hallita hyvin laajoja projekteja. Kaikkea ei kuitenkaan ole pakko tehdä itse. Mutkikkaammissa projekteissa huomattavasti helpompaa voi olla autoconf:n ja automake:n käyttö. Portattavuuden ohella nämä työkalut auttavat myös Makefile:n luomisessa. Näihin tutustutaan seuraavissa kappaleissa.

autoconf

autoconf on erittäin hyödyllinen työkalu. Viimeistään siinä vaiheessa, kun aiot julkaista tekemäsi ohjelman laajemman piirin käyttöön, sinun kannattaa ottaa autoconf käyttöön.

Kaikki Unix-ympäristöt eivät ole täsmälleen samanlaisia. Portattavuudessa voi törmätä useisiin eri ongelmiin, esimerkiksi:

  • Yhden järjestelmän vakiokirjastosta löytyvä funktio ei välttämättä löydy toisesta järjestelmästä.
  • Joissain järjestelmissä tietty funktio voi löytyä vakiokirjastosta, kun se toisessa järjestelmässä on kirjastossa x ja kolmannessa järjestelmässä se puuttuu kokonaan.
  • Kirjastojen header-tiedostot voivat poiketa toisistaan ja aiheuttaa ongelmia käännöksessä, vaikka itse kirjastoissa ei olisikaan merkittäviä eroja.
  • Kääntämisessä käytettävät työkalut ja esimerkiksi niiden hyväksymät komentorivivipuset ja tiedostomuodot voivat poiketa eri järjestelmissä. Jopa komentojen nimet voivat vaihdella.
  • Hakemistorakenne voi olla erilainen.
  • Ohjelmiston asentaja voi haluta vaikuttaa siihen, mihin hakemistoon ohjelmisto asennetaan.
  • Jossain järjestelmässä dynaaminen linkitys ei toimi, jossain se puolestaan on toteutettu eri tavalla.

autoconf on työkalu, jonka avulla luodaan helposti portattava ohjelma. GNU-maailmassa autoconf:sta on tullut erittäin suosittu väline ja käytännössä kaikissa ohjelmistoissa käytetäänkin autoconf:ia ohjelmiston konfiguroitiin.

Ohjelmiston asentajan kannalta autoconf:n avulla tuotettu paketti on - ainakin teoriassa - erittäin helposti käännettävissä ja asennettavissa:

  1. Ajetaan ohjelmiston mukana tullut skripti ./configure (ja annetaan tuolle tarvittaessa parametreja, joilla esimerkiksi kerrotaan ohjelmiston asennushakemisto).
  2. Ajetaan make, jolloin ohjelma käännetään.
  3. Ajetaan make install, jolloin ohjelma asennetaan.

Olennainen vaihe on konfigurointiskriptin ajaminen. Tuo skriptin on lähes aina luotu autoconf-työkalulla. Skripti sisältää joukon tarkistuksia, joilla selvitetään, millaisessa ympäristössä ohjelmaa ollaan kääntämässä. Tarkistusten pohjalta autoconf luo ohjelman kääntämisessä tarvittavat Makefile:t ja muut tiedostot.

automake

Pelkän autoconf:n käyttö on hyvin hankalaa aloittelijalle. Ohjelmoija joutuu kirjoittamaan ja ylläpitämään hyvinkin mutkikkaita Makefile.in-tiedostoja, joiden perusteella configure luo varsinaiset Makefile:t.

automake nimensä mukaisesti automatisoi Makefile.in:en luomista. Ohjelmoijan tarvitsee kirjoittaa vain yksinkertaiset rungot ja automake tekee raa'an työn.

Seuraavassa on tiivistetty esimerkki siitä, millä tavalla esimerkkiprojektissa voisi automake/autoconf -yhdistelmän ottaa käyttöön.

Lähes minkä tahansa asian voi toteuttaa automake/autoconf -systeemissä hyvin monilla eri tavoilla. Tässä esitettävä ei ole missään nimessä ainoa, suositeltava eikä edes välttämättä hyvä tapa.

Tässä esimerkissä käytetään aiemmin mainittuja kirjastoja. Jokaisesta projektin alihakemistosta luodaan oma kirjasto ja päätasolla ne sitten linkitetään yhteen pääohjelman kanssa. Tämä lähestymistapa toimii erityisen näppärästi automake:n kanssa. Kirjastojen muuttaminen myöhemmin dynaamisesti linkitettäviksi onnistuu erillisen libtool-työkalun avulla varsin portattavasti.

Käyttöönotto

Poista kaikki vanhat Makefile:t (tai mieluummin siirrä ne muualle talteen...).

Luo hakemisto aux aputiedostoille.

Luo tiedosto configure.in:

dnl Tämä on kommenttirivi.
dnl Tiedosto käsitellään m4-makroprosessorilla, ja dnl
dnl tarkoittaa m4:ssä "poista kaikki rivin loppuun asti".

dnl Aluksi perusasetukset
dnl ---------------------

AC_INIT(helloworld.c)
dnl Tämä alustaa autoconf/automake-systeemin.
dnl Parametrina annetaan jokin yksilöllisesti nimetty tiedosto
dnl polkuineen. Tiedoston tarkoituksena on se, että löydettyään
dnl kyseisen tiedoston configure tietää samalla myös löytäneensä
dnl lähdekoodin.

AC_CONFIG_AUX_DIR(aux)
dnl Kerrotaan, missä aputiedostot sijaitsevat.

AM_INIT_AUTOMAKE(helloworld, 1.0)
dnl Kerrotaan ohjelman nimi ja versionumero. Tätä käytetään esim.
dnl luotaessa ohjelmasta jakelupakettia. Tässä tapauksessa paketin
dnl nimeksi tulee automaattisesti helloworld-1.0.tar.gz.

AM_CONFIG_HEADER(config.h)
dnl Kerrotaan, että makrot tallennetaan config.h -tiedostoon.
dnl Muutoin makrot välitettäisiin C-kääntäjälle komentorivillä.


dnl Tarkistettavat ohjelmat, headerit ym.
dnl -------------------------------------

AC_PROG_CC
AC_PROG_RANLIB
AC_PROG_INSTALL
dnl Nämä selvittävät C-kääntäjän sekä ranlib- ja install-ohjelmat:
dnl millä nimellä ne löytyvät, mitä komentorivivipuja niille pitää
dnl antaa, toimivatko ne jne...

AC_HEADER_STDC
dnl Tämä puolestaan pyrkii selvittämään, onko normaalit ANSI C:n
dnl otsikkotiedostot käytettävissä, ja jos on, määrittelee
dnl STDC_HEADERS -makron.


dnl Luotavat tiedostot
dnl ------------------

AC_OUTPUT(Makefile hello/Makefile world/Makefile) 
dnl Mitä Makefileja configuren pitää luoda.
dnl Muista ylläpitää tätä listaa! 

Luo tiedosto Makefile.am:

SUBDIRS = hello world
bin_PROGRAMS = helloworld
helloworld_SOURCES = helloworld.c helloworld.h
helloworld_LDADD = hello/libhello.a world/libworld.a
EXTRA_DIST = aux/install-sh aux/missing aux/mkinstalldirs 

Luo tiedosto hello/Makefile.am:

noinst_LIBRARIES = libhello.a
libhello_a_SOURCES = hello.c hello.h 

Luo vastaavasti tiedosto hello/Makefile.am.

Luo tiedostot NEWS, README, AUTHORS ja ChangeLog, jos noita ei vielä ole, ja kirjoita niihin jotain hyödyllistäkin...

Aja aclocal. Tuloksena hakemistoon syntyy tiedosto aclocal.m4.

Aja autoconf, tämä puolestaan luo configure-skriptin.

Aja autoheader, joka luo config.h.in-tiedoston.

Aja automake -a, joka luo Makefile.in-tiedostot Makefile.am-tiedostojen perusteella ja lisäksi asentaa joukon tarvittavia tiedostoja.

Aja ./configure, joka luo varsinaiset Makefile:t Makefile.in-tiedostoista.

Aja make.

Edellämainittu työ pitää tehdä vain yhden kerran. Loppu onkin sitten helpompaa.

Käyttö

Kun muokkailet tulevaisuudessa ohjelmaasi, riittää uudelleenkääntämiseen pelkkä make.

Luonnollisesti, jos luot esimerkiksi uuden hakemiston, joudut muokkaamaan tiedostoja configure.in ja Makefile.am vastaavasti sekä luomaan uuteen hakemistoon Makefile.am-tiedoston.

Ok, entäs sitten?

Kaiken pitäisi nyt toimia hienosti. Lähdekoodeista käännetään objektikoodit, niistä kootaan kirjastot ja lopulta linkitetään lopullinen helloworld-ohjelma.

Syntyneet Makefile:t ovat täynnä hyödyllisiä toimintoja. Esimerkiksi make clean ja make install toimivat kuten missä tahansa GNU-ohjelmassa. Asennuskohteen voit määritellä configure:n --prefix -vivulla jne.

Voit jopa luoda valmiin tar + gzip -asennuspaketin yksinkertaisesti komentamalla make dist, tuloksena paketti nimeltä helloworld-1.0.tar.gz. Tuo paketti sisältää kaikki tarvittavat tiedostot. Käyttäjän tarvitsee vain purkaa paketti, sanoa ./configure; make; make install ja ohjelmasi kääntyy ja asentuu. Käyttäjällä itsellään ei tarvitse olla mitään autoconf/automake -työkaluja, kaikki tarvittavat skriptit liitetään jakelupaketin mukaan.

Erittäin hyödyllinen toiminto on make distcheck. Tuo paitsi luo jakelupaketin, myös kokeilee sen purkamista uuteen hakemistoon, konfiguroimista, kääntämistä ja jopa asentamista ja lopuksi vielä siivoaa jälkensä. Tuota kannattaa ehdottomasti käyttää ennen paketin levittämistä.

Kaikkien muiden herkkujen lisäksi eri lähdekoodien väliset riippuvuudet hoidetaan automaattisesti. Kokeile! Muokkaa jotain tiedostoa ja sano uudelleen make. Järjestelmä itse pitää huolen siitä, että esimerkiksi configure luodaan ja/tai ajetaan aina tarvittaessa uudelleen.

Varsinainen konfigurointi

Työkalujen olennaisinta hyötyä, eri systeemien erilaisten piirteiden tunnistamista automaattisesti, ei näin yksinkertaisessa esimerkissä pysty helposti esittelemään ja ylläolevassa esimerkissä konfigurointi rajoittui muutamien hyvin yksinkertaisten piirteiden tunnistamiseen.

Esimerkissä configure hoiti lähinnä käytettävän C-kääntäjän, ranlib-ohjelman ja install-ohjelman tunnistamisen. Nämä tarkistukset määriteltiin configure.in:n AC_PROG_... -määreillä.

Hiukan kehittyneemmän esimerkin saa header-tiedostojen tunnistuksesta. Jos haluat käyttää esimerkiksi errno-muuttujaa, törmäät ongelmaan: joissain systeemeissä tuo muuttuja määritellään standardinmukaisesti errno.h-tiedostossa, joissain ei. Haluaisit tutkia automaattisesti, löytyykö tuota header-tiedostoa vai ei.

Lisää seuraava määre configure.in-tiedostoon: AC_CHECK_HEADERS(errno.h). Tämän jälkeen configure tutkii, löytyykö tuota header-tiedostoa, ja jos löytyy, määrittelee HAVE_ERRNO_H-makron.

Makromäärittelyt tallennetaan AM_CONFIG_HEADER-määreen osoittamaan tiedostoon. Tuo tiedosto pitää luonnollisesti ottaa #include:lla mukaan, jotta määrittelyt näkyisivät. Tiedosto luodaan configure-skriptiä ajettaessa ja se sisältää tavallisia C-esikääntäjän makroja tyyliin #define HAVE_ERRNO_H 1.

Ohjelmakoodi voisi siis olla jotain tällaista:

#include <config.h>
#ifdef HAVE_ERRNO_H
#include <errno.h>
#else
extern int errno;
#endif
... 

Tutkimme siis, onko HAVE_ERRNO_H-makro määritelty, ja jos on, otamme vastaavan otsikkotiedoston mukaan. Jos makroa ei ole määritelty, ei otsikkotiedostoakaan löydy, ja joudumme esittelemään errno-muuttujan itse.

Elämä ilman config.h:ta

Jos et olisi määritellyt AM_CONFIG_HEADER-riviä, makromäärittelyt annettaisiin C-kääntäjälle suoraan komentorivillä. Tällöin komentoriville lisättäisiin automaattisesti tämänkaltaisia parametreja: -DHAVE_ERRNO_H=1.

Lopputulos olisi aivan sama, makrot olisivat C-esikääntäjän nähtävissä aivan kuin ne olisi määritelty #define-direktiivillä. Tämä tapa on kuitenkin ongelmallisempi: paitsi, että käännöskomennoista tulee hyvinkin mutkikkaita ja pitkiä ja siten hankalasti luettavia, voi joissain ympäristöissä komentorivin suurin sallittu pituus ylittyä. Niinpä AM_CONFIG_HEADER kannattaa aina määritellä.

Omat konfigurointivivut

Voit myös luoda omia vipuja configure-skriptiin. Vipuja on kahdenlaisia:

  1. --with-paketti ja --with-paketti, joilla valitaan, mitä muita paketteja hyödynnetään. Näin voidaan valita, käännetäänkö ohjelmasta esimerkiksi X:ää tai curses:ia tukeva versio.
  2. --enable-ominaisuus ja --disable-ominaisuus, joilla valitaan, mitä tämän ohjelman piirteitä kytketään päälle.

Lisätäänpä ohjelmaamme oma piirre "foo", joka on kytkettävissä päälle vivulla --enable-foo. Aluksi lisätään lähdekoodiin ehdollinen kääntäminen:

...
#ifdef FOO
    tee_toiminto_foo();
#endif
... 

Manuaalisesti käännettäessä foo-toiminto saataisiin mukaan lisäämällä C-kääntäjän komentoriville vipu -DFOO, joka määrittelisi makron FOO. Muokataanpa nyt tiedostoa configure.in, jotta saadaan sama toiminto hoidettua --enable-foo -vivulla:

dnl Perusasetukset:
...

dnl Vivut:

AC_ARG_ENABLE(foo,
[  --enable-foo            Turn on foo],
[case "${enableval}" in
  yes) foo=true ;;
   no) foo=false ;;
    *) AC_MSG_ERROR(bad value ${enableval} for --enable-foo) ;;
esac], [foo=false])

if test x$foo = xtrue; then
    CPPFLAGS="$CPPFLAGS -DFOO"
fi
       
...

dnl Luotavat tiedostot:
AC_OUTPUT(...) 

Nyt voitkin kokeilla komentoja ./configure --enable-foo; make clean; make ja ./configure --disable-foo; make clean; make. Kokeile myös ./configure --help.

Hiukan sielunelämää

Kannattaa vilkaista automaattisesti luotavaa configure-skriptiä. AC_ARG_ENABLE-makrohässäkkä puretaan normaalin shelliskriptin osaksi. Makron eri argumentit sijoitellaan m4-makroprosessorin ohjaamina oikeisiin paikkoihin skriptissä.

if test ... -osuus puolestaan ei ole minkään makron sisällä, joten se jää sellaisenaan skriptin osaksi. Vertailu muokkaa $CPPFLAGS-ympäristömuuttujaa.

Makefile-tiedostoja luotaessa configure lopulta korvaa Makefile.in-tiedostoissa olleet @CPPFLAGS@-merkkijonot $CPPFLAGS-muuttujan arvolla.

Mistäs ne @CPPFLAGS@-merkkijonot sitten ovat Makefile.in-tiedostoihin tulleet? Vastaus on luonnollisesti automake. Yksinkertaisen Makefile.am-tiedoston pohjalta automake luo hyvin monipuolisen Makefile.in-tiedoston, jossa on mm. lukuisia muuttujamäärittelyitä. Ne asiat, mitkä selvitetään configure-skriptiä ajettaessa, on korvattu Makefile.in-tiedostoissa @...@-merkkijonoilla.

Aivan samalla tavalla AC_PROG_CC-makro laajenee shelliskriptiksi, joka tutkii, mitä kääntäjää käytetään. Sen perusteella asetetaan mm. $CC-ympäristömuuttuja. Lopulta Makefile:a luodessaan skripti korvaa Makefile.in:ssä olevan @CC@-merkkijonon tuolla varsinaisella käännöskomennolla. @CC@-merkkijono taas on automake:n tuotoksia, yksi osa niitä lukuisia sääntöjä, joiden avulla kääntäminen hoidetaan.

Kannattaa huomata vertailun kirjoittaminen muodossa x$foo = xtrue. Tämä lienee varmimpia tapoja kirjoittaa shelliskripteissä vertailuita. Näin vältetään paitsi mahdolliset ongelmat tyhjien merkkijonojen kanssa, myös erityisesti ongelmat erikoismerkeillä alkavien merkkijonojen kanssa. Esimerkiksi -:lla alkavat merkkijonot saatetaan jossain test:n versiossa tulkita väärin.

Tutki rohkeasti eri tiedostojen sisältöä, kyllä kokonaisuus vähitellen aukeaa...

C++-projektit

automake tukee hyvin myös C++-projekteja. C++-projekteissa pitää tehdä configure.in-tiedostoon seuraavat muutokset:

  • Lisää alkupuolelle (esim. AM_CONFIG_HEADER:n jälkeen) määre AC_LANG_CPLUSPLUS.
  • Korvaa tarpeeton C-kääntäjän tunnistus C++-kääntäjän tunnistuksella eli korvaa AC_PROG_CC määreellä AC_PROG_CXX.

Kirjastot

Normaalia ohjelmaa luotaessa Makefile.am:ssä on tyypillisesti tämänkaltaiset rivit:

bin_PROGRAMS = foo
foo_SOURCES = foo.c foo.h 

bin_PROGRAMS viittaa normaalien binäärien joukkoon (esim. /usr/local/bin) asennettaviin ohjelmiin.

Edellä on käytetty myös tämänkaltaisia sääntöjä:

noinst_LIBRARIES = libfoo.a
libfoo_a_SOURCES = foo.c foo.h 

Tässä noinst_LIBRARIES tarkoittaa kirjastoja, joita ei asenneta minnekään. Kyseistä kirjastoa käytettiin vain apuna ohjelmaa luotaessa.

Jos haluamme, että kirjasto asennetaan, jotta muutkin ohjelmat voisivat tuota kirjastoa käyttää, joudutaan sääntöä hiukan muokkaamaan:

lib_LIBRARIES = libfoo.a
libfoo_a_SOURCES = foo.c foo.h 

Nyt kirjasto asennetaan muiden kirjastojen joukkoon (esim. /usr/local/lib).

Kirjastojen yhteydessä halutaan monesti myös asentaa samalla kirjaston käytössä tarvittavat otsikkotiedostot. Muutetaanpa sääntöä vielä hiukan:

lib_LIBRARIES = libfoo.a
include_HEADERS = foo.h
libfoo_a_SOURCES = foo.c 

Nyt halutut otsikkotiedostot asentuvat oikealle paikalleen (esim. /usr/local/include).

Huomaa, että otsikkotiedostoja ei tarvitse luetella kuin yhdessä paikassa. Riippuvuudethan selvitetään automaattisesti ja otsikkotiedostojen luetteleminen SOURCES-säännön kohdalla on ainoastaan sitä varten, että automake tietäisi, mitä kaikkia tiedostoja levityspakettiin kuuluukaan.

Esimerkkejä

X:ää vaativan ohjelman konfigurointi onnistuu lisäämällä tämänkaltainen pätkä configure.in:iin. Tämä esimerkki hoitaa kaiken työn, myös tarvittavien vipusten lisäämisen linkityskomentoon.

AC_PATH_XTRA

if test x$no_x = xyes; then
    AC_MSG_ERROR([X11 required.])
fi

LIBS="$LIBS $X_LIBS"
CFLAGS="$CFLAGS $X_CFLAGS"

AC_CHECK_LIB(X11, XOpenDisplay, , 
   AC_MSG_ERROR([Couldn't find X11 library.])
) 

curses:ia vaativan ohjelman voi puolestaan konfiguroida vaikkapa näin. Ensin configure.in:

AC_CHECK_LIB(ncurses, initscr, ,
    AC_CHECK_LIB(curses, initscr, ,
        AC_MSG_ERROR([Couldn't find curses library.])
    )
)

AC_CHECK_HEADERS(curses.h ncurses.h) 

Ja sitten itse ohjelmakoodi:

#if defined HAVE_NCURSES_H
# include <ncurses.h>
#elif defined HAVE_CURSES_H
# include <curses.h>
#else
# error no [n]curses.h
#endif 

Jos sama otsikkotiedosto voi esiintyä lukuisilla eri nimillä, voit käyttää tämänkaltaista sääntöä:

AC_CHECK_HEADERS(algorithm algorithm.h algo algo.h, break) 

Näin etsiminen lopetetaan heti, kun jokin otsikkotiedostoista löytyy.

Vihjeitä

config.cache kannattaa poistaa ennenkuin yrittää uudelleenkonfigurointia muuttuneessa ympäristössä.

Joskus ison remontin yhteydessä voi olla syytä manuaalisesti ajaa uudelleen edellämainitut aclocal, autoconf, autoheader ja automake -a.

Lisätietoja löytyy autoconf:n ja automake:n manuaaleista. Molempien lukeminen - tai ainakin selailu - on hyvin suositeltavaa.

Suosittelen lämpimästi, että käyt lukemassa kaikkien edellä esiteltyjen makrojen kuvaukset manuaaleista. Jos haluat lisätietoja jostain AM_-alkuisesta konfigurointimakrosta, se löytyy automake:n manuaaleista. Sen sijaan AC_-alkuisten kohdalla kannattaa useimmiten vilkaista molempia oppaita, sillä automake tarjoaa lisätoimintoja joihinkin autoconf:n makroihin. WWW-selaimen hakutoimintoa kannattaa hyödyntää ahkerasti...

Edellämainittujen makrojen lisäksi kannattanee tutustua ainakin AC_TYPE_-alkuisiin makroihin ja AC_CHECK_FUNCS-makroon. Kannattaa myös huomata, että joihinkin yksittäisiinkin tarpeisiin on olemassa valmiina käteviä makroja, esimerkiksi AC_FUNC_SELECT_ARGTYPES.


Versionhallinta

Laajoissa projekteissa versionhallintaan tulee kiinnittää erityistä huomiota. Suosittelen lämpimästi, että tutustut oikeisiin versionhallintatyökaluihin ennenkuin aloitat isoa projektia. Alkuun päässee hyvin rcs:llä. Usean ohjelmoijan projekteissa voi cvs olla hyödyllinen.

Ensimmäisessä ohjelmointiprojektissa järeät versionhallintatyökalut eivät tietenkään ole välttämättömiä. Muutamalla perustyökalulla pääsee jo pitkälle ja niistä voi olla hyötyä muuallakin kuin ohjelmoinnissa.

diff

diff-ohjelman perusajatus on etsiä kahdesta tiedostosta erot ja listata ne.

Otetaanpa esimerkki. Olet työstämässä uutta versiota helloworld-projektista. Vanha versio 1.0 on hakemistossa hw-1.0 ja uusi versio 1.1 on hakemistossa hw-1.1 (hakemiston nimeä voi vaihtaa mv-komennolla ja kokonainen hakemisto on helpointa kopioida komennolla cp -a hw-1.0 hw-1.1). Olet muokannut uudessa versiossa tiedostoa hello/hello.c hiukan:

Versio 1.0:

#include "hello.h"
#include "../helloworld.h"

#include <stdio.h>

void hello(void)
{
  printf("Hello, ");
}

Versio 1.1:

#include "hello.h"
#include "../helloworld.h"

#include <stdio.h>

void hello(void)
{
  printf("Hi, ");
}

Voit helposti tutkia diff-ohjelmalla, mitkä ovatkaan noiden kahden tiedoston erot. Käyttö on hyvin yksinkertaista: diff vanha_tiedosto uusi_tiedosto.

~$ diff hw-1.0/hello/hello.c hw-1.1/hello/hello.c
8c8
<   printf("Hello, ");
---
>   printf("Hi, ");

Tulostus on hyvin yksinkertainen: 8. rivillä on vasemmanpuoleisessa (<) tiedostossa Hello ja oikeanpuoleisessa (>) Hi.

Suosittelen kuitenkin, että heti alussa opettelet ns. unified diff -muodon käytön. Ainoa ero on -u -vivun lisääminen:

~$ diff -u hw-1.0/hello/hello.c hw-1.1/hello/hello.c
--- hw-1.0/hello/hello.c        Fri Nov 13 19:05:15 1998
+++ hw-1.1/hello/hello.c        Sat Jan  9 15:48:26 1999
@@ -5,5 +5,5 @@
 
 void hello(void)
 {
-  printf("Hello, ");
+  printf("Hi, ");
 }

Nyt tulostus on paljon monipuolisempi. Listauksessa näytetään vanhan ja uuden tiedoston nimet ja aikaleimat. Erityisen tärkeää on myös se, että näytetään muutama rivi myös muutoskohdan ympäriltä. Ne rivit, mitkä ovat tulleet uuteen versioon lisää, on merkitty +:lla ja ne, mitkä ovat poistuneet, on merkitty -:lla.

diff-komentoa voidaan käyttää myös kokonaisiin hakemistopuihin. Muokataanpa vielä uutta versiota vielä hiukan ja ajetaan sitten diff vivulla -r, jolloin käydään kokonaiset hakemistot läpi:

~$ diff -ur hw-1.0 hw-1.1
--- hw-1.0/Makefile     Fri Nov 13 20:02:23 1998
+++ hw-1.1/Makefile     Sat Jan  9 16:06:33 1999
@@ -15,7 +15,7 @@
 # Päätason Makefile:n vastuulla on ainoastaan pääohjelman luonti.
 
 helloworld.o: helloworld.c helloworld.h
-       gcc -c helloworld.c
+       gcc -O -c helloworld.c
 
 # Kaikki muu hoidetaan alihakemistoissa.
 
diff -ur hw-1.0/hello/hello.c hw-1.1/hello/hello.c
--- hw-1.0/hello/hello.c        Fri Nov 13 19:05:15 1998
+++ hw-1.1/hello/hello.c        Sat Jan  9 15:48:26 1999
@@ -5,5 +5,5 @@
 
 void hello(void)
 {
-  printf("Hello, ");
+  printf("Hi, ");
 }

Osaatkin jo lukea diff:n tulostusta, joten näet helposti, mikä toinen muutos on uudessa versiossa tehty: Makefilessä olevaan käännöskomentoon on lisätty -O-vipu parantamaan gcc:n suorittamia optimointeja.

Jos sinulla oli ongelmia esimerkkikomentojen kokeilemisessa, varmistu, ettei hakemistoissa ole lojumassa binääritiedostoja (ohjelmatiedostot ja objektitiedostot) eikä varmuuskopioita. Monet editorit luovat automaattisesti varmuuskopion muokattaessa tiedostoa, varmuuskopion nimi on yleensä alkuperäisen tiedoston nimi + tilde (~).

patch

Seuraavaksi tulee varsinainen versionhallintaan liittyvä oivallus: Ylläolevasta diff-komennon listauksesta näkee yhdellä kertaa kätevästi kaikki versioiden 1.0 ja 1.1 väliset erot.

Tallennetaanpa nuo erot tiedostoon: diff -ur hw-1.0 hw-1.1 > hw-1.0-1.1-diff

Nyt meillä on tiedostossa hw-1.0-1.1-diff ns. patch versiosta 1.0 versioon 1.1 eli selostus siitä, mitä pitää muuttaa, jotta voisimme päivittää version 1.0 versioksi 1.1. Hyvin usein puhutaan suomessakin patcheista, niillä tarkoitetaan juuri tällaisia tiedostoja, joissa on tietyllä tavalla lueteltuna ohjelman lähdekoodin eri versioiden väliset erot. Patchit ovat käytännöllisiä, koska niiden koko on yleensä huomattavasti pienempi kuin koko lähdekoodin koko.

Luonnollisesti tähän päivittämiseen on olemassa valmis työkalu. Myös työkalu on nimeltään patch. Työkalu osaa lukea esimerkiksi unified diff -muotoisia (eli diff -u -komennolla luotuja) patcheja.

patch:n käyttö

Lähdetään tilanteesta, jossa meillä on käytettävissä version 1.0 lähdekoodi sekä tuo patch-tiedosto. Olkoot tuo patch-tiedosto kotihakemistossa nimellä hw-1.0-1.1-diff.

Ennen patchin käyttöä kannattaa vilkaista patchin sisältöä ja katsoa, miten hakemistot on patchissa määritelty. Tässä tapauksessa nähdään, että patchissa on viitattu hakemistoihin hw-1.0/.... Esimerkiksi päätason Makefile on patchissa hw-1.0/Makefile.

Nämä patcheihin tallennetut polut ovat ainoa hiukan hankala asia patch:n käytössä. Meidän on valittava, kuinka monta "tasoa" haluamme noista hakemistopoluista unohtaa. Useimmiten oikea valinta on 0 tai 1. Yleensä patch alkaa valittaa puuttuvista tiedostoista, jos valitsemme väärin. Tämä "unohdettavien hakemistotasojen" määrä kerrotaan patch-komennolle -p-vivulla.

Tutkitaanpa aluksi, mitä tehdään, jos lähdekoodi on esimerkiksi hakemistossa ~/hw. Huomataan, että hakemisto on muu, kuin mitä patchiin on tallennettu. Tässä tapauksessa haluamme unohtaa yhden tason: pätkän hw-1.0/ polkujen alussa. Lisäksi meidän on siirryttävä tuohon ~/hw-hakemistoon ennen patchin asentamista. Näin esimerkiksi viittaus päätason Makefileen ei osoitakaan tiedostoon hw-1.0/Makefile vaan pelkästään tiedostoon Makefile nykyisessä hakemistossa. Koska nykyinen hakemisto on ~/hw, lopputulos on se, että patchaus kohdistuu tiedostoon ~/hw/Makefile aivan kuten pitikin. Komennot menevät siis näin:

~$ cd hw
~/hw$ patch -p1 < ../hw-1.0-1.1-diff
patching file `hw-1.0/Makefile'
patching file `hw-1.0/hello/hello.c'

Annetaan vielä selvyyden vuoksi toinen esimerkki: Jos meillä olisi versio 1.0 hakemistossa ~/hw-1.0 (eli samassa hakemistossa kuin missä se oli patchin luojallakin), voisimme suoraan kotihakemistosta asentaa patchin -p0-vivulla. Nythän meidän ei tarvitsisi unohtaa yhtään tasoa hakemistoista, vaan esimerkiksi päätason Makefile löytyisi nykyiseen hakemistoon nähden paikasta hw-1.0/Makefile aivan kuten patchissa lukeekin. Tällöin voisimme siis komentaa:

~$ patch -p0 < hw-1.0-1.1-diff
patching file `hw-1.0/Makefile'
patching file `hw-1.0/hello/hello.c'

Kerran vielä, pojat:

  • Jos patchattava on samassa alihakemistossa kuin patchin tekijälläkin, voit suoraan päähakemistossa patchata -p0-vivulla.
  • Jos patchattava on eri alihakemistossa, siirry alihakemistoon ja patchaa -p1-vivulla. Tällöin patchissa mainituista poluista unohdetaan yksi taso ja kaikki sujuu hienosti.

patch-komento on monipuolinen. Esimerkin vuoksi voisi mainita vivun -R, jonka avulla voit soveltaa patchia väärin päin: jos sinulla on uusi versio, voit patchata sen vanhaksi versioksi. Tätä vipusta voi käyttää myös silloin, jos joku on onnistunut hölmöilemään ja kirjoittanut diff-komennossa vanhan ja uuden hakemiston väärin päin.

rcs

diff- ja patch-komentojen avulla voit hoitaa yksinkertaista versionhallintaa käsityönä. Jossain vaiheessa tulee kuitenkin mieleen, että tietokone voisi hoitaa likaisen työn.

rcs on yksinkertainen versionhallintatyökalu. rcs:n avulla voit automaattisesti hallita tiedostojen eri versioita. rcs on kevyt ja helppokäyttöinen työkalu ja soveltuu jossain määrin myös pienen työryhmän käyttöön usean henkilön kesken jaetun projektin versionhallintaan.

Yleistä käsitteistä

Käydäänpä ensin läpi muutamia käsitteitä, joihin törmää kaikissa dokumenttienhallintajärjestelmissä:

  • Check in = sisäänkirjaus = tiedoston vienti järjestelmään.
  • Check out = uloskirjaus = tiedoston hakeminen järjestelmästä.

Tyypillisesti missä tahansa dokumenttienhallintajärjestelmässä peruskäyttö sujuu näin:

  1. Käyttäjä haluaa muokata tiedostoa. Tiedosto haetaan ensin järjestelmästä, tehdään siis uloskirjaus.
  2. Nyt tiedosto on käyttäjän muokattavissa. Tyypillisesti järjestelmä huolehtii siitä, että muut käyttäjät eivät pysty samanaikaisesti (ainakaan vahingossa) tekemään uloskirjausta muokkaamaan tuota uloskirjattua tiedostoa.
  3. Kun muokkaukset on tehty, käyttäjä sisäänkirjaa tiedoston takaisin järjestelmään. Tiedostosta tallentuu tällöin uusi versio (tai revisio tai millä nimellä missäkin järjestelmässä noita kutsutaan). Vanha versio jää talteen ja on tarvittaessa haettavissa.

rcs:n komennot

rcs:ssä check in hoidetaan komennolla ci ja check out vastaavasti komennolla co. Aloittelija pärjääkin jo noilla kahdella komennolla.

rcs:n käyttöönotto

rcs:n käyttö ei sinällään välttämättä vaadi mitään valmisteluita. Suositeltavaa on kuitenkin luoda hakemisto RCS kaikkiin niihin hakemistoihin, joissa pidetään rcs:llä hallittavia tiedostoja.

rcs pitää versionhallintatiedot automaattisesti RCS-hakemistossa, jos sellainen on olemassa. Näin versionhallintatiedot eivät ole lojumassa muiden tiedostojen seassa.

Luodaan siis projektissamme tarvittavat RCS-hakemistot:

~/hw$ mkdir RCS
~/hw$ mkdir hello/RCS
~/hw$ mkdir world/RCS

Tiedostojen vienti rcs:ään

ci-komennolla voi kirjata sisään paitsi olemassaolevien tiedostojen uuden version, myös kokonaan uuden tiedoston. Esimerkiksi Makefile:n saa vietyä järjestelmään yksinkertaisesti näin:

~/hw$ ci Makefile
RCS/Makefile,v <-- Makefile
enter description, terminated with single '.' or end of file:
NOTE: This is NOT the log message!
>> helloword-projektin Makefile.
>> .
initial revision: 1.1
done

ci huomasi, että tiedostoa Makefile ei ollut vielä viety järjestelmään. Niinpä luodaan ensimmäinen versio ja pyydetään kuvausta tiedostosta.

Huomaa, että Makefile häviää nykyisestä hakemistosta. Se on tallennettuna (rcs:n sisäisessä muodossa) RCS-hakemistoon.

rcs:ään viedyn tiedoston muokkaaminen

Kun haluat muokata Makefile:ä, se pitää ensin uloskirjata:

~/hw$ co -l Makefile
RCS/Makefile,v --> Makefile
revision 1.1
done

co-komennon vipu -l lukitsee tiedoston. Tällöin kukaan muu ei voi samanaikaisesti hakea tiedostoa muokattavaksi. Tiedosto on nyt muokattavissasi.

Pelkällä co-komennolla saisit tiedostosta otettua kopion, jota voit katsella, muttet muokata. Joissain dokumenttienhallintajärjestelmissä tätä toimintoa kutsutaan copy out -toiminnoksi erotuksena check out -toiminnosta.

Nyt voit tehdä tiedostoon haluamasi muutokset, vaikkapa tuon diff:n yhteydessä esimerkkinä käytetyn -O-vivun lisäämisen.

Voit halutessasi katsoa rcsdiff-komennolla, mitä muutoksia olet tehnyt. Oletuksena rcsdiff vertaa uloskirjattua versiota viimeisimpään sisäänkirjattuun versioon.

~/hw$ rcsdiff Makefile
===================================================================
RCS file: RCS/Makefile,v
retrieving revision 1.1
diff -r1.1 Makefile
18c18
<       gcc -c helloworld.c
---
>       gcc -O -c helloworld.c 

Hyvältä näyttää, juuri se muutos, mitä pitikin tehdä. Kirjataanpa siis tämä uusi versio tiedostosta sisään, jälleen yksinkertaisella ci-komennolla:

~/hw$ ci Makefile
RCS/Makefile,v <-- Makefile
new revision: 1.2; previous revision: 1.1
enter description, terminated with single '.' or end of file:
>> Muutettu optimointeja.
>> .
done

Komennolla rlog Makefile voit katsoa tiedoston versiohistoriaa. Kokeile!

Kokeile tehdä vielä yksi uusi versio. Yksinkertaisilla co -l Makefile ja ci Makefile -komennoilla tämä onnistuu.

Vanhan version hakeminen

co-komennolla voit hakea myös vanhoja versioita tiedostoista. Kokeile komentoa co -r1.1 Makefile.

Vihjeitä

Se, että rcs-järjestelmään viedyt tiedostot poistuvat alkuperäisestä hakemistostaan, on monessa tilanteessa vähemmän toivottavaa.

Kuten jo todettu, komennolla co Makefile saat tiedostosta haettua takaisin kopion. Kannattaa myös huomata, että tämänkin jälkeen co -l Makefile toimii aivan ongelmitta, vain lukua varten otettu kopio korvautuu lukitulla, muokattavissa olevalla kopiolla.

ci -u tiedosto puolestaan toimii käytännössä samoin kuin ci tiedosto; co tiedosto.

Niinpä voit korvata co -l + editointi + ci -rutiinin co -l + editointi + ci -u -rutiinilla. Kaikki toimii kuten ennenkin, mutta tiedostoista on aina kopiot alkuperäisillä paikoillaan.

rcs osaa paljon muutakin hyödyllistä. Kokeile lisätä seuraavanlainen kommenttilohko jonkin tiedoston alkuun ja tee tämän jälkeen tiedostolle muutaman kerran sisään- ja uloskirjaus. Katso tämän jälkeen tiedoston sisältöä ja ylläty iloisesti. co:n manuaalista löytyy lisää vastaavia avainsanoja.

/*
 * $Id$
 *
 * $Log$
 */ 

RCS-hakemiston sisältöä kannattaa myös vilkaista. Huomaa, että rcs tallentaa uusista versioista pelkästään muutokset. Niinpä isoistakin tiedostoista voi huoletta pitää useita versioita tallella.

Kannattaa lukea lisää manuaalisivuilta rcsintro, ci, co, rcs ja rcsdiff. Esimerkiksi ci -l on monissa tilanteissa hyvin kätevä.

Emacs tarjoaa näppäriä aputoimintoja rcs:n käyttöön. Valikoiden vilkuilu auttaa alkuun.


Loppusanat

Olisi mukava kuulla palautetta tästä tekstistä. Löysitkö virheitä? Oliko tästä hyötyä? Vai onko tästä mielestäsi enemmän haittaa kuin hyötyä aloittelijoille? Lähetä rohkeasti palautetta sähköpostitse osoitteella suo@iki.fi. Kiitos jo etukäteen palautteestasi!

Historia

  • 1998 - Aloitin kirjoittamisen.
  • 1999-08-15 - Teksti julkaistu.
  • 1999-08-16 - Korjattu monia pikkuvirheitä. Lisäilty linkkejä. Lisätty asiaa mm. editointi-, make- ja automake-osioihin.
  • 1999-08-21 - Lisätty tekstiä kopiointioikeuksista.
  • 1999-10-12 - Lisätty tekstiä automake-osioon. Teknisiä korjauksia ja muutoksia. Tyylitiedosto ja esimerkkiohjelma liitetty samaan tiedostoon.
  • 1999-10-28 - Teknisiä muutoksia.

Kopiointi

© Jukka Suomela 1998-1999.

FLUG:lla on oikeus julkaista tämä teksti muuttamattomana WWW-sivuillaan.

Tätä tekstiä saa tulostaa ja monistaa epäkaupallisiin opetustarkoituksiin Opetusministeriön alaisissa oppilaitoksissa. Teksti on kopioitava joko muuttamattomana tai muutosten tulee erottua selvästi alkuperäisestä tekstistä. Lähde ja kirjoittaja on mainittava ja tällaisesta käytöstä on ilmoitettava minulle, ilmoitus sähköpostitse riittää.

Kiitokset

Suuret kiitokset kaikille palautetta lähettäneille!

Erityisesti haluan kiittää seuraavia arvokkaasta palautteesta:

  • Antti-Juhani Kaijanaho

Liitteet

Esimerkkiohjelma

Tässä tutoriaalissa käytettävät esimerkkiohjelmat on listattu ohessa.

Koska projekti on näinkin valtava, se on jaettu hallittavuuden helpottamiseksi useisiin eri lähdetiedostoihin.

Hakemistorakenne

C

  • helloworld.c
  • helloworld.h
  • hello/
    • hello.c
    • hello.h
  • world/
    • world.c
    • world.h

C++

  • helloworld.cc
  • helloworld.h
  • hello/
    • hello.cc
    • hello.h
  • world/
    • world.cc
    • world.h
Pääohjelma

helloworld.c

#include "helloworld.h"
#include "hello/hello.h"
#include "world/world.h"

int main(int argc, char **argv)
{
  hello();
  world();
  return 0;
}
	  

helloworld.cc

#include "helloworld.h"
#include "hello/hello.h"
#include "world/world.h"

int main(int argc, char **argv)
{
  hello();
  world();
  return 0;
}
	  

helloworld.h

#ifndef _HELLOWORLD_H_
#define _HELLOWORLD_H_

#endif
	  
Hello-moduli

hello/hello.c

#include "hello.h"
#include "../helloworld.h"

#include <stdio.h>

void hello(void)
{
  printf("Hello, ");
}
	  

hello/hello.cc

#include "hello.h"
#include "../helloworld.h"

#include <iostream>

void hello()
{
  cout << "Hello, ";
}
	  

hello/hello.h

#ifndef _HELLO_HELLO_H_
#define _HELLO_HELLO_H_

void hello(void);

#endif
	  

hello/hello.h

#ifndef _HELLO_HELLO_H_
#define _HELLO_HELLO_H_

void hello();

#endif
	  
World-moduli

world/world.c

#include "world.h"
#include "../helloworld.h"

#include <stdio.h>

void world(void)
{
  printf("world!\n");
}
	

world/world.cc

#include "world.h"
#include "../helloworld.h"

#include <iostream>

void world()
{
  cout << "world!" << endl;
}
	

world/world.h

#ifndef _WORLD_WORLD_H_
#define _WORLD_WORLD_H_

void world(void);

#endif
	  

world/world.h

#ifndef _WORLD_WORLD_H_
#define _WORLD_WORLD_H_

void world();

#endif
	  


[Python Powered]
Sivun sisällöstä vastaa Ilpo Nyyssönen <webmaster miuku flug.fi>
Viimeisin päivitys: 17.03.2005, valid?