Software Architect/LINUX

출처  : http://greenfishblog.tistory.com/150


일반적인 빌드 환경(예, Makefile(gcc) / Visual C++, ...) 작성시 다음과 같은 요구 사항이 있습니다.

- DEBUG / RELEASE 컴파일 옵션과 빌드 경로 분리
- 필요한 부분만 re-compile하는 dependency 처리
- Main source 경로 이외의 다른 경로의 source를 함께 빌드
- 각각의 Group 별로 컴파일 설정 관리
- compile 전처리와 후처리의 분리

보통 Visual C++나 QtCreator같은 IDE 툴에서는 프로젝트 셋팅등을 통해 간편하게 위 요구사항을 만족할 수 있지만, Makefile을 이용하는 경우는 생각보단 간단하지 않습니다. 그리고, 잘 정리된 문서 또한 쉽게 구할 수 없습니다. 즉, DIY, 알아서 하라! 입니다.

본 포스트에서는 위 요구사항 뿐 아니라, 일상적인 빌드 환경에서도 요구되는 사항을 만족하는 Makefile 예제를 공유합니다. 해당 Makefile을 통해 Makefile의 구조 학습 뿐만 아니라, 기능을 확장하는데 도움이 되었으면 좋겠습니다. (ps, 보통의 GNU open source는 통상 많은 platform의 지원과 여라가지 이유로 인해 autoconf(./configure)를 사용하지만, 본 포스트는 Makefile에 한해서만 설명합니다. 또한 dist와 install target은 고려하지 않습니다.)

본 포스트의 내용은 길어질 수 있는데, Makefile의 개념 정리 보다, 빨리 개발을 해야 하는 경우에는, 아래의 makefile_sample.zip과 그 안의 Makefile을 확인하면 됩니다. 그리고, 아래쪽의 "응용" 부분으로 바로 넘어가셔도 될 듯 합니다.

예제

우선 이해를 돕기 위해 예제로 부터 시작하겠습니다. 위 sample은 다음과 같은 구성으로 되어 있습니다.

그리고 some_source 경로는 다음과 같습니다.

File1.cpp에 main()가 있으며, 각 File2.c, File3.cpp, File4.cpp에는 foobar2(), foobar3(), foobar4()를 호출합니다. 그리고 defines.h는 FileN.cpp에서 각각 #include하고 있으며, File3.cpp에서는 defines_2nd.h를 #include하는데, 해당 defines_2nd.h는 defines.h를 #include하고 있습니다. 이는 Makefile의 dependency 체크를 위한 테스트를 위한 용도로 설계되었습니다. 위 설명을 그림으로 도식하면 다음과 같습니다.

이러한 .cpp 파일 4개(2개+2개)를 빌드하는 Makefile은 다음과 같습니다.

그리고, 본 포스트에서 예를 들어 설명할 Makefile은 다음과 같습니다.

###############################
# Makefile ver 0.1 by greenfish
###############################
#
# make                          : debug 빌드
# make debug                    : debug 빌드 
# make release                  : release 빌드
# make clean [debug, release]   : 해당 빌드 경로 파일 삭제
# make rebuild [debug, release] : rebuild 실행

# 빌드의 기본값은 debug이며,
# 이곳 section에서는 debug와 release의 공통 부분을 정의한다.
# CFLAGS와 CPPFLAGS는 -c 옵션은 제외한다. rule에서 직접 -c를 넣어야 한다.
# LFLAGS는 Link 옵션으로 필요시 추가한다.
# config는 configuration으로 debug/release를 구별하며, 빌드 경로값에 영향을 준다.
# BUILD_DIR는 빌드에 사용될 경로를 지정한다.
# TARGET은 최종 빌드의 산출물을 지정한다. 빌드 상대 경로를 포함시킨다.
CC        = g++
CFLAGS    = -I/usr/local/include
CPPFLAGS  = -I/usr/local/include
LFLAGS    =
CONFIG    = debug
BUILD_DIR = ./Build/$(CONFIG)
TARGET    = $(BUILD_DIR)/Execute

##################################
# makefile에 들어온 argument를 구한다.
ifneq "$(findstring clean, $(MAKECMDGOALS))" ""
    ARG.CLEAN  = 1
endif

ifneq "$(findstring release, $(MAKECMDGOALS))" ""
    ARG.RELEASE = 1
endif

ifneq "$(findstring debug, $(MAKECMDGOALS))" ""
    ARG.DEBUG = 1
endif

ifneq "$(findstring rebuild, $(MAKECMDGOALS))" ""
    ARG.REBUILD = 1
endif

##################################
# DEBUG / RELEASE 빌드를 설정한다.
ifeq ($(ARG.RELEASE),1)
    # ------------------------------
    # release에 특화된 설정을 한다.
    # ------------------------------
    CFLAGS    += -O -DNDEBUG
    CPPFLAGS  += -O -DNDEBUG
    CONFIG    = release
else
    # ------------------------------
    # debug에 특화된 설정을 한다.
    # ------------------------------
    CFLAGS    += -DDEBUG -g
    CPPFLAGS  += -DDEBUG -g
    CONFIG    = debug
endif

##################################
# makefile 실행 처리

.PHONY: debug release build clean rebuild PRE_BUILD POST_BUILD all

# 일반적인 Build의 스텝을 결정한다.
BUILD_STEP = PRE_BUILD $(TARGET) POST_BUILD

# Makefile의 최종 Target과 Depend를 결정한다.
ifeq ($(ARG.REBUILD),1)
    # rebuild인 경우,...
    # 빌드 이전에 clean을 수행한다.
    rebuild: | clean $(BUILD_STEP)
    debug: ; @true
    release: ; @true
else ifeq ($(ARG.CLEAN),1)
    # clean인 경우,...
    # clean target은 rule part에 정의되어 있다.
    release: ; @true
    debug: ; @true
else
    # clean/rebuild와 함께 쓰이지 않은 경우,...
    # 이곳에서 빌드가 이뤄진다.
    # release, debug는 단독 사용할 수 있어 @true하지 않는다.
    build: | $(BUILD_STEP)
    release: build
    debug: build
endif

# release, debug가 명령에 포함되어 조합되면,
# release, debug target을 찾게 되는데,
# 의도하지 않은
#    "make: Nothing to be done for 'release'"
#    "make: Nothing to be done for 'debug'"
# 를 방지하기 위해 @true를 사용하였다.

# ------------------------------
# Other macro
# ------------------------------
# .o를 .d로 바꿔준다.
DEPEND_FILE = $(patsubst %.o,%.d,$@)

##################################
# 빌드 항목 구성
##################################

# Group은 빌드 환경 설정(컴파일 옵션, 빌드 경로, 소스 확장자, ...)이 같은 것을 묶은 것을 말한다.

#--------------------------------
# 주의 : ../../ 와 같이 상위 directory로 넘어가는 source는 가져오면 안된다.
# 왜냐하면, ./Build/ 밑으로 모든 빌드 파일이 들어가야 하는데,
# ../../등이 포함되면 ./Build/ 상위로 빌드 파일이 들어갈 수 있기 때문에, 위험하다.
# 즉, 다음의 기본 가정을 지켜야 한다.
# `makefile의 위치는 모든 빌드되는 source의 최상위 경로에 있어야 한다.'

##################################
# Group 01
GROUP.01.SRC = File1.cpp               \
               ./some_source/File3.cpp \
               ./some_source/File4.cpp
GROUP.01.OBJ = $(addprefix $(BUILD_DIR)/,$(GROUP.01.SRC:.cpp=.o))
GROUP.01.DEP = $(GROUP.01.OBJ:.o=.d)

##################################
# Group 02
GROUP.02.SRC = File2.c
GROUP.02.OBJ = $(addprefix $(BUILD_DIR)/,$(GROUP.02.SRC:.c=.o))
GROUP.02.DEP = $(GROUP.02.OBJ:.o=.d)

##################################
# [Link] Link Part
# 빌드된 .o 들을 링크하여 TARGET을 생성한다. 
$(TARGET): $(GROUP.01.OBJ) $(GROUP.02.OBJ)
	@echo ----------------------------------------
	@echo Link : $(TARGET)
	@echo ----------------------------------------
	$(CC) $(LFLAGS) $(GROUP.01.OBJ) $(GROUP.02.OBJ) -o $(TARGET)
	@echo

##################################
# [Compile] Compile Part
# Group1 소스(*.cpp)들에 대해, 대상경로($(@D))를 생성하고, dependency 파일을 생성한뒤 컴파일 한다.
$(GROUP.01.OBJ): $(BUILD_DIR)/%.o: %.cpp
	@echo ----------------------------------------
	@echo Compile : [GROUP.01] $<
	@echo ----------------------------------------
	@test -d $(@D) || mkdir -p $(@D)
	$(CC) -MM -MF $(DEPEND_FILE) -MT"$(DEPEND_FILE:.d=.o)" $(CFLAGS) $<
	$(CC) $(CFLAGS) -c -o $@ $<
	@echo

$(GROUP.02.OBJ): $(BUILD_DIR)/%.o: %.c
	@echo ----------------------------------------
	@echo Compile : [GROUP.02] $<
	@echo ----------------------------------------
	@test -d $(@D) || mkdir -p $(@D)
	$(CC) -MM -MF $(DEPEND_FILE) -MT"$(DEPEND_FILE:.d=.o)" $(CFLAGS) $<
	$(CC) $(CFLAGS) -DSPECIAL_DEFINE -c -o $@ $<
	@echo
	
##################################
# Build가 시작되었다.
# 필요한 전처리가 있다면 구현한다.
PRE_BUILD:
	@echo ========================================
	@echo Make file started [config =\> $(CONFIG)]
	@echo ========================================
	@echo 

##################################
# Build가 종료되었다.
# 필요한 후처리가 있다면 구현한다.
POST_BUILD:
	@echo ========================================
	@echo Make file finished [config =\> $(CONFIG)]
	@echo ========================================

##################################
# Clean up 한다.
clean:
	rm -f $(GROUP.01.OBJ)
	rm -f $(GROUP.01.DEP)
	rm -f $(GROUP.02.OBJ)
	rm -f $(GROUP.02.DEP)
	rm -f $(TARGET)
	@echo ----------------------------------------
	@echo Clean work finished [config =\> $(CONFIG)]
	@echo ----------------------------------------

##################################
# 끝 부분에 dependency 파일들을 include한다.
-include $(GROUP.01.DEP)
-include $(GROUP.02.DEP)

##########################################################
# document
##########################################################
#
# Group 추가 요령
#   - 추가한다는 것은 다른 Group과 비교하여 서로간에,
#       ; 빌드 환경이 다른 경우 (컴파일 옵션, 빌드 경로, 소스 확장자, ...) 
# 가 발생하였기 때문이다.
#
# 1. Group 정의 추가
#
# Group 선언부에 아래 부분을 추가한다.
# 만일 여러 파일이 동일한 sub directory에 있는 경우 addprefix를 이용할 수 있다.
#
# ##################################
# # Group 03
# GROUP.03.SRC = G1.cpp G2.cpp
# GROUP.03.SRC.TMP = G3.cpp G4.cpp G5.cpp G6.cpp G7.cpp
# GROUP.03.SRC += $(addprefix ./some_group/, $(GROUP.03.SRC.TMP))
# GROUP.03.OBJ = $(addprefix $(BUILD_DIR)/,$(GROUP.03.SRC:.cpp=.o))
# GROUP.03.DEP = $(GROUP.03.OBJ:.o=.d)
#
# 2. Link 추가
#
# Target에 아래와 같이 GROUP.03.OBJ를 depend하여 Link되도록 한다.
#
# $(TARGET): $(GROUP.01.OBJ) $(GROUP.02.OBJ) $(GROUP.03.OBJ)
#
# 3. Compile 추가
#
# 아래와 같은 규칙을 추가한다. 확장자나 컴파일 옵션등을 따로 설정할 수 있다.
#
# $(GROUP.03.DEP): $(BUILD_DIR)/%.d: %.cpp
#	@echo ----------------------------------------
#	@echo Depend : $<
#	@echo ----------------------------------------
#	@test -d $(@D) || mkdir -p $(@D)
#	$(CC) -MM -MF $(DEPEND_FILE) -MT"$(DEPEND_FILE:.d=.o)" $<
#	$(CC) $(CFLAGS) -c -o $@ $<
#	@echo 
#
# 4. clean 추가
#
#  	rm -f $(GROUP.03.OBJ)
# 	rm -f $(GROUP.03.DEP)
#
# 5. include 추가
#
# -include $(GROUP.02.DEP)
#
# * 지원
# make
# make debug
# make release
# make clean [release, debug]
# make rebuild [release, debug]
#
# (EOF)

위 Makefile을 이용하여 빌드하면 다음과 같습니다.

make
========================================
Make file started [config => debug]
========================================

----------------------------------------
Compile : [GROUP.01] File1.cpp
----------------------------------------
g++ -MM -MF Build/debug/File1.d -MT"Build/debug/File1.o" File1.cpp
g++ -I/usr/local/include -DDEBUG -g -c -o Build/debug/File1.o File1.cpp

----------------------------------------
Compile : [GROUP.01] ./some_source/File3.cpp
----------------------------------------
g++ -MM -MF Build/debug/./some_source/File3.d -MT"Build/debug/./some_source/File3.o" ./some_source/File3.cpp
g++ -I/usr/local/include -DDEBUG -g -c -o Build/debug/./some_source/File3.o ./some_source/File3.cpp

----------------------------------------
Compile : [GROUP.01] ./some_source/File4.cpp
----------------------------------------
g++ -MM -MF Build/debug/./some_source/File4.d -MT"Build/debug/./some_source/File4.o" ./some_source/File4.cpp
g++ -I/usr/local/include -DDEBUG -g -c -o Build/debug/./some_source/File4.o ./some_source/File4.cpp

----------------------------------------
Compile : [GROUP.02] File2.c
----------------------------------------
g++ -MM -MF Build/debug/File2.d -MT"Build/debug/File2.o" File2.c
g++ -I/usr/local/include -DDEBUG -g -DSPECIAL_DEFINE -c -o Build/debug/File2.o File2.c

----------------------------------------
Link : ./Build/debug/Execute
----------------------------------------
g++  ./Build/debug/File1.o ./Build/debug/./some_source/File3.o ./Build/debug/./some_source/File4.o ./Build/debug/File2.o -o ./Build/debug/Execute

========================================
Make file finished [config => debug]
========================================
./Build/debug/Execute
foobar, define value=1
foobar 2, define value=1
foobar 3, 2nd value=2
foobar 4, define value=1
 

즉, 위와 같이 빌드가 진행되었으며, foobar, foobar 2, ..., foobar 4까지 제대로 출력됨을 확인하였습니다.
전체적으로 대략 Compile -> Link 순서로 빌드가 진행됨이 확인됩니다.
예제 Makefile은 다음 옵션을 지원합니다.

  • make [debug, release]
    ; 각 빌드(debug, release) 설정으로 빌드한다. 기본값으로 debug 이다.
  • make clean [debug, release]
    ; 각 빌드(debug, release) 경로를 clean한다. 기본값으로 debug 이다.
  • make rebuild [debug, release]
    ; 각 빌드(debug, release) 설정으로 clean 후 다시 빌드한다. 기본값으로 debug이다.

그리고 빌드하면 다음과 같은 구조를 가지게 됩니다.

/tmp/makefile_sample/Build/debug$ ls -laR
.:
total 40
drwxr-xr-x 3 greenfish greenfish 4096 2013-01-28 09:48 .
drwxr-xr-x 3 greenfish greenfish 4096 2013-01-28 09:48 ..
-rwxr-xr-x 1 greenfish greenfish 9819 2013-01-28 09:48 Execute
-rw-r--r-- 1 greenfish greenfish   41 2013-01-28 09:48 File1.d
-rw-r--r-- 1 greenfish greenfish 2844 2013-01-28 09:48 File1.o
-rw-r--r-- 1 greenfish greenfish   39 2013-01-28 09:48 File2.d
-rw-r--r-- 1 greenfish greenfish 2640 2013-01-28 09:48 File2.o
drwxr-xr-x 2 greenfish greenfish 4096 2013-01-28 09:48 some_source

./some_source:
total 24
drwxr-xr-x 2 greenfish greenfish 4096 2013-01-28 09:48 .
drwxr-xr-x 3 greenfish greenfish 4096 2013-01-28 09:48 ..
-rw-r--r-- 1 greenfish greenfish  114 2013-01-28 09:48 File3.d
-rw-r--r-- 1 greenfish greenfish 2672 2013-01-28 09:48 File3.o
-rw-r--r-- 1 greenfish greenfish   85 2013-01-28 09:48 File4.d
-rw-r--r-- 1 greenfish greenfish 2676 2013-01-28 09:48 File4.o

즉, debug에 대한 빌드 경로는 /Build/debug이며, 그 형식은 소스 경로와 동일합니다. 즉, 소스에 있던 some_source directory가 빌드 경로에도 그대로 반영됩니다(이는 각 경로에 동일한 파일명에 대한 처리를 위해서 입니다). 그리고 빌드 경로의 root에 최종 산출물인 실행 가능한 파일(Execute)가 있습니다. 이러한 구조는 release 빌드에서도 동일하게 반경됩니다.

Makefile Layout

Makefile은 http://www.gnu.org/software/make/manual/make.html(한글)백서를 통해 학습할 수 있습니다. 사전지식은 해당 링크에서 확인하시기 바랍니다. DIY!!!
우선, 위 공유한 Makefile의 전체적인 layout은 다음과 같습니다.

configuration part에서는 빌드를 위한 변수 선언 및 환경 설정을 하게 됩니다. 그다음으로 Makefile에서 가장 중요한 rule part로 진행됩니다.

규칙

makefile의 규칙(rule)은 대략 다음과 같은 형식으로 나열됩니다.

OUTPUT_NAMEINPUT_NAME01 INPUT_NAME02 INPUT_NAME03 ...
    ... script code (tab키로 시작)

Makefile의 백서에서는 targets : prerequisites로 표현됩니다. 위에서는 다소 다르게 표현한 이유는, input/output의 개념으로 설명하려고 했기 때문입니다. 즉, INPUT_NAMExx 등등의 항목으로 OUT_NAME이 형성됩니다. 즉, OUTPUT_NAME이 만들어지기 위해서는 INPUT_NAMExx가 필요하다는 뜻입니다. 그리고 이는, 현재 OUTPUT_NAME이 만들어져있고, INPUT_NAMExx가 변경이 없었다면, OUTPUT_NAME의 처리는 skip된다는 뜻도 있습니다. 그리고, 위와 같이 Makefile에 있는 OUTPUT_NAME 규칙은 실제로 언제 실행되냐면, 빌드 Target이 OUTPUT_NAME을 필요로 할때 실행이 됩니다. 즉,

MAINOUTPUT_NAME
    ... script code (tab키로 시작)

OUTPUT_NAMEINPUT_NAME01 INPUT_NAME02 INPUT_NAME03 ...
    ... script code (tab키로 시작)

main.omain.cpp main.h function.cpp

위와 같은 상황에서 make MAIN을 실행하게 되면, OUTPUT_NAME이 실행하게 됩니다. 만일 make XXXX를 실행한다면,MAIN과 OUTPUT_NAME은 실행되지 않으며, XXXX가 없기 때문에 다음 오류가 발생합니다.

$ make XXXX
make: *** No rule to make target `XXXX'.  Stop.

OUTPUT_NAME이 실행되려면 INPUT_NAME01, INPUT_NAME02...이 준비가 되어야 합니다. 즉, INPUT_NAMExx에 해당되는 rule을 찾아 먼저 실행하게 됩니다.
여기에서, OUTPUT에 해당되는 OUTPUT_NAME은 일반적인 Target 이름이 되는데, Target 이름이 아니더라도, 파일 이름이 되기도 합니다. 즉, main.o는 main.cppmain.hfunction.cpp를 가지고 실행하게 됩니다. 역시 다시 말하자면 해당 .cpp/.h의 변경 사항이 있을때 main.o에 해당되는 rule이 실행됩니다.
그리고 예제 Makefile에는 다음과 같은 중요한 암묵적 규칙(implicit rule, 백서)이 포함됩니다.
이러한 암묵적 규칙은 다음과 같이 설명됩니다.

a.o b.o c.o%.o: %.c
    ... $@ ... $<

알기 힘든 암호같은 문장으로 되어 있는데, 우선 a.o b.o c.o는 %.o: %c라는 패턴으로 동작하게 됩니다. %는 wildcard와 같이 이해하면 되는데, .o->.c 즉, a.o b.o c.o는 a.c b.c c.c 파일의 존재여부를 찾아 실행하게 됩니다. 그리고 $@는 *.o에 해당되고$<는 *.c에 해당됩니다. 이곳이 뭔가 이해나 가독성이 떻어진다면, 다음과 같이 이해하면 됩니다.
우선 제일 왼쪽의 a.o b.o c.o는 이것들을 "생산"한다라고 이해하면 됩니다. 즉, OUTPUT에 해당되니깐요. 그리고, %.o:%.c인데, 이것 또한 OUPUT: INPUT이라 보면 됩니다. 즉, .c를 가지고 .o를 "생산"한다고 보시면 됩니다. 그리고 아래에 있는$@와 $<인데, @는 o와, 그리고 <는 c와 모양이 비슷합니다. 즉, 각각의 .o에 해당 되는 파일은 $@에 해당되고, .c 파일은 $<에 연관된다고 보면 이해가 빠를 것입니다.
다시 설명하자면 위 암묵적 규칙은 다음과 같이 확장된다고 생각하면 됩니다. (이렇게 설명하는 것이 옳은지 모르겠습니다. 단순히 이해를 돕는 차원입니다.)

a.oa.c
    ... a.o ... a.c
b.ob.c
    ... b.o ... b.c
c.oc.c
    ... c.o ... c.c 

이러한 암묵적 규칙을 이용하면 효과적인 빌드 환경을 구축하는데 도움을 줍니다.
물론, 위는 다음과 같이 일반적인 규칙으로 사용할 수 있습니다. 이렇게 되면 한번의 gcc 호출로 여러개의 .o를 만들게 됩니다.

a.o b.o c.oa.c b.c c.c
    ... a.o b.o c.o ... a.c b.c c.c 

장/단점을 따질 수는 없지만, 저같은 경우에는 여러번 gcc를 실행하되 한번에 한개의 파일을 compile하는 편을 선호해서 암묵적 규칙으로 Makefile을 구성하였습니다. 어떻든 선택은 자유입니다. (저는 효율보다는 효과를 중요시 합니다.)

dependency

위 예제에서, define_2nd.h는 File3.cpp에서만 사용되는 header file입니다. 해당 파일이 변경되면 당연히 다시 빌드를 해야 합니다. gcc를 사용하는 Makefile은 다음과 같이 .d라는 dependency 파일을 만들면 자동으로 해결할 수 있습니다.

make clean
(...생략...)
----------------------------------------
Clean work finished [config => debug]
----------------------------------------
make
========================================
Make file started [config => debug]
========================================

----------------------------------------
Compile : [GROUP.01] File1.cpp
----------------------------------------
g++ -MM -MF Build/debug/File1.d -MT"Build/debug/File1.o" File1.cpp
g++ -I/usr/local/include -DDEBUG -g -c -o Build/debug/File1.o File1.cpp

----------------------------------------
Compile : [GROUP.01] ./some_source/File3.cpp
----------------------------------------
(...생략...)
========================================
Make file finished [config => debug]
========================================

즉 위와 같이 빌드를 진행합니다.
그리고 아무런 코드 수정없이 다시 make를 하면 아래와 같이 빌드과정 없이 바로 종료하게 됩니다.

make
========================================
Make file started [config => debug]
========================================

========================================
Make file finished [config => debug]
========================================

그런다음, File3.cpp에서 사용하고 있는 define_2nd.h를 수정하겠습니다. 이는 linux의 touch를 이용하는 것으로, 실제로 내용 수정은 없지만 File의 timestamp를 변경하여 수정하는 것으로 하였습니다.

$ touch defines_2nd.h

그런다음 아래와 같이 다시 make를 하면, File3.cpp만 컴파일한뒤 링크를 실행하게 됩니다.

make
========================================
Make file started [config => debug]
========================================

----------------------------------------
Compile : [GROUP.01] some_source/File3.cpp
----------------------------------------
g++ -MM -MF Build/debug/./some_source/File3.d -MT"Build/debug/./some_source/File
3.o" some_source/File3.cpp
g++ -I/usr/local/include -DDEBUG -g -c -o Build/debug/./some_source/File3.o some
_source/File3.cpp

----------------------------------------
Link : ./Build/debug/Execute
----------------------------------------
g++  ./Build/debug/File1.o ./Build/debug/./some_source/File3.o ./Build/debug/./s
ome_source/File4.o ./Build/debug/File2.o -o ./Build/debug/Execute

========================================
Make file finished [config => debug]
========================================

이는 gcc(g++)의 -MM -MF -MT 명령을 이용해 dependency 파일을 만들어 Makefile에서 연결했기 때문에 가능한 일입니다. 해당 파일(.d)은 다음과 같습니다.

cat ./Build/debug/some_source/File3.d
Build/debug/./some_source/File3.o: some_source/File3.cpp \
 some_source/../defines_2nd.h some_source/../defines.h

즉, File3.o는 File3.cpp, defines_2nd.h, 그리고 define.h로 구성된다고 알려주는데, Makefile의 규칙 표현 방식으로 구성됩니다. 그래서 Makefile의 마지막에 해당 .d들을 include하여 Makefile의 규칙을 확장하게 됩니다. 참고로, include 앞에있는 -는 해당 .d가 없더라도 오류없이 넘어가도록 해주는 장치입니다.

##################################
# 끝 부분에 dependency 파일들을 include한다.

-include $(GROUP.01.DEP)
-include $(GROUP.02.DEP)

Makefile로 빌드시 즉, compile할 때, File3.o를 Target으로 만드는 과정이 있는데, 해당 .d를 include하였기 때문에, 위 규칙(즉, File3.o 생성에 대한)이 실행되기전, dependency 체크가 이뤄지게끔 하는 것입니다. 만일 include하지 않았다면 빌드 오류는 없겠지만, define_2nd.h가 아무리 변경되더라도 File3.o나 Execute파일을 다시 만들어 주지 못합니다. 물론 암묵적 규칙의 %.c를 통해 File3.cpp가 변경되는 경우에는 include없이도 File3.o와 Execute파일을 다시 만들수는 있습니다.
해당 dependency 파일은 compile전, 즉, gcc -c ...를 실행하기 전에 미리 실행하게 됩니다. (그 순서는 사실 크게 중요하지 않습니다). 따라서, 규칙의 실행은 아래와 같이 dependency 파일 생성과 compile이 batch로 동작하는 방식입니다.

$(GROUP.01.OBJ)$(BUILD_DIR)/%.o%.cpp
    $(CC) -MM -MF $(DEPEND_FILE) -MT"$(DEPEND_FILE:.d=.o)" $<
    $(CC) $(CFLAGS) -c -o $@ $<


group

group은 Makefile 백서에 등장하지 않는 개념입니다. 즉, 제가 만든 형식입니다. group은 동일한 규칙을 받아 들이는 소스 파일 리스트로 정의하였습니다. 즉, 컴파일 옵션이나 대상 경로, 그리고 파일 확장자가 같다면 동일한 group으로 인정합니다. 즉, group은 다음과 같이 선언됩니다.

##################################
# Group 01
GROUP.01.SRC = File1.cpp               \
               ./some_source/File3.cpp \
               ./some_source/File4.cpp
GROUP.01.OBJ = $(addprefix $(BUILD_DIR)/,$(GROUP.01.SRC:.cpp=.o))
GROUP.01.DEP = $(GROUP.01.OBJ:.o=.d)

이와 같이 group은 소스 파일을 입력하고, 그 파일을 기반으로 object 파일과 dependency 파일의 경로 선언으로 이뤄져 있습니다(주의점은, .h와 같이 object를 생성하지 못하는 파일을 넣어서는 안됩니다. 그래서 .h의 변경사항시 재빌드를 만들기 위해 앞선 chapter와 같이 dependency를 명시적으로 만들어 넣게 되었습니다). 즉, 사용자는 단순히 "소프(.c, .cpp) 파일 리스트 추가" 작업만 진행하면 됩니다!
암묵적 규칙의 특징으로, 하나의 규칙은 하나의 동일한 확장자만 입력받도록 되어 있습니다(백서를 보면, OBJ%.o:: 혹은 OBJ:%o%로 설명되어 있는데, 빌드 경로가 분리되어서 그런지, 아니면 dependency 파일이 include되어서 그런지, 실제로 동작되지 않더군요. 혹시 가능하게 만들 수 있을까요?). 위 예에서는 File2.c가 예외 case로, 확장자가 .c이기 때문에 GROUP.01.OBJ에 해당되는 규칙으로는 실행되지 않습니다. 그래서 빌드 오류가 발생하게 됩니다. 따라서, GROUP.02.OBJ를 만들어 따로 관리하여 만들게 되었습니다. 또한 위 예에서, GROUP.02.OBJ를 컴파일할때는 -DSPECIAL_DEFINE를 추가하여 빌드하였습니다. 이런 것들이 GROUP.01과 GROUP.02를 구별하게 하는 요소입니다.

make 실행과 debug, release 분리

make가 실행되면 해당 경로의 makefile 혹은 Makefile을 찾게 됩니다. make 뒤에는 Makefile이 실행할 main target을 옵션으로 명시할 수 있습니다. 또는 Makefile에 전달할 변수를 전달할 수 있습니다. 따라서, make를 실행은 다음과 같은 모습일 겁니다.

make debug
...
make DEBUG=1
...

즉, 첫번째 방법은 makefile내의 debug target을 실행해라는 것이고, 두번째 방법은 DEBUG 변수값에 1을 전달하여 실행해라는 것으로, 저는 첫번째 방법으로 구성하였습니다(보다 사람에게 친숙한 표현이니깐요).
우선, makefile의 선두의 configuration 부분을 보겠습니다.

CC        = g++
CFLAGS    = -I/usr/local/include
CPPFLAGS  = -I/usr/local/include
LFLAGS    =
CONFIG    = debug
BUILD_DIR = ./Build/$(CONFIG)
TARGET    = $(BUILD_DIR)/Execute

CFLAGS나 CPPFLAGS등은 google을 통해 확인하십시요. 여기에서 중요한건 CONFIG와 BUILD_DIR입니다. 즉, CONFIG는 debug이냐 release이냐를 판단하는 부분입니다. 현재는 debug, release만 지원하는데, 그 이외에도 static_release와 같이 여러개를 만들 수 있습니다. 물론, 그에 따라 makefile도 수정해야 겠죠. 일단 기본값은 debug이며 빌드의 경로는 해당 config 값으로 결정되도록 하였습니다.
그다음 부분은 아래와 같습니다.

##################################
# makefile에 들어온 argument를 구한다.

ifneq "$(findstring clean, $(MAKECMDGOALS))" ""
    ARG.CLEAN  = 1
endif

ifneq "$(findstring release, $(MAKECMDGOALS))" ""

    ARG.RELEASE = 1
endif
...

즉, makefile로 들어온 argument 값을 저장하고 있습니다. 만일 make clean이 실행되었다면 ARG.CLEAN은 1이 됩니다.

##################################
# DEBUG / RELEASE 빌드를 설정한다.

ifeq ($(ARG.RELEASE),1)
    # ------------------------------
    # release에 특화된 설정을 한다.
    # ------------------------------

    CFLAGS    += -O -DNDEBUG
    CPPFLAGS  += -O -DNDEBUG
    CONFIG    = release
else
    # ------------------------------
    # debug에 특화된 설정을 한다.
    # ------------------------------

    CFLAGS    += -DDEBUG -g
    CPPFLAGS  += -DDEBUG -g
    CONFIG    = debug
endif

위 부분에서 debug와 release의 구분을 만들고 있습니다. 컴파일 옵션을 변경하는데, debug에서는 debug관련 설정을 추가하고, release에서는 release관련 설정을 넣었습니다. 역시 중요한건 CONFIG로 release인 경우에는 release 값을 전달하여 빌드 경로가 ./Build/release가 될 것입니다.

##################################
# makefile 실행 처리

.PHONY: debug release build clean rebuild PRE_BUILD POST_BUILD all

# 일반적인 Build의 스텝을 결정한다.

BUILD_STEP = PRE_BUILD $(TARGET) POST_BUILD

#
 Makefile의 최종 Target과 Depend를 결정한다.
ifeq ($(ARG.REBUILD),1)
    # rebuild인 경우,...
    # 빌드 이전에 clean을 수행한다.

    rebuild: | clean $(BUILD_STEP)
    debug: ; @true
    release: ; @true
else ifeq ($(ARG.CLEAN),1)
    # clean인 경우,...
    # clean target은 rule part에 정의되어 있다.

    release: ; @true
    debug: ; @true
else
    # clean/rebuild와 함께 쓰이지 않은 경우,...
    # 이곳에서 빌드가 이뤄진다.
    # release, debug는 단독 사용할 수 있어 @true하지 않는다.

    build: | $(BUILD_STEP)
    release: build
    debug: build
endif

우선 phony를 지정하고 있는데, 해당 target은 file과는 상관없는 의도적으로 지정된 이름으로 사용하겠다는 것입니다. 그리고, BUILD_STEP을 만들었는데, 선처리, 후처리가 포함되었습니다. makefile의 최종 산출물은 $(TARGET) 규칙이기 때문에, 해당 rule이 중간에 포함되었습니다.
그리고 make rebuild 실행을 처리하는 부분이 있는데, rebuild라는 규칙을 새로 정의하였습니다. 많일 해당 규칙을 정의하지 않으면 make rebuild를 실행하면, rebuild 규칙이 없다고 오류가 발생할 것입니다. 그리고 규칙의 input에 pipe(|) 문자를 넣었는데, 이는 규칙의 순서를 명시적으로 지켜라라는 의미입니다. 즉, clean -> $(BUILD_STEP)으로 진행해라는 뜻입니다. 만일 pipe 문자가 없다면 makefile이 병렬로 처리될 때 문제(즉, 순서가 바뀌는)가 발생할 수 있습니다.
그리고 특이한건, semi-colon(;)의 사용인데, 세미콜론 뒤의 내용은 규칙의 recipe(실행 스크립트)를 의미합니다. 즉, 규칙을 한줄로 표현한 것입니다. 또, 연속해서 @true가 사용되었는데, 해당 규칙이 의미있게 실행되었다고 밝히는 것으로 이해하면 됩니다. 만일, 아래와 같이 정의되었다면,

ifeq ($(ARG.REBUILD),1)
    # rebuild인 경우,...
    # 빌드 이전에 clean을 수행한다.

    rebuild: | clean $(BUILD_STEP)
    debug: rebuild
    release: rebuild

다음과 같이 "make: Nothing to be done for 'release'"와 같은 경고가 발생할 수 있습니다.

make rebuild release
...
make: Nothing to be done for 'release'

현재 make뒤에 따라온 release와 debug는 실제로 의미있는 target은 아닙니다. 그냥 상태값을 전달해 주기 위한 용도로 사용되었습니다. 만일 make뒤에 release가 실행되었는데, release 규칙은 있어서 실행하였지만 아무런 스크립트가 실행되지 않았을 경우에 위 경고가 발생하는 것입니다. 즉, 위에서 release: rebuild로 선언하였는데, $ makefile rebuild release가 실행되면 makefile에 전달된 rebuild는 의미있게 스크립트로 연결되어 실행이 되었는데, release는 이미 실행된 rebuild로 또 연결되어, 아무런 영향을 주지 못하기 때문입니다. 그래서, release 규칙은 @true라는 지시자로 연결하였는데, 이 경우는 make에 "잘 수행 하였다"를 전달해 주기 때문에 위 경고가 표시되지 않습니다. 향후 makefile의 명령을 추가하려면 이와 같은 사항을 유의하시기 바랍니다.

응용

예제로 등록된 makefile을 응용하는 방법을 공유하겠습니다.

일반적으로 보통 빌드환경에서 거의 모든 컴파일 옵션은 동일할 것입니다. 그래서 왠만해서는 GROUP의 개수는 1개로 유지될 것입니다. 만일 빌드에 파일(.c / .cpp)이 추가되었을 경우, 

GROUP.01.SRC = File1.cpp               \
               ./some_source/File3.cpp \
               ./some_source/File4.cpp \
               ./File999.cpp

와 같이 GROUP.01.SRC의 값만 변경하면 됩니다.  보통 이 정도의 수준으로 진행될 것입니다.
물론, CFLAGS나 CPPFLAGS 설정은 사용자가 직접 검토해 보시기 바랍니다.

만일, SQLITE와 같이 확장자가 .c로 구성되거나, 특정 소스들에게만 -DSQLITE_CORE와 같은 것이 포함되어야 한다면, GROUP 추가를 고려해야 합니다. 추가하는 방법은 예제 Makefile 끝 부분 주석을 참고하시면 됩니다.

흔하지 않겠지만, 만일 debug, release 말고 static_release같은 것이 추가되어야 한다면 대략 다음과 같은 부분이 추가로 들어갈 것입니다.

ifneq "$(findstring static_release, $(MAKECMDGOALS))" ""
    ARG.STATIC_RELEASE = 1
endif

...

else ifeq ($(ARG.STATIC_RELEASE),1)
    CFLAGS    += something...
    CPPFLAGS  += something...
    LFALGS    += something...
       
    CONFIG    = static_release
endif

...

.PHONY: ... static_release ...

...

ifeq ($(ARG.STATIC_RELEASE),1)
    static_release: SOMETHING rule targets
    ...

...

GROUP.STATIC_RELEASE.SRC = GROUP.01.SRC
GROUP.STATIC_RELEASE.SRC += ...

...

와 같은 과정이 필요할거 같습니다. 명령이 들어왔는지 검토하는 부분은 반듯이 있어야 할 것이며, 그에 따라 CONFIG를 설정하여 빌드 경로를 분리합니다. 또한 static이라면 LFLAGS도 수정될 수 있습니다. 소스 파일 리스트의 차이가 있거나 컴파일 환경등이 다른 경우에 대비하여 새로운 GROUP을 만드는 것도 좋은 방법일 듯 합니다.

$(GROUP.STATIC_RELEASE.OBJ): $(BUILD_DIR)/%.o: %.c
    @echo ----------------------------------------
    @echo Compile : [GROUP.STATIC_RELEASE] $<
    @echo ----------------------------------------
    @test -d $(@D) || mkdir -p $(@D)
    $(CC) -MM -MF $(DEPEND_FILE) -MT"$(DEPEND_FILE:.d=.o)" $<
    $(CC) $(CFLAGS) -DSTATIC_LIB -c -o $@ $<
    @echo

위와 같이 이전의 group과 다른 옵션이 들어갈 수 있습니다.
만일 $(TARGET)의 input이 달라져야 한다면, 다음과 같이 수정할 수 있습니다.

...
TARGET     = $(BUILD_DIR)/Execute
TARGET_DEP = $(GROUP.01.OBJ) $(GROUP.02.OBJ)
...
else ifeq ($(ARG.STATIC_RELEASE),1)
    TARGET     = $(BUILD_DIR)/static_lib
    TARGET_DEP = $(GROUP.STATIC_RELEASE.OBJ)
...
$(TARGET): $(TARGET_DEP)
...

즉, 위와 같이 하면, static_release인 경우에만, $(TARGET)의 dependency를 달리 하여, 다른 빌드에는 영향은 없을 것입니다.

Revision

First :

R2 : 2013/02/04

depend 파일 (.d)를 만드는 과정에서 compile 에러가 발생할 소지가 있습니다. 왜냐하면, depend 파일을 만들때 compile 옵션이 모두 포함되지 않았기 때문입니다. 만일 -I와 같은 옵션이 CFLAGS에 정의되어 있다면, .d파일을 만들때 오류가 발생하게 됩니다. 왜냐하면, 해당 옵션이 포함되지 않았기 때문입니다.
다음과 같이 $(CFLAGS)가 추가되었습니다.

$(GROUP.01.OBJ): $(BUILD_DIR)/%.o: %.c
	...
	$(CC) -MM -MF $(DEPEND_FILE) -MT"$(DEPEND_FILE:.d=.o)" $(CFLAGS) $<
	...
$(GROUP.02.OBJ): $(BUILD_DIR)/%.o: %.c
	...
	$(CC) -MM -MF $(DEPEND_FILE) -MT"$(DEPEND_FILE:.d=.o)" $(CFLAGS) $<
	...


저작자 표시 비영리 변경 금지
신고
3 4