Difference between r1.11 and the current
@@ -266,6 +266,8 @@
}}}
=== 좀더 다양한 응용 ===
==== 기본 치환 함수 ====
* subst : 주어진 문자열(text)에서 특정 문자열(from)을 다른 문자열(to)로 치환해줍니다.
* 사용법) {{{$(subst <from>,<to>,<text>)}}}
* 예) {{{$(subst A,B,ABC)}}}
=== 좀더 다양한 응용 ===
==== 기본 치환 함수 ====
좀더 다양한 함수는 [^http://korea.gnu.org/manual/release/make/make-sjp/make-ko_8.html 여기]를 참고하세요.
* 사용법) {{{$(subst <from>,<to>,<text>)}}}
* 예) {{{$(subst A,B,ABC)}}}
@@ -434,5 +436,4 @@
* GNU make manual 번역본
* 1부: [^http://korea.gnu.org/manual/release/make/make-sjp/]
* 2부: [^http://korea.gnu.org/manual/release/make/make-cwryu/]
* 1부: [^http://korea.gnu.org/manual/release/make/make-sjp/]
* 2부: [^http://korea.gnu.org/manual/release/make/make-cwryu/]
Make 기초사용법
- 작성자
- 고친과정
2004년 어느날 : 처음씀
1.1. 시작하기전에
make 는 개발과정에서 반복적인 빌드 과정을 효율적으로 수행할 수 있도록 빌드에 필요한 명세를 작성하여 이에 따른 빌드시간의 단축과 복잡한 빌드 과정의 반복을 손 쉽게 할 수 있도록 하는 도구입니다.
복잡한 빌드과정을 명세화하여 개발과정에서 수 많은 빌드의 반복 작업에서 소중한 시간을 줄일 수 있는 것을 찾으신다면 make 를 통해서 절약해볼 수 있겠습니다. (여기서 make 는 수정한 파일에 대한 부분만 빌드하도록 할 수 있다는 것)
복잡한 빌드과정을 명세화하여 개발과정에서 수 많은 빌드의 반복 작업에서 소중한 시간을 줄일 수 있는 것을 찾으신다면 make 를 통해서 절약해볼 수 있겠습니다. (여기서 make 는 수정한 파일에 대한 부분만 빌드하도록 할 수 있다는 것)
1.2. Make 의 목적
우리가 일반적으로 개발하고 관리하는 하나의 프로젝트는 C source를 컴파일하여 Object 파일을 만들고 이러한 Object 몇개를 묶어서 링크(Link)과정을 거쳐서 최종 목적인 실행파일을 생성합니다. 이때 사용하는 명령은 다음과 같겠죠. 아~ 무지 길다~!
소스의 내용을 조금만 수정해도 이 과정은 반복되어야 합니다. 참~~~ 번거로운 일이 우리의 앞길?막고 있는듯한 느낌입니다. 그래서 이를 쉽게 간략화 해주는 유틸리티가 탄생했는데 "make" 가 바로 그것입니다. 실제로 "make"의 몇몇 기능만 사용하면 위의 문제는 쉽게 해결되지만 그 밖에 대규모 프로젝트에서 겪을수 있는 불편함을 해소해줄수 있다는 점이 보다 매력으로 다가올것입니다.
bash# cc -O2 -Wall -Werror -fomit-frame-pointer -c -o test.o test.c bash# ls test.c test.o bash# ld -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o test.o bash# ./test Hello world ! bash# _
1.3. 기본규칙
"make"는 크게 Target, Depend, Command, Macro 로 구성되어 있습니다. 우선 기본적으로 Target, Depend, Command 의 구성을 살펴보겠습니다.
일반적인 make들은 이러한 기본규칙을 포함한 내용을 파일명 "makefile"과 "Makefile"에서 기본적으로 찾아서 이를 해석하게 됩니다. (GNU make는 "GNUmakefile" 을 우선 찾습니다.) 그 밖에 다른 파일명으로 make를 작성하려면 "-f <make파일>" 옵션을 사용할 수 있습니다.
여기서 "Target"은 생성하고자 하는 목적물을 지칭하며 Depend 는 Target을 만들기 위해서 필요한 요소를 기술하게 됩니다. 그리고 Command 는 일반 Shell 명령이 옵니다. 이때 Command는 Depend 의 파일생성시간(또는 변경된 시간)을 Target과 비교하여 Target 보다 Depend의 파일이 시간이 보다 최근인 경우로 판단될때에만 실행됩니다. 물론 이것에 대한 예외적인 규칙이 있습니다만 일단 무시하고 받아들이세요. 이제 간단히 다음과 같이 "Makefile" 이라는 파일명으로 다음과 같이 작성하여 "make"의 행동을 기술합니다. 단, 주의할것은 Command 는 반드시 앞에 <TAB>문자가 와야 합니다. 물론 예외상황이 있기는 하지만 나중에 그에 대한 내용을 다루기로 하고 일단 ld, cc 명령 앞에 반드시 <TAB>문자로 입력하세요. 그리고 너무 긴 줄은 백슬래쉬(\)와 개행을 통하여 나눌수 있습니다.
이제 명령프롬프트상에서 "make test" 라고만 입력하면 다음과 같이 실행됩니다.
엇? 그런데 다시 실행해보시면 놀랍게도 이런 메세지가 나올겁니다. 이것이 바로 "make" 의 중요한 기능중에 한가지 입니다. 바로 똑같은 작업은 다시 해봤자 어차피 결과가 같을것이라는 예상때문에 더이상 같은 작업을 하지 않는 것이지요.
그러면 간단히 다음과 같이 "test.c" 의 변경날짜를 바꿔봅시다. 그리고 다시 한번 "make test"명령을 수행해봅시다. 어떻습니까? 이번에는 다시 명령을 수행하는것을 보실수 있을겁니다.
이제 천천히 분석해보자면 Target 보다 Depend의 변경시간이 최근이라면 Command 를 수행한다고 하였습니다. 그래서 "test" 라는 Target이 "test.o" 에 의존관계를 갖고 있는데 "test.o"는 다시 Target으로 기술되어 있고 여기에 "test.c"가 의존관계로 기술되어 있습니다. 때문에 "test.c"가 최근에 변경되어 "test.o"보다 변경된 날짜가 최근이 되면 "test.o"를 새로 생성하게 됩니다. 또한 "test.o"는 "test"보다 최근에 변경된것으로 보이므로 "test"는 "test.o"에 의해서 새로 생성되는 결과를 가져옵니다. 결국 소스가 변경되지 않으면 "make"는 아무것도 안하지만 소스가 변경되면(변경된 날짜가 갱신되면) "test"는 새롭게 빌드되는 것입니다. 이제 대충 의존관계 성립을 어떻게 기술하는지 보았습니다. 하지만 이렇게 작성하면 오히려 더 불편하다고 불만을 토하는 분들이 속출할것입니다. 그렇습니다. 실제로는 저렇게 작성하는 경우는 별로 쓰이지 않습니다. 조금더 세련되게 작성하도록 Macro 의 사용이 준비되어 있으니 불만은 이제 그만 하세요.
<Target>: <Depend> ?... [[;] <Command>] <탭문자><Command>
여기서 "Target"은 생성하고자 하는 목적물을 지칭하며 Depend 는 Target을 만들기 위해서 필요한 요소를 기술하게 됩니다. 그리고 Command 는 일반 Shell 명령이 옵니다. 이때 Command는 Depend 의 파일생성시간(또는 변경된 시간)을 Target과 비교하여 Target 보다 Depend의 파일이 시간이 보다 최근인 경우로 판단될때에만 실행됩니다. 물론 이것에 대한 예외적인 규칙이 있습니다만 일단 무시하고 받아들이세요. 이제 간단히 다음과 같이 "Makefile" 이라는 파일명으로 다음과 같이 작성하여 "make"의 행동을 기술합니다. 단, 주의할것은 Command 는 반드시 앞에 <TAB>문자가 와야 합니다. 물론 예외상황이 있기는 하지만 나중에 그에 대한 내용을 다루기로 하고 일단 ld, cc 명령 앞에 반드시 <TAB>문자로 입력하세요. 그리고 너무 긴 줄은 백슬래쉬(\)와 개행을 통하여 나눌수 있습니다.
test: test.o ld -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 \ -o test \ /usr/lib/crt1.o \ /usr/lib/crti.o \ /usr/lib/crtn.o \ test.o test.o: test.c cc -O2 -Wall -Werror -fomit-frame-pointer -c -o test.o test.c
bash# make test cc -O2 -Wall -Werror -fomit-frame-pointer -c -o test.o test.c ld -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o test.o bash# _
bash# make test make: `test'는 이미 갱신되었습니다. bash# _
bash# touch test.c bash# make test cc -O2 -Wall -Werror -fomit-frame-pointer -c -o test.o test.c ld -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o test.o bash# _
1.4. Macro
매크로는 다음과 같이 "=" 문자의 왼편에는 Macro의 대표이름(Label)을 기술하고 오른편에는 그 내용을 적습니다. 이때 "=" 문자에 인접한 양쪽의 공백(Space)문자는 무시됩니다. 이것은 매우 기본적인 Macro를 예기하는것이며 이 밖에도 몇가지 확장된 Macro 가 있습니다. 하지만 일단 그것은 나중에 예기하도록 하겠습니다.
자! 이제 간단히 다음과 같이 조금 개선해서 "Makefile"을 작성해보겠습니다.
여기서 하나만 살펴본다면 "CC = cc" 라고 매크로 선언을 보면 "CC"라는 매크로명은 "cc"라는 명령어로 정의됩니다. 이것을 사용하기 위해서는 "$"기호뒤에 괄호"("과 ")"을 두고 그 안에 매크로 이름을 넣어 사용합니다. 즉, "$(CC)" 는 "cc"로 해석됩니다. 하지만 아직도 이것이 너무 복잡하다는 불만을 가지신분이 계실겁니다. "make"는 그래서 기본 확장자 규칙이라는 방법도 지원하고 있습니다.
<Macro name> = <Macro 내용>
CC = cc LD = ld CFLAGS = -O2 -Wall -Werror -fomit-frame-pointer -c LDFLAGS = -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 STARTUP = /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o test: test.o $(LD) $(LDFLAGS) -o test $(STARTUP) test.o test.o: test.c $(CC) $(CFLAGS) -o test.o test.c
1.5. 기본 확장자 규칙
우리는 보통 C source를 목적파일로 컴파일합니다. 이것은 확장자가 통상 ".c"에서 ".o"를 만들어 내는 규칙이 생성될법 합니다. "make"는 이점에 착안하여 다음과 같은 확장자 규칙을 이용할수 있습니다. 물론 이것은 한가지 방법일 뿐이며 이 밖에도 몇가지 방법이 있습니다.
이제 여기서 새롭게 등장한 "$@"와 "$^", "$<" 의 의미를 예기할때가 왔습니다. 이것은 "make"의 기본 정의된 Macro중에 한가지 입니다. "$@" 또는 "$(@)"는 바로 Target 을 말합니다. 그리고 "$<"는 열거된 Depend중에 가장 왼쪽에 기술된 1개의 Depend를 말하며 "$^"는 Depend 전체를 의미합니다. 이것은 앞으로도 "make"를 사용하는데 있어서 굉장히 많은 부분 기여하는 매크로이므로 눈여겨 보셔야 할 부분입니다. 이 밖에도 "$?" 로 있는데 이것은 Target과 Depend의 변경날짜를 비교하여 Depend의 변경날짜중에 최근에 변경된것만 선택하는 매크로입니다. "$?"는 주로 라이브러리의 생성 및 관리시에 사용되는데 나중에 시간되면 설명하겠습니다. 위의 기술은 확장자 ".c"를 가진 파일을 확장자 ".o"를 가진 파일로 생성하는 공통적인 확장자 규칙을 예로 작성한 것입니다. 한번 사용해볼까요?
이게 ".c.o" 는 .c 를 .o로 만들어 보자는 내용입니다. 그리고 "$<"는 .c 에 취해지는 소스파일명인 "test.c"로 대체됩니다. 또한 "$@"는 Target인 "test.o"로 대치될것입니다.
.c.o: $(CC) $(CFLAGS) -o $@ $<
CC = cc LD = ld CFLAGS = -O2 -Wall -Werror -fomit-frame-pointer -c LDFLAGS = -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 STARTUP = /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o test: test.o $(LD) $(LDFLAGS) -o $@ $(STARTUP) $^ .c.o: $(CC) $(CFLAGS) -o $@ $<
1.6. 가짜(pseudo) target
이제 지금까지 작성한 "Makefile"을 사용해보면 한가지 불편한 점을 느낀 분들이 계실겁니다. "test.c"로 "test.o"를 만들고 "test.o"로 "test"를 만들도록 작성하였지만 소스파일의 수정이 없는 경우 항상 "make"는 더이상의 컴파일 및 링크과정을 하지 않는 점입니다. 때문에 간혹 다시 깨끗히 빌드하고 싶을때 "touch test.c"를 수행하거나 "rm test.o test" 를 수행해야 합니다. 이것 역시 매우 불편합니다. 이를 해소하고자 다음과 같이 작성될수 있겠습니다.
새로운 Target 인 "clean"이 보입니다. 그런데 Target "clean"에 대한 Depend가 없습니다. 이렇게 Depend 가 없으면 "clean"에 기술된 명령 "$(RM) test.o test"는 항상 실행되며 이것은 실제로 "clean"이라는 파일이 없습니다. 때문에 이것을 가짜 Target 이라고 합니다. 이제 "make clean" 하시고 "make test"하시면 항상 다시 빌드하는것을 볼수 있습니다. 하지만 만약 clean이라는 실행파일이 존재한다면 우리가 원하지 않는 결과가 나올법도 합니다. 물론 거의 대부분 "clean"이라는 실행파일을 만들지 않겠지만 우리는 여기서 "make"에서 Target "clean"는 가짜 Target 이라고 명확히 전달할 필요가 있습니다. 물론 불필요한 경우가 대부분의 경우지만 그래도 습관적으로 명시해주는게 좋습니다.
자! 여기서 새로운 Target 인 ".PHONY" 가 보입니다. 이것은 "make"구문에서 예약된 Target 중에 하나이며 ".PHONY"에 명시된 Depend 는 모두 가짜 Target으로 보게 됩니다. 그리고 여기서 "all"이라는 Target이 추가되었습니다. 이것은 매우 관습적인 Target 으로 보통 최상단에 "all"과 "clean"이 위치하게 됩니다. 꼭 이렇게 해야 하는게 아니고 통상적인 관습이므로 반드시 따를 필요는 없습니다. 다만 다른 사람들도 함께 작업하는 프로젝트에서 통일성을 부여하기 위해서 이렇게 많이들 작성하게 됩니다. 위의 예제에서 Target "all"은 실제로 "all"이라는 파일을 생성하는것이 아니고 단지 "무엇이 최종 Target이다" 라는 암시적인 가짜 Target이 되며 "clean"은 "무엇을 깨끗이 정리하겠다" 라는 암시적인 가짜 Target이 됩니다. 이제 우리는 "make test"대신에 "make all"을 사용하게 될수 있습니다. 이때 "all"의 Depend로 "test"가 빌드될것입니다. 또한 "clean"은 관련된 파일을 정리하는것으로 용도가 정해집니다. 이때 "."으로 시작하는 Target을 제외하고 가장 처음 나오는 Target은 "make <target>" 에서 <target>을 생략해도 무관하게 됩니다. 때문에 ".PHONY"를 제외한 "all"이 가장 처음 나오는 Target이고 이제부터는 "make all"이 아니고 그냥 "make"만 입력해도 된다는 겁니다. 그리고 "clean"에서 $(RM) 명령 앞에 대쉬(-) 문자가 오도록 하였는데 이것은 rm명령어에서 에러를 반환하는 경우 무시할 수 있도록 하여 계속 진행하도록 할수 있게 합니다. 즉, 에러 반환을 무시하도록 할 수 있습니다.
CC = cc LD = ld RM = rm -f CFLAGS = -O2 -Wall -Werror -fomit-frame-pointer -c LDFLAGS = -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 STARTUP = /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o clean: $(RM) test.o test test: test.o $(LD) $(LDFLAGS) -o $@ $(STARTUP) $^ .c.o: $(CC) $(CFLAGS) -o $@ $<
CC = cc LD = ld RM = rm -f CFLAGS = -O2 -Wall -Werror -fomit-frame-pointer -c LDFLAGS = -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 STARTUP = /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o .PHONY: all clean all: test clean: -$(RM) test.o test test: test.o $(LD) $(LDFLAGS) -o $@ $(STARTUP) $^ .c.o: $(CC) $(CFLAGS) -o $@ $<
1.7. 응용
이제 다음과 같이 4개의 파일을 작성하여 하나의 프로젝트를 작성하였다고 합시다.
hello.c
test.c
hello.h
Makefile
자! 이제 이 4개의 파일로 어떤 임의의 프로젝트를 만들었습니다. 엇? 근데 조금 이상한 부분이 보이죠? 하지만 이것은 매우 잘 동작하는 "Makefile"입니다. 이제 "make clean" 후에 "make" 명령을 입력하면 조용히 컴파일을 하게 될겁니다. 그리고 make 가 어떤 확장을 보이는지 궁굼하다면 "make -p" 명령을 통해서 "-p"옵션을 사용할수 있습니다. 꽤 많은 메세지를 확인할수 있을겁니다. 앞으로도 "-p"를 이용하면 make 가 어떤 확장을 보이는지 확인가능하니 낚시하는 법을 배운셈입니다.
위에서 "$($@:.o=.c)" 라는 이상한 문자열이 좀 마음에 안들겁니다. 하지만 이것은 매우 함축적인 매크로이며 많이들 애용하고 있는 겁니다. 대략 다음과 같은 형식을 사용합니다. "$(<문자열>:<우측으로부터 매칭될 문자열>=<치환될 문자열>)" 이것은 즉, "$@" 부분은 자신의 Target인 "hello.o" 또는 "test.o"를 말합니다. 그리고 거기서 우측으로부터 ".o"가 발견되면 ".c"로 치환하라는 뜻입니다. 즉, "$(hello.o:.o=.c)" 또는 "$(test.o:.o=.c)"로 확장되고 여기서 다시 각각 "hello.c" 와 "test.c"로 치환되어 결국 해당 소스를 지칭하게 되는 셈입니다.
그리고 Command 부분이 <TAB>이 쓰이지 않고 한줄에 ";"(세미콜론)으로 분리되어서 해당 라인에 직접 Command 가 쓰이는 것을 확인하실수 있을겁니다. 무지 거대한 "Makefile"을 간략히 보이게 하기 위해서 이렇게도 사용할수 있다는 것을 예로 보인것입니다. 의존관계를 성립하는 부분은 Command 가 없는것을 볼수 있는데 이것은 비슷한 다른 Target에서 Command 가 결합되어 수행될수 있고 여기서는 ".c.o: ; ..." 부분의 Command 가 결합됩니다. 여기서 의존관계를 최대한 자세하게 기술하였는데 만약 "hello.h" 가 변경된다면 "hello.o"와 "test.o"는 다시 빌드될것입니다. 또한 "Makefile" 도 수정되면 다시 빌드될것이라는 것이 예상됩니다. 이처럼 의존관계를 따로 기술하는 이유는 차후에 여러분들이 사용하시다보면 이유를 알게 될겁니다. 의존관계라는게 서로 굉장히 유기적으로 걸리는 경우가 많기 때문에 보다 보기 편하게 하는 이유도 있고 차후에 의존관계가 변경되었을때 쉽게 찾아서 변경을 할수 있도록 하는것도 한가지 이유입니다.
아주 조금만 공통적인 의존관계를 정리해서 작성한다면 다음과 같이도 작성할수 있습니다. 바로 Target이 한번에 2개 이상이 정의될수도 있다는 겁니다.
hello.c
#include <stdio.h> void HelloWorld(void) { fprintf(stdout, "Hello world.\n"); }
#include <stdio.h> #include "hello.h" int main(void) { HelloWorld(); return(0); }
extern void HelloWorld(void);
CC = cc LD = ld RM = rm -f CFLAGS = -O2 -Wall -Werror -fomit-frame-pointer -v -c LDFLAGS = -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 STARTUP = /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o BUILD = test OBJS = test.o hello.o .PHONY: all clean all: $(BUILD) clean: ; $(RM) *.o $(BUILD) test: $(OBJS) ; $(LD) $(LDFLAGS) -o $@ $(STARTUP) $^ # 의존관계 성립 hello.o: $($@:.o=.c) $($@:.o=.h) Makefile test.o: $($@:.o=.c) hello.h Makefile # 확장자 규칙 (컴파일 공통 규칙) .c.o: ; $(CC) $(CFLAGS) -o $@ $<
위에서 "$($@:.o=.c)" 라는 이상한 문자열이 좀 마음에 안들겁니다. 하지만 이것은 매우 함축적인 매크로이며 많이들 애용하고 있는 겁니다. 대략 다음과 같은 형식을 사용합니다. "$(<문자열>:<우측으로부터 매칭될 문자열>=<치환될 문자열>)" 이것은 즉, "$@" 부분은 자신의 Target인 "hello.o" 또는 "test.o"를 말합니다. 그리고 거기서 우측으로부터 ".o"가 발견되면 ".c"로 치환하라는 뜻입니다. 즉, "$(hello.o:.o=.c)" 또는 "$(test.o:.o=.c)"로 확장되고 여기서 다시 각각 "hello.c" 와 "test.c"로 치환되어 결국 해당 소스를 지칭하게 되는 셈입니다.
그리고 Command 부분이 <TAB>이 쓰이지 않고 한줄에 ";"(세미콜론)으로 분리되어서 해당 라인에 직접 Command 가 쓰이는 것을 확인하실수 있을겁니다. 무지 거대한 "Makefile"을 간략히 보이게 하기 위해서 이렇게도 사용할수 있다는 것을 예로 보인것입니다. 의존관계를 성립하는 부분은 Command 가 없는것을 볼수 있는데 이것은 비슷한 다른 Target에서 Command 가 결합되어 수행될수 있고 여기서는 ".c.o: ; ..." 부분의 Command 가 결합됩니다. 여기서 의존관계를 최대한 자세하게 기술하였는데 만약 "hello.h" 가 변경된다면 "hello.o"와 "test.o"는 다시 빌드될것입니다. 또한 "Makefile" 도 수정되면 다시 빌드될것이라는 것이 예상됩니다. 이처럼 의존관계를 따로 기술하는 이유는 차후에 여러분들이 사용하시다보면 이유를 알게 될겁니다. 의존관계라는게 서로 굉장히 유기적으로 걸리는 경우가 많기 때문에 보다 보기 편하게 하는 이유도 있고 차후에 의존관계가 변경되었을때 쉽게 찾아서 변경을 할수 있도록 하는것도 한가지 이유입니다.
아주 조금만 공통적인 의존관계를 정리해서 작성한다면 다음과 같이도 작성할수 있습니다. 바로 Target이 한번에 2개 이상이 정의될수도 있다는 겁니다.
CC = cc LD = ld RM = rm -f CFLAGS = -O2 -Wall -Werror -fomit-frame-pointer -v -c LDFLAGS = -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 STARTUP = /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o BUILD = test OBJS = test.o hello.o .PHONY: all clean all: $(BUILD) clean: ; $(RM) *.o $(BUILD) test: $(OBJS) ; $(LD) $(LDFLAGS) -o $@ $(STARTUP) $^ # 의존관계 성립 $(OBJS): $($@:.o=.c) hello.h Makefile # test.o hello.o: $($@:.o=.c) hello.h Makefile # 확장자 규칙 (컴파일 공통 규칙) .c.o: ; $(CC) $(CFLAGS) -o $@ $<
1.8. 다른 Makefile의 포함
make를 작성하다 보면 굉장히 큰 규모의 프로젝트에서는 Makefile이 매우 커져서 다루기 어려울 수 있습니다. 이때 Makefile을 적절히 나누어 다른 파일로 저장하고 include 구문으로 이를 포함하여 해석할 수 있도록 할 수 있습니다.
include 구문에는 변수를 사용할 수 있으며 파일과 파일 사이는 공백으로 구분합니다. 그리고 포함하고자 하는 Makefile이 없을수도 있는 것을 허용하기 위해서는 대쉬(-)를 include 앞에 붙여 사용하도록 하면 있는 경우는 포함하고 그렇지 않은 경우는 무시하도록 할 수도 있습니다.
include <Makefile-names...>
include 구문에는 변수를 사용할 수 있으며 파일과 파일 사이는 공백으로 구분합니다. 그리고 포함하고자 하는 Makefile이 없을수도 있는 것을 허용하기 위해서는 대쉬(-)를 include 앞에 붙여 사용하도록 하면 있는 경우는 포함하고 그렇지 않은 경우는 무시하도록 할 수도 있습니다.
EXT_MAKEFILE:=build.mk -include *.mk $(EXT_MAKEFILE)
1.9. 좀더 다양한 응용
1.9.1. 기본 치환 함수
- subst : 주어진 문자열(text)에서 특정 문자열(from)을 다른 문자열(to)로 치환해줍니다.
- 사용법)
$(subst <from>,<to>,<text>)
- 예)
$(subst A,B,ABC)
- 치환 내용 : 'BBC'
- 사용법)
- patsubst : 주어진 문자열(text)에서 특정 패턴(pattern)을 다른 문자열(replacement)로 치환해줍니다. 패턴(pattern)은 와일드카드 역할을 하는 '%' 를 가질 수 있으며 이 때 replacement 에도 '%' 를 사용하여 대응할 수 있으며 '\'를 '%' 앞에 두어 escape 할 수 있습니다.
- 사용법)
$(patsubst <pattern>,<replacement>,<text>)
- 이 경우 다음과 같이 사용하여 동일한 효과로 사용할 수도 있습니다. =>
$(<var>:<suffix>=<replacement>)
- 이 경우 다음과 같이 사용하여 동일한 효과로 사용할 수도 있습니다. =>
- 예)
$(patsubst %.c,%.o,A.c B.c C.c)
- 치환 내용 : 'A.o B.o C.o'
- 사용법)
- strip : 주어진 인자(string) 의 앞뒤 공백문자들을 제거하고 하나 이상의 공백 문자들을 하나의 공백으로 치환합니다.
- 사용법)
$(strip <string>)
- 예)
$(strip A B C )
- 치환 내용 : 'A B C'
- 사용법)
- findstring : 주어진 첫 번째 인자(find)를 공백으로 구분된 두 번째 인자(text)에서 찾고 있으면 첫 번째 인자(find)로 치환하고 없으면 빈 문자로 치환됩니다.
- 사용법)
$(findstring <find>,<text>)
- 예)
$(findstring linux,android windows macos androideabi linux unix)
- 치환 내용 : 'linux'
- 사용법)
- filter : 주어진 두 번째 인자(text) 에서 첫 번째 인자(pattern)과 일치하지 않는 단어는 제외하고 반환합니다. 즉, pattern과 일치하는 단어만 추출합니다.
- 사용법)
$(filter <pattern>,<text>)
- 예)
$(filter %.c %.s, A.c B.c C.s D.h)
- 치환 내용 : 'A.c B.c C.s'
- 사용법)
- filter-out : 주어진 두 번째 인자(text) 에서 첫 번째 인자(pattern)과 일치하는 단어는 제외하고 반환합니다. 즉, pattern과 일치하지 않는 단어만 추출합니다.
- 사용법)
$(filter-out <pattern>,<text>)
- 예)
$(filter-out %.c %.s, A.c B.c C.s D.h)
- 치환 내용 : 'D.h'
- 사용법)
- sort : 주어진 인자(list)의 단어들을 사전 순서로 정렬하고 중복된 단어들을 제외하여 반환합니다.
- 사용법)
$(sort <list>)
- 예)
$(sort C B A A B C)
- 치환 내용 : 'A B C'
- 사용법)
- abspath : 주어진 인자(pathname)를 절대 경로로 치환해줍니다.
- 사용법)
$(abspath <pathname>[...])
- 예)
$(abspath ./Makefile)
- 치환 내용 : '/home/me/project/Makefile'
- 사용법)
- dir : 주어진 인자(pathname)에서 경로 부분으로 치환해줍니다. dirname 명령과 흡사합니다.
- 사용법)
$(dir <pathname>[...])
- 예)
$(dir /bin/bash)
- 치환 내용 : '/bin/'
- 사용법)
- notdir : 주어진 인자(pathname)에서 경로를 제외한 부분으로 치환해줍니다. basename 명령과 흡사하지만 경로 부분을 완전히 제외하고 치환해줍니다.
- 사용법)
$(notdir <pathname>[...])
- 예)
$(notdir /bin/bash)
- 치환 내용 : 'bash'
- 사용법)
- call : macro 함수 (name : define 으로 정의한 함수) 에 인자를 전달하고 치환하여 해석합니다.
- 사용법)
$(call <name>[,<args>[...]])
- 예)
$(call mymacro,arg1,arg2,arg3)
- 사용법)
- foreach : 공백으로 구분된 문자들을 두 번째 인자(words)로 주면 첫 번째 인자(var)로 지정한 변수로 각각 대응하여 세 번째 인자(text)에 치환합니다.
- 사용법)
$(foreach <var>,<words>,<text>)
- 예)
$(foreach s_this,boy girl tiger,i am $(s_this):)
- 치환 내용 : 'i am boy:i am girl:i am tiger:'
- 사용법)
- wildcard : 주어진 인자(pattern, '%' 가 아닌 와일드 패턴)의 경로 또는 파일이 존재하는 경우 치환하여 반환해주고 그렇지 않은 경우는 빈 문자로 치환해줍니다. 인자로는 와일드 문자들을 사용할 수 있습니다.
- 사용법)
$(wildcard <pattern>[...])
- 예)
$(wildcard /bin/sh)
- 예)
$(wildcard /etc/*)
- 사용법)
- shell : 주어진 인자(cmdline)를 명령줄로 하는 shell 실행 결과로 치환해줍니다.
- 사용법)
$(shell <cmdline>)
- 예)
$(shell uname -m)
- 예)
$(shell hostname)
- 예)
$(shell whoami)
- 사용법)
1.9.2. GNU Make에서 기본적으로 예약되어 사용되는 환경변수
SHELL
- Command를 수행해줄 shell 을 지정하는 변수입니다. Unix 계열 OS환경에서 GNU Make는 SHELL을 기본적으로 "/bin/sh"로 지정되어 사용합니다. MS-DOS에서는 MAKESHELL도 같은 역할을 하는데 MAKESHELL이 우선순위가 높습니다. (또한 Windows 용 GNU make 버젼에서는 "sh.exe"로 값이 저장되어 있습니다.)
- 아래의 예시는 가급적 bash 를 shell 로 지정하도록 하는 겁니다.
ifneq ($(wildcard /bin/bash),) SHELL=/bin/bash# default bash shell endif ifeq ($(SHELL),) SHELL=/bin/sh# default unix shell endif
MAKE
- make 명령어 자체를 담고 있습니다. 경우에 따라서 명령어가 gmake인 경우가 있는데 이 경우 MAKE환경변수에 gmake 로 담겨있거나 직접 지정하여 사용할 수 있습니다. 재귀적 사용시에 make 명령을 직접 사용하기 보다는 "
$(MAKE)
" 처럼 변수를 이용하여 사용하는게 좋습니다.
- make 명령어 자체를 담고 있습니다. 경우에 따라서 명령어가 gmake인 경우가 있는데 이 경우 MAKE환경변수에 gmake 로 담겨있거나 직접 지정하여 사용할 수 있습니다. 재귀적 사용시에 make 명령을 직접 사용하기 보다는 "
MAKE_VERSION
- make의 버젼을 담고 있습니다.
- 아래의 예시는 make v3.81 미만의 버젼에서는 빌드를 멈추도록 하는 겁니다. (make가 v10.xx 버젼 이상이 되면 이 구문은 문제가 좀 있으며 그때가 되면 이에 대한 좀더 좋은 예시가 나오겠죠? ㅎㅎ)
# check for minimal make version (NOTE: this check will break at make 10.x !) DEF_HWPORT_REQUIRE_MINIMUM_MAKE_VERSION:=3.81# ifneq ($(firstword $(sort $(MAKE_VERSION) $(DEF_HWPORT_REQUIRE_MINIMUM_MAKE_VERSION))),$(DEF_HWPORT_REQUIRE_MINIMUM_MAKE_VERSION)) $(error you have make "$(MAKE_VERSION)". GNU make >= $(DEF_HWPORT_REQUIRE_MINIMUM_MAKE_VERSION) is required !) endif
MAKEFLAGS
- make 명령어의 인수를 담고 있는 변수입니다.
MAKEFILES
- 이 변수에 지정된 파일들은 "-include <Makefile-names>..." 과 비슷한 효과를 가집니다. 보통은 이 변수사용보다는 include 구문으로 명시하는것을 권장합니다.
MAKELEVEL
- make의 재귀적 호출 깊이를 담고 있습니다.
MAKECMDGOALS
- make의 명령행에서 주어진 타겟명을 담고 있으며 이 변수를 설정한다고 기본 타겟이 바뀌는 영향력을 갖지는 못합니다.
CURDIR
- Make가 실행된 현재 디렉토리를 담고 있습니다. 이 변수는 통상 읽는 용도로 사용하며 이 변수를 설정한다고 실행된 디렉토리가 바뀌는 영향력을 갖지는 못합니다.
VPATH
- 이 변수에 담겨진 경로는 가능한 모든 파일명에 대한 자동탐색이 수행되며 적철한 처리를 수행하도록 합니다.
SUFFIXES
- 접미사들의 디폴트 리스트를 지정할 수 있습니다.
1.9.3. Windows에서의 GNU Make 사용
- Windows용 GNU Make v3.81 다운로드 (105.89 KB) : 이 실행파일을 다운로드 받아서 적절한 위치에 복사후 PATH환경변수를 잡아주고 사용하면 됩니다. 통상적으로
C:\Windows
디렉토리에 복사해주면 명령행에서 편하게 사용할 수 있겠죠.- Windows용 GNU Make인지를 Makefile 에서 식별하려면 SHELL 환경변수가 "sh.exe" 로 되어 있는지를 확인하면 됩니다.
ifneq ($(findstring sh.exe,$(SHELL)),) # sh.exe가 Make 환경변수인 SHELL 로 지정되어 있는 경우 Windows용 GNU Make 가 실행되었음을 알 수 있습니다. 실제로 Windows에서의 shell 은 "cmd /c" (환경에 따라 다를수도 있음) 입니다. endif
- Windows용 GNU Make인지를 Makefile 에서 식별하려면 SHELL 환경변수가 "sh.exe" 로 되어 있는지를 확인하면 됩니다.
- 그 밖에 빌드에 필요한 Windows용 GNU tool 들은 http://gnuwin32.sourceforge.net/ 또는 http://gnuwin64.sourceforge.net/ 에서 이미 빌드된 실행파일 형태로 다운로드 받아 사용할 수 있습니다.
- echo 명령에서의 Quote(") 문제
- Windows의 echo 명령은 인자에 Quote(") 가 나오면 Quote(") 자체도 출력하게 됩니다. 때문에 Unix 의 echo 명령과 다소 호환문제가 걸리는데요. 다음과 같은 방식으로 해결할 수 있습니다. 여기서 역슬래쉬(\)를 사용할 때와 그렇지 않을때의 정확한 이해가 필요합니다. Windows와 Unix 간 호환가능한 makefile을 작성하기 위해서는 아래의 예제를 곰곰히 분석하여 이해하는게 필요합니다.
ifneq ($(findstring sh.exe,$(SHELL)),) # Windows GNU Make's echo command ECHO:=echo# QUOTE:=# else # Unix GNU Make's echo command ECHO:=echo# QUOTE:=$(subst \,,\")# endif .PHONY: all all: ; $(ECHO) $(QUOTE)Hello world !$(QUOTE)
- Windows의 echo 명령은 인자에 Quote(") 가 나오면 Quote(") 자체도 출력하게 됩니다. 때문에 Unix 의 echo 명령과 다소 호환문제가 걸리는데요. 다음과 같은 방식으로 해결할 수 있습니다. 여기서 역슬래쉬(\)를 사용할 때와 그렇지 않을때의 정확한 이해가 필요합니다. Windows와 Unix 간 호환가능한 makefile을 작성하기 위해서는 아래의 예제를 곰곰히 분석하여 이해하는게 필요합니다.
1.9.4. 복잡한 치환
- Source(*.c) 파일들의 목록으로부터 Object(*.o)파일들의 목록으로 변환하기 (변수치환의 활용)
SOURCE:=a.c b.c c.c d.d test.c example.c# OBJECT:=$(SOURCE:%.c=%.o)#
- patsubst 함수로도 이와 비슷한 결과를 만들수 있습니다.
SOURCE:=a.c b.c c.c d.d test.c example.c# OBJECT:=$(patsubst %.c,%.o,$(SOURCE))#
- 디렉토리 목록에 대하여 다양한 명령 수행하기 (foreach 함수의 활용) 아래의 예제에서 명령의 구분자로 세미콜론(;)이 사용되었는데 이것은 쉘에서 해석하는 것이므로 만약 쉘에서 명령의 구분자가 다른 문자인 경우 적절히 바꿔줄 필요가 있습니다.
### ### Copyright (C) MINZKN.COM ### All rights reserved. ### Author: JAEHYUK CHO <mailto:minzkn@minzkn.com> ### SUBDIRS :=alpha beta gamma MySource# .PHONY: all % all: ;@$(foreach s_sub_directory,$(SUBDIRS),$(MAKE) $(@) --no-print-directory --directory="$(s_sub_directory)";) %: ;@$(foreach s_sub_directory,$(SUBDIRS),$(MAKE) $(@) --no-print-directory --directory="$(s_sub_directory)";) # End of makefile
1.10. 제약사항
1.10.1. 공백을 포함한 디렉토리명 또는 파일명에 대한 문제
- make 의 많은 내장 함수들은 공백을 내부적으로 특별한 구분자로 인식하기 때문에 치환함수를 이용하여 이것을 적절히 변환하면서 다루더라도 제대로 경로를 다룰수 없는 경우가 발생합니다. 때문에 가급적이면 make가 다뤄야 할 대상 디렉토리명이나 파일명에는 공백을 사용하지 않도록 만들 필요가 있습니다. 또는 공백대신에 밑줄(_) 문자로 애초부터 구조를 잡는게 문제를 만들지 않는 가장 좋은 예방이 될겁니다.
- 일단 <target> 에서 공백이 들어가면 공백간 문자열이 서로 다른 target으로 인식되며 역슬래쉬는 의미 없는 문제가 있습니다.
- <depend>에서 공백이 있는 경우 역슬래쉬로 이를 방어할 수는 있으나 <command>항에서 역슬래쉬가 모두 제거된다는 점으로 인하여 notdir, dir, foreach, wildcard 등의 함수와 함께 혼용시 치환권법을 아무리 잘해도 문제가 풀리지 않게 되는 상황도 있게 됩니다.
- 게다가 Windows에서는 OS 자체가 path구분자로 역슬래쉬를 사용하기 때문이기도 하지만 Windows의 echo 또한 Unix의 echo 와 다루는 문자관계가 매우 상이하여 굉장히 어려운 치환을 요구하게 됩니다.
1.10.2. GNU make v3.81 이후 버젼과 그 이전의 버젼간 기능의 제약
- GNU make v3.81부터 많은 기능과 내장함수, 그리고 eval 구문등이 지원되는데 비로소 많은 빌드의 어려움을 해소할수 있게 되었습니다. 하지만 그 이전 버젼에서는 지원하지 않는 기능들이 있기 때문에 이 부분에 대한 명확한 이해가 있으면 좋습니다. 아래의 예시는 내가 원하는 기능이 있는 make 버젼인지 식별하기 위해서 유용할겁니다. 이런 조치를 해두지 않으면 지원하지 않는 구문이 예전 버젼에서 의도되지 않은 결과를 도출할 수 있어서 미연에 방지하는 것이 좋을겁니다.
# check for minimal make version (NOTE: this check will break at make 10.x !) DEF_HWPORT_REQUIRE_MINIMUM_MAKE_VERSION:=3.81# ifneq ($(firstword $(sort $(MAKE_VERSION) $(DEF_HWPORT_REQUIRE_MINIMUM_MAKE_VERSION))),$(DEF_HWPORT_REQUIRE_MINIMUM_MAKE_VERSION)) $(error you have make "$(MAKE_VERSION)". GNU make >= $(DEF_HWPORT_REQUIRE_MINIMUM_MAKE_VERSION) is required !) endif
1.11. 참고문헌
- http://korea.gnu.org/manual/ : Make 에 대하여 깊히 알고 싶다면 꼭! 이 문서를 읽어보시길 강력히 추천합니다.
- GNU Automake manual 번역본: http://korea.gnu.org/manual/release/automake/
- GNU make manual 번역본