Variables and Functions

Make 은 shell 과달리 강력한 변수, 대입 연산 기능을 가지고 있습니다. make 의 메인 기능은 물론 의존관계를 잘 분석해서 룰을 작성하는 것이지만 실제 makefile 작성에 있어서 중요하고 활용을 잘해야 되는 부분은 변수입니다.

Makefile 의 특징 중에 하나는 기본적으로 라인 단위라는 것입니다. 이것은 코드를 작성할 때뿐만 아니라 shell 명령의 출력이 변수에 저장될 때도 newline 이 space 로 변경되어 하나의 라인으로 결과가 저장됩니다. make 에서는 기본적으로 라인 사이즈에 제한이 없습니다.

make 변수는 변수 이름과 value 를 작성하는데 문자 사용에 제한이 없습니다. 이것은 make 을 사용하는데 있어서 아주 중요한 개념인데 value 에는 make script, shell script 를 작성해 대입할 수 있기 때문에 newline 외에 어떤 문자도 사용할 수 있고 변수 이름은 :( rule ), #( comment ), =, whitespace 문자를 제외하고 어떤 문자도 사용할 수 있습니다.

변수 이름으로 대,소문자, 숫자, _ 이외의 문자를 사용하는 것은 shell 종류에 따라 export 가 안될 수 있습니다. 그리고 . 으로 시작하는 대문자 변수는 make 자체에서 사용될 수 있습니다.
( whitespace 문자는 space, tab, newline 을 말합니다. )

@!%/'"& := ' " ` \ \\ / * ? ! ~ = | & ; @ + ^ < > { } ( ) [ ]
^^^^^^^    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  name                         value

$(info $(@!%/'"&))      # @!%/'"&  변수값 출력
실행 결과: ' " ` \ \\ / * ? ! ~ = | & ; @ + ^ < > { } ( ) [ ]

make 에서의 대입 연산은 shell 과 달리 먼저 변수 확장, 함수 실행이 완료된 후에 처리됩니다. 따라서 연산자 왼쪽, 오른쪽 모두에서 변수, 함수를 사용할 수 있고, 기본적으로 변수는 nesting 해서 사용하는 것이 가능합니다.

aa := foo
bb := bar
$(aa)$(bb) := hello $(aa)$(bb) variable   # foobar 변수에 값을 대입

$(info $(aa)$(bb))        # foobar
$(info $($(aa)$(bb)))     # 먼저 $(aa)$(bb) 가 foobar 로 확장되고 $(foobar) 값이 출력된다.
$(info $(foobar))

######  실행 결과  ######
foobar
hello foobar variable
hello foobar variable

테스트할 때는 Makefile 파일에 위 코드를 작성한 후에 프롬프트 상에서 make 명령을 실행하면 됩니다.
한가지 주의해야 될 점은 대입 연산 라인에 주석이 포함된 경우에는 먼저 제거해야 됩니다. ( 참조 )
그리고 실행이 안되고 오류가 발생하면 항상 recipe 라인에서 tab 문자를 확인하세요 !

변수 사용 방법

1 문자 변수를 사용할 때는 괄호가 필요 없지만, 2 문자 이상은 반드시 괄호를 사용해야 합니다.

A := hello
text := THISIS$ATEST          # 1 문자 변수는 사용시 괄호가 필요없다.      
$(info $(text))                  
결과: THISIShelloTEST

AA := hello                   # 2 문자 변수
text := THISIS$AATEST         # $A 값은 empty 가되고 뒤의 A 는 그대로 프린트
$(info $(text))                  
결과: THISISATEST

AA := hello
text := THISIS$(AA)TEST       # 2 문자 이상은 반드시 괄호를 사용해야 합니다.
$(info $(text))                  
결과: THISIShelloTEST

괄호는 $( ) ,${ } 두 가지 형태를 모두 사용할 수 있습니다. 그런데 ${ } 는 shell 의 매개변수 확장과 같으므로 구분하기가 어렵습니다.

FOO := 100
BAR := $(FOO) ${FOO}
$(info $(BAR) ${BAR})
결과: 100 100 100 100

변수의 origin 우선순위

make 에서 사용되는 변수는 makefile 에서 정의한 변수 외에, 환경 변수, 명령 라인에서 설정한 변수, make 에서 default 로 제공되는 변수가 사용될 수 있습니다. 따라서 makefile 에서 변수를 정의하지 않았더라도 환경 변수가 존재하면 조건 지시자를 이용해 값을 체크할 때 참이 되므로 주의할 필요가 있습니다.

sh$ export PROGRAMS=foobar                   sh$ cat Makefile
                                             # 환경 변수 적용을 방지하기 위한 가드
sh$ cat Makefile                             PROGRAMS :=
ifdef PROGRAMS                               ifdef PROGRAMS
  $(info $(PROGRAMS))                           $(info $(PROGRAMS))
endif                                        endif

--------------------------                   ---------------------- 
sh$ make                                     sh$ make
foobar

make 명령 실행시 명령 라인에서 변수값을 전달할경우 makefile 에서 정의한 변수에 우선해서 사용됩니다. 따라서 이것을 활용하면 매번 직접 makefile 을 수정할 필요가 없습니다.

자세한 설명은 orgin 함수를 참조하세요

# CC 변수 값으로 makefile 에서 정의한 값 대신에 clang 이 사용된다.

sh$ make CC=clang

Functions

make 변수는 단순 value 뿐만 아니라 스크립트 코드를 대입해서 함수처럼 사용할 수가 있습니다. 다음을 보면 search 변수에 스크립트를 대입해서 call 함수를 이용해 실행시키는데 이때 date 이 함수의 첫 번째 인수로 전달되고 $(1) 의 값으로 사용됩니다.

자세한 설명은 call 함수를 참조하세요

# PATH 환경변수 목록에서 파일이 존재하는지 검색
search = $(firstword $(wildcard $(addsuffix /$(1),$(subst :, ,$(PATH)))))
result := $(call search,date)
$(info $(result))
실행 결과: /bin/date

위에서 스크립트 코드를 보면 ( ) 괄호가 계속 이어지는 것이 LISP 함수형 언어를 닮았죠. LISP 언어가 대단한게 실제 언어의 설계도가 A4 용지 한장 정도뿐이 안된다고 합니다. 그런데도 하나의 프로그래밍 언어로 충분히 역할을 할 수 있다는 것입니다 ( 가장 안쪽 괄호부터 차례로 실행해 올라가기만 하면 되죠. 어떤 언어건 작성한 코드를 파싱하면 결과가 LISP 언어 형태가 됩니다 [참조] ).

make 에서 제공되는 스크립트 기능은 LISP 언어와 달리 기본적인 연산자도 제공되지 않고, 데이터 타입도 없고 단순 텍스트 프로세싱입니다. 따라서 스크립트 자체로 할 수 있는 것은 많지 않고 프로젝트를 빌드 하는데 주로 사용되는 파일명, 경로명, 리스트 프로세싱 built-in 함수들을 활용하는 것입니다. 하지만 $(shell ...) 함수를 이용하면 외부 명령을 실행시킬 수 있기 때문에 필요한 작업을 처리할 수 있습니다.

Built-in 함수, 사용자 정의 함수

make 에서 사용되는 함수에는 built-in 함수와 사용자 정의 함수가 있습니다. 차이점은 함수에 인수를 전달할 때 사용자 정의 함수는 call 함수를 사용해야 됩니다. 전달할 인수가 없을 경우는 일반 변수를 사용하는 것과 call 함수를 사용하는 것은 차이가 없습니다. 함수에 인수를 전달할 때는 , 문자로 구분합니다.

# patsubst, filter 함수는 built-in 함수로 호출시 call 함수가 필요없다.
# 함수에 인수를 전달할 때는 ',' 문자로 구분하여 작성합니다.
file_y := $(patsubst %.y,%.c,$(filter %.y,$(files)))

# 사용자 정의 함수를 호출할 때 인수를 전달하려면 call 함수를 사용해야 합니다.
reverse = $2 $1                  
res := $(call reverse,aaa,bbb)

# 함수 호출시 전달할 인수가 없을 경우는 (1), (2) 는 차이가 없습니다.
AA  = $(shell echo hello make function)
res := $(AA)                           ----- (1)
res := $(call AA)                      ----- (2)

전달된 인수는 shell 에서처럼 숫자 변수에 자동으로 할당됩니다.

# $1 값은 aaa 가 되고, $2 값은 bbb 가 된다. ( $0 은 함수명인 reverse )
reverse = $2 $1                  
res := $(call reverse,aaa,bbb)    # 결과로 bbb aaa 가 반환된다.

foo = $(info name: $0, arg1: $1, arg2: $2 )
$(call foo,111,222) 
결과: name: foo, arg1: 111, arg2: 222

함수에 인수를 전달할 때는 , 문자로 구분하는데 사이에 공백이 있을경우 값에 포함되므로 주의할 필요가 있습니다.

define foo                        define foo
$(info xxx$1xxx$2xxx$3xxx)        $(info xxx$1xxx$2xxx$3xxx)
endef                             endef

$(call foo,111,222,333)           $(call foo, 111,222 , 333 )  # ',' 다음에 공백이 포함됨

#####  실행 결과  #####             #####  실행 결과  #####
xxx111xxx222xxx333xxx             xxx 111xxx222 xxx 333 xxx    # 전달된 값에도 공백이 포함된다.

예를 들어 인수의 값으로 파일 리스트가 오는 곳에서는 공백이 문제가 되지 않지만 ( 공백에 의해 파일들이 분리되므로 ) 함수의 반환값으로 사용되는 인수에서는 공백이 그대로 결과값에 포함됩니다.

files := aaa.c bbb.c ccc.c

# 인수 값으로 파일 리스트가 오는 곳에는 공백이 포함돼도 상관없다.
file_o := $(patsubst %.c,%.o,    $(filter %.c,    $(files)))
$(info $(file_o))
실행결과: aaa.o bbb.o ccc.o

# 함수의 반환값으로 사용되는 인수에서는 공백이 그대로 값에 포함된다.
file_o := $(patsubst %.c,     %.o,$(filter %.c,$(files)))
$(info $(file_o))
실행결과:     aaa.o      bbb.o      ccc.o

기본적으로 변수, 함수는 makefile 내의 어떤 위치에서도 사용할 수 있습니다. rule 의 targets 이나 prerequisites 부분에서도 사용할 수 있고 recipe 를 작성하는데도 사용할 수 있습니다. 특히 함수 반환값이 empty 인 info, warning, error, eval 같은 builtin 함수들은 syntax error 없이 어떤 위치에서도 사용할 수 있습니다.

대입 연산자

위의 search 사용자 정의함수 예제를 보면 search 변수에 스크립트 코드를 대입할 때는 = 연산자가 사용되고, 함수 실행 결과를 result 변수에 대입할 때는 := 연산자가 사용된 것을 볼 수 있습니다.

make 변수에는 두 가지 종류 ( flavor ) 가 있습니다.

  1. 대입 연산시 우측 value 에 사용된 변수, 함수가 확장, 실행이 완료되어 결과가 저장되는 simple 변수
  2. 대입 연산시 우측 value 에 사용된 변수, 함수가 확장, 실행 없이 문자 그대로 저장되는 recursive 변수
    ( recursive 변수는 나중에 변수가 사용되는 시점에서 확장, 실행이 발생하게 됩니다.)

따라서 스크립트 코드는 실제 변수가 사용되는 시점에서 전달된 인수와 함께 실행돼야 하므로 = recursive 연산자를 사용하고, result 변수는 실행이 완료된 값이 저장돼야 하므로 := simple 연산자를 사용한 것입니다.

다음은 make 에서 사용할 수 있는 대입 연산자를 정리해 놓은 것입니다.

Operator Description Flavor Parse
:= 대입 simple immediate
= 대입 recursive deferred
+= append recursive or simple
( default 는 recursive )
deferred or immediate
?= undefined 일경우 대입 recursive deferred
!= shell 명령 실행 후 대입 recursive immediate

Parse 항목은 value 에 있는 식이 언제 실행되는지를 나타내는데 simple 연산자의 경우는 대입 연산시 즉시( immediate ) 실행되고 recursive 연산자의 경우는 해당 변수 사용 시까지 연기( deferred ) 됩니다. != 연산자는 우측 value 값을 shell script 로 실행하므로 Parse 항목은 immediate 이지만 변수는 recursive 가 됩니다.

flavor builtin 함수를 이용하면 변수가 simple 변수인지 recursive 변수인지 알 수 있습니다.

:=

원래 전통적 make 에서는 simple 변수 개념이 없었습니다. 모두 recursive 변수를 사용했습니다. ( 위의 표를 보면 모두 recursive 변수죠 ). := 연산자를 사용하는 simple 변수는 나중에 gnu make 에 추가된 기능입니다. 다음은 := 연산자와 = 를 비교해본 것인데요. = 연산자의 경우는 val 변수 정의가 foo 변수 뒤에 와도 되는 것을 알 수 있습니다.

# foo 변수는 simple                       # foo 변수는 recursive
val := 100                               val := 100

# 대입 완료후 foo 값은 100 이 된다.           # 대입 완료후 foo 값은 '$(val)' 가 된다.
foo := $(val)                            foo = $(val)

val := 200                               val := 200

$(info $(foo))                           $(info $(foo))  # 변수가 사용되는 시점에 확장된다.
결과: 100                                 결과: 200

=

아래 예제는 foo 변수 자신의 값에 다시 world 단어를 추가하려고 한 것인데요. 이때 recursive 연산자를 사용하면 변수가 사용되는 시점에 $(foo) world 가 확장될 때 $(foo) 가 또다시 자신을 가리키게 되므로 오류가 됩니다. 따라서 이와 같은 경우 두 번째와 같이 simple 연산자를 사용해야 합니다.

value builtin 함수를 이용하면 확장되기 전의 변수값을 볼 수 있습니다.

foo = hello
foo = $(foo) world        # 대입 연산이 완료되면 foo 값은 '$(foo) world' 가 된다.
$(info $(foo))
결과: Makefile:3: *** Recursive variable 'foo' references itself (eventually).  Stop.

$(info $(value foo))      # value builtin 함수를 이용해 확장되기 전의 변수값을 조회.
결과: $(foo) world
----------------------------

foo = hello
foo := $(foo) world       # 대입 연산이 완료되면 foo 값은 'hello world' 가 된다
$(info $(foo))
결과: hello world

$(info $(value foo))
결과: hello world

+=

+= 는 append 연산자로 사용되는 변수의 flavor 에 따라 각각 다르게 동작합니다. 예를 들어 simple 변수에 += 연산자가 사용되면 simple 연산자로 동작하여 value 에있는 식이 실행이 완료되어 결과가 append 되고, recursive 변수에 사용되면 recursive 연산자로 동작하여 value 에있는 식이 그대로 추가됩니다. default 는 recursive 이므로 처음 변수 사용시 += 연산자가 사용되면 recursive 변수가 됩니다. 값이 대입될 때는 먼저 앞에 space 가 하나 추가됩니다.

# foo 변수를 simple 변수로 초기화              # foo 변수를 recursive 변수로 초기화
foo := aaa.c                               foo = aaa.c
val := bbb.c                               val := bbb.c

# 대입 완료후 foo 값은 aaa.c bbb.c 가 된다.     # 대입 완료후 foo 값은 'aaa.c $(val)' 가 된다.
foo += $(val)                              foo += $(val)

val := ccc.c                               val := ccc.c

$(info $(foo))                             $(info $(foo))
결과: aaa.c bbb.c                           결과: aaa.c ccc.c

?=

?= 연산자는 변수가 undefined 상태일 경우만 값이 대입되므로 아래 첫 번째 식은 두 번째 식과 같게됩니다.

AA ?= 100                         ifeq "$(origin AA)" "undefined"
                                    AA = 100
                                  endif

따라서 다음과 같이 include 되는 파일에서 변수를 정의할때 ?= 연산자를 사용하면 나중에 특정 makefile 에서는 다른 값을 전달하고 싶을 경우 include 전에 해당 변수를값을 정의해 주면됩니다.

sh$ cat module.mk

EXCL_1 ?= foo.c bar.c
EXCL_2 ?= zoo.c
. . . .

$(info $(EXCL_1)) 
$(info $(EXCL_2)) 

----------------------------------------------------------------
sh$ cat Makefile                    sh$ cat Makefile

include module.mk                   EXCL_1 := baz.c   # include 전에 변수를 다른값으로 정의
                                    include module.mk

#####  실행 결과  #####               #####  실행 결과  #####
foo.c bar.c                         baz.c     # '?=' 연산자에 의해 baz.c 값이 사용된다.
zoo.c                               zoo.c

!=

이 연산자는 shell builtin 함수 와 동일하게 동작합니다. 따라서 실행되는 명령의 stdout 출력이 반환값이 되고 newline 이 모두 space 로 변환됩니다.

!= 연산자는 우측 value 는 immediate 실행되지만 변수는 recursive 가 됩니다.
$(shell ...) 함수와 := 연산자를 사용하면 immediate 실행되고 simple 변수가 되고
$(shell ...) 함수와 = 연산자를 사용하면 defered 실행되고 recursive 변수가 됩니다.

AA != find /bin/ -name 'p*'
$(info $(AA))
$(info $(flavor AA))

#####  실행 결과  #####     stdout 출력값에서 newline 이 모두 space 로 변경된다.
/bin/pwd /bin/ping6 /bin/plymouth /bin/ping4 /bin/pidof /bin/ps /bin/ping
recursive

----------------------------------

AA := $(shell find /bin/ -name 'p*')      # shell 함수와 ':=' 연산자 사용
$(info $(AA))
$(info $(flavor AA))

#####  실행 결과  #####
/bin/pwd /bin/ping6 /bin/plymouth /bin/ping4 /bin/pidof /bin/ps /bin/ping
simple

변수의 flavor 변경

:= 연산자에 의해 simple 변수가 되어도 이후에 =, != recursive 연산자가 사용되면 다시 recursive 변수로 변경됩니다. 마찬가지로 =, !=, ?= 연산자에 의해 recursive 변수가 되어도 이후에 := 연산자가 사용되면 다시 simple 변수로 됩니다. += 연산자만 변경이 없습니다.

AA := 100                      AA := 100                      AA  = 100
AA  = 200                      AA != echo 200                 AA := 200
$(info $(flavor AA))           $(info $(flavor AA))           $(info $(flavor AA))
$(info $(AA))                  $(info $(AA))                  $(info $(AA))

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

recursive                      recursive                      simple
200                            200                            200

대입 연산시 공백문자

make 에서는 대입 연산시에 shell 에서처럼 연산자와 공백 없이 붙일 필요가 없습니다. 연산자 양쪽에 있는 공백은 값에 포함되지 않습니다. 하지만 value 끝부분에 붙는 공백은 값에 그대로 포함되므로 주의해야 됩니다.

AA:=hello make variable
$(info xxx$(AA)yyy)
결과: xxxhello make variableyyy

AA      :=      hello make variable
$(info xxx$(AA)yyy)
결과: xxxhello make variableyyy

Recursive 변수는 사용시 주의해야 합니다.

recusive 변수는 실제 변수가 사용되는 시점에서 value 에 있는 식이 확장되므로 사용시 주의할 필요가 있습니다. 가령 value 에 포함된 변수가 중간에 다른 값으로 변경된다든지 아니면 값이 설정되기 전에 변수가 사용된다든지 하면 정상적으로 실행되지 않겠죠. 따라서 가능하면 simple 변수를 사용하고 필요한 경우에 recursive 변수를 사용하는 것이 좋습니다.

다음은 두 개의 include 파일에서 각각+= 연산자를 이용해 SRCS 변수에 append 를 하는 경우입니다. 이때 만약에 Makefile 1 에서처럼 SRCS 변수가 recursive 변수로 사용된다면 각 모듈에서 value 값으로 사용된 $(module_srcs) 가 실제 사용되는 시점에서 확장이 일어나게 되므로 결과적으로 module2.mk 에서 설정한 값이 두번 출력되게 됩니다. Makefile 2 에서와 같이 SRCS 변수를 simple 변수로 초기화하여 사용하면 append 시에 변수값이 바로 확장되어 저장되므로 이와 같은 문제가 발생하지 않습니다.

sh$ cat module1.mk
module_srcs := aaa.c bbb.c
SRCS += $(module_srcs)

sh$ cat module2.mk
module_srcs := ccc.c
SRCS += $(module_srcs)
-----------------------
                         # Makefile 1
include module1.mk
include module2.mk
$(info $(SRCS))          # 현재 $(SRCS) 변수값은 $(module_srcs) $(module_srcs) 가 된다.
결과 : ccc.c ccc.c
----------------------
                         # Makefile 2
SRCS :=                  # SRCS 를 simple 변수로 초기화
include module1.mk
include module2.mk
$(info $(SRCS))          # 현재 $(SRCS) 변수값은 aaa.c bbb.c ccc.c 가 된다.
결과 : aaa.c bbb.c ccc.c

eval 함수를 이용한 lazy initialization

대입 연산에서 $(shell ...) 함수를 이용해서 외부 명령을 실행할 경우 makefile 전체에 실행할 외부 명령이 많아지게 되면 make 실행 시간이 느려질 수 있습니다. simple 변수는 대입 연산이 처리될 때 바로 외부 명령이 실행되므로 나중에 변수값이 사용되지 않을 경우 불필요하게 외부 명령이 실행되는 결과를 갖게 되고, recursive 변수는 매번 변수가 사용될 때마다 외부 명령이 실행되게 되므로 한 번만 외부 명령이 실행돼도 될 경우 ( 매번 실행 결과가 같아서 ) 또한 불필요하게 외부 명령이 실행되는 결과를 갖게 됩니다. 이럴 경우 recursive, simple 변수 특성과 eval 함수를 활용하면 외부 명령의 실행을 실제 변수가 사용되는 시점에서 한 번만 실행되게 할 수 있습니다.

# 1. 변수를 정의할때 `=` recursive 연산자를 사용하였으므로 value 부분에서 실행이 발생하지 않게됩니다.
# 2. $(LAZY) 변수가 사용될 때 value 부분이 실행되는데 이때 shell 명령이 실행됨과 동시에 
#    eval 함수에 의해 LAZY 변수가 다시 simple 변수로 정의가 됩니다. (`:=` 연산자 사용)
# 3. 마지막에 추가된 `$(LAZY)` 에 의해 shell 명령 실행 결과가 출력됩니다.
LAZY = $(eval LAZY := $(shell date))$(LAZY)
$(info $(flavor LAZY))        
$(info $(LAZY))
$(info $(flavor LAZY))        

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

recursive                           # 처음에는 recursive 변수이지만
Tue 14 May 2019 09:43:47 AM KST     # 변수가 한번 사용되고 난 후에는
simple                              # simple 변수가 된다.

대입 연산시 escape 해서 사용해야 되는 문자

대입 연산에는 기본적으로 값으로 어떤 문자도 올수 있지만 escape 해서 사용하는 문자가 3 개 있습니다.

  1. $ 문자는 make 에서 변수를 나타내는데 사용되므로 일반 문자로 사용하려면 $$ 변수를 사용해야 합니다.
  2. # 문자는 comment 의 시작을 알리는데 사용되므로 일반 문자로 사용하려면 \# 해야 합니다.
  3. newline 문자를 escape 하면 하나의 라인을 개행해서 작성할 수 있습니다.
# 여기서 '$' 문자는 '$ ' 변수로 인식되고 '#' 문자는 주석으로 인식되어 이후로 출력이 안된다.
AA :=  aaa $ bbb # ccc ddd eee
$(info $(AA))
결과: aaa bbb

# 다음과 같이 escape 해야 해당 문자가 출력 됩니다.
AA :=  aaa $$ bbb \# ccc ddd eee
$(info $(AA))
결과: aaa $ bbb # ccc ddd eee
--------------------------------------------------------

# newline 을 escape 하여 개행할 경우는 다음 라인과의 사이에 하나의 space 만 남습니다.
AA :=  aaa     bbb ccc        \
                         ddd eee
$(info $(AA))
결과: aaa     bbb ccc ddd eee      # ccc ddd 사이에는 하나의 space 만 남는다.
--------------------------------------------------------

# 만약에 '#' 와 'newline' 바로 앞에 '\' 문자를 포함하려면 '\\' 두개를 사용해야 합니다.
AA :=  aaa $$ bbb \\\# ccc \\\
       ddd eee
$(info $(AA))
결과: aaa $ bbb \# ccc \ ddd eee

escape 하지 않고 다음과 같이 변수에 대입해서 사용하는 방법도 있습니다.

hash   != printf '\043'
dollar != printf '\044'
text   := hash : $(hash), dollar : $(dollar)
$(info $(text))
결과: hash : #, dollar : $

특수 문자를 변수에 대입해서 사용

함수 호출시 인수를 구분하기 위해 사용되는 , 문자나 space 같은 문자는 일반적인 방법으로 값을 전달하기 어려운 경우가 있습니다. 이럴 경우에는 해당 문자를 변수에 대입해서 사용하면 됩니다.

# files 변수값에서 ',' 문자를 삭제하려고 하지만 정상적으로 실행되지 않는다.
# 삭제하고자 하는 문자와 인수들을 분리할 때 사용하는 문자가 같다.
files := foo.c, bar.c, zoo.c
$(info $(subst ,,,$(BB)))      

---------------------------------

# 파일명에서 space 문자를 '_' 로 변경하려고 하지만 정상적으로 실행되지 않는다.
# subst 함수명 바로 뒤의 space 는 값으로 인식되지 않는다.
file := foo bar.c
$(info $(subst  ,_,$(file)))

따라서 이와 같은 경우는 다음과 같이 comma, space 문자를 변수에 대입해서 사용하면 됩니다.

comma := ,
space := $(empty) $(empty)

files := foo.c, bar.c, zoo.c
$(info $(subst $(comma),,$(files)))      # $(comma) 변수를 사용
결과: foo.c bar.c zoo.c
-------------------------------------

file := foo bar.c
$(info $(subst $(space),_,$(file)))      # $(space) 변수를 사용
결과: foo_bar.c

# 또는 다음과 같이 해도 됩니다.
$(info $(subst $(empty) ,_,$(file)))
결과: foo_bar.c

newline 문자를 정의할 때는 multi-line 변수를 사용해야 합니다.

comma := ,
space := $(empty) $(empty)
define nl


endef

AA := Here$(space)is a message$(comma)$(nl)with embedded$(comma)$(nl)newlines.
$(info $(AA))

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

Here is a message,     # 개행이 돼서 출력된다.
with embedded,
newlines.

---------------------------------------

, := ,
space != echo " "
$(space) := $(space)
define \n


endef

AA := Here$( )is a message$(,)$(\n)with embedded$(,)$(\n)newlines.
$(info $(AA))

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

Here is a message,
with embedded,
newlines.

# 문자는 주석의 시작을 나타내므로 변수값으로 사용하려면 다음과 같이 변수에 대입해 사용합니다.

AA := The brown fox # jumped over the lazy dog
$(info $(AA))

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

The brown fox       '#' 문자 이후는 주석처리가 되어 표시되지 않는다.

--------------------------------

pound := \#
AA := The brown fox $(pound) jumped over the lazy dog
$(info $(AA))

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

The brown fox # jumped over the lazy dog

대입 연산을 정의하는 라인에는 주석이 포함되면 안된다 !

make 에서는 대입 연산을 정의하는 라인에 # 문자를 이용해 comment 를 추가하면 # 문자 앞부분에 존재하는 공백도 변수의 값에 추가되므로 주의해야 합니다. 다음을 보면 코드 자체는 문제가 없지만 # empty .... 주석 때문에 실질적으로 space 변수에 5 개의 space 가 설정되어 subst 함수가 정상적으로 실행되지 않습니다.

space := $(empty) $(empty)    # empty 는 존재하지 않는 변수
file := foo bar.c

$(info $(subst $(space),_,$(file)))    # 정상적으로 실행되지 않는다.

-------------------------------------

$(info xxx$(space)yyy)
결과: xxx     yyy             # space 변수값으로 5 개의 space 가 설정됨

따라서 대입 연산 라인에 주석이 필요할 경우는 다음과 같이 value 가 끝나는 부분에 # 문자를 추가해야 합니다.

SDIR := src#              Path to directory of source files
# 또는
HDIR := include#        # Path to directory of header files
BDIR := bin#            # Path to directory of binary files

변수를 사용하는 이유

makefile 을 보면 굳이 변수를 사용하지 않아도 될 것 같은 명령 이름부터 변수가 많이 사용되는 것을 볼 수 있습니다. 이렇게 하는 이유는 변수를 사용함으로써 코드의 중복을 제거하여 간결히 할 수 있고 값의 수정이 필요할 경우 한 곳에서 관리가 가능하므로 전체적으로 버그도 줄일 수 있기 때문입니다. 일단 사용하고자 하는 파일 목록을 하나의 변수에 설정해놓으면 이후에는 해당 변수를 가지고 확장자를 변경하거나, 경로를 변경하면서 원하는 목록을 가지는 다른 변수들을 쉽게 생성해서 사용할 수가 있습니다.

EXCLUDES := zoo.c                  # 테스트를 위해 제외할 파일 목록

SRCS := foo.c bar.c zoo.c          # SRCS 변수에 전체 파일 목록 작성
SRCS := $(filter-out $(EXCLUDES),$(SRCS))    # foo.c bar.c

SRCS := $(SRCS:%=lib/%)            # lib/foo.c lib/bar.c                  # 경로 추가
OBJS := $(SRCS:%.c=BUILD/%.o)      # BUILD/lib/foo.o BUILD/lib/bar.o ...  # 확장자 변경
EXES := $(OBJS:%.o=%)              # BUILD/lib/foo BUILD/lib/bar ...

예를 들어 gcc 를 clang 으로 바꾼다거나 다른 경로로 설정해 사용해야 될 경우가 생길 수 있는데 이럴 때 변수로 설정해 놓았다면 정의된 곳의 위치를 찾아서 한번만 변경해주면 됩니다.

AS            = $(CROSS_COMPILE)as
LD            = $(CROSS_COMPILE)ld
CC            = $(CROSS_COMPILE)gcc
CPP           = $(CC) -E
AR            = $(CROSS_COMPILE)ar
NM            = $(CROSS_COMPILE)nm
STRIP         = $(CROSS_COMPILE)strip
OBJCOPY       = $(CROSS_COMPILE)objcopy
OBJDUMP       = $(CROSS_COMPILE)objdump
PAHOLE        = pahole
LEX           = flex
YACC          = bison
AWK           = gawk
INSTALL       = /usr/local/bin/install -c
INSTALLDATA   = /usr/local/bin/install -c -m 644