Automatyzacja zarządzania złożonymi programami

Zbigniew Jurkiewicz

Make

Na przykładzie narzędzia Make przedstawimy automatyzację zarządzania złożonymi programami. Cel:

Zasady są następujące:

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:

    $ 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.

Reguły

Reguły jawne określają zależności pomiędzy obiektem docelowym (produktem) a obiektami, wymaganymi do jego skonstruowania (surowcami):

Poznana reguła opisywała tylko zależności między plikami, nie określała natomiast sposobu regeneracji pliku docelowego.

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.

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:

ARProgram do budowy bibliotek (ar)
ASAsembler (as)
CCKompilator języka C (cc)
CFLAGSFlagi dla kompilatora C
CXXKompilator języka C++ (g++)
CXXFLAGSFlagi dla kompilatora C++
LDFLAGSFlagi 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)

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.
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.

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

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)

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 $<

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.

Istnieją również inne predefiniowane makra systemowe, których nazwami są pojedyncze znaki przestankowe. Ich znaczenie podaje poniższa tabelka:

MakroZnaczenie
$*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

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
    # Makra make specyficzne dla MS Windows

    OBJ=.obj
    EXE=.exe
    CC=gcc -g -o
    LIBS=-lm

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.

Narzędzia

Program makedepend w Unixie (pół)automatycznie generuje zależności. Ale to samo daje kompilator gcc wywołany z opcją -MM

[zbyszek@katastrofa4 ch12.molecule]$ gcc -MM *.c
createmenu.o: createmenu.c
filesel.o: filesel.c
frontend.o: frontend.c
matrix3d.o: matrix3d.c atom.h matrix3d.h
misc.o: misc.c
molecule.o: molecule.c atom.h matrix3d.h

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
# 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).
# -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  \
      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
                      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
Zbigniew Jurkiewicz, Instytut Informatyki UW
Copyright © 2007 Zbigniew Jurkiewicz