make란?

: 이전 게시물에서 gcc를 통한 컴파일 명령어들을 살펴 보았다. gcc의 문제는 프로젝트의 크기가 커질 때, gcc를 통한 컴파일이 상당히 번거로운 작업이 될 것이다. 따라서 이를 위해 파일간의 종속 관계를 기술해 컴파일 명령을 순차적으로 수행하는 것이 make이다.

간단한 Makefile

다음과 같은 파일 구조가 있다고 가정하자.

jay@jay-VirtualBox:~/tutorial/make$ tree .
.
├── plus.c
├── calculator.h
├── main.c
└── minus.c

이를 위한 간단한 Makefile은 다음과 같을 것이다.

all: calculator

calculator : minus.o plus.o main.o
    gcc -W -Wall -o calculator minus.o plus.o main.o

minus.o : minus.c
    gcc -W -Wall -c -o minus.o minus.c

plus.o : plus.c
    gcc -W -Wall -c -o plus.o plus.c

main.o : main.c
    gcc -W -Wall -c -o main.o main.c

clean : 
    rm -rf *.o diary

여기서 make 명령어를 사용하면, Makefile 내에서 제일 처음 오는 타겟 을 찾는다.
all 타겟의 종속 항목인 minus.o가 만들어지지 않았으므로 minus.o를 생성하는 룰을 찾아 해당 명령을 수행하며, 이는 반복적으로 수행되어진다.

매크로를 사용한 Makefile

: 매크로를 정의하면 이는 Makefile 내에 정의된 문자열로 치환되어 사용되어 진다. 이는 일관성있고 이식성 높은 Makefile을 만드는 데 도움을 준다. 다음은 매크로 작성의 규칙이다.

  1. #은 주석문을 의미한다.
  2. 정의한 매크로를 참조할 때에는 다음의 방법이 있다.
    • NAME = string
    • ${NAME}
    • $(NAME)
    • 여기서 중괄호는 셸의 환경변수 치환에도 사용되므로, 소괄호를 사용하는 것을 권장한다.
  3. 정의되지 않은 매크로는 null 문자열(공백)로 치환된다.
  4. 중복 정의된 경우 최후에 정의된 값으로 치환된다.
  5. 매크로 정의 시, 이전에 정의한 값을 참조해 정의할 수 있다.
    • NAME2 = my valuable $(NAME)
  6. 대입에는 다음의 방법들이 있다.
    • NAME1 = string: 재귀적 확장 매크로, 대입문에 post-defined variable이 사용될 경우 이를 계속적으로 스캔하며 대입한다.
    • NAME2 := string: 단순 확장 매크로, 대입문에 pre-defined variable만이 대입된다.
    • NAME3 += string: 기존의 매크로에 공백을 두고 덧 붙인다.
    • NAME4 ?= string: NAME4가 기존에 정의되어있지 않은 경우 이 명령을 통해 정의하고, 그렇지 않다면 무시한다.

NOTE

대입문에 따옴표(")를 넣으면 따옴표(") 또한 문자열로 인식하여 대입한다.

 

내부적으로 정의되어 있는 매크로

: 내부적으로 정의되어 있는 매크로를 확인하기 위해서는 make -p 명령을 사용하면 된다. 다음은 그 일부를 나타낸다.

# default
CC = cc
# environment
_ = /usr/bin/make
# default
CHECKOUT,v = +$(if $(wildcard $@),,$(CO) $(COFLAGS) $< $@)
# environment
MANAGERPID = 1220
# environment
CLUTTER_IM_MODULE = ibus
# environment
LESSOPEN = | /usr/bin/lesspipe %s
# environment
LC_NAME = ko_KR.UTF-8
# default
CPP = $(CC) -E
# default
LINK.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
# default
MAKE_HOST := x86_64-pc-linux-gnu
# environment
PATH = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
# default
LD = ld

...
...

# Implicit Rules

%: %.o
#  recipe to execute (built-in):
    $(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@

%.c:

%: %.c
#  recipe to execute (built-in):
    $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@

%.ln: %.c
#  recipe to execute (built-in):
    $(LINT.c) -C$* $<

%.o: %.c
#  recipe to execute (built-in):
    $(COMPILE.c) $(OUTPUT_OPTION) $<

%.cc:

%: %.cc
#  recipe to execute (built-in):
    $(LINK.cc) $^ $(LOADLIBES) $(LDLIBS) -o $@

%.o: %.cc
#  recipe to execute (built-in):
    $(COMPILE.cc) $(OUTPUT_OPTION) $<

여기서 주요하게 살펴볼 것들은 CC, CXX, CFLAGS, CXXFLAGS, LD 등이 있다.

또한 다음의 자동 매크로 리스트를 살펴보자.

매크로 설명
$? 현재 타겟보다 최근에 변경된 종속 항목 리스트
(확장자 규칙에서 사용 불가)
$< 현재 타겟보다 최근에 변경된 종속 항목 리스트
(확장자 규칙에서만 사용 가능)
$* 현재 타겟보다 최근에 변경된 종속 항목의 이름(확장자 제외)
(확장자 규칙에서만 사용 가능)
$^ 현재 타겟의 종속 항목 리스트
(확장자 규칙에서 사용 불가)
$@ 현재 타겟의 이름
$% 현재 타겟이 라이브러리 모듈일 때, .o 파일에 대응되는 이름

위의 자동 매크로를 이용하면 아래와 같이 편리하게 기술할 수 있다.

target : dep1.c dep2.c
    gcc -o target dep1.c dep2.c
CC = gcc
CFLAGS = -W -Wall
TARGET = calculator

all : $(TARGET)

$(TARGET) : dep1.c dep2.c
    $(CC) $(CFLAGS) -o $@ $^

확장자 규칙의 사용

: *.c 파일을 오브젝트로 컴파일(gcc의 -c 옵션)하면 그에 상응하면 *.o파일이 생성된다. 이를 이용한 것이 make -p 명령어의 아래에서 볼 수 있는

%.o: %.c
#  recipe to execute (built-in):
    $(COMPILE.c) $(OUTPUT_OPTION) $<

등의 pre-defined 명령문이다. 따라서 gcc는 확장자 규칙에 맞게 생성된 파일들(*.cc, *.java, *.c)의 파일을 보고 암묵적으로 적절한 컴파일러를 이용해 컴파일한다. 이를 이용하면 위의 Makefile은 더욱 간단해 질 수 있다.

OBJECTS = minus.o plus.o main.o

all : calculator
calculator : $(OBJECTS)
    $(CC) -o $@ $^
clean :
    rm -rf *.o calculator

위의 기술문에서 calculator를 생성하기 위한 종속항목들(minus.o, plus.o, main.o)을 생성하기 위해 확장자가 .o인 내부 확장자 규칙을 이용해 현재 디렉토리에서 minus.o, plus.o, main.o를 생성할 파일을 찾는다. 이러한 파일들은 확장자를 제외하고 동일한 이름을 가지고 있어야 한다.

더 나아가서 추가적으로 내부 확장자 규칙을 재 정의할 수 있다. 내부 확장자 규칙에서 디버깅 메시지를 활성화하고 싶은 경우, 다음과 같이 재정의 할 수 있다.

OBJECTS = minus.o plus.o main.o

.SUFFIXES : .o .c
%.o : %.c          # .o와 대응되는 .c를 발견 시 아래 명령어를 수행한다.
    $(CC) -DDEBUG -c -o $@ $<

all : calculator
calculator : $(OBJECTS)
    $(CC) -o $@ $^
clean :
    rm -rf *.o calculator

여기서 확장자 규칙을 재 정의하기위해 .SUFFIXES라는 특수 타겟을 사용했다. .SUFFIXES종속 항목은 확장자 규칙을 검사하는 데 사용되는 확장자들의 리스트이다. 따라서 확장자 규칙을 사용하려는 확장자는 먼저 .SUFFIXES 타겟의 종속 항목으로 설정되어야 한다.

여기서 %는 일치하는 확장자를 제외한 파일명을 의미한다.

프로젝트의 파일이 수 없이 많을 경우 이를 모두 기술하는 것은 거의 불가능에 가깝다. 따라서 다음과 같이 편리하게 만들 수 있다.

SRCS = $(shell ls *.c)
SRCS = $(wildcard *.c)
OBJECTS = $(SRCS:.c=.o)
  • $(shell 실행할 셸 명령): 셸 명령을 수행하고 결과를 리턴한다.
  • $(wildcard *.c): *.c와 일치하는 파일들을 공백으로 구분하여 대입
  • $(SRCS:.c=.o): 대입 참조기법을 사용해 .c가 .o로 바뀐다.

NOTE

clean:
    cd ~/Desktop/tutorial
    rm -rf *.o
    @echo "with @ option print only result, not with command itself"

위와 같은 명령을 수행하면 원하는 결과를 얻지 못할 수 있다.
각각의 명령은 새로운 셸을 띄워

/bin/sh -c cd ~/Desktop/tutoril
/bin/sh -c rm -rf *.o

를 수행하므로 이에 유의해야한다.

.

NOTE

echo $?
위 명령을 수행하면 직전에 수행한 명령의 리턴값을 나타낸다. 0일 경우 ERROR_NONE을 나타낸다.

재귀적 make의 사용

all : calculator.o plus.o minus.o main.o
    $(CC) -o calculator $^

calculator.o:
    cd calculator && make

...

: 재귀적으로 Makefile을 선언해 사용할 경우, 부모 Makefile에 기술한 매크로를 자식 Makefile에 전달하고 싶을 때가 있다. 이 경우 다음과 같이

TARGET := calculator
export CC = gcc

export 구문을 사용한다. 만약 모든 매크로에 대해 전달하고 싶을 경우 export 구문을 단독으로 사용하면 된다.

+ Recent posts