Rules

make 의 메인 기능은 rule 입니다. 앞서 소개한 make 에서 제공되는 변수, 함수기능, 대입연산 은 rule 을 작성하는데 활용됩니다. makefile 을 작성한 후에 프롬프트 상에서 make 명령을 실행하면 최종적으로 실행되는 것은 shell script 명령입니다. 이것은 결과적으로 보면 shell script 파일을 실행하는 것과 차이가 없는 것인데요. 하지만 make 이 다른 점은 사용가 작성한 rule 에 따라서 어떤 rule 의 shell 명령을 실행할지, 실행할 경우는 어떤 순서에 따라 실행할지가 결정되어 실행된다는 것입니다.

makefile 에 임의의 단어를 입력한 후 실행하면 기본적으로 rule 정의로 인식하는 것을 볼 수 있습니다.

sh$ cat Makefile

hello

sh$ make                                      # 기본적으로 rule 정의로 인식하여
Makefile:2: *** missing separator.  Stop.     # ':' separator 문자가 없다는 오류발생
------------------------------------------

sh$ cat Makefile

hello :

sh$ make                                      # ':' separator 문자는 존재하지만
make: Nothing to be done for 'hello'.         # 실행할 명령이 없으므로 
-----------------------------------------

sh$ cat Makefile

hello :
    @echo hello make rule !

sh$ make
hello make rule !

Rule 기본 구조

rule 은 targets 과 prerequisites 을 작성하는 타겟 라인과 recipe 로 구성됩니다. targets 과 prerequisites 은 : separator 문자를 이용해 구분하고 prerequisites 들은 다시 다른 rule 의 target 이 될 수 있습니다. rule 은 두 가지 형태로 작성할 수 있는데 (2) 번과 같이 prerequisites 뒤에 타겟 라인의 끝을 나타내는; 문자를 붙인 후에 하나의 라인에 연이어서 recipe 를 작성할 수도 있고, (1) 번과 같이 tab 문자를 이용해 recipe 라인을 별도의 라인에 분리해 작성할 수도 있습니다.

make 이 실행되면 global 영역에서 변수, 함수, 대입연산 이 처리됨과 동시에 rule 의 타겟 라인도 처리됩니다.
rule 이 말해주는 것은 두 가지입니다.

  1. target 파일을 갱신해야 되는지 말아야 되는지 ( target 라인 )
  2. target 파일 갱신이 필요할 경우 어떻게 갱신해야 되는지 ( recipe 라인 )

여기서 target 파일의 갱신이 필요할 때는 다음과 같이 두 가지 경우입니다.

  1. target 파일이 존재하지 않을 때
  2. target 파일과 prerequisites 에 있는 파일들의 last-modification time 을 비교했을 때 하나라도 target 파일보다 최신이 있을 경우

make 은 작성된 순서에 따라 rule 들을 읽어들이는데 이때 만약에 앞선 rule 과 동일한 이름의 타겟을 갖는 rule 이 중복되어 나타나면 앞선 rule 이 override 됩니다 ( 이때 warning 메시지로 알려줍니다 ). makefile 이 실행될 때 처음 실행되는 rule 을 default goal 이라고 하는데 따로 .DEFAULT_GOAL 변수를 설정하지 않았다면 rule 작성시 제일 위에 위치한 rule 이 default goal 이 되어 실행됩니다.

. 으로 시작하는 타겟은 default goal 이 되지 않습니다. 하지만 타겟 이름에 / 문자가 포함되면 가능합니다.

Rule 은 기본적으로 파일을 대상으로 한다.

rule 의 타겟 라인에 존재하는 targets 과 prerequisites 은 모두 파일을 나타냅니다. 따라서 파일이 존재하지 않으면 파일을 생성하기 위해 해당 rule 을 찾아서 recipe 를 실행합니다. 만약에 매칭되는 rule 을 발견하지 못하면 오류로 make 실행이 종료됩니다.

다음은 rule 에 prerequisites 이 존재하지 않을 경우 테스트입니다.

sh$ cat Makefile 

foo :
    @echo 1111111

-------------------
sh$ make        # 타겟 파일이 존재하지 않으면 파일 생성을 위해 recipe 가 실행된다.
1111111

sh$ touch foo   # 임의로 foo 파일 생성

sh$ make        # 룰에 prerequisites 이 없으므로 타겟 파일이 존재하면 'up to date' 상태가 된다.
make: 'foo' is up to date.

다음은 rule 에 prerequisites 이 존재할 경우 테스트입니다.

# 현재 디렉토리에 main.o main.c main.h 파일이 모두 존재하지 않는다면

main.o : main.c main.h
    @echo gcc -c -o $@ $<

----------------------------
sh$ make              # main.c 파일 생성을 위한 rule 이 존재하지 않으므로 종료된다.
make: *** No rule to make target 'main.c', needed by 'main.o'.  Stop.

sh$ touch main.c      # 임의로 main.c 파일 생성

sh$ make              # main.h 파일 생성을 위한 rule 이 존재하지 않으므로 종료된다.
make: *** No rule to make target 'main.h', needed by 'main.o'.  Stop.

sh$ touch main.h      # main.h 파일 생성

# prerequisites 에 해당하는 main.c main.h 파일이 모두 존재하므로
# 타겟 파일에 해당하는 main.o 를 생성하기 위해 해당 recipe 가 실행된다.
sh$ make
gcc -c -o main.o main.c

다음은 prerequisites 에 해당하는 main.c 의 rule 이 존재하는 경우입니다. make 은 기본적으로 rule 이 실행되면 타겟 파일의 실제 생성 여부에 상관없이 진행을 계속합니다.

# 현재 디렉토리에 main.o main.c 파일이 모두 존재하지 않는다고 하면

main.o : main.c
    @echo target : $@

main.c :
    @echo target : $@

---------------------
sh$ make
target : main.c      # main.o, main.c 파일이 모두 존재하지 않으므로 prerequisites 에
target : main.o      # 해당하는 main.c 룰이 먼저 실행되고 main.o 룰이 실행된다.

sh$ touch main.c     # main.c 파일 생성

sh$ make             # main.o 파일만 존재하지 않으므로
target : main.o

sh$ touch main.o     # main.o 파일 생성

sh$ make             # main.o, main.c 파일이 모두 존재하므로 rule 이 실행되지 않는다.
make: 'main.o' is up to date.

sh$ touch main.c     # prerequisites 에 해당하는 main.c 파일을 최신으로 갱신하면

sh$ make             # main.o 파일을 갱신해야 되므로 recipe 가 실행된다.
target : main.o

디렉토리도 파일이다.

따라서 파일과 동일하게 targets 및 prerequisites 으로 사용될 수 있습니다.

foo : BUILD
    @echo target : $@

BUILD :
    mkdir -p $@

#####  실행 결과  #####

sh$ ls
Makefile

sh$ make
mkdir -p BUILD    # BUILD 파일이 존재하지 않으므로 recipe 가 실행된다.
target : foo

sh$ ls
BUILD/  Makefile

sh$ make          # BUILD 파일이 존재하므로 더이상 recipe 가 실행되지 않는다.
target : foo

up to date 타겟과 파일이 아닌 가짜 타겟

rule 을 타겟 파일을 생성하기 위한 용도가 아닌 단순히 recipe 에 존재하는 shell script 명령을 실행하기 위한 용도로 사용해야될 경우가 있습니다. 이때는 타겟 파일이 존재하던 않던 상관없이 항상 해당 rule 의 recipe 가 실행돼야 하는데요. 여기에는 다음과 같은 방법들이 사용됩니다.

타겟 파일이 존재하지 않으면 파일을 생성해야 되기 때문에 항상 recipe 가 실행됩니다. 이때 만약에 실행할 recipe 가 없으면 해당 타겟은 up to date 상태가 됩니다.

# 현재 디렉토리에 foo 파일이 존재하지 않을 경우

foo :                                  foo : ;
    @echo fooooo                       

####  실행 결과  ####                    ####  실행 결과  ####    
fooooo                                 make: 'foo' is up to date.

이것을 활용하면 특정 타겟이 항상 실행되게 할 수 있습니다. 다음 첫 번째 예제를 보면 touch 명령으로 foo 타겟 파일을 생성한 후에는 다시 recipe 가 실행되지 않는 반면에 bar : 타겟을 사용할 경우는 barfoo 보다 항상 up to date 상태가 되므로 foo 파일을 생성한 후에도 계속해서 recipe 가 실행되는 것을 볼 수 있습니다.

                                        foo : bar
foo :                                       @echo fooooo
    @echo fooooo                        
                                        bar :

####  실행 결과  ####                     ####  실행 결과  ####
sh$ make                                sh$ make                                   
fooooo                                  fooooo

sh$ touch foo                           sh$ touch foo

sh$ make                                sh$ make
make: 'foo' is up to date.              fooooo         # 계속해서 실행된다.

bar : 와 같은 역할을 하는 타겟을 보통 이름으로 FORCE : 를 많이 사용하는데요. 이 방법은 실제 예전에 make 에서 사용하던 방식입니다. 하지만 이 방법의 단점은 만약에 현재 디렉토리에 FORCE 파일이 존재하고 타겟 파일인 foo 가 보다 최신이라면 해당 룰은 실행되지 않게 되겠죠. 그래서 이와 같은 단점을 보완하기 위해 나온 것이 .PHONY 타겟 입니다.

phony 는 사전에서 찾아보면 가짜라는 의미를 가지고 있는데 말 그대로 파일이 아닌 가짜 타겟을 지정할 때 사용합니다. .PHONY: 타겟으로 등록을 하면 update 체킹도 하지 않고 타겟 파일이 존재하던 않던 상관없이 항상 recipe 가 실행됩니다.

                                        # FORCE 타겟에 의해 foo 타겟은 항상 실행된다.
                                        foo : bar zoo FORCE
.PHONY: clean                               @echo fooooo
clean :                            
    rm -f $(objects)                    FORCE :

                                        .PHONY: FORCE

PHONY 타겟을 사용하는 것과 FORCE 타겟을 사용하는 것은 둘 다 많이 사용합니다. prerequisites 에 FORCE 타겟이 존재하면 이 룰은 항상 실행된다는 것을 쉽게 알 수 있습니다. 또한 PHONY 타겟에는 % 문자를 이용하는 pattern 은 등록하지 못하므로 만약에 특정 pattern rule 이 항상 실행되게 하려면 FORCE 타겟 방법을 사용해야 합니다.

Subroutine 으로서의 타겟

.PHONY 타겟을 이용하면 rule 을 일종의 subroutine 을 실행하는 용도로 활용할 수 있습니다. 그런데 여기에는 디렉토리와 관련해서 한가지 문제점이 있는데요. 다음 예제는 오브젝트 파일을 별도의 빌드 디렉토리에 저장하려고 한 것입니다. 따라서 컴파일 하기 전에 먼저 디렉토리를 생성해야 하는데요. 이때는 $(OBJS) : $(BUILD_DIR) 와 같이 의존성을 추가하면 $(OBJS) 파일들이 빌드 되기 전에 먼저 $(BUILD_DIR) 타겟이 실행되게 할 수 있습니다.

그런데 여기서 한가지 문제는 컴파일 결과로 오브젝트 파일이 빌드 디렉토리에 생성될 때마다 디렉토리의 timestamp 도 함께 update 가 된다는 것입니다. 따라서 make 실행이 완료되면 마지막 파일은 디렉토리와 timestamp 가 같게되겠지만 처음 두 파일은 디렉토리보다 old 상태가 되겠죠. 따라서 다음에 다시 make 명령을 실행하면 old 파일들을 다시 빌드를 시작합니다. 이것은 prerequisites 에 있는 파일들은 항상 target 과 timestamp 를 비교하기 때문인데요. 이와 같은 문제를 해결하기 위한 것이 order-only prerequisites 입니다.

# 테스트하려면 먼저 touch 명령으로 foo.c bar.c baz.c 파일을 생성하세요.

BUILD_DIR := BUILD
OBJS := $(addprefix $(BUILD_DIR)/,foo.o bar.o baz.o)

all : $(OBJS)

$(OBJS) : $(BUILD_DIR)     # $(OBJS) 파일들을 빌드하기전에 먼저 $(BUILD_DIR) 타겟 실행

$(BUILD_DIR)/%.o : %.c
    gcc -c -o $@ $<

$(BUILD_DIR) : 
    mkdir -p $@

############  실행 결과  ############

sh$ make
mkdir -p BUILD                    # $(BUILD_DIR) 타겟 실행
gcc -c -o BUILD/foo.o foo.c       # foo.o: time 47:17, BUILD: time 47:17
gcc -c -o BUILD/bar.o bar.c       # bar.o: time 47:18, BUILD: time 47:18
gcc -c -o BUILD/baz.o baz.c       # baz.o: time 47:19, BUILD: time 47:19

sh$ make                          # 빌드가 완료되었지만 foo.o bar.o 파일의 timestamp 는
gcc -c -o BUILD/foo.o foo.c       # BUILD 디렉토리 보다 old 상태가 되므로 다시 빌드가 된다.
gcc -c -o BUILD/bar.o bar.c

. . . . .      # gcc 는 컴파일시 타겟 파일이 존재하면 먼저 삭제한 후 다시 생성합니다.

Order-only Prerequisites

Order-only prerequisites 은 타겟 라인의 마지막에 | 문자를 추가한 후에 작성합니다. order-only prerequisites 은 normal prerequisites 과달리 update 체킹에 사용되지 않습니다. 아래 두 번째 예제를 예로 들면 bar 가 foo 보다 최신이면 foo 의 recipe 가 실행되지만 zoo 가 foo 보다 최신이라고 해서 foo 의 recipe 가 실행되지는 않습니다.

order-only prerequisite 인 zoo 의 recipe 가 실행될 경우는 zoo 타겟 파일이 존재하지 않을 때입니다. zoo 의 recipe 가 실행되어 타겟 파일이 존재하면 이후부터는 zoo 의 recipe 는 실행되지 않습니다. 만약에 zoo 파일의 존재 여부에 상관없이 항상 실행되게 하려면 .PHONY 타겟에 등록하면 됩니다.

실행되는 순서는 먼저 normal prerequisites 이 실행이 되고 완료되면 foo 타겟이 실행되기 전에 order-only prerequisites 이 실행됩니다. 다시 말해서 prerequisites 순서대로 실행이 되는데 이것은 single thread 일 경우이고 make -j8 와 같이 병렬 실행을 하게 되면 prerequisites 들은( order-only 포함 ) 모두 동시에 실행되고 실행이 완료되면 마지막으로 foo 타겟이 실행됩니다.

위에서 발생하는 문제는 $(BUILD_DIR) 가 normal prerequisites 이라서 update 체킹을 하게 되어 발생하는 문제이므로 $(OBJS) : $(BUILD_DIR) 타겟 라인을 $(OBJS) : | $(BUILD_DIR) 로 변경한 후 다시 make 명령을 실행해보면 위와 같은 문제가 발생하지 않는 것을 알 수 있습니다.

# 먼저 BUILD 디렉토리를 삭제하고                         # order-only prerequisites 실행순서
# 다음 라인으로 변경한 후 실행합니다.
$(OBJS): | $(BUILD_DIR)                             foo : bar | zoo
                                                        @echo fooooo
#######  실행 결과  #######                      
                                                    bar :
sh$ make                                                @echo barrrr
mkdir -p BUILD                                      
gcc -c -o BUILD/foo.o foo.c                         zoo :
gcc -c -o BUILD/bar.o bar.c                             @echo zooooo
gcc -c -o BUILD/baz.o baz.c                     
                                                    #####  실행 결과  #####
sh$ make                                         
make: Nothing to be done for 'all'.                 barrrr
                                                    zooooo
                                                    fooooo

Multiple Target Lines

recipe 가 존재하는 rule 이 동일한 타겟 이름을 가지고 중복이 되면 warning 메시지가 출력되고 앞선 rule 이 override 됩니다. 이때 앞선 rule 의 타겟 라인 설정값은 유지가 되지만 recipe 는 실행되지 않습니다. 다음을 보면 overriding warning 메시지와 함께 두 번째 rule 에 해당하는 222 ... recipe 가 실행되고 $^ 값은 앞선 rule 의 타겟 라인 설정값인 main.h 가 포함되는 것을 볼 수 있습니다.

main.o : main.c main.h
    @echo 111 $@ : $^

main.o : main.c
    @echo 222 $@ : $^

.DEFAULT: ;

#####  실행 결과  #####

Makefile:5: warning: overriding recipe for target 'main.o'
Makefile:2: warning: ignoring old recipe for target 'main.o'
222 main.o : main.c main.h   # 앞선 룰의 main.h 가 포함된다.

rule 의 recipe 를 하나만 남기면 정상적으로 실행됩니다.

main.o : main.c main.h
main.o : main.c
    @echo 222 $@ : $^

.DEFAULT: ;

########  실행 결과  ########

222 main.o : main.c main.h

위에서 살펴보았듯이 recipe 가 포함되는 rule 은 중복이 허용되지 않지만 타겟 라인은 여러 개를 사용할 수 있습니다. 또한 여러 개의 타겟 라인에 동일한 이름의 prerequisites 이 중복되어 나타날 수도 있는데 이때는 $^ automatic 변수의 경우는 중복이 제거되고 $+ 변수는 그대로 포함됩니다.

타겟 라인에는 다음과 같이 의존성만 설정할 수 있는 것이 아니고 target-specific 변수도 설정해 사용할 수 있습니다.

  1. 의존성 설정 ( rule 정의에서 dependencies 와 prerequisites 은 같은 의미입니다. )
  2. target-specific 변수 설정
  3. order-only prerequisites 설정

Prerequisites 들이 합쳐지는 순서

multiple 타겟 라인에 의해 설정된 prerequisites 들이 최종적으로 하나의 룰로 합쳐질 때는 recipe 가 있는 룰의 prerequisites 이 제일 앞에 오고 나머지는 작성 순서대로 append 됩니다. 여기에 order-only prerequisites 은 포함되지 않습니다.

prog : aaa.o libxxx.a
prog : bbb.o libyyy.a ccc.o libxxx.a
prog : main.o
    @echo gcc $+ -o $@

.DEFAULT: ;

######  실행 결과  ######
sh$ make
gcc main.o aaa.o libxxx.a bbb.o libyyy.a ccc.o libxxx.a -o prog

Multiple Targets

보통 예제에는 타겟 파일이 하나로 나오는데 실제는 타겟 파일도 prerequisites 처럼 여러개를 사용할 수 있고 다음과 같이 중복도 가능합니다.

aaa.o : aaa.c aaa.h                 # aaa.o 에만 해당
aaa.o : CPPFLAGS := -DFOO

bbb.o : bbb.c bbb.h bar.h           # bbb.o 에만 해당
bbb.o : CPPFLAGS := -DBAR

aaa.o bbb.o : zoo.h                 # aaa.o bbb.o 공통 부분
aaa.o bbb.o : CFLAGS := -g -Wall

aaa.o bbb.o :
    @echo $@ : $^ $(CPPFLAGS) $(CFLAGS)

prog : aaa.o bbb.o

.DEFAULT: ;
.DEFAULT_GOAL := prog

############  실행 결과  #############

aaa.o : -DFOO -g -Wall aaa.c aaa.h zoo.h
bbb.o : -DBAR -g -Wall bbb.c bbb.h bar.h zoo.h

하지만 다음과 같이 recipe 를 갖는 타겟이 중복되는 것은 안됩니다.

aaa.o : aaa.c aaa.h 
    command ...

bbb.o : bbb.c bbb.h bar.h
    command ...

aaa.o bbb.o : zoo.h       # aaa.o 타겟과 bbb.o 타겟과 중복
    command ...

-------------------------
aaa.o : aaa.c
    command ...

aaa.o : aaa.m             # aaa.o 타겟 중복
    command ...

rule 에 타겟이 여러개 일경우 make 명령 실행시 타겟을 지정하지 않으면 기본적으로 첫 번째 타겟이 사용됩니다.

foo bar zoo :
    @echo target : $@
----------------------

sh$ make
target : foo

sh$ make bar
target : bar