Automatyzacja zarządzania złożonymi programami Zbigniew Jurkiewicz 13 lutego 2012 1 MAKE Na przykładzie narzędzia MAKE przedstawimy automatyzację zarządzania złożonymi programami. Cel: automatyzacja selektywnej kompilacji, linkowania itp. zmodyfikowanych modułów (fragmentów) złożonych programów komputerowych; wyeliminowanie doraźnie pisanych skryptów powłoki (plików wsadowych). Zasady są następujące: Programista określa reguły opisujące sposób budowy programu z plików źródłowych. Reguły te składają się na specjalny plik, tzw. makefile (o takiej właśnie domyślnej nazwie). Program MAKE na podstawie reguł steruje pracą kompilatorów, linkerów itp. narzędzi programisty. Może być też używany do sterowania pracą innych programów, np. programu TEX do składania tekstów. Wywołanie: $ make powoduje zbudowanie domyślnego obiektu (zob. poniżej), natomiast $ make prog1 powoduje zbudowanie obiektu prog1. Oba wywołania korzystają z domyślnego pliku reguł o nazwie makefile. Można to zmienić korzystając z opcji -f: 1
$ make -f moje.reguly $ make -f moje.reguly prog1 Inna użyteczna opcja to: -n Wyświetla polecenia do wykonania, ale ich nie wykonuje. Przydatna podczas tworzenia pliku z regułami do testowania go. Składowe pliku makefile: komentarze Rozpoczynają się znakiem #. makrodefinicje Wprowadzają symbole, które mogą być używane w regułach. reguły Określają zależności między produktami i surowcami oraz sposób tworzenia produktów z surowców. Dzielą się na jawne i domyślne. 1.1 Reguły Reguły jawne określają zależności pomiędzy obiektem docelowym (produktem) a obiektami, wymaganymi do jego skonstruowania (surowcami): Plik skompilowany w języku C zależy od odpowiadającego mu pliku źródłowego i być może jakichś plików nagłówkowych, np. prog1.o: prog1.c stale.h Reguła powyższa określa, że do zbudowania pliku prog1.o wymagane jest istnienie plików prog1.c oraz stale.h. Po uruchomieniu programu MAKE porównuje się daty ostatnich modyfikacji dla plików docelowych i źródłowych. Jeśli pliki źródłowe były modyfikowane później, plik docelowy jest regenerowany. Podobnie postępuje się, jeśli plik docelowy wogóle nie istnieje. Poznana reguła opisywała tylko zależności między plikami, nie określała natomiast sposobu regeneracji pliku docelowego. Przebieg regeneracji może być specyficzny dla danej reguły bądź też wspólny dla całej klasy reguł. W pierwszym przypadku przebieg regeneracji opisywany jest w kolejnych wierszach reguły jawnej. Wiersze te powinny rozpoczynać się znakiem tabulacji. W takim przypadku powyższa reguła przybrałaby postać 2
prog1.o: prog1.c stale.h cc -c prog1.c Polecenia występujące w regułach mogą być poprzedzane znakami specjalnymi. Poprzedzenie polecenia znakiem specjalnym @ zapobiega wyświetleniu polecenia podczas wykonania clean: @echo Usuwamy zbędne już pliki rm zbedny.o rm niepotrzebny.o @echo nawet jeśli ich nie ma. Polecenia w treści reguły wykonuje się w nowym shellu normalnie jest to /bin/sh, ale można na początku pliku określić inny, nadając wartość zmiennej SHELL. 1.2 Makra Makrodefinicje służą do zwięzłego nazywania ciągów symboli. Zdefiniowane nazwy, tzw. makra, mogą następnie być wystąpić w regułach i są wtedy zastępowane odpowiadającymi im ciągami symboli. Możemy np. zdefiniować # # Definicja makra INCLUDE # INCLUDE=stale.h prototypy.h Makra wołane w regułach poprzedza się znakiem $ (dolar) i otacza nawiasami (nie dotyczy to jednoznakowych makr systemowych). Tak więc zdefiniowane przez nas makro może być następnie użyte w regule w następujący sposób prog1.o: prog1.c $(INCLUDE) Reguła ta po rozwinięciu makra przyjmie postać prog1.: prog1.c stale.h prototypy.h Typowe makra: AR AS CC CFLAGS CXX CXXFLAGS LDFLAGS Program do budowy bibliotek (ar) Asembler (as) Kompilator języka C (cc) Flagi dla kompilatora C Kompilator języka C++ (g++) Flagi dla kompilatora C++ Flagi dla linkera ld Dodatkowo w makrowołaniu można użyć podstawienia. Podstawienie zastępuje podany po znaku : ciąg znaków innym, podanym po znaku =, ale jedynie na końcu symboli SRCS=glowny.c proc1.c proc2.c OBJS=$(SRCS:.c=.o) 3
1.3 Phony targets Obiekt docelowy w regule nie musi być plikiem. Takie obiekty określa się jako phony. Ponieważ w bieżącym katalogu może przypadkowo znaleźć się plik o takiej samej nazwie, obiekty takie można (i warto) deklarować.phony: clean Typowe obiekty, którym nie odpowiadają pliki, to: all Jego obiektami źródłowymi są wszystkie zwykłe obiekty docelowe. Akcje są na ogół zbędne. check Wykonuje testy akceptacyjne programu budowanego tym makefilem przed jego zainstalowaniem w docelowym miejscu. test Wykonuje testy akceptacyjne programu budowanego tym makefilem po jego zainstalowaniu w docelowym miejscu. clean Usuwa z bieżącego katalogu wszystkie robocze pliki pośrednie tworzone podczas budowy tego programu. Nie usuwa plików konfiguracyjnych. distclean Działa jak clean, ale dodatkowo usuwa pliki konfiguracyjne. dist Tworzy plik dystrybucyjny programu, np. typu tar albo skompresowany. Zaleca się, aby nazwy wszystkich plików rozpoczynały się podkatalogiem o nazwie takiej jak dystrybuowany pakiet. Warto, aby nazwa obejmowała numer wersji, np. plik dystrybucyjny tar kompilatora GCC w wersji 1.40 rozpakowuje się do podkatalogu gcc-1.40. Najprościej wykonać to tworząc odpowiednio nazwany podkatalog, a następnie kopiując do niego właściwe pliki i budując z niego archiwum. install Umieszcza binarny plik wykonywalny w systemowym katalogu z takimi plikami (np. /usr/bin oraz pliki pomocnicze w katalogach, w których powinny być przechowywane (np. /usr/lib). W przypadku braku odpowiednich katalogów tworzy je. uninstall Usuwa wszystkie pliki instalowane w tym pakiecie. print Drukuje wszystkie zmienione pliki źródłowe. Program MAKE wywołany bez argumentu próbuje zbudować obiekt domyślny docelowy obiekt pierwszej napotkanej reguły jawnej. Obiekt docelowe pozostałych reguł są budowane jedynie w miarę potrzeby. Często jako pierwszą regułę jawną podaje się regułę all: prog1 prog2 prog3 aby domyślnie zbudować wszystkie wymienione obiekty. Można też oczywiście wywołać MAKE, podając mu jawnie (jako argument) obiekt do zbudowania. 4
1.4 Błędy niefatalne Normalnie program MAKE przerywa pracę po napotkaniu pierwszego polecenia, którego wykonanie zakończy się błędem. Poprzedzenie polecenia przedrostkiem - zapobiega sprawdzaniu jego poprawności. Inaczej mówiąc, niezależnie od wyniku wykonania polecenia MAKE kontynuuje pracę prog1.o: prog1.c stale.h @echo Teraz będziemy kompilować -cc -c prog1.c 1.5 Przykład objects=main.o kbd.o command.o display.o \ insert.o search.o files.o utils.o edit: $(objects) cc -o edit $(objects) main.o: defs.h kbd.o: defs.h command.h command.o: defs.h command.h display.o: defs.h buffer.h insert.o: defs.h buffer.h search.o: defs.h buffer.h files.o: defs.h buffer.h command.h utils.o: defs.h clean : -rm edit $(objects) 1.6 Reguły domyślne Często sposób regeneracji objektu nie zależy od konkretnych plików, lecz jest wspólny dla wszystkich plików tego samego typu. Można wtedy skorzystać z reguły domyślnej. Opisuje ona, jak plik o pewnym rozszerzeniu otrzymuje się z pliku o tej samej nazwie, różniącego się tylko rozszerzeniem. Oba rozszerzenia podaje się wtedy przed dwukropkiem, np..c.o: cc -c $< 1.7 Makra predefiniowane W regule powyższej wystąpiło systemowe makro $<, zastępowane podczas użycia reguły (pełną) nazwą pliku źródłowego. 5
Istnieją również inne predefiniowane makra systemowe, których nazwami są pojedyncze znaki przestankowe. Ich znaczenie podaje poniższa tabelka: Makro Znaczenie $* Bazowa nazwa pliku docelowego (bez rozszerzenia) $< Pełna nazwa pliku $: Katalog zawierający plik (odcięta ostatnia część pełnej nazwy, tzn. nazwa właściwa i rozszerzenie) $. Właściwa nazwa pliku wraz z rozszerzeniem $& Sama nazwa pliku (bez katalogu/ścieżki ani rozszerzenia) Jeśli pełną nazwą pliku jest /home/pjotr/projekt/prog1.c, to poszczególne makra oznaczać będą: $* /home/pjotr/projekt/prog1 $< /home/pjotr/projekt/prog1.c $: /home/pjotr/projekt/ $. prog1.c $& prog1 1.8 Zalecenia Gdy nasz program ma działać na wielu platformach (np. Linux i MS Windows), warto wydzielić w osobne pliki fragmenty zależne od środowiska. Dla prostego programu w pliku głównym pozostałoby tylko # Główny plik makefile all = program$(exe) include environ all : program$(obj) $(CC) $@ program$(obj) $(LIBS) natomiast zależny od platformy plik environ dla UNIXA miałby postać # Makra make specyficzne dla UNIXA OBJ=.o EXE= CC=cc -g -o LIBS=-lX11 -lm a dla MS Windows 6
# Makra make specyficzne dla MS Windows OBJ=.obj EXE=.exe CC=gcc -g -o LIBS=-lm 1.9 Przenośność Warto poprzedzać wszystkie odwołania do plików źródłowych prefiksem $(SRCDIR)/, na przykład ${SRCDIR}/parser.tab.c: ${SRCDIR}/parser.y ${YACC} -d ${SRCDIR}/parser.y mv y.tab.c ${SRCDIR}/parser.tab.c mv y.tab.h ${SRCDIR}/parser.tab.h Dzięki temu można wywołać make z katalogu innego niż żródłowy. Zwróćmy uwagę na plik parser.tab.c plik w języku C generowany przez $(YACC). Po utworzeniu przenosimy go do katalogu źródłowego. Podobnie dzieje się z plikiem y.tab.h. 1.10 Narzędzia Program makedepend w Unixie (pół)automatycznie generuje zależności. 1.11 Duży przykład Pora na większy przykład oryginalny makefile dla programu tar w wersji GNU. Domyślnym obiektem docelowym jest all. # Generated automatically from Makefile.in by configure. # Un*x Makefile for GNU tar program. # Copyright (C) 1991 Free Software Foundation, Inc. # This program is free software; you can redistribute # it and/or modify it under the terms of the GNU # General Public License......... SHELL = /bin/sh #### Start of system configuration section. #### srcdir =. # If you use gcc, you should either run the 7
# fixincludes script that comes with it or else use # gcc with the -traditional option. Otherwise ioctl # calls will be compiled incorrectly on some systems. CC = gcc -O YACC = bison -y INSTALL = /usr/local/bin/install -c INSTALLDATA = /usr/local/bin/install -c -m 644 # Things you might add to DEFS: # -DSTDC_HEADERS If you have ANSI C headers and # libraries. # -DPOSIX If you have POSIX.1 headers and # libraries. # -DBSD42 If you have sys/dir.h (unless # you use -DPOSIX), sys/file.h, # and st_blocks in struct stat. # -DUSG If you have System V/ANSI C # string and memory functions # and headers, sys/sysmacros.h, # fcntl.h, getcwd, no valloc, # and ndir.h (unless # you use -DDIRENT). # -DNO_MEMORY_H If USG or STDC_HEADERS but do not # include memory.h. # -DDIRENT If USG and you have dirent.h # instead of ndir.h. # -DSIGTYPE=int If your signal handlers # return int, not void. # -DNO_MTIO If you lack sys/mtio.h # (magtape ioctls). # -DNO_REMOTE If you do not have a remote shell # or rexec. # -DUSE_REXEC To use rexec for remote tape # operations instead of # forking rsh or remsh. # -DVPRINTF_MISSING If you lack vprintf function # (but have _doprnt). # -DDOPRNT_MISSING If you lack _doprnt function. # Also need to define # -DVPRINTF_MISSING. # -DFTIME_MISSING If you lack ftime system call. # -DSTRSTR_MISSING If you lack strstr function. # -DVALLOC_MISSING If you lack valloc function. # -DMKDIR_MISSING If you lack mkdir and # rmdir system calls. # -DRENAME_MISSING If you lack rename system call. # -DFTRUNCATE_MISSING If you lack ftruncate # system call. # -DV7 On Version 7 Unix (not # tested in a long time). 8
# -DEMUL_OPEN3 If you lack a 3-argument version # of open, and want to emulate it # with system calls you do have. # -DNO_OPEN3 If you lack the 3-argument open # and want to disable the tar -k # option instead of emulating open. # -DXENIX If you have sys/inode.h # and need it 94 to be included. DEFS = -DSIGTYPE=int -DDIRENT -DSTRSTR_MISSING \ -DVPRINTF_MISSING -DBSD42 # Set this to rtapelib.o unless you defined NO_REMOTE, # in which case make it empty. RTAPELIB = rtapelib.o LIBS = DEF_AR_FILE = /dev/rmt8 DEFBLOCKING = 20 CDEBUG = -g CFLAGS = $(CDEBUG) -I. -I$(srcdir) $(DEFS) \ -DDEF_AR_FILE=\"$(DEF_AR_FILE)\" \ -DDEFBLOCKING=$(DEFBLOCKING) LDFLAGS = -g prefix = /usr/local # Prefix for each installed program, # normally empty or g. binprefix = # The directory to install tar in. bindir = $(prefix)/bin # The directory to install the info files in. infodir = $(prefix)/info #### End of system configuration section. #### SRC1 = tar.c create.c extract.c buffer.c \ getoldopt.c update.c gnu.c mangle.c SRC2 = version.c list.c names.c diffarch.c \ port.c wildmat.c getopt.c SRC3 = getopt1.c regex.c getdate.y SRCS = $(SRC1) $(SRC2) $(SRC3) OBJ1 = tar.o create.o extract.o buffer.o \ getoldopt.o update.o gnu.o mangle.o OBJ2 = version.o list.o names.o diffarch.o \ port.o wildmat.o getopt.o OBJ3 = getopt1.o regex.o getdate.o $(RTAPELIB) OBJS = $(OBJ1) $(OBJ2) $(OBJ3) AUX = README COPYING ChangeLog Makefile.in \ 9
makefile.pc configure configure.in \ tar.texinfo tar.info* texinfo.tex \ tar.h port.h open3.h getopt.h regex.h \ rmt.h rmt.c rtapelib.c alloca.c \ msd_dir.h msd_dir.c tcexparg.c \ level-0 level-1 backup-specs testpad.c all: tar rmt tar.info tar: $(OBJS) $(CC) $(LDFLAGS) -o $@ $(OBJS) $(LIBS) rmt: rmt.c $(CC) $(CFLAGS) $(LDFLAGS) -o $@ rmt.c tar.info: tar.texinfo makeinfo tar.texinfo install: all $(INSTALL) tar $(bindir)/$(binprefix)tar -test! -f rmt $(INSTALL) rmt /etc/rmt $(INSTALLDATA) $(srcdir)/tar.info* $(infodir) $(OBJS): tar.h port.h testpad.h regex.o buffer.o tar.o: regex.h # getdate.y has 8 shift/reduce conflicts. testpad.h: testpad./testpad testpad: testpad.o $(CC) -o $@ testpad.o TAGS: $(SRCS) etags $(SRCS) clean: rm -f *.o tar rmt testpad testpad.h core distclean: clean rm -f TAGS Makefile config.status realclean: distclean rm -f tar.info* shar: $(SRCS) $(AUX) shar $(SRCS) $(AUX) compress \ > tar- sed -e /version_string/!d \ -e s/[^0-9.]*\([0-9.]*\).*/\1/ \ -e q 10
version.c.shar.z dist: $(SRCS) $(AUX) echo tar- sed \ -e /version_string/!d \ -e s/[^0-9.]*\([0-9.]*\).*/\1/ \ -e q version.c >.fname -rm -rf cat.fname mkdir cat.fname ln $(SRCS) $(AUX) cat.fname -rm -rf cat.fname.fname tar chzf cat.fname.tar.z cat.fname tar.zoo: $(SRCS) $(AUX) -rm -rf tmp.dir -mkdir tmp.dir -rm tar.zoo for X in $(SRCS) $(AUX) ; do \ echo $$X ; \ sed s/$$/^m/ $$X \ > tmp.dir/$$x ; done cd tmp.dir ; zoo am../tar.zoo * -rm -rf tmp.dir 11