Recipes

shell 명령 실행에 의해 실제 작업 처리가 일어나는 곳이 rule 의 recipe 영역입니다. 그 외 global 영역에서 처리되는 변수, 함수, 대입 연산과 사용자가 정의한 rule 들은 recipe 가 실행되기 전에 준비 역할을 하는 셈이죠. 어떤 rule 들이 실행돼야 하는지 그리고 어떤 순서대로 실행돼야 하는지가 결정된 후에 recipe 영역에 있는 shell 명령들이 실행됩니다.

recipe 는 기본적으로 tab 문자를 이용해 구분합니다. makefile 에 tab 문자로 시작하는 임의의 단어를 입력한 후에 실행해보면 recipe 라인으로 인식하는 것을 볼 수 있습니다.

sh$ cat Makefile

    hello recipe    # 앞의 공백은 tab 문자

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

sh$ make    # 타겟라인 없이 recipe 가 시작되어 오류 발생
Makefile:2: *** recipe commences before first target.  Stop.

recipe 도 라인 단위입니다. 이 말은 라인 단위로 sh -c 에 의해 명령이 실행된다는 의미입니다. 따라서 recipe 를 shell script 파일 작성하듯이 작성하면 안됩니다. 왜냐하면 다음 라인은 다른 PID 갖는 sh -c 이 되므로 앞서 설정한 shell 변수값을 사용할 수 없기 때문입니다. 다음을 보면 COUNT 변수값이 첫 라인에서는 정상적으로 표시되지만 나머지 라인에서는 표시되지 않는 것을 볼 수 있습니다.

foo :
    @COUNT=100; echo shell PID: $$$$, COUNT: $$COUNT
    @echo shell PID: $$$$, COUNT: $$COUNT
    @echo shell PID: $$$$, COUNT: $$COUNT

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

sh$ make
shell PID: 10790, COUNT: 100    
shell PID: 10791, COUNT:             # 각 라인 별로 PID 값이 다르다
shell PID: 10792, COUNT:

이렇게 recipe 에서 라인 단위를 사용하는 이유는 해당 라인의 sh -c 가 오류로 종료할 경우 make 실행을 종료하기 위해서입니다. 타겟 파일을 만드는 작업 중에 하나의 라인에서라도 오류가 발생하면 안되기 때문입니다. 만약에 하나의 sh -c 에서 모두 shell 명령을 실행하고 싶으면 &&; 를 이용해서 연이어서 작성하고 라인이 길어질 경우는 newline 을 escape 해서 하나의 라인으로 만들어야 합니다.

# newline 을 escape 하여 하나의 라인이 되므로 tab 문자는 처음에만 입력해도 된다.
foo :
    @COUNT=100; echo shell PID: $$$$, COUNT: $$COUNT; \
echo shell PID: $$$$, COUNT: $$COUNT; \
echo shell PID: $$$$, COUNT: $$COUNT

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

shell PID: 10909, COUNT: 100
shell PID: 10909, COUNT: 100
shell PID: 10909, COUNT: 100
----------------------------------------------------------

LIST = one two three
all:
    @for i in $(LIST); do \     # 모두 하나의 라인이 되므로 명령을 작성할 때는 
        echo $$i; \             # ';' 문자를 사용해야 한다.
    done

#########  실행 결과  #########
one
two
three

Rule context 와 comment

recipe 작성 중에는 make comment 라인이 와도 되고 공백 라인이 와도 됩니다. 하지만 comment 라인이 tab 으로 시작하면 그것은 make comment 가 아니라 shell comment 가 됩니다. recipe 작성 중간에 tab 으로 시작하지 않는 make 구문 ( 대입 연산, 타겟 라인, script ) 이 오면 그것은 해당 rule context 의 종료가 됩니다.

newline 을 escape 하여 하나의 라인으로 작성할 경우는 comment 도 공백 라인도 안됩니다.

foo :
    @echo aaaaa
# make comment .....            # 중간에 make comment 라인이 와도 된다.
    # shell comment 111 ....    # tab 으로 시작할 경우는 shell comment 라인이 된다.
    @# shell comment 222 ....
    @echo bbbbb
                                # 공백 라인이 와도 된다.

    @echo ccccc

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

aaaaa
# shell comment 111 ....
bbbbb
ccccc

recipe 에서 사용되는 함수는 마찬가지로 tab 으로 시작해야 합니다. 그렇지 않으면 해당 rule context 가 종료되는 결과가 발생하고 함수는 global 영역에 포함됩니다.

AA := 100

foo :
    @echo target $@ : $(AA) 
    $(if $(filter 100,$(AA)),$(eval AA := 200))    # tab 으로 시작해야 한다.

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

target foo : 200

rule 의 recipe 작성을 마치고 다른 make 구문을 작성할 때는 앞에 tab 문자가 오지 않도록 주의해야 됩니다. 그렇지 않으면 앞선 rule context 에 연이어 shell 명령문으로 인식됩니다.

foo :
    @echo end.....

ifneq "$(MAKECMDGOALS)" "clean"
    include $(sources:.c=.d)            # include 앞에 tab 문자가 포함됨
endif

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

end.....
include 
make: include: Command not found        # include 가 shell 명령으로 인식된다.
make: *** [Makefile:3: foo] Error 127

$$ 변수와 recipe 실행 순서

recipe 에서는 shell 명령문을 작성하지만 명령문 어떤 위치에서든지 make 변수와 함수도 사용할 수 있습니다. 그런데 여기서 문제는 변수를 나타내는 문자로 $ 를 사용하므로 shell 에 $ 문자가 전달되기 위해서는 $$ 변수를 사용해야 합니다. 그러니까 $(var) 는 make 변수로 처리되지만 $${var}$$ 변수가 $ 로 변경된 후에 shell 에 전달되면 ${var} shell 변수가 됩니다.

recipe 가 실행될 때는 처음부터 shell 명령이 실행되지 않습니다. 먼저 recipe 전체에서 make 레이어에 해당하는 변수, 함수, $$ 처리가 완료된 후에 결과에 따라 shell 명령이 실행됩니다.

foo :
    @echo shell 111
    $(info make 222)
    @echo shell 333$(info make 444)
    @echo shell 555
    $(info make 666)

######  실행 결과  ######
make 222
make 444
make 666
shell 111    # make 레이어 처리가 완료된 후에 shell 명령이 실행된다.
shell 333
shell 555

따라서 다음과 같은 사용방법은 문제가 될수 있습니다. shell script 가 실행되면서 call 함수가 실행되는 것이 아닙니다. shell 명령이 실행되기 전에 먼저 make 레이어에 해당하는 $(val) 변수가 확장되고 $(call ...) 함수가 실행이 완료된 후에 그 결과를 가지고 shell script 가 실행되는 것입니다. 따라서 첫 번째 예제는 실행에 문제가 있지만 오른쪽 예제 같은 경우는 call 함수의 반환값으로 shell 에서 실행 가능한 명령문이 반환된다면 가능합니다.

target :                                      target :                        
    @for v in $(val); do  \                       @if test "$(val)" = ""; then  \
        $(call foo,$$v);  \                           $(call foo ...);          \
    done                                          fi

명령 라인의 종료 상태 값

명령 라인에서 여러 개의 명령을 작성할 때 주의할 점은 make 실행이 중단될 때는 sh -c 의 종료 상태 값에 따라 중단된다는 것입니다. 이것은 해당 라인에서 마지막으로 실행되는 명령의 종료 상태 값을 말합니다. 다음 첫 번째 예제를 보면 cd 명령이 오류로 종료하였지만 뒤 이어지는 date 명령이 정상 종료하여 결과적으로 sh -c 의 종료 상태 값은 정상이 됩니다. 따라서 make 실행이 중단되지 않고 다음 date 명령도 실행되는 것을 볼 수 있습니다. 반면에 두 번째 예제의 경우는 명령 라인에서 && 연산자를 사용하여 전체 명령의 종료 상태 값이 오류가 되므로 make 실행이 종료됩니다.

foo :        (';' 를 사용)                    foo :        ('&&' 를 사용)
    @cd not-exist; date                          @cd not-exist && date
    @date                                        @date

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

/bin/sh: 1: cd: can't cd to not-exist        /bin/sh: 1: cd: can't cd to not-exist
Wed 29 May 2019 02:52:54 PM KST              make: *** [Makefile:3: foo] Error 2
Wed 29 May 2019 02:52:54 PM KST

또는 다음과 같이 -e ( errexit ) shell 옵션을 설정해 사용할 수도 있습니다. 그러면 sh -e -c 형태로 shell 이 실행되므로 실행 중에 명령이 오류로 종료할 경우 바로 sh 도 오류로 종료됩니다. errexit 옵션은 사용시 몇가지 주의할 사항이 있는데 자세한 내용은 여기 를 참고하세요.

foo : .SHELLFLAGS += -e                       # 또는 직접 해당 라인에 설정 
foo :                                         foo :
    @cd not-exist; date                           @set -e; cd not-exist; date
    @date                                         @date

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

/bin/sh: 0: Can't open cd not-exist; date
make: *** [Makefile:4: foo] Error 127

pipe 로 명령을 연결해 사용할 경우도 마지막 명령의 종료 상태 값이 사용되므로 중간에 어떤 명령에서 오류가 발생하더라도 마지막 명령이 정상 종료하면 전체 명령의 종료 상태 값은 0 이됩니다. 따라서 이와 같은 경우도 make 실행이 종료돼야 한다면 bash 를 이용해 pipefail 옵션을 사용할 수 있습니다.

# error_command 에서 오류가 발생했지만 sed 명령은 항상 참이 되므로 종료 상태 값은 0 이된다.
$ error_command ... | sed 's/foo/bar/' > file
$ echo $?
0

-----------------------------------------------
# date -@ 명령에서 오류가 발생했지만          # bash 를 이용해 pipefail 옵션을 사용하면 된다.
# 종료되지 않고 끝까지 실행된다.              # (exec 에의해 sh 프로세스가 bash 로 변경된다. )
foo :                                   foo :
    @echo $@ start ...                      @echo $@ start ...
    @date -@ | date                         @exec /bin/bash -c 'set -o pipefail && date -@ | date'
    @echo $@ end ...                        @echo $@ end ...

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

foo start ...                           foo start ...
date: invalid option -- '@'             date: invalid option -- '@'
Try 'date --help' for more information. Try 'date --help' for more information.
Mon Apr 20 09:48:16 KST 2020            Mon Apr 20 10:03:05 KST 2020
foo end ...                             make: *** [Makefile:4: foo] Error 1

$ echo $?                               $ echo $?
0                                       2

bash -c ... 또는 sh -c ... 사용시 multi-line 명령 작성법

보통 bash -c ... 형태로 명령을 실행할 때는 quote 을 하고 single-line 으로 작성하는데요. 다음과 같이 multi-line 변수를 활용하면 shell 스크립트 파일을 작성할 때와 동일하게 작성해 실행시킬 수 있습니다.

define cmd
AA=100
echo -e "double quotes\n$AA"
echo 'single quotes\n$AA'
cat <<\EOF
'here'
"document"
EOF
endef

export cmd := $(value cmd)#       child process 에서 사용할수 있게 export

foo :
    @echo $@ start ...
    @exec /bin/bash -c "$$cmd"
    @echo $@ end ...

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

$ make
foo start ...
double quotes
100
single quotes\n$AA
'here'
"document"
foo end ...

echo 명령 사용시 주의할 점

shell 에서 실행할 명령이 하나만 존재하면 굳이 sh -c 를 실행하고 거기서 또 해당 명령을 실행할 필요가 없겠죠. 그래서 이때는 sh -c 실행 없이 바로 명령을 실행합니다. 그럼 echo 명령은 shell builtin 명령인데 어떻게 실행할까요? 이때는 /bin/echo 명령을 사용합니다. 그런데 /bin/echo 명령은 sh 에서와 달리 기본적으로 quotes 에서 escape 문자를 처리 안합니다 ( /bin/echo --help 를 해보면 bash 와 같이 escape 문자 처리를 위한 옵션이 따로 있습니다 ). 따라서 echo 명령 사용시 주의할 필요가 있습니다.

다음은 명령 라인에서 실행할 명령이 echo 명령 하나만 있을 경우 quotes 사용 방식에 따라 어떻게 실행되는지를 비교해놓은 것입니다. 실행할 명령이 두 개 이상이면 물론 sh -c 에 의해 실행됩니다.

                                          # double quotes 이 존재하면 sh -c 에서 실행된다
foo :                                        foo :    
    @echo '111\n222'        (/bin/echo)          @echo "111\n222"       (sh -c)
    @ :; echo '333\n444'    (sh -c)              @echo '333\n444'""     (sh -c)

#####  실행 결과  #####                        #####  실행 결과  #####
111\n222                                     111
333                                          222
444                                          333
                                             444

recipe 실행 중에는 대입 연산은 정의할 수 있지만 rule 은 정의할 수 없다.

rule 은 make 에 의해서 읽기가 완료된 후에 결과에 따라서 순서대로 recipe 가 실행되는 것이므로 recipe 실행 중에는 동적으로 다시 rule 을 정의해 사용할 수 없습니다. 하지만 대입 연산의 경우는 recipe 실행 중에 동적으로 정의해 사용하는 것이 가능합니다.

foo : bar
    @echo target $@ : $(AA) 
    $(if $(filter 100,$(AA)),$(eval AA := 200))   # recipe 실행중에 동적으로 AA 값을 변경

bar :
    @echo target $@ : $(AA)

AA := 100

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

target bar : 100
target foo : 200

recipe 실행 중에는 rule 을 정의할 수 없습니다.

define onerule
$1 :
    @echo target $$@
endef

# $(eval $(call onerule,foo))       # OK : 여기서는 가능

bar :
    $(eval $(call onerule,foo))     # Error

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

Makefile:7: *** prerequisites cannot be defined in recipes.  Stop.

Prefix 문자

문자 설명
@ 명령문이 실행전에 프린트되는 것을 금지합니다.
- 명령이 오류로 종료했을 때 ignore 합니다.
+ -n, -t, -q 옵션을 이용해 make 명령을 실행했을 때도 실행되게 합니다.

@ ( at )

shell 에서 명령이 실행될 때는 실행 결과로 별다른 출력이 발생하지 않는 경우가 많습니다. 이럴 경우 실행전에 먼저 명령문을 출력해서 보여주지 않는다면 어떤 명령이 실행되는지 알수가 없겠죠. 따라서 make 은 기본적으로 shell 명령을 실행하기 전에 먼저 실행할 명령문을 출력합니다. 하지만 이와 같은 처리 방식이 특정 명령에서는 불편할 때가 있습니다. 이때는 명령문 라인 제일 앞에 @ 문자를 추가하면 해당 명령문이 출력되지 않습니다.

-s, --silent 옵션을 이용해 make 명령을 실행하여 makefile 전체에 적용하거나
.SILENT: 타겟을 이용하면 전체 또는 rule 별로 적용할 수가 있습니다.
-n 옵션을 이용해 make 명령을 실행할 때는 무시되고 명령문이 출력됩니다.

foo :                                                  foo :
    echo hello recipe                                      @echo hello recipe

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

echo hello recipe    먼저 실행할 명령문이 출력되고           hello recipe
hello recipe         다음에 실행 결과가 출력된다.

- ( minus )

make 은 기본적으로 shell 명령문이 오류로 종료하면 Error 메시지와 함께 make 실행을 종료합니다. 하지만 경우에 따라서 make 실행이 종료될 필요가 없을 때가있는데 이럴 경우 명령 라인 제일 앞에 - 문자를 추가하면 make 실행이 종료되지 않습니다. ( 이때도 Error ignored 메시지는 출력됩니다 )

-i, --ignore-errors 옵션을 이용해 make 명령을 실행하여 makefile 전체에 적용하거나
.IGNORE: 타겟을 이용하면 전체 또는 rule 별로 적용할 수가 있습니다.

다음 첫 번째 예제의 경우 false 명령에 의해 make 실행이 종료되어 마지막 echo 22222 명령이 실행되지 않는 반면에 두 번째 예제의 경우는 false 명령 앞에 - 문자를 추가하여 오류가 ignored 되어 마지막 명령도 실행됩니다.

foo :                                     foo :
    @echo 11111                               @echo 11111
    false                                     -false
    @echo 22222                               @echo 22222

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

11111                                     11111
false                                     false
make: *** [Makefile:4: foo] Error 1       make: [Makefile:4: foo] Error 1 (ignored)
                                          22222    # 마지막 명령도 실행된다.

+ ( plus )

make 명령을 실행할 때 -t ( --touch ), -n ( --just-print ), -q ( --question ) 옵션을 사용하게 되면 recipe 에 있는 명령들이 실행되지 않습니다. 하지만 명령 라인 앞에 + 문자를 추가하면 -t, -n, -q 옵션 사용 시에도 명령이 실행됩니다.

foo :
    +date      # '+' prefix 추가
    date
    date

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

sh$ make                           # '-t' '-n' '-q' 옵션을 사용할지 않을 경우는
date                               # 모든 명령이 실행된다.
Wed 29 May 2019 01:27:28 PM KST
date
Wed 29 May 2019 01:27:28 PM KST
date
Wed 29 May 2019 01:27:28 PM KST

sh$ make -n                        # '-n' 옵션 사용
date                               # '+' prefix 가 추가된 명령만 실행되고
Wed 29 May 2019 01:27:33 PM KST
date                               # 나머지는 실행되지 않는다.
date

sh$ make -q                        # '-q' 옵션 사용
date
Wed 29 May 2019 01:27:33 PM KST

sh$ make -t                        # '-t' 옵션 사용
date
Wed 29 May 2019 01:27:45 PM KST
touch foo
touch foo

다음은 -t 옵션을 이용해 make 명령을 실행할 경우 아카이브 파일을 최신으로 만듭니다.

ifneq "$(findstring t, $(MAKEFLAGS))" ""
    +touch libfoo.a
    +ranlib -t libfoo.a
else
    ranlib libfoo.a
endif

옵션에 따라 출력 메시지 조절하기

프로젝트를 빌드할때 gcc 명령에 옵션이 많이 포함되면 출력을 보고도 읽기가 어려운데요. 이럴 경우 make 의 강력한 변수 기능과 recipe 의 prefix 기능을 활용하면 옵션에 따라 출력을 원하는 형태로 조절할 수가 있습니다. 아래 예제의 경우 make 명령을 실행할때 따로 옵션을 설정하지 않으면 다음과 같이 실행되어 복잡한 gcc 명령 라인은 출력되지 않게 됩니다.

@echo "CC $<"; gcc -Wall -O2 -I./include -c -o $@ $<

반면에 명령 라인에서 verbose 값을 1 로 설정하면 make 에의해 gcc 명령 라인 전체가 출력됩니다.

gcc -Wall -O2 -I./include -c -o $@ $<

MAKEFLAGS += -rR

CC      := gcc
CFLAGS  := -O2 -Wall -I./include
LDFLAGS := -L/usr/local/lib
LDLIBS  := -lpthread

BUILD_DIR := BUILD
OBJ_DIR   := $(BUILD_DIR)/obj
BIN_DIR   := $(BUILD_DIR)/bin

QUIET_CC = $(if $(verbose),,@echo "CC  $@";)       # recipe 실행 시점에 변수가
QUIET_LD = $(if $(verbose),,@echo "LD  $@";)       # 확장되기 위해 recursive 변수를 사용
Q        = $(if $(verbose),,@)

$(BIN_DIR)/foo : $(OBJ_DIR)/foo.o | $(BIN_DIR)
    $(QUIET_LD)$(CC) $(LDFLAGS) $+ $(LDLIBS) -o $@

$(OBJ_DIR)/foo.o : src/foo.c | $(OBJ_DIR)
    $(QUIET_CC)$(CC) -c $(CFLAGS) -o $@ $<

$(BIN_DIR) $(OBJ_DIR) :
    $(Q)mkdir -p $@

.DEFAULT: ;

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

sh$ mkdir src && echo 'int main() {}' > src/foo.c

sh$ make 
CC  src/foo.o
LD  BUILD/bin/foo

sh$ rm -rf BUILD

sh$ make verbose=1       # verbose 값을 1 로 설정
mkdir -p BUILD/obj
gcc -c -Wall -O2 -I./include -o BUILD/obj/foo.o src/foo.c
mkdir -p BUILD/bin
gcc -L/usr/local/lib BUILD/obj/foo.o -lpthread -o BUILD/bin/foo