Παρασκευή, 12 Αυγούστου 2011

Δημιουργώντας ένα απλό makefile (C/C++)

Ξεκινάτε όλο ενθουσιασμό και αισιοδοξία για το μεγάλο project, το οποίο θα σας καθιερώσει στον κόσμο του προγραμματισμού! Χωρίζετε το πρόγραμμά σας σε πολλά πηγαία και header αρχεία (είτε επειδή το απαιτεί το project, είτε για δική σας διευκόλυνση, συνήθως και τα 2). Υποθέτοντας ότι δεν δουλεύετε σε κάποιο IDE προγραμματισμού, αλλά κλασικά με terminal και έναν απλό και σωστό text editor, η ώρα του compile είναι ιδιαίτερα επίπονη.

Η συγγραφή της εντολής

gcc -o <executable> <sourcefile-1> <sourcefile-2> ... <sourcefile-n>

καταντάει βαρετή κάποια στιγμή (όπου gcc μπορεί να είναι και g++).

Θα μου πείτε ΟΚ, το τερματικό μου αποθηκεύει την εντολή και πατώντας το up arrow μου τη βγάζει αυτόματα. Όταν μεταφέρουμε όμως το πρόγραμμά μας σε κάποιον άλλο υπολογιστή τί γίνεται; Πάλι το ίδιο! Πέρα από αυτό σε μερικά συστήματα (όπως τα unix/suns της σχολής) τα τερματικά τους σβήνουν από τη μνήμη τους το ιστορικό των εντολών μόλις κλείσουν. Αυτά είναι 2 απλά παραδείγματα προβλημάτων τα οποία είναι ικανά να λύσουν τα λεγόμενα makefiles.



Τα makefiles εκμεταλλεύονται το πρόγραμμα-εργαλείο make (ή και το αντίθετο), το οποίο υπάρχει προεγκατεστημένο στα περισσότερα Unix συστήματα.

Στη διανομή Ubuntu μπορείτε να το εγκαταστήσετε πληκτρολογώντας την εντολή

sudo apt-get install make

σε ένα τερματικό.

Το make αυτοματοποιεί τις διαδικασίες της μεταγλώττισης και σύνδεσης των πηγαίων αρχείων.
Στην ουσία ένα αρχείο make περιέχει εντολές, οι οποίες εκτελούνται από το σύστημα. Ένα αρχείο make δεν είναι πηγαίο αρχείο κάποιας γλώσσας προγραμματισμού. Το πρόγραμμα make αναγνωρίζει τις “εντολές” που περιέχονται στο makefile του προγράμματός σας και καλεί τον compiler που του λέει το τελευταίο μαζί με τις υπόλοιπες παραμέτρους για μεταγλώττιση και σύνδεση που του δίνει.
Το make μπορεί να μειώσει δραματικά τον χρόνο που απαιτείται για τη μεταγλώττιση (αυτό είναι κυρίως εμφανές σε πολύ μεγάλα project), καθώς είναι αρκετά έξυπνο ώστε να αναγνωρίζει ότι το πρόγραμμά σας αποτελείται από πχ. 10 πηγαία .c αρχεία, αλλά έχετε κάνει αλλαγές μόνο στο ένα από αυτά από την τελευταία μεταγλώττιση και έτσι μόνο αυτό θα μεταγλωττιστεί εκ νέου πριν τη διαδικασία της σύνδεσης. Γενικά το make έχει αρκετά περίπλοκα χαρακτηριστικά, αλλά η χρήση του για απλά πράγματα είναι αρκετά εύκολη.

Το εργαλείο make μπορεί να εκτελεστεί κατ’ ευθείαν από το φάκελο του προγράμματός μας.
Ενω βρισκόμαστε σε ένα τερματικό, στον κατάλογο με τα πηγαία μας αρχεία, πληκτρολογώντας

make

όπως φαίνεται πιο πάνω, χωρίς ορίσματα, το make θα ψάξει στον τρέχοντα φάκελο για ένα αρχείο με όνομα Makefile ή makefile για να βρει τις εντολές για την μεταγλώττιση και σύνδεση των αρχείων (build process). Αν προκύψουν σφάλματα κατά τη διαδικασία, αυτά εκτυπώνονται στην standard έξοδο σφαλμάτων (stderr ή cerr για C και C++ αντίστοιχα).

Ας δούμε τώρα τη μορφή ενός makefile.
Ένα makefile αποτελείται από μια σειρά δηλώσεων μεταβλητών και κανόνων (dependencies).
Η λέξη μεταβλητή σε αυτή την περίπτωση, δεν είναι η ίδια με αυτή που χρησιμοποιούμε στη συγγραφή των προγραμμάτων. Μια μεταβλήτη σε ένα makefile, είναι απλά ένα όνομα, το οποίο δηλώνεται για να αντιπροσωπεύσει μια συμβολοσειρά (μια λέξη). Μπορείτε να το φανταστείτε ότι δουλεύει περίπου σαν τις μακροεντολές στον προεπεξεργαστή της C.

Για παράδειγμα, στη μακροεντολή

#define NAME takis

η λέξη NAME αντικαθίσταται με τη λέξη takis όπου αυτή βρεθεί μέσα στο πηγαίο αρχείο. Κάτι τέτοιο συμβαίνει και στο makefile.
Οι μεταβλητές χρησιμοποιούνται συνήθως για να αντιπροσωπεύσουν μια λίστα φακέλων για αναζήτηση, εντολές για τον μεταγλωττιστή και ονόματα προγραμμάτων. Τις μεταβλητές τις δηλώνουμε απλά με ένα “=”.

Για παράδειγμα η γραμμή

CC = gcc

δηλώνει μια μεταβλητή με όνομα CC και της αποδίδει την τιμή ‘gcc’. Τα ονόματα των μεταβλητών είναι case sensitive (δηλαδή άλλο το CC και άλλο το cc) και παραδοσιακά τα ονόματα των μεταβλητών στα makefiles αποτελούνται από κεφαλαίους χαρακτήρες.

Αν και πρακτικά για ονόματα μεταβλητών μπορείτε να βάλετε ότι θέλετε, υπάρχουν μερικά standard ονόματα τα οποία κάνουν την συγγραφή και ανάγνωση ενός makefile πολύ πιο εύκολη. Τα πιο σημαντικά είναι:

CC
Το όνομα του μεταγλωττιστή της C, gcc (αντίστοιχα για τον μεταγλωττιστή της C++, g++, χρησιμοποιείται συνήθως το όνομα μεταβλητής CXX).

CFLAGS
Μια σειρά εντολών που περνάμε στον μεταγλωττιστή της C, για όλα τα πηγαία μας αρχεία. Χρησιμοποιείται κυρίως για τον ορισμό ενός include path για να κάνουμε include αρχεία διαφορετικά από τα standard που παρέχει η C (όπως το stdio.h) (επιλογή -I) ή για τη μεταγλώττιση με πληροφορίες αποσφαλμάτωσης (debugging) (επιλογή -g).

LDFLAGS
Μια σειρά εντολών για τον συνδέτη. Χρησιμοποιείται κυρίως για τη συμπερίληψη αρχείων βιβλιοθήκης κάποιας συγκεκριμένης εφαρμογής (επιλογή -l) (πεζό λατινικό L) και τον ορισμό του μονοπατιού αναζήτησης της βιβλιοθήκης (επιλογή -L).

Για να αναφερθούμε στην τιμή μιας μεταβλητής,βάζουμε το σύμβολο του δολλαρίου ($) ακολουθούμενο από το όνομα της μεταβλητής σε παρενθέσεις. Για παράδειγμα:

1. CFLAGS = -g -I/usr/class/cs107/include
2. $(CC) $(CFLAGS) -c binky.c

Η πρώτη γραμμη θέτει την τιμή της μεταβλητής CFLAGS ώστε εκείνη να ενεργοποιήσει τις πληροφορίες αποσφαλμάτωσης και να προσθέσει το φάκελο /usr/class/cs107/include στο μονοπάτι αναζήτησης για τα αρχεία include.
Η δεύτερη γραμμή χρησιμοποιεί τη μεταβλητή CC για να ανακτήσει το όνομα του μεταγλωττιστή και τη μεταβλητή CFLAGS για να ανακτήσει τις επιλογές για τον μεταγλωττιστή.

Σημειώνεται ότι μια μεταβλητή στην οποία δεν έχει ανατεθεί κάποια τιμή έχει ως τιμή την κενή συμβολοσειρά.

Το δεύτερο πιο σημαντικό χαρακτηριστικό ενός makefile είναι οι κανόνες εξάρτησης σε ελεύθερα ελληνικά, η αλλιώς dependency/build rules. Ένας κανόνας είναι στην ουσία μια εντολή για το χτίσιμο του προγράμματός μας, την παραγωγή του εκτελέσιμου. Μπορεί να υπάρχουν πολλοί κανόνες, η σειρά δεν έχει καμία σημασία, εκτός απο το ότι αυτός που γράφεται πρώτος θεωρείται ο προεπιλεγμένος κανόνας, αυτός που θα εκτελεστεί όταν το make κληθεί χωρίς ορίσματα (το πιο σύνηθες).

Ένας κανόνας αποτελείται γενικά από δυο γραμμές: μια γραμμή ‘εξάρτησης’ ακολουθούμενη από μια γραμμή ‘εντολής’. Σε αυτό το σημείο ίσως πρέπει να εξηγήσω λίγο τί σημαίνει ‘εξάρτηση’. Έστω ότι το πρόγραμμά μας αποτελείται από τα αρχεία:
  • main.c
  • functions.c
  • functions.h
  • includes.h
και τα δύο πηγαία αρχεία (αυτά με την κατάληξη .c) κάνουν include και τα δυο αρχεία επικεφαλίδας (αυτά με την κατάληξη .h). Αυτό σημαίνει πως και τα δυο πηγαία αρχεία ‘εξαρτώνται’ άμεσα και από τα δυο αρχεία επικεφαλίδας. Δηλαδή, αν αλλάξει έστω και ένα από τα αρχεία επικεφαλίδας, τότε το αντίστοιχο πηγαίο πρέπει να μεταγλωττιστεί εκ νέου.

Ένα παράδειγμα κανόνα είναι το ακόλουθο:

1. binky.o : binky.c binky.h akbar.h
2. TAB$(CC) $(CFLAGS) -c binky.c

Αυτός ο κανόνας λέει πως το αντικειμενικό αρχείο binky.o πρέπει να ξαναμεταγλωττιστεί όποτε ένα τουλάχιστον από τα αρχεία binky.c, binky.h, akbar.h μεταβληθεί. Λέμε ότι το αρχείο binky.o ‘εξαρτάται’ από τα 3 αυτά αρχεία. Στην ουσία, ένα αντικειμενικό αρχείο ‘εξαρτάται’ από το αντίστοιχο πηγαίο του και οποιοδήποτε άλλο αρχείο που το τελευταίο κάνει #include. Είναι ευθύνη του προγραμματιστή να εκφράσει τις ‘εξαρτήσεις’ μεταξύ των πηγαίων αρχείων μέσα στο makefile. Στο πιο πάνω παράδειγμα, το πηγαίο binky.c προφανώς κάνει #include τα αρχεία binky.h και akbar.h. Αν κάποιο από αυτά μεταβληθεί, τότε το αρχείο binky.c πρέπει να μεταγλωττιστεί ξανά.

Η γραμμή ‘εντολής’ περιλαμβάνει τις εντολές που μεταγλωττίζουν το αρχείο binky.c ώστε να παραχθεί το αρχείο binky.o — επικαλούμενο το μετταγλωττιστή της C με τις οποιεσδήποτε επιλογές έχουν τεθεί προηγουμένως (καταλαβαίνετε ότι μπορεί να υπάρχουν πολλές γραμμές ‘εντολής’). Στην ουσία, η γραμμή ‘εξάρτησης’ λειτουργεί σαν ένας “διακόπτης”, ο οποίος λέει πότε πρέπει να γίνει κάτι (η μεταγλώττιση στην συγκεκριμένη περίπτωση), ενώ η γραμμή ‘εντολής’ λέει πώς θα γίνει αυτό το κάτι.

Οι γραμμές ‘εντολής’ πρέπει να στοιχιστούν με το χαρακτήρα TAB (η χρήση spaces δεν θα δουλέψει, ακόμα και αν τα spaces θα φαίνονται σωστά στον editor σας). Ο λόγος για τον οποίο συμβαίνει αυτό είναι μια μεγάλη ιστορία που έχει να κάνει με τo ιστορικό του TAB. Ας μην το αναλύσουμε καλύτερα!

Πάντως λόγω του παραπάνω φαινομένου, σιγουρευτείτε ότι δεν χρησιμοποιείτε κάποιον text editor που να αντικαθιστά τα tabs με spaces. Αυτό μπορεί να συμβεί και κατά την αντιγραφή και επικόλληση από κάποια terminals. Για να σιγουρευτείτε για την ύπαρξη TAB, πηγαίνετε το δείκτη στην αρχή της γραμμής και στη συνέχεια μετακινήστε τον κατά ένα χαρακτήρα δεξιά. Αν ο δείκτης μετακινηθεί κατά 4 ή 8 θέσεις, τότε έχετε TAB και είστε OK.

Σε standard περιπτώσεις, η γραμμή ‘εντολής’ μπορεί να παραληφθεί, αφού το make θα χρησιμοποιήσει τον προεπιλεγμένο κανόνα παραγωγής του εκτελέσιμου από τα πηγαία αρχεία, βασιζόμενο στην κατάληξη των αρχείων, .c για τα πηγαία C, .f για τα πηγαία Fortran κοκ. Ο προεπιλεγμένος κανόνας για τα αρχεία C μοιάζει κάπως έτσι:

$(CC) $(CFLAGS) -c source-file.c

ενώ για τα αρχεία C++ κάπως έτσι:

$(CXX) $(CXXFLAGS) -c source-file.cpp

Συνηθίζεται αρκετά το να βασιζόμαστε στον παραπάνω προεπιλεγμένο κανόνα για την παραγωγή του εκτελέσιμου. Οι περισσότερες προσαρμογές μπορούν να γίνουν αλλάζοντας την μεταβλητή CFLAGS. Ακολουθεί ένα πολύ απλό αλλά τυπικό makefile. Μεταγλωττίζει το πηγαίο πρόγραμμα που περιέχεται στα αρχεία main.c, binky.c, binky.h, akbar.c, akbar.h, και defs.h. Αυτά τα αρχεία θα παράξουν τα αντικειμενικά main.o, binky.o και akbar.o. Αυτά τα 3 αντικειμενικά αρχεία θα συνδεθούν για να παράξουν το εκτελέσιμο με όνομα ‘program’. Οι κενές γραμμές σε ένα makefile αγνοούνται και ο χαρακτήρας # σχολιάζει μια γραμμή.

Ακολουθεί ένα ενδεικτικό makefile (προσοχή στα TABS!).


## A simple makefile

CC = gcc
CFLAGS = -g -I/usr/class/cs107/include
LDFLAGS = -L/usr/class/cs107/lib -lgraph
PROG = program
HDRS = binky.h akbar.h defs.h
SRCS = main.c binky.c akbar.c

## This incantation says that the object files
## have the same name as the .c files, but with .o

OBJS = $(SRCS:.c=.o)

## This is the first rule (the default)
## Build the program from the three .o's

$(PROG) : $(OBJS)
TAB$(CC) $(LDFLAGS) $(OBJS) -o $(PROG)

## Rules for the source files -- these do not have
## second build rule lines, so they will use the
## default build rule to compile X.c to make X.o

main.o : main.c binky.h akbar.h defs.h
binky.o : binky.c binky.h
akbar.o : akbar.c akbar.h defs.h

## Remove all the compilation and debugging files

clean :
TABrm -f core $(PROG) $(OBJS)

## Build tags for these sources

TAGS : $(SRCS) $(HDRS)
TABetags -t $(SRCS) $(HDRS)

Τα περισσότερα από τα παραπάνω πάρθηκαν από το εγχειρίδιο “Unix Programming Tools” των Parlante, Zelenski και πολλών άλλων από το πανεπιστήμιο του Stanford.

Δεν υπάρχουν σχόλια:

Δημοσίευση σχολίου