Εισαγωγή

Το εργαλείο make χρησιμοποιείται για τη διαχείριση μεγάλων εργασιών με πολλά αρχεία. Το make ελέγχει ποια αρχεία έχουν αλλάξει και τα μεταγλωττίζει αυτόματα όταν χρειάζεται.

Makefiles

Οι πληροφορίες για τον τρόπο που το make μεταγλωττίζει τα διάφορα αρχεία περιέχονται σε ένα αρχείο που ονομάζεται (συνήθως) Makefile. Το Makefile μπορεί να περιέχει κανόνες, μεταβλητές, σχόλια, γενικούς κανόνες και οδηγίες (στο παρόν κείμενο δεν θα ασχοληθούμε με τις οδηγίες ενώ με τους γενικούς κανόνες θα ασχοληθούμε μόνο επιφανειακά). Από τα παραπάνω μόνο οι κανόνες είναι απολύτως απαραίτητοι.

Κανόνες

Οι κανόνες είναι ο κορμός του Makefile. Η μορφή κάθε κανόνα είναι η παρακάτω:

<target> : <dependencies>
	<action1>
	<action2>...

Ο κανόνας ορίζει τρία πράγματα. Το όνομα του αρχείου που πρέπει να ανανεωθεί (στόχος - target), πότε πρέπει να ανανεωθεί (εξαρτήσεις - dependencies) και πώς θα ανανεωθεί (ενέργειες - actions). Οι εξαρτήσεις (dependencies) είναι ονόματα αρχείων από τα οποία εξαρτάται ο στόχος (target) και μπορεί να είναι και ονόματα στόχων από άλλους κανόνες. Το make θα εκτελέσει τις ενέργειες (actions) με τη σειρά που ορίζονται όταν κάποιο από τα αρχεία που αναφέρονται στις εξαρτήσεις έχει νεότερη ημερομηνία από το στόχο. Οι ενέργειες (actions) μπορεί να είναι οποιεσδήποτε εντολές και πρέπει υποχρεωτικά να ξεκινούν με ΤΑΒ. Η παράλειψη του αρχικού TAB σε κάθε γραμμή ενέργειας είναι και το συνηθέστερο λάθος στην κατασκευή του Makefile.

Έστω για παράδειγμα ότι σε μια εργασία του μαθήματος έχουμε τα αρχεία list.C και list.h . Ένας κανόνας για να δημιουργούμε το list.o είναι:

list.o : list.C list.h
	g++ -c list.C

Αν το εκτελέσιμο της εργασίας ονομάζεται test_data_structures και χρειάζεται ένα επιπλέον αρχείο main.C, τότε για να το δημιουργήσουμε αρκεί να προσθέσουμε στην αρχή του αρχείου Makefile τους κανόνες:

test_data_structures : list.o main.o
	g++ -o test_data_structures list.o main.o

main.o : main.C
	g++ -c main.C

Έτσι όταν δίνουμε την εντολή make, θα μεταγλωττίζεται μόνο όποιο αρχείο είναι νεότερο από το εκτελέσιμο.

Σχόλια

Τα σχόλια στο Makefile εισάγονται με τη χρήση του χαρακτήρα #: ο χαρακτήρας # και όλοι οι χαρακτήρες μέχρι το τέλος της γραμμής αγνοούνται. Τα σχόλια μπορούν να εμφανίζονται οπουδήποτε στο Makefile. Παράδειγμα:

LEDA_LIBS = -lL -lm             # Link with the LEDA library of basic data types

Ειδικοί κανόνες και ορίσματα γραμμής εντολών

Το όνομα ενός στόχου (target) δεν είναι ανάγκη να είναι κάποιο αρχείο. Μπορούμε να ορίσουμε έναν ειδικό κανόνα που όταν κληθεί θα κάνει μια εργασία διαφορετική από μεταγλώττιση. Ο συνηθέστερος τέτοιος κανόνας είναι αυτός που ορίζουμε για να διαγραφούν ορισμένα αρχεία όταν δεν χρειάζονται:

clean : 
	rm -f *.o test_data_structures core 

Ένας άλλος τέτοιος κανόνας μπορεί να δημιουργεί το κατάλληλο αρχείο για παραδοση μιας άσκησης:

submit : 
	mkdir exercise1
	cp *.C *.h report exercise1
	tar czvf exercise1.tar.gz exercise1
	rm -rf exercise1

Τέτοιους κανόνες τους καλούμε δίνοντας το όνομα του στόχου σαν όρισμα στη γραμμή εντολών αμέσως μετά το make, π.χ.:

make clean

Το make δέχεται διάφορα ορίσματα στη γραμμή εντολών. Το πιο συνηθισμένο είναι να δοθεί σαν όρισμα το όνομα ενός στόχου (όπως το make clean που έχουμε δει παραπάνω). Αν δεν υπάρχει όνομα στόχου στη γραμμή εντολών το make επεξεργάζεται τον πρώτο στόχο του Makefile. Άλλο ένα σύνηθες όρισμα είναι το -f <Makefile> που λέει στο make να χρησιμοποιήσει κάποιο άλλο αρχείο σαν Makefile. Παράδειγμα:

make -f Makefile2

Μεταβλητές

Στην αρχή κάθε Makefile μπορούμε να ορίσουμε μεταβλητές για να αποφεύγουμε περιττές επαναλήψεις και να το κάνουμε πιο ευανάγνωστο. Οι ορισμοί των μεταβλητών είναι της μορφής:

<όνομα_μεταβλητής> = <κείμενο>

Αν υπάρχουν κενά στην αρχή του κειμένου αγνοούνται. Οι μεταβλητές μπορούν να χρησιμοποιηθούν οπουδήποτε (ακόμη και για τον ορισμό άλλων μεταβλητών). Στην τιμή μιας μεταβλητής αναφερόμαστε με τη σύνταξη:

$(<όνομα>) ή ${<όνομα>}

Έστω για παράδειγμα ότι έχουμε την εργασία που περιγράψαμε παραπάνω και έστω ότι θέλουμε να καλέσουμε τον μεταγλωττιστή με την επιλογή -g (για debugging). Μπορούμε στην αρχή του Makefile να ορίσουμε:

CXXFLAGS = -g

και να τροποποιήσουμε τους κανόνες ως εξής:

test_data_structures : list.o main.o
	g++ -o $(CXXFLAGS) test_data_structures list.o main.o

list.o : list.C list.h
	g++ -c ${CXXFLAGS} list.C

main.o : main.C
	g++ -c $(CXXFLAGS) main.C

Έτσι αν θέλουμε να αλλάξουμε τα ορίσματα του compiler π.χ. από -g σε -O2 (για βελτιστοποίηση), αρκεί να αλλάξουμε μόνο τον ορισμό της CXXFLAGS σε:

CXXFLAGS = -O2

Η ίδια τακτική μπορεί να χρησιμοποιηθεί και για άλλα ορίσματα, π.χ. βιβλιοθήκες. Για παράδειγμα αν για την παραπάνω εργασία πρέπει να χρησιμοποιήσουμε και τη βιβλιοθήκη βασικών τύπων της LEDA θα προσθέσουμε στο Makefile:

LEDA_LIBS = -lL -lm

και θα τροποποιήσουμε τον κανόνα για το test_data_structures ως εξής:

test_data_structures : list.o main.o
	g++ -o $(CFLAGS) test_data_structures list.o main.o $(LEDA_LIBS) 

Ενσωματωμένοι κανόνες

Το make έχει ενσωματωμένους κανόνες για τις πιο συνηθισμένες λειτουργίες μεταγλώττισης (π.χ. παραγωγή object από αρχείο κώδικα C++). Σε αυτές τις περιπτώσεις δεν χρειάζεται να ορίσουμε εμείς τους κανόνες μεταγλώττισης. Αυτοί οι κανόνες χρησιμοποιούν ειδικές μεταβλητές τις οποίες μπορούμε να ορίσουμε ώστε να επηρεάσουμε τη συμπεριφορά του κανόνα.

Σχετικά με τα αρχεία C++ το make παράγει το αρχείο .o από το αντίστοιχο αρχείο .C με χρήση του κανόνα

$(CXX) -c $(CPPFLAGS) $(CXXFLAGS)

όπου η μεταβλητή CXX έχει προεπιλεγμένη τιμή g++, ενώ οι μεταβλητές CPPFLAGS και CXXFLAGS θεωρούνται ότι έχουν κενή προεπιλεγμένη τιμή.

Για να χρησιμοποιήσουμε έναν τέτοιο κανόνα πρέπει είτε να ορίσουμε έναν κανόνα χωρίς ενέργειες (actions) ή να μην ορίσουμε κανένα κανόνα για κάποιο αρχείο. Για παράδειγμα για την εργασία που έχουμε προαναφέρει ο κανόνας που δημιουργεί το main.o μπορεί να παραλειφθεί. Όταν το make χρειαστεί το main.o θα ελέγξει αν υπάρχει το main.C και αν ναι, θα χρησιμοποιήσει τον αντίστοιχο ενσωματωμένο κανόνα για C++. Οταν θέλουμε να ορίσουμε επιπλέον εξαρτήσεις (dependecies), τότε ορίζουμε ένα σχετικό κανόνα αλλά δεν ορίζουμε ενέργειες (actions). Στην παραπάνω εργασία ο κανόνας για το list.o μπορεί να τροποποιηθεί ως εξής:

list.o : list.h

Η συμπεριφορά των ενσωματωμένων κανόνων μπορεί να τροποποιηθεί με το να δώσουμε διαφορετικές τιμές στις μεταβλητές που χρησιμοποιούν. Έτσι αν θέλουμε, μπορούμε να προσθέσουμε στo Makefile:

CXXFLAGS = -g 

για να τροποποιήσουμε τη συμπεριφορά του ενσωματωμένου κανόνα. (Φυσικά δεν είναι αναγκαίο να χρησιμοποιήσουμε τους ενσωματωμένους κανόνες). Στο τέλος του κειμένου υπάρχει ένα ολοκληρωμένο παράδειγμα.

Δημιουργία βιβλιοθηκών

Το make μπορεί να χρησιμοποιηθεί και για την αυτοματοποίηση της δημιουργίας βιβλιοθηκών. Μια βιβλιοθήκη δημιουργείται από αρχεία object (.o) με τη χρήση των εντολών ar και ranlib. Μπορούμε επομένως να ορίσουμε έναν κανόνα που θα εκτελεί τις παραπάνω εντολές. Για παράδειγμα έστω ότι στην εργασία που έχουμε αναφέρει υπάρχει και ένα τρίτο αρχείο, το stack.C, και θέλουμε πέρα από το εκτελέσιμο της εργασίας να δημιουργήσουμε μια βιβλιοθήκη my_lib.a για μελλοντική χρήση. Ορίζουμε τότε στο Makefile τον κανόνα:

my_lib : list.o stack.o
	ar crv my_lib.a list.o stack.o
	ranlib my_lib.a

(Τα ορίσματα της ar σημαίνουν τα εξής : c: να δημιουργήσει - create - το αρχείο my_lib.a, να προσθέσει - add with replacement - τα αρχεία list.o και stack.o και να αναφέρει αναλυτικά - verbose - τις ενέργειες καθώς εκτελείται). Για το stack.o δε χρειάζεται να ορίσουμε τίποτα αφού θα δημιουργηθεί από το stack.C με τη χρήση ενσωματωμένου κανόνα. Για να δημιουργήσουμε τη βιβλιοθήκη πρέπει να δώσουμε στη γραμμή εντολών το όνομα του σχετικού στόχου σαν όρισμα, δηλαδη:

make my_lib

Ακολουθεί ένα ολοκληρωμένο παράδειγμα.

Παράδειγμα

Έστω ότι έχουμε την παραπάνω εργασία που χρησιμοποιεί και τη βιβλιοθήκη LEDA που υποθέτουμε ότι βρίσκεται στο /usr/local/LEDA-6.2 (το path μπορεί στην πραγματικότητα να είναι λίγο διαφορετικό). Ένα Makefile για τη συγκεκριμένη εργασία είναι το παρακάτω:

CXX = g++   # use the g++ compiler

OBJS = main.o list.o stack.o       # project files
LIB_OBJS = list.o stack.o          # my_lib files

LEDA_PATH = /usr/local/LEDA-6.2    # directory where LEDA libraries are stored
LEDA_LIBS = -lL -lm             # Link with the LEDA library of basic data types

CXXFLAGS =  -g                  # for debugging; can be changed or omitted later
CPPFLAGS = -I$(LEDA_PATH)

all : test_data_structures my_lib     # rule to create everything

test_data_structures : $(OBJS)        # rule to create executable
	$(CXX) -o test_data_structures $(OBJS) -L$(LEDA_PATH) $(LEDA_LIBS) 

my_lib : $(LIB_OBJS)           # rule to create my_lib.a from list.o and stack.o
	ar crv my_lib.a list.o stack.o
	ranlib my_lib.a

# we write this rule only to state the dependency of list.o from list.h
list.o : list.h             

#rules for main.o and stack.o are not necessary

clean : 			# rule to delete unnecessary files
	rm -f *.o test_data_structures core