Makefile의 기초적인 사용법을 알아봅시다.

Makefile이 필요한 경우

먼저, 다음과 같이 3개의 source 파일이 있을 때 컴파일하는 과정을 알아보겠습니다.

첫 번째 파일: main.f

       implicit none
       print *, 'main'
       call sub1()
       call sub2()
       end

두 번째 파일: sub1.f

       subroutine sub1()
       print*, 'sub1'
       end

세 번째 파일: sub2.f

       subroutine sub2()
       print*, 'sub2'
       end

main.f 파일에서 sub1.f와 sub2.f에 있는 subroutine들을 불러오는 매우 간단한 프로그램입니다. 여기서는 하나의 파일 안에 subroutine을 다 넣는 것이 더 편하지만, Makefile 연습을 위해 세 개의 파일로 나누어 놓았습니다.
이렇게 세 개의 파일을 가지고 실행 파일을 만들기 위한 명령은 다음과 같죠.

f77 -c -O2 -o main.o main.f
f77 -c -O2 -o sub1.o sub1.f
f77 -c -O2 -o sub2.o sub2.f
f77 -o main main.o sub1.o sub2.o

위 명령에서 -c 옵션은 source code를 가지고 object file을 생성하라는 의미입니다. 각각의 source file들에 대해 object file을 생성하고 나중에 링크시켜서 실행파일을 만듭니다. -O2는 compile 할 때 optimization level 을 2로 하라는 의미, -o 파일명 은 output file을 -o 다음에 나오는 파일명으로 만들라는 의미입니다. 마지막 줄에서 세 개의 objective file들을 링크시켜서 main 이라는 실행파일을 생성합니다. 실행 파일의 실행 결과는 다음과 같습니다.

$ ./main
main
sub1
sub2

잘 실행됩니다. 그런데 만약 sub1.f 파일을 수정했다면 어떻게 해야할까요? 다시 컴파일하기 위해서는

f77 -c -O2 -o sub1.o sub1.f
f77 -o main main.o sub1.o sub2.o

라고 수정한 파일만 다시 컴파일한 후, 다른 object file들과 링크시켜서 실행파일을 만들어야겠죠. 좀 귀찮습니다. 자동으로 할 수 있으면 좋겠죠. shell script를 하나 만들어서 처음의 컴파일 명령 4줄을 다 써 넣으면 자동으로 실행할 수 있습니다. 하지만, 그렇게 되면 sub1.f 파일을 고쳤을 때 main.f와 sub2.f 파일까지 새로 컴파일하게 됩니다. 프로그램이 간단할 때는 큰 문제가 없지만, 프로그램이 크고 복잡해지면 컴파일 시간이 오래 걸린다는 문제가 생기게 됩니다. Source code 내용에 따라서 특정 code는 다른 code보다 먼저 컴파일해야만 하는 경우도 생길 수 있습니다. 이런 경우에 편리하게 쓸 수 있는 프로그램이 바로 make입니다. make는 실행했을 때 현재 디렉토리에 있는 Makefile 이라는 파일을 찾아 build 작업을 수행합니다.

Makefile 작성법

Makefile의 작성법은

Target: Dependency list
[Tab] Command

와 같습니다. Target은 만들고 싶은 대상, Dependency list는 Target을 만들기 전에 먼저 만들어져야 할 대상들, Command는 Target을 만드는 방법(command line 명령어)입니다. 한 가지 주의할 점은 Command 앞에는 Tab이 들어가야 한다는거죠. 그럼 위의 세 파일을 컴파일하기 위한 기초적인 Makefile을 살펴보겠습니다.

# target: dependency list
# [tab] command
F77=gfortran

all: main

main: main.o sub1.o sub2.o
    $(F77) -O2 -o main main.o sub1.o sub2.o
main.o: main.f
    $(F77) -O2 -c main.f
sub1.o: sub1.f
    $(F77) -O2 -c sub1.f
sub2.o: sub2.f
    $(F77) -O2 -c sub2.f
clean:
    rm main main.o sub1.o sub2.o

F77=gfortran 이라고 먼저 선언을 했습니다. 여기서 F77은 매크로(일종의 변수)입니다. 이런식으로 선언을 해두면 뒤에 $(F77)과 같이 필요할 때 불러서 쓸 수 있습니다. 컴파일러를 바꿀 때 gfortran 대신에 f77 이나 ifort 등으로 바꿔주면 되겠죠.

다음에 나오는게 all 이라는 Target입니다. Dependency list에는 main이 있고 Command는 없네요. Command line에서 make를 실행하면 현재 디렉토리에 있는 Makefile을 찾아 제일 처음에 나오는 Target만 실행합니다. make target1과 같이 실행하면 Makefile내에서 target1 이라는 Target을 찾아 실행합니다. 따라서 맨 처음 Target을 all 이라고 지정해두고 Dependency list에 자신이 만들고 싶은 Target들을 적어두면 make만 쳐서 원하는 Target들을 한 번에 만들 수 있겠죠. 전체적인 compile 과정은 다음과 같습니다.

  1. 제일 처음에 나오는 all 이라는 Target을 만나서 Dependency list를 확인한다. main이라는 Dependency를 찾았다.
  2. Dependency를 만족하기 위해 main이라는 Target을 찾는다. 그리고 main의 Dependency list - main.o, sub1.o, sub2.o 를 찾았다.
  3. main.o라는 Target을 찾아서 Dependency main.f를 찾고 현재 디렉토리에 main.o 파일이 없거나 main.o 파일의 수정 시간이 main.f 파일의 수정시간보다 이전일 때 gfortran -O2 -c main.f 라는 Command를 실행한다. 그렇지 않은 경우에는 아무 것도 실행하지 않는다.
  4. main.o가 잘 만들어졌으면 다시 main 이라는 Target으로 넘어가 sub1.o, sub2.o라는 Dependency를 같은 방법으로 만족하고 돌아온다.
  5. main의 Dependency 세 개가 다 만족되었으면 gfortran -O2 -o main main.o sub1.o sub2.o 라는 Command를 실행하여 main이라는 Target을 만든다.
  6. main이라는 Target이 만들어졌으면 all이라는 Target으로 돌아간다.
  7. all의 Dependency가 다 만족되었지만, Command가 없으므로 make가 끝난다.

clean이라는 Target은 처음에 나오지도 않고 다른 Target의 Dependency 에도 들어가지 않으니 실행이 안 됩니다. 명령줄에서 make clean이라고 실행했을 때만 실행이 되죠. clean은 Dependency list가 비어있으니까 make clean이라고 실행하면 해당하는 Command를 항상 실행하게 됩니다. 보통 make로 생성된 파일들을 지우기 위해 clean이라는 Target을 만듭니다.

여기까지만 배우고 끝내기에는 아쉽습니다. Makefile에는 강력한 기능들이 많기 때문이죠. 몇 개만 더 살펴봅시다.

확장자 규칙

아래의 Makefile은 compiler과 compile option이 약간 바뀐 것 말고는 위의 Makefile과 같은 기능을 합니다.

# $^ : dependency list
# $@ : target

F77=ifort
FFLAG=-assume byterecl -O2
TARGET=main
OBJECTS=main.o sub1.o sub2.o

all: $(TARGET)

$(TARGET): $(OBJECTS)
    $(F77) -o $@ $^

.SUFFIXES: .o .f
%.o: %.f
    $(F77) ${FFLAG} -c $<

clean:
    rm $(TARGET) $(OBJECTS)

Command 위치에서 $^는 Dependency list를 자동으로 입력해줍니다. 또한 $@는 Target 이름을 자동으로 입력해줍니다.

.SUFFIXES: .o .f
%.o: %.f
    $(F77) ${FFLAG} -c $<

는 확장자 규칙으로, .SUFFIXES: .o .f.o 라는 확장자와 .f라는 확장자를 특별히 중요하게 생각하라는 뜻입니다. 그 아래에 나오는 내용은 .o 확장자를 가진 Target에 대해 .f 파일을 이용한 Dependency 와 Command를 자동으로 생성해주는 기능을 합니다. 따라서 처음의 Makefile에 있었던 main.o, sub1.o, sub2.o 라는 Target과 Dependency, Command를 자동으로 만들어줍니다. 한 가지 주의할 점은, 확장자 규칙에서는 앞서 나왔던 $^가 아닌, $<를 사용한다는 점입니다. $<는 확장자 규칙에서만 사용되며, 타겟보다 나중에 변경된 종속 항목들을 의미합니다. Build 과정이 복잡할 때 Makefile은 큰 힘을 발휘합니다.

더 자세한 내용을 알고 싶으신 분들은 임대영님의 GNU Make 강좌를 참고하세요.

예전에 다른 블로그에 올렸던 글인데, 이곳에 복사해둡니다.