배경 지식
리눅스의 make 명령어를 이해하려면, 기본적인 compilation system을 이해하고 있어야 한다.
시스템 프로그래밍 과목을 수강하면 배웠을 것이라 생각하고 간단하게 백그라운드를 설명하겠다.
우리가 흔히 알고 있다. main.c 라는 파일을 작성하면 그 파일이 우리가 아는 프로그램으로 작동되기까지 여러 과정들이 존재한다.
[1] main.c (source code) > [2] main.s (assembly file) > [3] main.o (object file) > [4] main.out (executable file)
제너럴하게 [1]~[3]의 과정을 컴파일이라고 하고, 오브젝트 파일이 생성되면, 그 파일들을 묶는 [4]의 링킹과정을 통해서 최종 실행파일이 생성된다.
MakeFile 정의
MakeFile이란, 이 컴파일 과정과 링킹 과정을 쉽게 할 수 있도록 도와주는 GNU make의 설정 파일이라고 생각하면 된다. 아 뭔지는 알겠다. 그러면 왜 사용할까?
학교 과제를 위해서 test.c 파일을 생성해서 하다가 재컴파일 해야한다는 것을 발견했다. 평소의 나라면 그냥
gcc -o test.c test2.out
커맨드를 이용해서 재컴파일을 할 것이다.
하지만 대규모 프로젝트의 경우에는 어떨까? main.c 파일 안에 test.c 파일 뿐만아니라 foo.c, bar.c와 같은 의존성 있는 파일이 또 존재할 수 있을것이다. 그런데 그 중 test.c를 한 줄 바꾸자고, main.c 자체를 재컴파일을 하게 되면, 그 안에 있는 foo.c와 bar.c까지도 같이 재컴파일이 되게 되는 것이다 .이는 컴퓨팅 코스트 측면으로 생각했을 때는 손해이다.
이와 같은 비효율성을 극복하기 위해 MakeFile로 타겟을 지정해주며 사용하는 것이다. 바로 incremental build 기능을 이용하는 것이다. 이는 반복적으로 빌드를 하는 과정 내에서 변경된 소스 코드에서 의존성이 있는 부분의 코드만 다시 빌드를 하는 특성을 뜻한다.
MakeFile 문법
기초 스켈레톤을 보여주겠다.
<macro>
<target>:<dependencies>
<recipe>
macro는 소스코드 상에서 define하는 것과 비슷하게, 키워드를 대체한다고 생각하면 된다. $를 앞에 붙여서 사용 할 수 있다. 실제 사용 예시는 밑의 파일을 통해서 확인할 수 있다. 사용자가 직접 선언하는 것도 있지만, 내부 상에서 매크로로 지정된 것들이 매우 다양하게 있다.
$@는 현재 target의 이름을 나타낸다. $^는 target이 의존하고 있는 파일들의 목록이다. 그리고 $<는 dependencies($^)중에서 가장 왼쪽에 존재하는 파일을 뜻한다. $*는 현재 target에서 확장자를 제외한 파일 이름이고, $?는 dependencies 중에서 가장 최근에 변경된 파일을 나타낸다.
all을 사용해서 뒤에 타겟을 명시할 경우는, all 뒤 파일이 최종적으로 나올 산출물, 즉 파일이라는 뜻이다. 또한 정규표현식같은 규칙도 존재한다. 확장자 규칙은 두 개의 확장자를 연속으로 붙여씀으로써 사용할 수 있다. 예를 들어 파일에 .c.o로 명시한 경우, .c를 컴파일해서 .o로 만드는 경우를 나타낸다. clean을 사용할 때는 make clean 커맨드를 사용할 경우 빌드 할 때 발생한 부수적인 파일들을 정리해준다.
target은 빌드 대상, 즉 최종적으로 생성하고자 하는 파일명이 들어가면 된다.
dependency는 MakeFile이 incremental build를 할 수 있도록 해주는 파트로, 타겟을 make 할 때 필요한 파일들의 집합이다. 여기에 명시한 파일들이 실행파일을 생성하기 전에 먼저 빌드된다.
recipe는 빌드 대상을 생성하는 명령이 들어가는 자리이다. 즉 target을 make 하기 위해 필요한 명령어를 작성해야 한다. 각 명령어를 작성하기 전에는 무조건 tab을 해주어야 한다.
이제 앞에서 main.c를 구성하는 foo.c, bar.c, test.c를 가지고 실제 MakeFile을 작성하여 보여주도록 해주겠다.
# MakeFile Example
CC = gcc # compiler
TARGET = main.out # target file
OBJS = test.o foo.o bar.o # object files
CFLAGS = -Wall -g # compile options
all:$(TARGET) # final result
$(TARGET): $(OBJS)
$(CC) -o $(TARGET) $(OBJS)
main.o: foo.h bar.h test.h main.c
$(CC) -c main.c
test.o: test.h test.c
$(CC) -c test.c
foo.o: foo.h foo.c
$(CC) -c foo.c
bar.o: bar.h bar.c
$(CC) -c bar.c
clean:
rm -rf $(OBJS) $(TARGET)
위의 설명을 그대로 읽었다면 쭉 읽어도 이해가 될 것이다. 이제 프로젝트 진행할 때 일일이 컴파일하다기보다는 MakeFile을 이용해서 효율적으로 관리하자.