2014년 9월 10일 수요일

Makefile 사용

목차

  1. 소개
  2. Hello World
  3. target
  4. 변수 사용하기
  5. pattern 을 이용한 여러 파일 일괄 처리
  6. 부가 설명

1. 소개


만약 ant 를 이용한 java 개발 경험이 있다면 target 과 의존 target 에 대한 이해가 있을 것이다.
make 도 마찬가지로 target 이 존재하고 의존 target 개념이 존재한다.
간단한 Makefile 부터 시작해서 변수를 활용한 Makefile 작성 방법을 설명하겠다.

2. Hello World


all:
    echo "Hello World"

Makefile 이라는 이름으로 저장하고 아래와 같이 실행
주의) all: 다음 줄 시작은 반드시 tab 을 입력해야 함

$ make
echo "Hello World"
Hello World


또는 아래와 같이 target 이름을 명령 인자로 넘겨줌

$ make all
echo "Hello World"
Hello World


존재하지 않는 target 을 입력하면 에러 출력

$ make man
make: *** No rule to make target `man'.  Stop.


3. target

 

target 설명


target 은 make 가 수행하는 하나의 작업 단위를 의미함
기본적으로 make 는 target 을 파일명으로 간주함

예)

$ ls -l
합계 1
-rwxr-xr-x 1 root Domain Users 23 9월  11 11:26 Makefile


$ cat Makefile
all: hello.txt
    cat $<


$ make
make: *** No rule to make target 'hello.txt', needed by 'all'.  멈춤.


$ echo hello world > hello.txt

 
$ ls -l
합계 2
-rw-r--r-- 1 root Domain Users 12 9월  11 11:30 hello.txt
-rwxr-xr-x 1 root Domain Users 23 9월  11 11:26 Makefile


$ make
cat hello.txt
hello world



위 예에서 hello.txt 는 Makefile 에 target 으로 작성하지 않았지만 hello.txt 파일이 존재하는지 확인하고 all 을 수행한다.

target 문법


<target>: <prerequisite target> <another prerequisite target> ...
<tab><system command>
<tab><another system command>
...
<another target>: ...
...

예)
$ cat Makefile
all: prepare build

prepare:
    echo "prepare"
    touch .prepare

build:
    echo "build"
    touch product

clean:
    echo "clean"
    rm .prepare product


$ make
echo "prepare"
prepare
touch .prepare
echo "build"
build
touch product


$ ls -l
total 12
4 drwxrwxr-x 2 root
root 4096 Sep  4 16:08 ./
4 drwxrwxr-x 4
root root 4096 Sep  4 16:07 ../
0 -rw-rw-r-- 1
root root    0 Sep  4 16:08 .prepare
4 -rw-rw-r-- 1
root root  141 Sep  4 16:08 Makefile
0 -rw-rw-r-- 1
root root    0 Sep  4 16:08 product

설명:


prepare 와 build 는 단일 수행 target 이고 all 은 prepare 와 build 가 선행되어야 하는 target 이다.

위 에제를 통해 아래와 같은 특징을 알 수 있음
  • 여러줄의 명령 입력 가능함 (단, tab 으로 시작만 하면됨)
  • all 은 기본 target 이기 때문에 make 명령에 target 을 지정하지 않으면 all 을 실행함
  • make 프로그램은 기본적으로 작업 위치의 Makefile 이라는 이름의 파일을 찾아 실행함
  • make 프로그램은 Makefile 파일을 한줄씩 읽으며 바로바로 실행해 가는게 아니라 파일 전체를 먼저 파악하고 실행한다.
    • prepare 와 build 는 all 보다 하단에 정의 되었는데 실행에 문제 없었다.
make 실행시 명령행과 결과가 모두 출력되기 때문에 작동 순서를 파악할 수 있다.
  1. make 프로그램이 Makefile 을 찾아 실행한다.
  2. make 에 target 을 입력하지 않았기 때문에 기본 target 인 all 을 수행한다.
  3. all target 이 prepare 와 build target 을 사전에 실행하길 원하기 때문에 순서대로 prepare 와 build 를 수행한다.
  4. all target 에 command 가 있다면 수행하고 종료한다.

컴파일에 적용해 보기


$ cat Makefile
all:

    gcc -o hello main.c

$ cat main.c
#include <stdio.h>

int main(int argc, char * args[]) {
 
    printf("Hello World\n");
    return 0;
}


$ make
gcc -o hello main.c


$ ./hello
Hello World


위에서는 all target 에 컴파일 명령을 넣어 줌으로써 script 를 작성한 수준과 다르지 않다.
하지만 컴파일 과정은 위와 같이 단순한 경우도 있겠지만 컴파일과 링크 과정을 나눌 수 있고 라이브러리 별로 include 와 link 옵션 등을 구분지어야 할 경우도 있다.
아래에서 설명할 make 에서 지원하는 변수 기능과 dependency target 기능을 활용하여 컴파일 과정을 분리하는 등의 일을 할 수 있다.

4. 변수 사용하기


예)
$ cat Makefile
CC=gcc
TARGET=hello
SRC=main.c

all: $(TARGET)

$(TARGET):
    $(CC) -o $@ $(SRC)

clean:
    rm $(TARGET)


$ cat main.c
#include <unistd.h>
#include <stdio.h>

int main(int argc, char * args[]) {
    printf("Hello World\n");
    return 0;
}


$ make
gcc -o hello main.c
$ ./hello
Hello World


변수 선언


name=value

= 또는 :=, ?=, += 으로 변수값을 지정할 수 있다.
참고: http://stackoverflow.com/questions/448910/makefile-variable-assignment

변수 사용


$(name)

환경 변수


make 실행시 기본으로 환경 변수들이 make 변수로 선언되어 있다.

$ cat Makefile
all:
    echo $(PWD)
    echo $(PATH)


$ echo $PWD

/root/practice/makefile/variable_env

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games


$ make
echo /root/practice/makefile/variable_env
/root/practice/makefile/variable_env
echo /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games


PWD 는 shell 에서 사용하는 환경 변수지만 make 가 실행되면서 자동으로 환경 변수들을 make 변수로 선언했기 때문에 $(PWD) 를 이용해 변수를 사용할 수 있다.

assign 문


assign 에 사용할 수 있는 기호는 아래와 같다.

  • = : 값을 무조건 선언 (실행시 최종 값이 정해짐)
  • := : 값을 무조건 선언 (선언시 최종 값이 정해짐)
  • ?= : 변수가 선언되어 있지 않을 때만 선언
  • += : 변수에 값을 추가
예)
$ cat Makefile
A=a
B:=b
C?=c
D+=d

all:
    echo A : $(A)
    echo B : $(B)
    echo C : $(C)
    echo D : $(D)


$ make
echo A : a
A : a
echo B : b
B : b
echo C : c
C : c
echo D : d
D : d


위에서는 A 와 B, C, D 모두 Makefile 안에서 적용한 값이 출력되지만 아래와 같이 변수를 미리 선언한 경우 차이점을 확인할 수 있다.

$ export A=global_a
 
$ export B=global_b

 
$ export C=global_c

 
$ export D=global_d


$ make
echo A : a
A : a
echo B : b
B : b
echo C : global_c
C : global_c
echo D : global_d d
D : global_d d


= 와 := 의 차이점


예)
$ cat Makefile
A = a

CWD1=$(A)
CWD2:=$(A)

A += b

all:
    echo $(CWD1)
    echo $(CWD2)


$ make
echo a b
a b
echo a
a



= 로 지정한 CWD1 은 A += b 가 반영된 a b 를 출력하고 := 로 지정한 CWD2 은 선언시 정해진 A 의 값인 a 를 출력한다.

5. pattern 을 이용한 여러 파일 일괄 처리


프로젝트가 커지면 소스 파일의 개수가 많아 질 수 있다.
보통 .c 또는 .cpp 등의 확장자를 갖고 같은 작업을 통해 빌드 결과물을 얻는다.
이런 경우 pattern 키워드를 활용하여 여러 반복작업들을 표현할 수 있다.

$ ls -l
total 16
-rw-rw-r-- 1 root root 158 Sep  5 09:48 Makefile
-rw-rw-r-- 1 root root 102 Sep  5 09:50 hello.c
-rw-rw-r-- 1 root root  70 Sep  5 09:49 hello.h
-rw-rw-r-- 1 root root 121 Sep  5 09:49 main.c


$ cat Makefile
CC=gcc
TARGET=hello
OBJS=main.o hello.o

all: $(TARGET)

$(TARGET) : $(OBJS)
    $(CC) -o $@ $(OBJS)

%.o : %.c
    $(CC) -c -o $@ $<

clean:
    rm -rf *.o $(TARGET)


$ cat main.c
#include <unistd.h>
#include <stdio.h>
#include "hello.h"

int main(int argc, char * args[]) {

    hello();

    return 0;
}


$ cat hello.h
#ifndef __HELLO_H__
#define __HELLO_H__

extern void hello();

#endif


$ cat hello.c
#include <unistd.h>
#include <stdio.h>
#include "hello.h"

void hello() {
    printf("Hello World\n");
}


$ make
gcc -c -o main.o main.c
gcc -c -o hello.o hello.c
gcc -o hello main.o hello.o


$ ls -l
total 32
-rw-rw-r-- 1 root root  158 Sep  5 09:48 Makefile
-rwxrwxr-x 1 root root 7204 Sep  5 09:51 hello
-rw-rw-r-- 1 root root  102 Sep  5 09:50 hello.c
-rw-rw-r-- 1 root root   70 Sep  5 09:49 hello.h
-rw-rw-r-- 1 root root 1020 Sep  5 09:51 hello.o
-rw-rw-r-- 1 root root  121 Sep  5 09:49 main.c
-rw-rw-r-- 1 root root  936 Sep  5 09:51 main.o


$ ./hello
Hello World


$ make clean
rm -rf *.o hello


$ ls -l
total 16
-rw-rw-r-- 1 root root 158 Sep  5 09:48 Makefile
-rw-rw-r-- 1 root root 102 Sep  5 09:50 hello.c
-rw-rw-r-- 1 root root  70 Sep  5 09:49 hello.h
-rw-rw-r-- 1 root root 121 Sep  5 09:49 main.c


수행 과정 설명

  1. make 명령에 target 을 지정하지 않았으므로 all target 을 실행한다.
  2. TARGET 변수 값을 hello 로 지정했고 $(TARGET) 으로 target 을 하나 정의하였으므로 hello target 을 실행한다.
  3. hello target 은 $(OBJS) 가 먼저 수행되어 하므로 OBJS 에 지정한 main.o 와 hello.o 가 차례로 수행된다.
  4. main.o 와 hello.o 둘다 이름이 .o 로 끝난다는 공통점이 있고 %.o 의 조건에 맞기 때문에 %.o target 이 순차적으로 실행된다.
  5. %.o 는 main.o 로 바뀌고 %.c 는 main.c 로 바뀌며 main.c target 은 작성하지 않지만 make 는 기본적으로 target 을 파일명으로 인식하기 때문에 해당 파일이 존재하는지 확인하고 존재한다면 main.o 를 수행한다.
  6. gcc 의 -c 옵션은 링크 과정 제외하고 컴파일만 하라는 옵션이며 $@ 은 main.o 로 치환되고 $< 은 main.c 로 치환된다.
  7. hello.o 도 6번 과정을 거친다.

    gcc -c -o main.o main.c
    gcc -c -o hello.o hello.c
  8. $(OBJS) 의 과정을 모두 마쳤으므로 $(TARGET) 을 수행한다.

    gcc -o hello main.o hello.o
  9. all 에서는 명령을 지정하지 않았으므로 작업을 종료한다.

패턴 설명



%.o : %.c

  • make 실행시 % 기호가 .o 파일의 이름 부분으로 치환된다.
  • 위 예제에서는 OBJS=main.o hello.o 라고 지정했기 때문에 main.o 와 hello.o 로 치환된다.
  • %.c 는 .o 파일의 이름을 따라 main.c 와 hello.c 로 치환된다.
  • main.o 와 hello.o 두개를 지정했기 때문에 두번의 실행을 거친다.

gcc -c -o main.o main.c
gcc -c -o hello.o hello.c

$@ 와 $<


팁) .c 와 .cpp 파일이 같이 포함되어 있다면


$ ls -l
total 16
-rw-rw-r-- 1 root root 195 Sep  5 10:12 Makefile
-rw-rw-r-- 1 root root 102 Sep  5 10:10 hello.c
-rw-rw-r-- 1 root root  70 Sep  5 10:10 hello.h
-rw-rw-r-- 1 root root 121 Sep  5 10:09 main.cpp


$ cat Makefile
CXX=g++
OBJS=main.o hello.o
TARGET=hello

all: $(TARGET)

$(TARGET): $(OBJS)
    $(CXX) -o $@ $(OBJS)

%.o: %.c
    $(CXX) -c -o $@ $<

%.o: %.cpp
    $(CXX) -c -o $@ $<

clean:
        rm -rf $(OBJS) $(TARGET)


$ cat main.cpp
#include <iostream>
#include "hello.h"

using namespace std;

int main(int argc, char * args[]) {
    hello();
    return 0;
}


$ cat hello.h
#ifndef __HELLO_H__
#define __HELLO_H__

extern void hello();

#endif


$ cat hello.c
#include <unistd.h>
#include <stdio.h>
#include "hello.h"

void hello() {
    printf("Hello World\n");
}


$ make
g++ -c -o main.o main.cpp
g++ -c -o hello.o hello.c
g++ -o hello main.o hello.o


$ ./hello
Hello World


6. 부가 설명

 

.PHONY target 사용


참고: http://www.gnu.org/software/make/manual/make.html#Phony-Targets

make 의 특징 중에 하나는 컴파일 시간을 단축하기 위해 재작업이 필요없다고 판단한 경우 해당 작업을 skip 한다.

예)
$ ls -l
합계 1
-rwxr-xr-x 1 tjjang Domain Users 26 9월  11 14:27 Makefile

$ cat Makefile
all:
    echo "Hello World!"

$ make
echo "Hello World!"
Hello World!

$ touch all

$ ls -l
합계 1
-rw-r--r-- 1 root Domain Users  0 9월  11 14:28 all
-rwxr-xr-x 1 root Domain Users 26 9월  11 14:27 Makefile

$ make
make: 'all' is up to date.


all 파일을 생성한 이후 make 프로그램이 all target 이 이미 최신 상태로 갱신되어 있다고 판단하고 all 을 수행하지 않고 있다.
하지만 all 은 항상 수행되어야 하며 파일명과 무관하기 때문에 make 프로그램에게 해당 target 은 항상 실행해야 한다고 명시해 줘야 한다.
이 때 .PHONY target 을 지정한다.

예)
$ cat Makefile
.PHONY: all

all:
    echo "Hello World!"


$ ls -l
합계 1
-rw-r--r-- 1 rootDomain Users  0 9월  11 14:28 all
-rwxr-xr-x 1 root Domain Users 41 9월  11 14:31 Makefile

$ make
echo "Hello World!"
Hello World!


.PHONY 지정 후 all 파일이 존재하여도 all 을 수행한다.

댓글 2개: