이전 시간에는 간단히 컴파일하는 방법을 소개해 드리면서 프로그램 모듈화의 필요성을 말씀을 드렷습니다. 이번 시간에는 Makefile에 대해 소개해 드리겠습니다. Makefile을 사용하는 이유는 편리하기 때문입니다. Makefile을 이용하지 않아도 컴파일할 수 있기 때문이죠. 어떻게 편리한지 보겠습니다.

간단한 Makefile 만들기

  설명을 위해 아래와 같이 6개의 소스 파일이 있다고 하겠습니다.

  • main.c와 main.h
  • tcp.c와 tcp.h
  • rs232.c와 rs232.h

  여기서 생각해야 될 점은 main.c를 언제 컴파일하느냐 하는 것입니다. main.c 는 rs232.c의 함수와 tcp.c의 함수를 사용하기 때문에 main.c 자체가 변경되는 것 외에도 rs232.h와 tcp.h가 변할 때에도 컴파일하는 것이 안전합니다.

  • main.c 자체가 수정되었을 경우
  • main.h 가 수정되었을 경우
  • rs232.h 또는 tcp.h 가 변했을 경우

  tcp.c와 rs232.c는 자기 자신이 바뀌거나 해당 헤더파일이 변경되면 다시 컴파일이 되도록 합니다. 이제 이를 위한 간단한 Makefile을 보시겠습니다.

간단한 Makefile 만들기

  Makefile 의 구조는 아래와 같습니다.

목표: 아래 명령을 실행하게 되는 모든 조건에 해당되는 파일 목록
    실행 명령어

  이와 같은 구조로 필요한 만큼 나열하면 됩니다. 다음은 실제 Makefile 내용입니다.

주의

명령어 앞에는 반드시 탭 문자로 간격을 띄워야 합니다.
공백으로 띄우시면 make 실행 시 에러가 발생합니다.

sample : main.o tcp.o rs232.o
    gcc -lm -o sample main.o tcp.o rs232.o  <-- 명령어, 즉 gcc 앞에는 tab 키로 들여 쓰기를 해야합니다.

main.o : main.c main.h rs232.h tcp.h
    gcc -c main.c       <-- sample 에서 링크할 것이므로 여기에서는 -c를 사용하여 컴파일만 하겠습니다.

tcp.o : tcp.c tcp.h
    gcc -c tcp.c

rs232.o : rs232.c rs232.h
   gcc -c rs232.c

sample : main.o tcp.o rs232.o main.o tcp.o, rs232.o 파일 중 변경되는 파일이 있다면
아래의 명령을 실행합니다.
     gcc -lm -o sample main.o tcp.o rs232.o main.o, tcp.o, rs232.o로 실행파일 sample 를 만듭니다.
main.o : main.c main.h rs232.h tcp.h

main.c, main.h, rs232.h tcp.h 파일 중 변경되는 파일이
있다면 아래의 명령을 실행합니다.

     gcc -c main.c main.c 를 컴파일해서 main.o를 생성합니다.
tcp.o : tcp.c tcp.h tcp.c, tcp.h 파일 중 변경되는 파일이 있다면
아래의 명령을 실행합니다.
     gcc -c tcp.c tcp.c 를 컴파일해서 tcp.o를 생성합니다.
rs232.o : rs232.c rs232.h rs232.c, rs232.h 파일 중 변경되는 파일이 있다면
아래의 명령을 실행합니다.
     gcc -c rs232.c rs232.c 를 컴파일해서 rs232.o를 생성합니다.

  대충 이해가 되시나요? 뭐야 이거? 하시는 분이 계실지 모르겠습니다. 위이 예제는 아주 무식하지만 직관적으로 바로 이해되는 아주 간단한 Makefile 예제가 되겠습니다.

 

이제부터는 make 로 컴파일 완료

  이제 쉘에서 make 라는 명령으로 간단히 컴파일하고 실행 파일을 만들 수 있습니다.

]$ make

   편하죠? 그런데 Makefile 을 이렇게 만들면 타이핑을 해야할 것이 너무 많지요. 대부분의 프로그래머는 아주 게으릅니다. 타이핑으로 먹고 사는 사람들이 타이핑 많은 거, 좋와하지 않죠.

  이제, Makefile 만의 매크로를 이용하여 타이핑하는 횟수를 줄여 보겠습니다.

 

$@ $* $< $?

$@ 목표 이름
$* 목표 이름에서 확장자가 없는 이름
$<

조건 파일 중 첫번째 파일

$? 목표 파일 보다 더 최근에 갱신된 파일 이름

   자, 소개한 매크로를 이용하여 Makefile의 내용을 좀더 간단하게 만들 수 있습니다. 링크하는 부분을 먼저 보겠습니다.

sample : main.o tcp.o rs232.o
    gcc -lm -o sample main.o tcp.o rs232.o

  여기서 $@는 무엇일까요? 네, sample이 되겠습니다. 그러므로 아래와 같이 수정할 수 있습니다.

sample : main.o tcp.o rs232.o
    gcc -lm -o $@ main.o tcp.o rs232.o

  이해 되시죠. 이 번에는 $*오 $<, $? 에 대해서 말씀드리겠습니다.

tcp.o : tcp.c tcp.h
    gcc -c tcp.c

  $*는 목표 이름에서 확장자를 제거한 이름이니까 .o를 뺀 tcp가 되겠습니다. 그러므로 $*를 이용하면 이렇게 수정할 수 있습니다

tcp.o : tcp.c tcp.h
    gcc -c $*.c

  이해 되시죠? $<는 조건에 열거된 파일 목록 중 첫번재를 의미합니다. 그러므로 $<는 조건 파일인 tcp.c tcp.h 중에서 첫번째 파일의 이름에 해당되므로 tcp.c 가 되겠습니다. 그러므로 아래와 같이 바꿀 수도 있습니다. 저 같은 경우 제일 많이 사용합니다.

tcp.o : tcp.c tcp.h
    gcc -c $<

  $? 는 "현재 목표 파일 파일 보다 더 최근에 갱신된 파일 이름"을 나타내는 매크로입니다. 뜻은 알겠는데, 도대체 어디에 사용하는지 필요성을 잘 모르겠네요. 여하튼 예로 따져 보변 tcp.o 후에 tcp.h 가 수정되었다면 tcp.h가 된다는 것입니다.

 

확장자 규칙 .c.o

  일반적으로 .o 는 .c 로 만들어 지므로 실은 위의 예를 아래와 같이 명령 실행 없이 작성해도 make가 실행이 됩니다.

sample : main.o tcp.o rs232.o
    gcc -lm -o $@ main.o tcp.o rs232.o

main.o : main.c main.h rs232.h tcp.h -> 명령 실행이 없다.
tcp.o : tcp.c tcp.h -> 명령 실행이 없다.
rs232.o : rs232.c rs232.h -> 명령 실행이 없다.

  에이~ 그런데 왜 처음부터 복작하게 썼어? 하시겠지만, 이는 $*, $<, $? 를 설명 드리기 위함도 있지만 위와 같이 처리하면 컴파일은 되지만 컴파일에 대한 상세한 옵션을 처리할 수가 없습니다. 위와 같이 Makefile을 작성하고 make 를 실행하면 아마도 아래와 같이 단순한 모습으로 컴파일 될 것입니다.

]$make
cc -c -o main.o mainc
cc -c -o tcp.o tcp.c
cc -c -o rs232.o rs232.c

  요렇게 말이죠. 그러나 컴파일 할 때에는 인클루드 경로명을 지정하는 것과 같은 옵션을 사용해야 합니다. 이렇게 자동으로 진행되는 것만을 의지할 수 없습니다. 그래서 아래와 같이 .o 를 어떻게 만들어 낼지를 make 에 알려 줍니다.

sample : main.o tcp.o rs232.o
    gcc -lm -o $@ main.o tcp.o rs232.o

.c.o:
    gcc -I/home/jwjw/prjs/include -g -c $<

main.o : main.c main.h rs232.h tcp.h
tcp.o : tcp.c tcp.h
rs232.o : rs232.c rs232.h

  이제 make를 실행하면 아래와 같이 컴파일되는 모습을 보실 수 있습니다.

gcc -I/home/jwjw/prjs/include -g -c main.c
gcc -I/home/jwjw/prjs/include -g -c tcp.c
gcc -I/home/jwjw/prjs/include -g -c rs232.c
gcc -lm -o sample main.o tcp.o rs232.o

  또한 소스 파일이 Makefile과 같은 폴더 안에 있다면 아예 아래와 같이 작성하셔도 컴파일이 됩니다.

sample : main.o tcp.o rs232.o
    gcc -lm -o $@ main.o tcp.o rs232.o
.c.o:
    gcc -I/home/jwjw/prjs/include -g -c $<

  그러나 문제는 각 소스에 대한 컴파일 조건이 매우 단순해 지게 됩니다. 즉, main.c 는 main.c 자신이 수정될 때만 컴파일이 됩니다. 위의 예에서 처럼 rs232.h가 수정되거나 아예 관계가 아주 깊은 main.h 가 수정되더라도 main.c 는 재 컴파일이 안됩니다. 소스끼리 관계가 있다며 하단 부분을 서술해 주셔야 합니다.

sample : main.o tcp.o rs232.o
    gcc -lm -o $@ main.o tcp.o rs232.o
.c.o:
    gcc -I/home/jwjw/prjs/include -g -c $<

main.o: main.c main.h rs232.h
rs232.o: rs232.c rs232.h
tcp.o: tcp.c tcp.h

  그래도 많이 줄어 들었죠? 그러나 이게 다가 아닙니다.

 

매크로로 치환

  예제의 main.o tcp.o rs232.o 의 파일 이름이 2번 중복되어 있습니다. 두번 타이핑을 해야 하는데, 앞서 말씀드렸듯이 프로그래머는 게으릅니다. 2번~? 귀찮습니다. 아래와 같이 수정해 봅시다.

OBJS = main.o tcp.o rs232.o

sample : $(OBJS)
    gcc -lm -o $@ $(OBJS)
.c.o:
    gcc -I/home/jwjw/prjs/include -g -c $<

 또는 $^ 를 이용하여 아래와 같이 수정할 수 도 있습니다. $^는 조건에 있는 모든 파일 이름을 대신하는 매크로입니다.

OBJS = main.o tcp.o rs232.o

sample : $(OBJS)
    gcc -lm -o $@ $^
.c.o:
    gcc -I/home/jwjw/prjs/include -g -c $<

 하는 김에 컴파일 옵션과 링크 옵션도 매크로로 치환해 보겠습니다.


OBJS = main.o tcp.o rs232.o
CC = -I/home/jwjw/prjs/include -g -c

sample : $(OBJS)
    gcc -lm -o $@ $^
.c.o:
    gcc $(CC) $<

 이렇게 매크로로 치환하여 Makefile 의 윗 행에 모아 두면, 내용 전체를 볼 필요 없이 매크로 부분만 보거나 수정해도 되기 때문에 편리합니다. 이래서 아래와 같이 수정하여 완성할 수 있습니다.

TARGET = sample
OBJS = main.o tcp.o rs232.o
CC = -I/home/jwjw/prjs/include -g -c

$(TARGET : $(OBJS)
    gcc -lm -o $@ $^
.c.o:
    gcc $(CC) $<

main.o: main.c main.h rs232.h
rs232.o: rs232.c rs232.h
tcp.o: tcp.c tcp.h

gccmakedep

  다른 것들은 모두 편리하고 좋은 것 같은데, Makefile 하단에 있는 파일 간의 종속에 대한 정보를 모두 타이핑해서 넣어야 할까요? 파일이 많을 경우 어떻게 일일이 입력할 수 있있겠습니까?  당연한 말씀입니다. 게으른 프로그래머에게는 말도 안되죠. 그래서 이와 같은 귀찮은 작업을 make에 떠 넘기겠습니다. 바로 파일간의 의존성을 찾아서 그 내용을 직접 구성해 달라고 요청하는 것이죠.

  이렇게 파일의 의존성을 검색해서 그 내용을 작성해 주는 것이 gccmakedep 입니다. 아래와 같이 수정해서 make dep를 실행합니다.

TARGET = sample
OBJS = main.o tcp.o rs232.o
SRCS = $(OBJS:.o=.c)
CC = -I/home/jwjw/prjs/include -g -c

$(TARGET): $(OBJS)
    gcc -lm -o $@ $^

.c.o:
    gcc $(CC) $<

dep :
    gccmakedep $(SRCS)

  이렇게 추가 작성해서 make dep 를 실행하시면 make는 컴파일과 링크 작업 대신에 라벨 dep: 밑의 명령을 실행합니다. 새로 만들어진 SRCS는 OBJS에 열거된 파일 모록에 대해서 확장자를 .o를 .c로 바뀐 목록을 가지게 됩니다. gccmakedep는 소스 파일을 가지고 의존성을 검색할 수 있기 때문이죠.

]$ make dep
]$ vi Makefile

TARGET = sample
OBJS = main.o tcp.o rs232.o
SRCS = $(OBJS:.o=.c)
CC = -I/home/jwjw/prjs/include -g -c
    $(TARGET): $(OBJS) gcc -lm -o $@ $^
.c.o:
    gcc $(CC) $<
dep : gccmakedep $(SRCS)

# DO NOT DELETE
main.o: main.c /usr/include/stdio.h .........
tcp.o: tcp.c
/usr/include/stdio.h .........
rs232.o: rs232.c /usr/include/stdio.h .........

  하단에# DO NOT DELETE 행과 함께 밑으로 각 .o 에 대한 관련 파일 목록이 자동으로 생성되는 것을 보실 수 있습니다. 이제 make를 실행하면 위 정보에 맞추어 컴파일하게 됩니다.

결언

  어떻게 이해가 되십니까? 마지막 부분은 매크로로 되어 있어서 암호같이 보이는데, 지금 보시면 암호 풀이가 되시는지 모르겠습니다. 소개해 드린 것 보다 더 편리한 부분도 있습니다만 내용이 너무 많아 질 것 같아서 여기서 줄입니다.  긴 내용을 읽어 주셔서 감사합니다.

 

태그: *makefile *C언어 *초보