한국어
Linux Programming

4. Makefile를 작성할 때 알면 좋은 것들

pjk 2014.02.05 20:29 조회 수 : 2215

Makefile을 작성할 때 기본적으로 알고 있으면 유익한 것들을 기술한다. 이전 강좌의 내용을 대체로 이해하고 있다면 좋은 팁이 될 것이다. 메뉴얼에 나오는 광범위한 내용은 다루지 않고 기본적인 것들에 관심을 두기로 한다.


4.1 긴 명령어를 여러 라인으로 표시하기

Makefile을 작성할 때 명령어가 한 줄을 넘어간다고 가정하자. 이때 그냥 줄줄이 적는다면 읽기도 힘들고, 작성하는 사람도 조금 찜찜하다. 이때 '' 문자를 이용해서 여러 라인으로 나타낼 수 있다. 이미 C언어에 익숙한 사람이라면 낯익은 기호일 것이다. 아래의 예제를 보자.


예제 13 


OBJS = shape.o

rectangle.o

circle.o

line.o

bezier.o 


위의 예제는 OBJS = shape.o rectangle.o circle.o line.o bezier.o 라는 문장을 여러 라인으로 표시한 것이다. 보기에도 깔끔해 보이지 않은가.


4.2 확장자 규칙의 이용 (Use suffix rule !!)

두번째 장에서 확장자 규칙에 대해서 많이 설명을 했다. Makefile을 작성할 때 C, C++, tex 등의 파일은 이미 정의되어 있는 규칙을 이용하면 간단하고, 깔끔한 Makefile을 작성할 수 있다. 두번째 장에서 직접 우리가 규칙을 간단히 구현해 보기도 했는데, 이것은 확장자 규칙의 개념을 설명하기 위함이었다.


어떤 파일들이 이미 규칙으로 정해져 있는지 한번 살펴보기로 한다. 아래에 열거된 파일들은 특별히 따로 정의하지 않은 상태에서 바로 이용할 수 있는 것들이다.(GNU Make 매뉴얼에 바탕을 두고 작성되었다.)


 C 컴파일 (XX.c -> XX.o) 

 C++ 컴파일 (XX.cc 또는 XX.C -> XX.o) 

 Pascal 컴파일 (XX.p -> XX.o) 

 Fortran 컴파일 (XX.f 또는 XX.F -> XX.o) 

 Modula-2 컴파일 (XX.def -> XX.sym) 

 (XX.mod -> XX.o) 

 assembly 컴파일 (XX.s -> XX.o) 

 assembly 전처리 (XX.S -> XX.s) 

 single object file 의 링크 (XX.o -> XX) 

 Yacc 컴파일(?) (XX.y -> XX.o) 

 Lex 컴파일(?) (XX.l -> XX.o) 

 lint 라이브러리 생성 (XX.c -> XX.ln) 

 tex 파일 처리 (XX.tex -> XX.dvi) 

 texinfo 파일처리 (XX.texinfo 또는 XX.texi -> XX.dvi) 

 RCS 파일 처리 (RCS/XX,v -> XX) 

 SCCS 파일처리 (SCCS/XX.n -> XX) 



위에 정의된 파일만이 make에서 처리할 수 있는 것은 아니다. 그 밖의 파일에 대해서는 사용자가 직접 정의해 주면 얼마든지 make를 사용할 수 있다.


그럼 이젠 위와 같은 파일들을 처리하기 위한 명령어는 어떤 매크로로 정의되어 있는지 알아보자. 이미 말했듯이 아래에 열거된 매크로는 재정의 가능하다. 가령 TEX = tex 이지만 대부분 TEX = latex로 재정의 되어야 할 것이다.


 AR = ar (Archive maintaining program) 

 AS = as (Assembler) 

 CC = cc (= gcc , C compiler) 

 CXX = g++ (C++ compiler) 

 CO = co (extracting file from RCS) 

 CPP = $(CC) -E (C preprocessor) 

 FC = f77 (Fortran compiler) 

 LEX = lex (LEX processor) 

 PC = pc (Pascal compiler) 

 YACC = yacc (YACC processor) 

 TEX = tex (TEX processor) 

 TEXI2DVI = texi2dvi (Texiinfo file processor)  

 WEAVE = weave (Web file processor) 

 RM = rm -f (remove file) 



이미 두번째 장에서 밝혔지만 위의 명령어에서 사용될 FLAG(옵션)에 정의한 매크로에 대해서도 알아보기로 한다.


 ARFLAGS = (ar achiver의 플래그) * 

 ASFLAGS = (as 어셈블러의 플래그) 

 CFLAGS = (C 컴파일러의 플래그) * 

 CXXFLAGS = (C++ 컴파일러의 플래그) * 

 COFLAGS = (co 유틸리티의 플래그) 

 CPPFLAGS = (C 전처리기의 플래그) 

 FFLAGS = (Fortran 컴파일러의 플래그) 

 LDFLAGS = (ld 링커의 플래그) * 

 LFLAGS = (lex 의 플래그) * 

 PFLAGS = (Pascal 컴파일러의 플래그) 

 YFLAGS = (yacc 의 플래그) * 



위애서 '*'표시한 것은 자주 쓰이게 될 플래그이다. 위에서 표시한 여러 가지 매크로들을 무조건 재정의 하라는 배려에서인지, 대부분 값이 설정되어 있지 않다. 가령 C프로그램을 짤 때 CFLAGS를 재정의 해야 할 것이다.


4.3 매크로 치환 (Macro substitution)

매크로를 지정하고, 그것을 이용하는 것을 이미 알고 있다. 그런데, 필요에 의해 이미 매크로의 내용을 조그만 바꾸어야 할 때가 있다. 매크로 내용의 일부만 바꾸기 위해서는 $(MACRO_NAME:OLD=NEW)과 같은 형식을 이용하면 된다.



MY_NAME = Michael Jackson

YOUR_NAME = $(NAME:Jack=Jook)



위의 예제에서는 Jack이란 부분이 Jook으로 바뀌게 된다. 즉 YOUR_NAME 이란 매크로의 값은 Michael Jookson 이 된다. 아래의 예제를 하나 더 보기로 한다. 



OBJS = main.o read.o write.o

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



위의 예제에서는 OBJS에서 .c가 .o로 바뀌게 된다. 즉 아래와 같다.



SRCS = main.c read.c write.c



위의 예제는 실제로 사용하면 아주 편할 때가 많다. 가령 .o 파일 100개에 .c 파일이 각각 있을 때 이들을 다 적으려면 무척이나 짜증나는 일이 될 것이다.


4.4 자동 의존 관계 생성 (Automatic dependency)

일반적인 make의 구조는 아래와 같이 target, dependency, command가 연쇄적으로 정의되어 있는 것과 같다고 하였다.



target : dependency

                command

                ...


그런데 위에서 command가 없이 타겟과 의존 관계만 표시가 되면 이는 타겟이 어느 파일에 의존하고 있는지 표시해 주는 정보의 역할을 한다. 이런 정보는 Makefile을 작성할 때 없어서는 안되는 부분이다. (이 부분이 없으면, make는 정말 바보처럼 행동합니다.)


그런데 일일이 이런 정보를 만든다는 것은 쉬운 일이 아니다. 파일이 1000개라고 할 때 이것을 어케 다 표시하누...


이런 단조롭고 귀찮은 일을 자동으로 해주는 좋은 유틸리티가 있다. 우선 gccmakedep가 있는지 확인해 보자. gccmakedep는 어떤 파일의 의존 관계를 자동으로 조사해서 Makefile의 뒷부분에 자동으로 붙여 주는 유틸리티이다. gccmakedep가 없다면 gcc -M XX.c 라고 해보자. 그러면 XX.c의 의존 관계가 화면에 출력됨을 알 수 있을 것이다. (gccmakedep 도 내부적으로 gcc -M 을 사용한다.)


프로그램을 설치할 때 make dep 라는 것을 친 기억이 있을 것이다. 파일들의 의존 관계를 작성해 준다는 의미이다. 그럼 우리의 Makefile에도 이런 기능을 첨가해 보기로 한다.


예제 14 


.SUFFIXES : .c .o 

CFLAGS = -O2 -g


OBJS = main.o read.o write.o 

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


test : $(OBJS)

                $(CC) -o test $(OBJS)


dep :

                gccmakedep $(SRCS)



위의 Makefile을 이해할 수 있다면 이제 Makefile에 대해서 어느 정도 도가 텄다고 해도 무방할 것이다. 위의 예제에서 파일들간의 의존 관계가 없다. 그럼 이제 make dep 을 써서 자동적으로 생성시켜 보자.



% make dep 

% vi(emacs) Makefile



Makefile의 뒷부분에 다음과 같은 내용이 붙어 있는 것을 알게 될 것이다.


예제 15 


# DO NOT DELETE

main.o: main.c /usr/include/stdio.h /usr/include/features.h

/usr/include/sys/cdefs.h /usr/include/libio.h

/usr/include/_G_config.h io.h

read.o: read.c io.h

write.o: write.c io.h



main.o에는 조금 자질구레한 헤더 파일까지 붙어 있다. 이것은 헤더 파일 안에서 include 하는 파일들을 다 찾다 보니까 그런 것이다. 별로 신경쓸 것은 없고... 대충 우리가 지금까지 손으로 작성해 온 것과 거의 흡사함을 알 수있다. 아니 오히려 더 정확함을 알 수 있다. (이제부터 make는 스마트하게 동작한다.)


4.5 다중 타겟 (Multiple target)

하나의 Makefile에서 꼭 하나의 결과만 만들어 내라는 법은 없다. 가령 결과 파일이 3개가 필요하다고 하자. 아래의 예제를 보기로 한다.


예제 15 


.SUFFIXES : .c .o 

CC = gcc

CFLAGS = -O2 -g


OBJS1 = main.o test1.o <- 각각의 매크로를 정의

OBJS2 = main.o test2.o 

OBJS3 = main.o test3.o 

SRCS = $(OBJS1:.o=.c) $(OBJS2:.o=.c) $(OBJS3:.o=.c) 


all : test1 test2 test3 <- 요기에 주의 


test1 : $(OBJS1)

                $(CC) -o test1 $(OBJS1) 


test1 : $(OBJS2)

                $(CC) -o test2 $(OBJS2)


test1 : $(OBJS3)

                $(CC) -o test3 $(OBJS3)


dep :

                gccmakedep $(SRCS)



위의 프로그램은 make all 을 함으로써 동작한다. 실제로 동작시켜 보면 아래와 같은 결과가 나온다.



% make all (또는 make)

gcc -O2 -g -c main.c -o main.o

gcc -O2 -g -c test1.c -o test1.o

gcc -o test1 main.o test1.o <- test1 의 생성

gcc -O2 -g -c test2.c -o test2.o

gcc -o test2 main.o test2.o <- test2 의 생성

gcc -O2 -g -c test3.c -o test3.o

gcc -o test3 main.o test3.o <- test3 의 생성



4.6 순환 make (Recursive MAKE)

규모가 큰(?) 프로그램들은 파일들이 하나의 디렉토리에 있지 않는 경우가 많다. 여러 개의 서브시스템이 전체 시스템을 구성한다고 가정하면 각 서브시스템에 Makefile이 존재한다. (서브시스템 = 서브디렉토리) 따라서 여러 개의 Makefile을 동작시킬 필요가 있도록 Makefile을 고쳐 보자. 서브디렉토리에 있는 Makefile을 동작시키는 방법은 의외로 간단하다. 아래의 간단한 예제를 보자.


예제 16 


subsystem:

                cd subdir; $(MAKE) .....(1)


subsystem:

                $(MAKE) -C subdir .....(2)



위의 예제에서 (1)과 (2)는 동일한 명령을 수행한다 (1)을 기존으로 동작을 한번 묘사해 보자. 우리가 만들 시스템의 타겟이 subsystem이다. (이름은 아무래도 상관없다) 우선 subdir이라는 곳으로 가서, 거기에 있는 Makefile을 동작시키게 된다. (간단하죠.) MAKE라는 것은 그냥 make라는 명령어를 표시하는 매크로일 뿐... 그럼 완전한 예제를 한번 구성해 보기로 한다.


예제 16 


.SUFFIXES : .c .o

CC = gcc

CFLAGS = -O2 -g


all : DataBase Test <- 요기에 집중.


DataBase:

                cd db ; $(MAKE) <- db 로 이동해서 make 실행


Test: 

                cd test ; $(Make) <- db 로 이동해서 make 실행



위의 예제에서 db, test 디렉토리에 있는 Makefile은 지금까지 우리가 공부했던 Makefile과 거의 흡사하다고 가정하자. 그럼 위의 Makefile을 실행시켜 본다. 



% make

cd db ; make

make[1]: Entering directory`/home/raxis/TEST/src'

gcc -O2 -g -c DBopen.c -o DBopen.o

gcc -O2 -g -c DBread.c -o DBread.o

gcc -O2 -g -c DBwrite.c -o DBwrite.o

make[1]: Leaving directory `/home/windows/TEST/src'

cd test ; make

make[1]: Entering directory `/home/raxis/TEST/test'

gcc -O2 -g -c test.c -o test.o

make[1]: Leaving directory `/home/windows/TEST/test'



위의 가상 실행을 보면 우선 db로 가서 거기의 Makefile을 수행시키고, 다음에는 test로 가서 Makefile을 실행시킴을 볼 수 있다. 우선은 단순하게 컴파일만 시켰는데, 다르게 한번 생각해 보자. db 디렉토리에서의 최종 타겟으로 가령 db.a을 만들어 내고 test 디렉토리에서 이를 링크 시킨다고 생각하면 꽤 괜찮은 시나리오가 될 것이다. 위에서 1이라고 나타난 것은 현재의 레벨을 의미한다. 원래 디렉토리의 레벨이 0이고, 여기서는 레벨이 하나 더 내려갔으므로 1이라고 표시된 것이다.


4.7 불필요한 재컴파일 막기

의존 관계 규칙에 의해 하나가 바뀌면 그에 영향받는 모든 파일이 바뀐다고 앞에서 말했다. 그러나 다른 파일들에게 아무 영향을 주지 않도록 수정하였는데도 재컴파일을 시도한다면 시간 낭비가 될 수도 있다. 가령 모든 .c 파일에서 include 하는 헤더 파일에서 새로운 #define PI 3.14 라고 정의를 했다고 가정하자. 그리고 PI라는 것은 아무 곳에서도 사용을 하지 않는다.


이때는 'make -t' 라고 해보자. -t 는 touch를 의미하는 옵션으로써 컴파일을 하지 않는 대신 파일의 생성 날짜만 가장 최근으로 바꾸어 놓는다. 새로 컴파일 된 것처럼 처리를 하는 것이다. touch유틸리티 명렁어에 익숙한 사람이라면 이해할 것이다. touch는 파일의 생성 날짜를 현재로 바꾸어 주는 간단한 유틸리티이다.