include

makefile 에는 보이지는 않지만 recipe 가 실행되기 전에 먼저 실행되는 rule 이 하나 있습니다. 바로 target 으로 자기 자신의 파일 이름을 갖는 rule 입니다. 이 rule 은 주로 makefile 자신을 update 하거나 생성하는 용도로 사용됩니다. 그러면 최종 update 된 버전으로 make 이 실행되게 됩니다. 이것은 make 명령을 실행했을 때 default 로 읽혀지는 top makefile 뿐만 아니라 include 문에서 사용되는 파일들도 모두 해당됩니다.

이 rule 의 타겟 라인은 다음과 같은 형식입니다. 그러니까 외부 파일( prerequisites ) 로부터 자기 자신( Makefile, 또는 include 파일 ) 을 recipe 를 이용해 update 하거나 새로 생성합니다. include 문에서 사용되는 makefile 의 경우는 파일이 존재하지 않을 경우 생성하기 위한 target only 룰도 사용할 수 있습니다.

# top makefile                        # include 문에 사용되는 makefile

Makefile : prerequisites              include.mk : prerequisites

                                      include.mk :     # target only 룰

아무것도 없는 디렉토리에 touch 명령으로 Makefile 을 하나 생성한 다음 make -d 명령을 실행해보면 다음과 같이 Makefile 을 읽어들인 후에 자기 자신을 update 하기 위해 매칭되는 rule 들을 검색하는 것을 볼 수 있습니다. 여기서 출력되는 다양한 확장자를 가진 prerequisites 들은 make 에서 builtin 으로 제공하는 rule 들입니다.

Reading makefiles...
Reading makefile 'Makefile'...
Updating makefiles....
 Considering target file 'Makefile'.
  Looking for an implicit rule for 'Makefile'.
    . . . . .
    . . . . .
    . . . . .
   Trying pattern rule with stem 'Makefile.sh'.
   Trying implicit prerequisite 's.Makefile.sh'.
   Trying pattern rule with stem 'Makefile.sh'.
   Trying implicit prerequisite 'SCCS/s.Makefile.sh'.
  No implicit rule found for 'Makefile'.
  Finished prerequisites of target file 'Makefile'.
 No need to remake target 'Makefile'.

위 출력에서 마지막 부분에 보이는 Makefile.sh 파일을 prerequisite 으로 사용해서 실제 자기 자신을 update 하는 룰이 실행되는지 테스트해보겠습니다.

자기 자신을 update 하는 것을 보통 remake 한다고 합니다.

sh$ cat Makefile                # 현재 Makefile 은 공백인 상태
-----------------------------
sh$ cat Makefile.sh             # 다음과 같은 rule 을 포함하는 Makefile.sh 파일 작성

foo : ;@echo hello remake
-----------------------------   
sh$ ls                          # 현재 디렉토리에 두개의 파일이 존재
Makefile  Makefile.sh
-----------------------------
sh$ make                        # make 명령 실행
cat Makefile.sh >Makefile       # make 에 default 로 내장되어 있는 '.sh' 확장자 룰에의해
chmod a+x Makefile              # Makefile.sh 의 내용이 Makefile 로 copy 되고
hello remake                    # remake 이 완료된 Makefile 이 실행되어 foo 룰이 실행된다.
-----------------------------
sh$ make                        # 이제 prerequisite 에 해당하는 Makefile.sh 보다
hello remake                    # 타겟인 Makefile 이 최신이므로 다시 remake 되지 않는다.
-----------------------------
sh$ cat Makefile                # 공백이었던 Makefile 파일이 Makefile.sh 로부터 remake 되었다.

foo : ;@echo hello remake

현재 상태에서 다시 Makefile.sh 파일을 수정하게 되면 Makefile 보다 최신이 되므로 다음에 make 명령 실행시 다시 remake 하게 됩니다.

사용자가 직접 rule 을 정의해 사용

makefile 자신을 생성, 갱신하는 builtin rule 은 먼저 makefile 이 읽혀지고 global 영역에서 처리가 완료된 후에 recipe 가 실행되기 전에 실행되는 것이므로 makefile 과 동일한 이름의 타겟을 갖는 rule 을 정의하면 builtin rule 대신에 사용자가 정의한 rule 이 사용될 수 있습니다. 이때는 prerequisites 으로 사용자가 원하는 이름을 사용할 수 있습니다.

sh$ cat Makefile
foo ::
    @echo hello remake 111

Makefile : update.mk          # Makefile 과 동일한 이름의 타겟을 갖는 룰을 정의
    cat $< >> $@              # update.mk 파일의 내용을 Makefile 에 append 
---------------------------
sh$ cat update.mk             # prerequisite 에 해당하는 파일

foo ::
    @echo hello remake 222
---------------------------
sh$ ls
Makefile  update.mk
---------------------------
sh$ make                         
cat update.mk >> Makefile        # 먼저 remake 을 위한 룰이 실행되고, 완료되면
hello remake 111                 # 최종 update 된 버전으로 make 이 다시 실행된다.
hello remake 222                 # update.mk 에서 추가된 룰의 메시지
---------------------------
sh$ cat Makefile
foo ::
    @echo hello remake 111

Makefile : update.mk
    cat $< >> $@    

foo ::                           # 기존 Makefile 에 update.mk 파일 내용이 추가됨
    @echo hello remake 222

include 에 사용되는 파일 이름은 rule 의 타겟과 같다.

include 문에서 사용되는 makefile 들도 동일하게 동작합니다. 따라서 include 문에서 사용되는 파일 이름은 동일한 이름을 타겟으로 가진 rule 을 실행하는데 사용됩니다. 실제 remake 은 다음과같은 순서로 진행됩니다.

  1. global 영역 1차 파싱
    모든 rule 정보를 읽어 들이고 include 파일이 존재할 경우 해당 위치에 include 해서 함께 파싱합니다. 이때 파일이 존재하지 않는 것은 오류가 되지 않고 pending 상태가 됩니다.

  2. remake
    makefile 과 동일한 이름의 rule 이 존재하고 타겟 생성, 갱신이 필요하면 실행합니다. 만약에 include 파일이 존재하지 않는데 remake 단계에서 rule 도 존재하지 않으면 오류가 되고 make 실행이 종료됩니다.

  3. global 영역 2차 파싱
    update 된 makefile 을 가지고 다시 최종 파싱을 합니다.

    remake 단계에서 rule 이 실행되기만 하면 실제 타겟 파일 생성에 상관없이 ( include 성공에 상관없이 ) 정상적으로 진행됩니다. 이것은 make 에서 기본적으로 rule 이 처리되는 방식과 동일한 것입니다.

  4. recipe 실행 시작

sh$ cat Makefile 

include aaa.mk

aaa.mk :            # include 파일과 동일한 이름의 rule 설정
    echo 'foo : ;@echo hello remake' > $@
------------------------------------------
sh$ ls              # 현재 aaa.mk 파일은 존재하지 않는상태
Makefile
------------------------------------------
sh$ make            # aaa.mk 파일과 동일한 이름의 rule 이 존재하므로 remake 단계에서 실행된다.
echo 'foo : ;@echo hello remake' > aaa.mk      # 여기서 aaa.mk 파일이 생성됨
                    # 2차 파싱 단계에서 생성된 aaa.mk 파일이 include 되고 
hello remake        # recipe 실행 단계에서 foo 룰이 실행된다.
------------------------------------------
sh$ ls
aaa.mk  Makefile    # aaa.mk 파일이 새로 생성됨
------------------------------------------
sh$ make            # aaa.mk 파일이 이미 존재하므로 aaa.mk 룰은 다시 실행되지 않는다.
hello remake

makefile 을 동적으로 수정해 include

위에서 알아본 remake 기능을 활용하면 makefile 을 동적으로 수정해 include 할 수 있습니다. 다음은 실제 사용자가 작성하는 makefile 과 include 되는 makefile 이 다른 것입니다. 그러니까 사용자가 aaa.mk 파일을 작성하면 remake 룰에서 정의된 recipe 에의해 파일이 동적으로 변경되어 .aaa.mk 파일로 저장되고 aaa.mk 파일 대신에 .aaa.mk 파일이 include 되어 사용됩니다.

sh$ cat Makefile 

include .aaa.mk                       # 실제 include 되는 파일은 dot 파일

.aaa.mk : aaa.mk                      # prerequisite 은 aaa.mk 파일이 된다.
    sed 's/11111/22222/' $< > $@      # recipe 에서 동적으로 파일을 변경
------------------------------------
sh$ cat aaa.mk 
foo :
    @echo hello 11111                 # hello 11111
------------------------------------
sh$ ls -a
./  ../  aaa.mk  Makefile             # 현재 두개의 파일만 존재
------------------------------------
sh$ make                              # remake 룰에 의해 dot 파일이 생성되고 include 된다.
sed 's/11111/22222/' aaa.mk > .aaa.mk   
hello 22222                           # 11111 -> 22222 로 변경되어 출력된다.
------------------------------------
sh$ make                              # 현재 dot 파일이 최신이므로 다시 룰이 실행되지 않는다.
hello 22222                         
------------------------------------
sh$ ls -a
./  ../  .aaa.mk  aaa.mk  Makefile    # .aaa.mk 파일이 새로 생성됨
------------------------------------
sh$ cat .aaa.mk                       # 실제 include 되는 dot 파일
foo :
    @echo hello 22222                 # 11111 -> 22222 로 변경됨

이 방법을 사용할 때 한가지 주의할 점은 만약에 aaa.mk 파일을 수정하였는데 구문 오류가 발생하여 종료된다면 실제 include 되는 .aaa.mk 파일에서도 오류가 나겠죠. 이때 aaa.mk 파일의 오류를 수정하고 make 을 실행하면 1차 파싱 단계에서는 기존의 .aaa.mk 파일을 읽어들이게 되므로 오류가 지속됩니다. 따라서 먼저 .aaa.mk 파일을 삭제한 후에 aaa.mk 파일을 수정해야 합니다.

- ( minus ) Prefix

만약에 include 파일도 존재하지 않고 파일 생성을 위한 rule 도 존재하지 않는다면 그것은 오류가 됩니다. 다음 첫 번째 예제를 보면 1차 파싱을 거친 후에 ( end... 메시지까지 출력 ) remake 단계에서 매칭되는 rule 이 없다는 메시지와 함께 종료되는 것을 볼 수 있습니다. 이때 오류로 종료되는 것을 무시하고 싶으면 recipe prefix 에서와 같이 include 지시자 앞에 - 문자를 추가해 주면 됩니다.

이것은 include 된 파일 자체에서 발생하는 오류는 무시되지 않습니다.

$(info start...)                               $(info start...)
include fooooo.mk                              -include fooooo.mk   # '-' prefix 사용

bar :                                          bar :
    @echo barrrr                                   @echo barrrr
$(info end...)                                 $(info end...)

####  실행 결과  ####                            ####  실행 결과  ####
start...                                                  start...
end...                                                    end...
Makefile:2: fooooo.mk: No such file or directory          barrrr   # 오류가 무시되고
make: *** No rule to make target 'fooooo.mk'.  Stop.               # 정상 실행된다.

Automatic dependency generation

프로그래머가 작성한 각각의 C/C++ 소스파일들은 오브젝트 파일을 타겟으로 의존 관계를 명시한 타겟 라인을 작성해야 합니다. 그래야지 나중에 헤더 파일이나 소스파일이 수정되었을때 해당 타겟 파일이 갱신될 수 있습니다. 하지만 이것을 사람이 일일이 작성하기는 어려운데요. 왜냐하면 아래와 같은 소스파일이 있을 경우 실제 의존관계에 명시해야될 헤더 파일은 3 개 이상이 될수도 있고 ( 헤더 파일에서 다른 헤더 파일을 include 할수 있으므로 )
전처리기에 의해 추가되거나 제외될 수도 있기 때문입니다. 사실 이와 같은 정보를 제일 잘 알수있는 프로그램은 컴파일러입니다. 따라서 컴파일러는 makefile 에서 사용할 수 있는 의존파일 .d 생성을 위한 전처리기 옵션들을 제공합니다.

$ cat pty/pty_fork.c
#include <fcntl.h>
#include <termios.h>
#include <sys/ioctl.h>
#include "pty_master_open.h"
#include "pty_fork.h"
#include "lib/tlpi_hdr.h"
. . . .
. . . .

# pty/pty_fork.o 타겟 파일의 의존 관계를 명시한 타겟 라인
pty/pty_fork.o: pty/pty_fork.c pty/pty_master_open.h pty/pty_fork.h \
 lib/tlpi_hdr.h lib/get_num.h lib/error_functions.h

예전에는 의존파일 생성을 위해 makedepend 명령을 사용하고, 빌드 전에 make depend 명령을 실행하고 했었는데 지금은 대부분이 컴파일러에서 제공하는 옵션을 이용해 처리합니다. 아래는 makefile 작성시 종종 사용되는 형식들인데 그런데 이 예제들은 정상 동작하기는 하지만 좀 생각해봐야될 점들이 있습니다.

먼저 첫 번째 예제의 경우 include 문에서 파일이 존재하지 않을 경우 매칭되는 룰이 존재하므로 ( %.d : %.c ) remake 단계에서 생성되어 최종 include 된후 makefile 이 실행됩니다. 그런데 프로젝트를 처음 빌드 하는 경우에도 의존파일이 필요할까요? 파일의 의존관계를 검사해서 필요한 타겟을 갱신하는 것은 두 번째 빌드때부터 필요한 것이죠. 그리고 오른쪽 예제 같은 경우는 소스파일이 수정될 때마다 빌드전에 직접 make depend 명령을 먼저 실행해주어야 합니다.

OBJS := ....                           SRCS := .... 
DEPS := $(OBJS:.o=.d)                  OBJS := ....   

all : foo                              all : foo

foo : $(OBJS)                          foo : $(OBJS)  
    $(CC) $+ -o $@                         $(CC) $+ -o $@

$(OBJS) : $(BUILD_DIR)/%.o : %.c       $(OBJS) : $(BUILD_DIR)/%.o : %.c   
    $(CC) -c $(CFLAGS) -o $@ $<           $(CC) -c $(CFLAGS) -o $@ $<

$(BUILD_DIR)/%.d : %.c                 depend :
    @$(CC) -MM -o $@ $<                   @$(CC) -MM $(SRCS) > $(DEPEND_FILE)

ifneq "$(MAKECMDGOALS)" "clean"        ifeq "$(filter clean depend,$(MAKECMDGOALS))" ""
-include $(DEPS)                       -include $(DEPEND_FILE)
endif                                  endif

이와 같은 문제점들은 CFLAGS 에 전처리기 옵션인 -MMD -MP 옵션만 추가하면 쉽게 해결이 됩니다. 다음 예제를 보면 처음 프로젝트 빌드 시에는 의존파일이 없지만 include 지시자 앞에 추가된 - prefix 에의해 종료되지 않고 빌드가 진행됩니다. 소스파일이 컴파일 될때마다 -MMD 옵션에 의해 오브젝트 파일과 동일한 위치에 의존파일인 .d 파일이 자동으로 생성됩니다. 다음에 두 번째 빌드부터는 의존파일이 존재하므로 include 되어 사용되겠죠. 소스파일이 수정됐을 경우에도 다시 컴파일이 될때 자동으로 .d 파일이 갱신됩니다.

OBJS := ....
DEPS := $(OBJS:.o=.d)

all : foo

foo : $(OBJS)  
    $(CC) $+ -o $@

CFLAGS += -MMD -MP

$(OBJS) : $(BUILD_DIR)/%.o : %.c   
    $(CC) -c $(CFLAGS) -o $@ $<

ifneq "$(MAKECMDGOALS)" "clean"
-include $(DEPS)
endif

make clean 시에는 include 문을 제외

의존파일을 읽어들여서 의존관계를 검사하는 것은 프로젝트 빌드시에 필요한 것이지 make clean 시에는 불필요한 것입니다. 따라서 다음과 같이 조건 지시자를 이용해 include 문이 처리되는 것을 제외하는 것이 좋습니다.

ifneq "$(MAKECMDGOALS)" "clean"
-include $(DEPS)
endif

ifeq "$(filter clean depend,$(MAKECMDGOALS))" ""
-include $(DEPEND_FILE)
endif

의존파일 자동 생성을 위한 gcc 전처리기 옵션

-M

의존 타겟 라인을 생성할 때 시스템 헤더까지 모두 포함해서 출력합니다. 이 옵션은 기본적으로 -E ( 전처리 완료후 종료 ) 옵션과 -w ( suppress warning ) 옵션을 포함하므로 출력 후 컴파일 작업까지 이어지지 않습니다.

sh$ cat pty/unbuffer.c
#include <termios.h>
#include <sys/select.h>
#include "pty_fork.h"
#include "lib/tty_functions.h"
#include "lib/tlpi_hdr.h"
. . . .
. . . .

sh$ gcc -I. -M -o BUILD/pty/unbuffer.d pty/unbuffer.c  

sh$ cat BUILD/pty/unbuffer.d
unbuffer.o: pty/unbuffer.c /usr/include/stdc-predef.h \
 /usr/include/termios.h /usr/include/features.h \
 /usr/include/x86_64-linux-gnu/sys/cdefs.h \
 /usr/include/x86_64-linux-gnu/bits/wordsize.h \
 /usr/include/x86_64-linux-gnu/bits/long-double.h \
 . . . .
 . . . .

-MM

이 옵션은 -M 옴션과 동일한데 의존파일 출력시 시스템 헤더는 제외하고 출력합니다.

sh$ gcc -I. -MM -o BUILD/pty/unbuffer.d pty/unbuffer.c  

sh$ cat BUILD/pty/unbuffer.d
unbuffer.o: pty/unbuffer.c pty/pty_fork.h lib/tty_functions.h \
 lib/tlpi_hdr.h lib/get_num.h lib/error_functions.h

-MF file

이 옵션을 이용하면 출력 결과를 지정한 파일로 저장할 수 있습니다. -MD, -MMD 옵션의 경우 기본적으로 -o 옵션에 사용된 오브젝트 파일과 동일한 경로에 .d 파일을 생성하는데 이 옵션으로 변경할 수 있습니다.

sh$ gcc -I. -MM -MF BUILD/pty/unbuffer.d pty/unbuffer.c 

sh$ cat BUILD/pty/unbuffer.d 
unbuffer.o: pty/unbuffer.c pty/pty_fork.h lib/tty_functions.h \
 lib/tlpi_hdr.h lib/get_num.h lib/error_functions.h

-MT target

의존 타겟 라인을 생성할 때 타겟 파일명은 기본적으로 입력 소스파일에서 경로와 확장자를 제거한 후에 .o 확장자를 붙여서 출력하는데요. 이 옵션을 이용하면 직접 타겟 파일명을 지정할 수 있습니다.

sh$ cpp -I. -MM pty/unbuffer.c 
unbuffer.o: pty/unbuffer.c pty/pty_fork.h lib/tty_functions.h \
 lib/tlpi_hdr.h lib/get_num.h lib/error_functions.h

sh$ cpp -I. -MM -MT '$(BUILD_DIR)/pty/unbuffer.o' pty/unbuffer.c 
$(BUILD_DIR)/pty/unbuffer.o: pty/unbuffer.c pty/pty_fork.h lib/tty_functions.h \
 lib/tlpi_hdr.h lib/get_num.h lib/error_functions.h

-MQ target

이것은 위의 -MT 옵션과 동일하게 동작하는데 차이점은 make 에서 사용되는 특수문자를 Quote 해서 출력합니다. ( $ 문자는$$ 로 변경된다 )

# '-MQ' 옵션을 사용하면 출력이 '$$(BUILD_DIR)' 가 된다
sh$ cpp -I. -MM -MQ '$(BUILD_DIR)/pty/unbuffer.o' pty/unbuffer.c 
$$(BUILD_DIR)/pty/unbuffer.o: pty/unbuffer.c pty/pty_fork.h lib/tty_functions.h \
 lib/tlpi_hdr.h lib/get_num.h lib/error_functions.h

-MD

이 옵션은 실제적으로 -M -MF 옵션을 사용한 것과 같습니다. 아래 -MMD 옵션과 동일하게 동작하지만 차이점은 의존파일 출력에 시스템 헤더도 포함됩니다.

-MMD

이 옵션은 실제적으로 -MM -MF 옵션을 사용한 것과 같습니다. 컴파일시 -o 옴션에 사용된 오브젝트 파일명에서 확장자를 .d 로 바꾼 것이 -MF 에서 사용되는 파일명이 됩니다. 그러니까 컴파일이 완료된 오브젝트 파일이 위치한 곳에 자동으로 .d 파일이 함께 생성되는 것입니다. 또 한 가지는 아래 출력에서 볼 수 있듯이 타겟 파일명도 -o 옵션에 사용된 오브젝트 파일명과 동일하게 됩니다.

이 옵션은 -MM 옵션과 달리 -E ( 전처리 완료후 종료 ) 옵션을 포함하지 않습니다. 따라서 .d 파일 생성 후 이어서 컴파일 작업이 진행됩니다. 다시 말해서 컴파일시 side effect 로 .d 파일이 생성되는 것입니다. 나중에 소스파일이 수정되면 다시 컴파일 될것이고 이때 .d 파일도 자동으로 갱신되겠죠.

sh$ gcc -c -I. -MMD -o BUILD/pty/unbuffer.o pty/unbuffer.c

sh$ cat BUILD/pty/unbuffer.d 
BUILD/pty/unbuffer.o: pty/unbuffer.c pty/pty_fork.h lib/tty_functions.h \
 lib/tlpi_hdr.h lib/get_num.h lib/error_functions.h

생성되는 의존 파일의 경로를 변경할 때는 다음과 같은 방법을 사용할 수 있습니다.

sh$ gcc -c -I. -MMD -MF BUILD/ptr/unbuffer.d2 -o BUILD/pty/unbuffer.o pty/unbuffer.c

sh$ gcc -c -I. -Wp,-MMD,BUILD/ptr/unbuffer.d2 -o BUILD/pty/unbuffer.o pty/unbuffer.c

-MP

만약에 C 소스파일에서 #include 라인을 제거하고 해당 헤더 파일을 삭제했다면 기존 .d 파일의 목록에는 아직 해당 파일이 남아있기 때문에 다음에 make 명령 실행시 의존파일이 없다는 make 오류가 발생하게 됩니다. 이때 -MP 옵션을 사용하면 다음과 같이 각 헤더 파일의 더미룰을 생성하여 make 실행이 종료되지 않게 합니다.

sh$ gcc -c -I. -MMD -MP -o BUILD/pty/unbuffer.o pty/unbuffer.c

sh$ cat BUILD/pty/unbuffer.d 
BUILD/pty/unbuffer.o: pty/unbuffer.c pty/pty_fork.h lib/tty_functions.h \
 lib/tlpi_hdr.h lib/get_num.h lib/error_functions.h

pty/pty_fork.h:               # 더미 룰

lib/tty_functions.h:

lib/tlpi_hdr.h:

lib/get_num.h:

lib/error_functions.h:

-MG

이 옵션은 -MM, -M 과 함께 사용되는데 기본적으로 의존 파일을 생성할때 헤더 파일이 존재하지 않으면 오류가 되어 종료되지만 이 옵션을 설정하면 앞으로 생성될 파일로 간주하여 오류 없이 #include 지시자에 설정되어 있는 파일을 그대로 추가해 줍니다. -E ( 전처리 완료후 종료 ) 옵션이 포함되므로 의존 파일 생성과 컴파일 작업을 분리해 진행할 수 있습니다.

This feature is used in automatic updating of makefiles.