두근두근 울렁울렁 가슴 뛰지만 무섭고도 두려워서 겁이 나지만 오늘은 제가 사용하는 Makefile 의 기본 형식에 대해 써볼까해요 :) 한가지 주의할 점은, 모두 알고 있겠지만 Makefile 의 들여쓰기는 탭을 사용한다는 것이에요. 아래 코드에서 들여쓰기 한 것은 모두 탭으로 바꿔야해요. 일단, 기본 형식입니다.
# -*- mode: makefile-gmake; coding: utf-8; -*-
MAKEFLAGS += --no-builtin-rules
MAKEFLAGS += --no-builtin-variables
OBJECTS := libexample.o example0.o
TARGETS := example0
CC := gcc
CFLAGS := -flto -Wall
LDFLAGS := -flto
.PHONY: all debug release clean distclean
all: debug
debug: $(TARGETS)
debug: CFLAGS += -g -O0
release: $(TARGETS)
release: CFLAGS += -O2
release: CPPFLAGS += -DNDEBUG
release: TARGET_ARCH += -march=native -mtune=native -mfpmath=both
clean:
rm -f $(OBJECTS) $(OBJECTS:.o=.d)
distclean: clean
rm -f $(TARGETS)
%: %.o
$(CC) -o $@ $^ $(LDFLAGS) $(TARGET_ARCH) $(LOADLIBES) $(LDLIBS)
%.o: %.c
$(CC) -o $@ -c $< $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH)
%.d: %.c
@$(CC) -MM $(CPPFLAGS) $< | sed 's/\($*\)\.o[ :]*/\1.o $@: /g' > $@
@[ -s $@ ] || rm -f $@
ifneq ($(MAKECMDGOALS), clean)
ifneq ($(MAKECMDGOALS), distclean)
include $(OBJECTS:.o=.d)
endif
endif
$(TARGETS:=.d): %.d: %.c
@$(CC) -MM $(CPPFLAGS) $< | sed 's/\($*\)\.o[ :]*/\1.o $@: /g' > $@
@(for i in $$(sed 's/ /\n/g' $@ | sed -n '/\.h$$/p'); do \
f=$$(basename $$i .h); \
t=$(<:.c=); \
[ $$f == $$t ] || [ ! -e $$f.c ] || echo $$t: $$f.o; \
done) | uniq >> $@
@[ -s $@ ] || rm -f $@
이 Makefile 은 C 코드를 컴파일하기 위한 것 입니다. 만일 C++ 코드를 컴파일하려 했다면, CC 대신 CXX 를 정의했을 것 입니다. 그리고 %o: %c 규칙 대신 %o: %cc 규칙을 정의하고, %: %o 규칙에서 CC 대신 CXX 를 사용했겠죠. 위 예에서는 libexample.c 파일과 example0.c 파일을 컴파일해서 libexample.o 파일과 example0.o 파일을 만들도록 합니다. 최종적으로 만들어지는 것은 example0 이라는 바이너리입니다. 여기에 특이한 점이 있는데요... example0 이란 바이너리를 만들려면 example0.o 파일과 libexample.o 파일을 링크해야하는데 그것을 정의하는 부분이 보이지 않는 것 같습니다 :)
사실은... 의존성 규칙을 자동으로 만들도록 트릭을 쓴 것 입니다. 만일 example0.c 파일이 libexample.h 파일을 include 한다면, example0 바이너리를 만들면서 example0.o 파일 뿐 아니라 libexample.o 파일도 같이 링크하는 규칙을 생성하도록 한 것이에요. 이러한 트릭을 쓰지 않았다면...
example0: libexample.o
라는 규칙을 명시적으로 적시해야겠죠. 복잡해보이는 쉘 명령이 하는 일이 이것입니다. 의존성이 있는 해더 파일, 즉 인클루드한 해더파일과 같은 이름의 C 소스 파일이 있다면 그것의 오브젝트 파일을 타겟의 의존성에 추가합니다. 이러한 Makefile 이 있으면 example0.c 와 조금 다른 example1.c, example2.c 같은 파일들도 바이너리로 만들기 쉽습니다. Makefile 에서 수정할 부분이 이것밖에 없거든요.
OBJECTS := libexample.o example0.o example1.o example2.o
TARGETS := example0 example1 example2
만일 바이너리가 전부 example 이란 이름으로 시작한다면 이렇게 써도 됩니다.
TARGETS := $(patsubst %.o, %, $(filter example%.o, $(OBJECTS)))
이렇게 써두고 OBJECTS 부분에 example3.o, example4.o, ... 등 계속 추가해도 되죠 :) 실제로는... 최종 바이너리 이름이 다른 경우가 많으니 저는 TARGETS 에 하나 하나 적어두는 편입니다. 자... 이제 응용편 입니다.
프로젝트 루트 디렉터리에 include, lib, makefiles, src 라는 서브 디렉터리들이 있다고 가정합시다. makefiles 디렉터리에는 위 기본 Makefile 을 조금 수정한 다음 세 개의 조각 파일들이 있습니다. 첫번째 파일은 common.mk 라는 이름입니다.
# -*- mode: makefile-gmake; coding: utf-8; -*-
CC := gcc
CFLAGS := -flto -Wall
CPPFLAGS := -I$(ROOTDIR)/include
LDFLAGS := -flto
.PHONY: all debug release clean distclean
all: debug
debug: CFLAGS += -g -O0
release: CFLAGS += -O2
release: CPPFLAGS += -DNDEBUG
release: TARGET_ARCH += -march=native -mtune=native -mfpmath=both
clean:
rm -f $(OBJECTS) $(OBJECTS:.o=.d)
%.d: %.c
@$(CC) -MM $(CPPFLAGS) $< | sed 's/\($*\)\.o[ :]*/\1.o $@: /g' > $@
@[ -s $@ ] || rm -f $@
ifneq ($(MAKECMDGOALS), clean)
ifneq ($(MAKECMDGOALS), distclean)
include $(OBJECTS:.o=.d)
endif
endif
%.o: %.c
$(CC) -o $@ -c $< $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH)
%: %.o
$(CC) -o $@ $^ $(LDFLAGS) $(TARGET_ARCH) $(LOADLIBES) $(LDLIBS)
다음 파일은 target.mk 라는 이름입니다. common.mk 와 합치면 위 기본 Makefile 과 거의 비슷하게 됩니다. 차이점은 조금 뒤에 설명하겠습니다.
# -*- mode: makefile-gmake; coding: utf-8; -*-
debug: $(TARGETS)
release: $(TARGETS)
distclean: clean
rm -f $(TARGETS)
$(TARGETS:=.d): %.d: %.c
@$(CC) -MM $(CPPFLAGS) $< | sed 's/\($*\)\.o[ :]*/\1.o $@: /g' > $@
@(for i in $$(sed 's/ /\n/g' $@ | sed -n '/\.h$$/p'); do \
f=$$(basename $$i .h); \
d=$$(dirname $$i); \
[ $$(basename $$d) == include ] && d=$$(dirname $$d)/lib; \
[ $$d == . ] || f=$$d/$$f; \
t=$(<:.c=); \
[ $$f == $$t ] || [ ! -e $$f.c ] || echo $$t: $$f.o; \
done) | uniq >> $@
@[ -s $@ ] || rm -f $@
우선 눈에 띄는 것이 타겟의 의존성을 자동 설정하는 부분이 조금 길어졌습니다. 기본 Makefile 에서는 현재 디렉터리에 있는 해더 파일을 인클루드하면 해더 파일에 해당하는 오브젝트 파일을 링크하도록 했었습니다. 이 규칙을, 해더 파일이 include 디렉터리에 있을 경우 lib 디렉터리에 있는 오브젝트 파일을 링크하도록 확장한 것 입니다.
이런 방식으로 쉘 명령을 수정해서 다양한 작업을 할 수 있습니다. 예에서는 오브젝트 파일을 링크하도록 했지만, 라이브러리 파일을 링크하도록 수정할 수도 있습니다. 같은 이름의 오브젝트 파일을 링크하도록 만들었지만, 어떤 특정한 해더 파일이 사용될 경우 여러 개의 다른 오브젝트 파일들을 의존성에 추가할 수도 있습니다. 간단한 프로젝트에서는 그럴 필요까지는 없을지도 모르겠습니다. 이러한 위부 쉘 명령을 쓰지 않고, 각각의 타겟별로 의존성을 따로 추가하는 방법이 원래의 기본 방식이거든요. 조금 귀찮을 뿐이죠.
세번째 조각 파일은 그런 간단한 프로젝트를 위한 lib.mk 파일 입니다. 라이브러리를 만들지 않고 컴파일만 합니다.
# -*- mode: makefile-gmake; coding: utf-8; -*-
debug: $(OBJECTS)
release: $(OBJECTS)
distclean: clean
이 lib.mk 조각 파일은 lib 디렉터리의 Makefile 에서 사용됩니다. 이런 방식이 되겠죠.
# -*- mode: makefile-gmake; coding: utf-8; -*-
OBJECTS := libexample.o
CURRDIR := $(patsubst %/, %, $(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
ROOTDIR := $(abspath $(CURRDIR)/..)
include $(ROOTDIR)/makefiles/common.mk
include $(ROOTDIR)/makefiles/lib.mk
src 디렉터리의 Makefile 은 이렇게 됩니다.
# -*- mode: makefile-gmake; coding: utf-8; -*-
OBJECTS := local_libexample.o example0.o example1.o
TARGETS := example0 example1
CURRDIR := $(patsubst %/, %, $(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
ROOTDIR := $(abspath $(CURRDIR)/..)
include $(ROOTDIR)/makefiles/common.mk
include $(ROOTDIR)/makefiles/target.mk
프로젝트의 루트 디렉터리의 Makefile 은 이런 방식이 될 것 입니다.
# -*- mode: makefile-gmake; coding: utf-8; -*-
MAKEFLAGS += --no-builtin-rules
MAKEFLAGS += --no-builtin-variables
SUBDIRS := lib src
ROOTDIR := $(patsubst %/, %, $(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
.PHONY: all debug release clean distclean
all: debug
debug release clean distclean:
@for i in $(SUBDIRS); do \
$(MAKE) -C $(ROOTDIR)/$$i $@; \
[ $$? == 0 ] || exit 1; \
done
src1, src2, src3, ... 이런 식으로 다른 디렉터리가 생긴다면 SUBDIRS 에 추가하면 됩니다. 만일, 서브디렉터리에 또 다른 서브디렉터리가 있는 경우라면 어떨까요? 예를 들어 test 란 디렉터리 밑에 test1 과 test2 란 디렉터리가 있다면 test 디렉터리의 Makefile 은 이런 식이 됩니다. 프로젝트의 루트 디렉터리에 있는 Makefile 과 거의 같습니다.
# -*- mode: makefile-gmake; coding: utf-8; -*-
SUBDIRS := test1 test2
CURRDIR := $(patsubst %/, %, $(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
.PHONY: all debug release clean distclean
all: debug
debug release clean distclean:
@for i in $(SUBDIRS); do \
$(MAKE) -C $(CURRDIR)/$$i $@; \
[ $$? == 0 ] || exit 1; \
done
test1 디렉터리나 test2 디렉터리의 Makefile 은 src 디렉터리의 Makefile 과 거의 같습니다. 프로젝트 루트 디렉터리의 경로는 바뀌겠죠.
ROOTDIR := $(abspath $(CURRDIR)/../..)
덧붙임1: ROOTDIR 을 환경으로 하지 않고 CURRDIR 로부터 상대적 위치로 구하는 것은 각각의 디렉터리에서 make 명령을 내릴 경우를 위해서 입니다. 예를 들어 src 디렉터리의 파일을 계속 수정해가면서 프로그래밍하고 있는데 make 명령은 프로젝트의 루트 디렉터리에서 내려야 되는 것이 불편했습니다. 그 경우 src 디렉터리의 Makefile 이 독립적으로 동작하도록 하기 위해 CURRDIR 로부터 상대적 위치로 include 디렉터리나 lib 디렉터리를 구하도록 했습니다.
덧붙임2: 이 Makefile 에 사용된 가장 중요한 트릭은 동적으로 의존성을 추가하도록 한 것입니다. 또한, 소스 코드가 바뀔 때 마다 의존성을 다시 점검합니다. 의존성이 계속 변하는 단계에서 편리한 기능입니다. 더 이상 의존성이 변할 가능성이 없을 때 이 트릭을 제거하고 전통적인 방법으로 돌아가는 것도 고려해 볼 수 있습니다. 동적으로 변하는 기능은 다른 사람이 프로젝트를 분석해야 할 경우 단점이 될 가능성이 있습니다. 그런데, 배포까지 고려해야하는 프로젝트라면 이 예에서 상정한 간단한 프로젝트는 아닙니다.