GNU Make 와 gcc 를 이용한 auto-dependency Makefile 만들기

Autotools, CMake, qmake 같은 빌드 도구나 Open Watcom 의 wmake 같은 경우에는 자체적으로 auto-dependency 를 지원하거나 이를 위한 지시자를 제공한다. 하지만 GNU Make 의 경우에는 이러한 기능을 자체적으로 지원하지도 않고, 이를 위한 지시자를 제공하지도 않는다.

하지만, 다행히도 gcc 를 이용해서 이 기능을 구현할 수 있다. 우선 이에 필요한 gcc 의 기능부터 살펴보자. 

-M 옵션 : 해당 소스 파일의 의존성을 GNU Make 의존성 규칙에 맞추어 출력한다. 시스템 헤더 파일들도 포함된다.

예를 들어, 다음과 같은 hello.c 를 생각해보자.

1
2
3
4
5
6
7
8
9
// hello.c
#include <stdio.h>

int main( void )
{
    printf("Hello, world\n");

    return 0;
}


  • gcc -M hello.c

이를 실행한 결과는 이렇다.

  • hello.o: hello.c f:/lang/gcc/usr/include/stdio.h \
  •   f:/lang/gcc/usr/include/sys/cdefs.h \
  •   f:/lang/gcc/usr/include/sys/gnu/cdefs.h \
  •   f:/lang/gcc/usr/include/features.h f:/lang/gcc/usr/include/sys/_types.h \
  •   f:/lang/gcc/usr/include/machine/_types.h \
  •   f:/lang/gcc/usr/include/386/_types.h

-MM 옵션 : -M 옵션과 비슷한데, 시스템 헤더 파일은 제외한다.

  • gcc -MM hello.c 

다음은 실행 결과이다.

  •  hello.o: hello.c

 -MP 옵션 : -M 또는 -MM 옵션과 함께 쓰일 때, 헤더 파일들에 대한 phony 타겟을 만든다. 이는 헤더 파일이 지워졌을 때, 오류가 발생하는 것을 방지한다.

  •  gcc -M -MP hello.c

다음은 실행 결과이다.

  •  hello.o: hello.c f:/lang/gcc/usr/include/stdio.h \
  •   f:/lang/gcc/usr/include/sys/cdefs.h \
  •   f:/lang/gcc/usr/include/sys/gnu/cdefs.h \
  •   f:/lang/gcc/usr/include/features.h f:/lang/gcc/usr/include/sys/_types.h \
  •   f:/lang/gcc/usr/include/machine/_types.h \
  •   f:/lang/gcc/usr/include/386/_types.h
  • f:/lang/gcc/usr/include/stdio.h:
  • f:/lang/gcc/usr/include/sys/cdefs.h:
  • f:/lang/gcc/usr/include/sys/gnu/cdefs.h:
  • f:/lang/gcc/usr/include/features.h:
  • f:/lang/gcc/usr/include/sys/_types.h:
  • f:/lang/gcc/usr/include/machine/_types.h:
  • f:/lang/gcc/usr/include/386/_types.h: 

-MT 옵션 : -M 또는 -MM 옵션과 함께 쓰일 때, 타겟의 이름을 바꾼다.

  • gcc -M -MT hello_MT.o hello.c

다음은 실행 결과이다.

  • hello_MT.o: hello.c f:/lang/gcc/usr/include/stdio.h \
  •   f:/lang/gcc/usr/include/sys/cdefs.h \
  •   f:/lang/gcc/usr/include/sys/gnu/cdefs.h \
  •   f:/lang/gcc/usr/include/features.h f:/lang/gcc/usr/include/sys/_types.h \
  •   f:/lang/gcc/usr/include/machine/_types.h \
  •   f:/lang/gcc/usr/include/386/_types.h

-MF 옵션 : -M 또는 -MM 옵션과 함께 쓰일 때, 출력 결과를 해당 장치 또는 파일로 보낸다.

  • gcc -M -MF hello.d hello.c

이 때 실행 결과는 표준 출력 장치에 나타나는 것이 아니라 hello.d 라는 파일에 저장된다.

바로 이 옵션들을 통해서 auto-dependecy Makefile 파일을 만들 수 있다. 문제는 이 내용을 Makefile 파일에서 어떻게 활용할 것인가이다.

의존성 파일부터 만들어 보자. 의존성 파일을 만들기 위한 기본적인 형태는 다음과 같다.

  • hello.d : hello.c
  •     gcc -M -MP -MF $@ $<

이제는 이렇게 만들어진 의존성 파일을 포함시켜야 한다. GNU Make는 이를 위해 include 지시자를 지원한다.

  • include hello.d

이때 hello.d 가 없다면 주어진 생성 규칙을 통해서 새로이 만들거나 갱신한다. 그럼에도 불구하고 hello.d 를 만들 수 없다면, GNU Make 는 오류를 보고하고, 중지한다.

마지막으로 실행 파일의 의존성 규칙을 추가해주면 된다.

  •  hello.exe : hello.o

물론 다음과 같은 기본적인 내용이 Makefile 앞 부분에 더 추가되어야 한다.

  • .SUFFIXES : .c .o .exe
  •  
  • .PHONY : all
  •  
  • all : hello.exe

완성된 Makefile 의 모습은 이렇다.

1
2
3
4
5
6
7
8
9
10
11
12
13
.SUFFIXES : .c .o .exe

.PHONY : all

all : hello.exe

hello.d : hello.c
    gcc -M -MP -MF $@ $<

include hello.d

hello.exe : hello.o
    gcc -o $@ $<


이제 make 를 실행하면, hello.c 자체가 수정되거나 hello.c 가 포함하는 헤더 파일이 수정되면 새롭게 빌드된다.

그런데 문제는 hello.d 파일 자체는 한 번 만들어지면 hello.c 가 수정되지 않는 이상, 다시 갱신되지 않는다. 왜냐하면 hello.d 파일에 대한 의존성은 hello.c 밖에 없기 때문이다. 이 문제는 hello.d 의 의존성을 hello.o 의 의존성과 동일하게 만들어주면 된다. 위 8번째 줄을 아래와 같이 바꾸자.

  • gcc -M -MP -MT "$(@:.d=.o) $@" -MF $@ $<

GNU Make 의 다중 대상 기능을 이용한 것으로. 타겟이 "hello.o" 에서 "hello.o hello.d" 로 된다.

GNU Make 를 실행하다보면 hello.d 가 지워진 경우에,  다음과 같은 오류 메세지가 출력된다.

  • Makefile:10: hello.d: No such file or directory

이 오류 메세지가 나오더라도 중단되지는 않지만, 뭔가 꺼림칙하다. 어떻게 없앨 수 없을까 ? 바로 -include 를 쓰면 된다.

이 예제의 경우에는 소스 파일이 하나에 불과해서 이 정도이지만, 소스 파일이 여러 개일 경우에는 해당 소스 파일마다 일일이 추가해줘야 한다. 그리고 다른 프로젝트에 쓰기에도 수정해야 할 것이 많다. 이 얼마나 불편한가 ? 이를 개선해 보자.

우리가 고려할 것은 실행 파일과, 소스 파일이다. 다른 모든 것들은 이들로부터 변형될 수 있다. 이들을 위한 변수를 도입하자. PROGNAME 과 SRCS 에 실행파일과 소스 파일을 담자.

  • PROGNAME := hello.exe
  • SRCS := hello.c

이제 의존성 파일 변수와 실행 파일을 만들기 위한 목적 파일 변수를 도입하자.

  • PROGNAME_DEPS := $(SRCS:.c=.o)
  • DEPS := $(SRCS:.c=.d)

이 내용들을 반영하면 위 Makefile 은 다음과 같이 쓰여질 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PROGRAM := hello.exe
SRCS := hello.c

PROGRAM_DEPS := $(SRCS:.c=.o)
DEPS := $(SRCS:.c=.d)

.SUFFIXES : .c .o .exe

.PHONY : all

all : $(PROGRAM)

$(PROGRAM) : $(PROGRAM_DEPS)
     gcc -o $@ $^

%.d : %.c
     gcc -M -MP -MT "$(@:.d=.o) $@" -MF $@ $<

-include $(DEPS)


위 내용을 보면 특정 이름 대신에 변수를 사용한 것 외에도 몇 가지 달라진 점이 있는데, 하나는 16번째 줄에 있는 암묵적 규칙이고, 14번째 줄에 있는 $^ 이다. 암묵적 규칙은 여러개의 파일에 대해 동일한 규칙을  적용하기 위한 것이고, $^ 은 모든 의존 파일들을 뜻한다. 반면에 $< 은 첫번째 의존 파일을 뜻한다.
이렇게 해 두면, 다른 프로젝트에 적용을 한다든지 또는 소스가 변경되었다든지 할 때, PROGRAM 과 SRCS 만 바꾸어주면 된다. 소스 파일을 더 추가하고 싶다면 빈 칸을 사이에 두고 파일 이름을 적어주면 된다. 예를 들어 hello.c 외에 world.c 가 추가되었다면 2번째 줄을 이렇게 바꾸면 된다.

  • SRCS := hello.c world.c

이외에도 여러가지 컴파일 플래그나 링커 플래그, 컴파일러나 링커를 위한 여러 변수들을 도입하면 훨씬 더 유연한 Makefile 이 될 것이다.

그리고 기회가 된다면 추후에 여러 개의 실행 파일과 DLL 등을 한꺼번에 빌드할 수 있는 Makefile 에 대해서도 글을 쓰도록 하겠다.

댓글

이 블로그의 인기 게시물

토렌트: < 왕좌의 게임 > 시즌 1 ~ 시즌 8 완결편 마그넷

토렌트: NGC < 코스모스 > 우리말 더빙 전편(1편~13편) 마그넷

토렌트: < 스타워즈 > Ep.1 ~ Ep.6 마그넷