FrontPage FindPage TitleIndex RecentChanges UserPreferences E D R S I H C
 
GNU Make

GNU make 를 똑똑하게 쓰기

1.1 요약

유닉스 환경의 개발자라면 make 를 반드시 사용하게 된다고 해도 과언이 아니다. make 는 큰 프로그램을 짤 때 어느 부분이 다시 컴파일되어야 하는지를 자동으로 결정하고 실행하게 하는 유틸리티이다. 하지만 실무에서는 익숙하지 못해서, 혹은 귀찮아서 make 본래의 목적에 부합하게 자동으로 파일 의존성을 검사하거나 하위 디렉토리를 자동으로 검색하며 컴파일하는 기능을 십분 활용하지 못하는 경우도 있다. GNU make 는 고전적인 make 의 기능을 그대로 구현하고 있을 뿐 아니라 편리한 확장 기능을 많이 제공하고 있으며, 여러 플랫폼에 포팅되어 있으므로 GNU make 의 기능을 활용하는 것은 좋은 선택이다.

1.2 간단한 Makefile 보기

edit : main.o kbd.o command.o display.o \
       insert.o search.o files.o utils.o
	gcc -o edit main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o

main.o : main.c defs.h
	gcc -c main.c
kbd.o : kbd.c defs.h command.h
	gcc -c kbd.c
command.o : command.c defs.h command.h
	gcc -c command.c
display.o : display.c defs.h buffer.h
	gcc -c display.c
insert.o : insert.c defs.h buffer.h
	gcc -c insert.c
search.o : search.c defs.h buffer.h
	gcc -c search.c
files.o : files.c defs.h buffer.h command.h
	gcc -c files.c
utils.o : utils.c defs.h
	gcc -c utils.c

clean :
	rm -f edit main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o

위는 GNU make 매뉴얼에 처음으로 등장하는 예제 Makefile 에서 컴파일러 명령어를 cc 대신 gcc 로 고치고 rm 에 -f 옵션을 주기만 한 것이다. 일단 위와 같이 규칙의 나열로 이루어지는 간단한 Makefile 은 읽을 줄 아는 독자를 가정하고 글을 쓰겠다. make 에 대해 생소한 독자는 웹에서 쉽게 찾을 수 있는 make 강좌를 아무거나 골라서 잠시 읽어보기를 권한다. 위 Makefile 을 간단히 설명하자면 edit 라는 프로그램을 컴파일 하는데는 여러 개의 확장자가 .o 인 목적코드들이 있어야 한다는 조건과, 목적 코드들로부터 edit 프로그램을 gcc 로 컴파일하는 실제 쉘 명령이 첫번째 규칙이다. 그 다음에는 각각의 목적코드들에 대한 규칙이 나오고, 마지막으로 파일들을 삭제하여 프로젝트를 원래 소스만 있던 원래 상태로 청소하는 clean 규칙이 나온다. 명령 프롬프트에서 $ make 와 같이 아무 명령행 인자가 따라붙지 않도록 실행하면 make 는 기본적으로 첫번째 규칙을 실행하며, $ make clean 과 같이 명령행 인자를 주면 그 인자에 맞는 규칙을 실행한다.

1.3 간단한 Makefile 간단히 개선하기

1.3.1 변수(Variable)

OBJ = main.o kbd.o command.o display.o \
	  insert.o search.o files.o utils.o

edit : $(OBJS)
	gcc -o $@ $(OBJS)

변수(혹은 매크로)를 사용하면 첫번째 규칙을 위와 같이 보기좋게 만들 수 있다. 이미 많은 make 강좌에 나와 있는 변수에 대한 자세한 설명은 생략한다. 한가지 변수에 관해 유의할 점을 언급하자면 := 를 사용해 정의하고 ${} 로 내용을 확장하는 단순 확장 변수와 = 를 상용해 정의하고 $() 로 내용을 확장하는 반복 변수 확장변수가 있으니, 매뉴얼의 6.2 The Two Flavors of Variables 를 참고하기 바란다.

1.3.2 특별한 내장 표적

규칙의 첫머리에 오는 것을 표적이라 한다. 대부분의 규칙에서 표적은 어떤 파일에 해당하며, 그 규칙을 실행하면 표적과 이름이 같은 파일이 만들어진다. 하지만 표적이 파일이 아니라 단지 어떤 일을 할지를 나타내기만 하는 표적도 있다. 간단한 Makefile 보기의 clean 이 이에 해당한다. 문제는 make 가 기본적으로 표적을 파일로 생각한다는 것이다. 만일 디렉토리에 clean 이라는 파일이 생긴다면 clean 규칙은 전제조건이 없으므로 clean 이라는 파일이 만들어진 일시에 관계없이 갱신할 필요가 없는 최신 파일로 인식하고 clean 규칙의 명령을 실행하지 않게 된다. 이것은 간단한 Makefile 저자가 의도한 바가 아니다. 이럴 때 .PHONY 라는 특별한 내장 표적 파일로 clean 일반적인 표적 파일이 아님을 다음과 같이 지시할 수 있다.
.PHONY : clean

clean :
	rm -f edit $(OBJS)

.PHONY 외에도 여러 가지 기능의 특별한 내장 표적 이름들이 있다. 역시 자세한 설명은 매뉴얼에 있으므로 생략한다.

1.3.3 함수(Function)

함수는 고전적인 make 에는 없는 GNU make 의 매우 유용한 확장 기능이다. GNU make 자체가 이미 많은 플랫폼에 포팅되어 있으므로, GNU make 를 사용하지 않아야 하는 특별한 제약이 없는 한 이러한 확장 기능들을 사용하는 편리하고 이식성에도 문제가 없을 것이라 생각한다. 이중 가장 자주 쓰이는 wildcard 함수와 patsubst 함수이다.

어떤 디렉토리의 모든 파일, 혹은 특정한 확장자 등의 특정한 패턴을 갖는 파일로부터 하나의 표적을 만들어 내는 경우가 자주 나타난다. 라이브러리를 작성이 그 대표적인 예이다. 이러한 경우 매번 새로운 파일이 만들어질 때마다 Makefile 을 새로 편집하는 것은 비효율적이며 실수할 가능성만을 늘릴 뿐이다. 만일 우리의 간단한 Makefile 보기를 사용하는 디렉토리에 목적코드 edit 를 만들기 위한 소스코드밖에 없다면 다음과 같이 변수와 첫번째 규칙을 정의할 수 있다.
SRCS = $(wildcard *.c)
OBJS = $(patsubst %.c,%.o,$(SRCS))

edit : $(OBJS)
	gcc -o $@ $(OBJS)
또한 SRCS 를 정의할 필요가 없다면 OBJS 를 다음과 같이 정의하는 것도 가능하다.
OBJS = $(patsubst %.c,%.o,$(wildcard *.c))
그리고 미리 정의된 변수에 대한 패턴 치환은 다음과 같이 줄여 쓸 수 있으며,
SRCS = $(wildcard *.c)
OBJS = $(SRCS:%.c=%.o)
특히 확장자와 같은 접미사 치환은 다음과 같이 더욱 줄일 수 있다.
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
wildcard, patsubst 이외의 다른 함수들에 대해서는 더 알아보고 싶다면 매뉴얼 8. Functions for Transforming Text 를 참조하라.

1.3.4 패턴 규칙

OBJS = $(patsubst %.c,%.o,$(wildcard *.c))

edit : $(OBJS)
	gcc -o $@ $(OBJS)

%.o : %.c
	gcc -c -o $@ $<

clean:
	rm -f edit $(OBJS)
위는 패턴 규칙까지 적용하여 개선한 간단한 Makefile 보기이다. GNU make 의 패턴 규칙은 고전적인 make 의 접미사 규칙(suffix rule)보다 일반적인 규칙이다. 고전적인 접미사 규칙으로는 소스코드와 목적코드가 다른 디렉토리에 있는 경우를 처리하기 곤란하지만, 패턴 규칙을 사용하면 쉽게 처리할 수 있다. GNU make 도 접미사 규칙을 지원하며 호환성을 위해 기본적인 접미사 규칙들도 미리 정의되어 있다. 하지만 GNU make 에는보기에도 더 명확하고 확장성 있는 패턴 규칙이 있으므로 접미사 규칙을 사용할 필요가 없다. 패턴 규칙에 대한 더 자세한 내용은 매뉴얼을 참고하라.

1.4 간단한 Makefile 똑똑하게 고치기

1.4.1 자동으로 의존관계 생성하기

make 을 사용하는 가장 큰 이유는 어떤 파일이 수정되었을 때 Makefile 에 나타난 규칙에 의거하여 그것에 의존하는, 즉 그 수정된 파일로부터 생성되는 파일들을 자동으로 다시 생성해 주기 때문이다. 문제는 이 Makefile 이라는 것이 정확하게 작성되어 있느냐 하는 것이다. 엉성하게 Makefile 을 작성하면 파일들 간의 의존 관계가 정확하게 모두 기술되어 있지 않기 때문에 일부 파일을 수정하고 make 를 해도 실패하거나, 성공한다 해도 잘못된 결과를 얻게 될 가능성이 크다. 따라서 엉성하게 Makefile 을 작성해 놓으면 불안한 나머지 자주 전체 프로젝트를 몽땅 다시 컴파일하고 만다. 하지만 실제로 어느 정도 이상 큰 프로그램에서는 수동으로 정확한 의존관계를 표시하는 Makefile 을 작성하기란 매우 어렵다. 파일들이 한 단계의 의존성이 있는 것이 아나라 여러 단계에 걸쳐 의존성이 생기기 때문이다.

따라서 대부분의 유닉스 환경에서 돌아가는 컴파일러에서는 소스 파일을 컴파일할 때 필요한 다른 소스 파일의 정보를 출력해 주는 기능이 내장되어 있으며, 이들은 보통 Makefile 그대로 입력했을 때 곧바로 사용할 수 있는 형식이다. (gcc 의 경우 -M 으로 시작하는 옵션들이 이에 해당한다.) 또한 그러한 컴파일러 출력을 make 와 곧바로 연동할 수 있도록 하는 gccmakedep, makedepend, mkdep 등의 유틸리티들이 있다. 하지만 이 전통적인 유틸리티들은 어떤 특정 프로젝트의 일부로서 개발되었기 때문에, 컴파일러에 별도의 옵션을 지정한다든지 하는 유연성이 떨어진다. 또한 이들은 공통적으로 하나의 파일로 의존관계를 생성한다는 단점이 있다. 따라서 파일을 하나만 수정해도 같은 디렉토리 안의 전체 소스에 파일에 대한 의존관계를 다시 작성해야 하며 이는 컴퓨터 실행 시간 측면에서도 비효율적이며, 매번 명령 프롬프트에서 명령을 내려야 하므로 번거롭다. 그러므로 매뉴얼 4.14 Generating Prerequisites Automatically 에도 나오듯이, 다음과 같이 패턴 규칙을 이용하여 직접 각각의 파일에 대한 의존관계 파일을 생성할 것을 추천하고 있다.
%.d: %.c
	@set -e; rm -f $@; \
	gcc -M $< > $@.$$$$; \
	sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
	rm -f $@.$$$$
make 는 기본적으로 명령을 실행하기 전에 명령을 출력한다. 하지만 @ 으로 시작하는 명령은 출력하지 않고 실행한다. 또한 쉘에서 -e 는 일반적으로 에러가 다음 명령을 받지 않고 즉시 쉘을 종료하는 옵션이다. sed 명령이 하는 일은 원래 gcc 에 -M 옵션을 주었을 때 목적 코드에 대해서만 의존관계를 나타내는 다음과 같은 출력을
main.o : main.c def.h ...
생성된 의존규칙 파일 자체에도 의존성을 부여하는 규칙이 되도록 다음과 같이 변환하는 것이다.
main.o main.d : main.c def.h ...
이렇게 하면 파일을 수정했을 때 전체 소스에 대해 의존규칙을 다시 생성할 필요 없이 어떤 특정 의존규칙 파일들만 새로 만들면 되는지 make 에 알려줄 수 있다.

그래도 위 패턴 룰에 나타난 명령의 내용이 잘 이해가 가지 않는다면 매뉴얼 4.14 Generating Prerequisites Automatically 및 쉘 명령어에 대한 참고문서를 참고하라.

1.4.2 Makefile 갱신을 똑똑하게 처리하는 GNU make

고전적인 make 는 Makefile 자신이 갱신되었을 때에도 별다른 처리를 하지 않는다. 옛날 프로그램들을 컴파일할 때는 Makefile 자신을 갱신하는 규칙, 이를테면 자동으로 의존관계를 생성하는 규칙을 별도로 하나 정해 $ make depend 등과 실행한 후에야 $ make 를 실행하곤 했다. 하지만 매뉴얼 3.7 How Makefiles Are Remade 에 나오듯이, GNU make 는 Makefile 자신이 갱신되었을 때 똑똑하게 행동한다는 큰 장점을 있기 때문에 그러한 별도의 자동 의존관계 생성 루틴이 필요없다. GNU make 는 실행에 앞서 Makefile 자신 뿐 아니라 그 Makefile 이 include 하고 있는 모든 다른 Makefile 들을 갱신하는 규칙이 있는지 확인하고 그 규칙을 적용여여 하나라도 갱신되면 전체 Makefile 을 다시 읽어들인 후에 작업을 진행한다. 그렇기 때문에 다음과 같이 Makefile 을 작성하여 의존관계 파일들을 include 하기만 하면 별도로 의존관계를 생성하는 명령을 내리지 않아도 된다.
OBJS = $(patsubst %.c,%.o,$(wildcard *.c))
DEPS = $(OBJS:.o=.d)

edit : $(OBJS)
	gcc -o $@ $(OBJS)

%.o : %.c
	gcc -c -o $@ $<

clean:
	rm -f edit $(OBJS) $(DEPS) $(DEPS:.d=.d.*)

include $(DEPS)

%.d: %.c
	@set -e; rm -f $@; \
	gcc -M $< > $@.$$$$; \
	sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
	rm -f $@.$$$$
처음 make 를 실행하면 아직 확장자가 .d 인 의존관계 파일이 없어서 include 하지 못한다. include 에 성공하건 못하건, GNU make 는 이들을 생성할 규칙이 있나 일단 먼저 알아보며, 규칙이 있고 확장자 .d 인 파일들은 아예 존재하지 않기 때문에 최신 파일이 아니다. 따라서 make 는 확장자 .d 인 의존관계 파일들을 생성하는 명령들을 실행한다. 이제 include 되어야 할 대상인 의존관계 파일들이 갱신되었으므로 make 는 다시 Makefile 들을 불러들여 edit 라는 표적을 생성하는 규칙을 실행하게 되는 것이다.

1.5 하위 시스템 make 하기

1.5.1 하위 디렉토리 make

다음 Makefile 을 보면 별다른 설명이 필요없을 듯 하다.

SUBDIRS = foo bar baz

.PHONY: subdirs $(SUBDIRS)

subdirs: $(SUBDIRS)

$(SUBDIRS):
	$(MAKE) -C $@

foo: baz
자세한 설명은 매뉴얼을 참고하라.

1.5.2 변수를 하위 make 에 넘겨주기

정적으로 변수 정의를 할 수 있거나 구조상 상위 make 가 아니라 말단에서 결정해야 하는 식의 변수라면 공통적으로 include 하는 Makefile 을 만들어 그곳에 정의하는 편이 더 낫다. 위와 같은 방법은 상위 디렉토리에서 동적으로 생성한 변수 정의들을 하위 디렉토리로 전파해야 할 때 쓰는 방법이다.

쉘에서 make 를 실행하면 쉘 환경변수가 make 로 넘어간다. bash 쉘이라면 다음과 같이 export 명령으로 환경 변수를 사용자가 지정하여 make 로 넘길 덧이다.
$ export CC=gcc; make
Makefile 내부에서도 똑같은 형식으로 export 명령을 쓰면 환경변수가 설정되어 하위 make 을 를 부를 때 변수가 넘어간다. 그리고 더 이상 하위 make 로 넘겨주고 싶지 않은 정의는 unexport 명령으로 환경에서 제거한다. 더 자세한 내용은 매뉴얼을 참고하라.

1.6 참고문헌

last modified 2009-03-09 13:46:45
EditTextFindPageDeletePageLikePages