[Etc] Makefile 문법 및 예시
0. 들어가며
빌드를 쉽게 해주는 make와 Makefile에 대해 알아보자.
1. make란?
"의존성 관리”와 “증분 빌드” 기능을 갖춘 빌드 도구.
1-1. 의존성 관리
빌드 과정에서 의존성에 따른 빌드 순서는 무척 중요하다.
컴파일, 즉 a.c
로 a.o
를 만들고, a.o
로 a.out
을 만드는 상황을 가정하자.
여기서 a.out
은 a.o
에, a.o
는 a.c
에 의존성이 있다고 할 수 있다.
🔔 .o
파일은 컴파일 과정에서 생성되는 오브젝트 파일입니다.
이번엔 더 복잡한 상황을 생각해 보자.
만약 프로그램 exec.out
을 만들기 위해 file1.o
, file2.o
, file3.o
가 필요하고 각각은 file1.c
, file2.c
, file3.c
가 필요하다면 의존성 관계는 위의 케이스보다 복잡해진다.
실제로 필요한 프로그램을 코딩할 때는 필요한 소스 파일이 한 두개가 아니고, 이 많은 소스 파일들의 의존성을 직접 파악하기란 쉽지 않다.
실행파일을 만들기 위해 순서를 잘 지키면서 과정을 따라가야 한다. make는 규칙을 정의하면 그 규칙에서 적절한 작업 순서를 찾아서, 그 순서대로 작업을 수행한다. 이것을 의존성 관리라고 한다.
1-2. 증분 빌드
만약 의존성을 파악하지 않고 모든 파일을 다시 컴파일 한다면, 변경된 내용이 없는 .c
파일도 모두 다시 .o
파일로 만들어야 하므로 컴파일에 불필요한 비용이 든다.
소스 파일에 변경된 내용이 있다면, 변경사항이 있는 소스파일만 .o
파일을 새로 만들어 링킹하는 것이 효율적이다.
즉, 의존성 그래프에서 변경사항을 추적해서 변경이 필요한 것들만 다시 만들면 된다. 하지만 이 과정을 직접 하는 것은 두통을 유발할 수 있다. make는 규칙을 잘 정의하면 무언가가 변경되었을 때, 변경이 필요한 것들만 다시 만들어준다. 이것을 증분 빌드라고 한다.
2. Makefile이란?
linux상에서 반복적으로 발생하는 컴파일을 쉽게하기 위해서 사용하는 make 프로그램의 설정 파일이다.
make가 규칙을 정의하면 변경이 필요한 것만 만들어 주는 프로그램이라면, Makefile은 그 규칙을 정의하는 파일이다. <br> ## 2-1. 규칙
1-1에서의 예시와 같이 a.c
→ a.o
→ a.out
의 경우를 생각해 보자.
여기서 a.c
는 사람이 직접 만들 파일이므로 a.c
를 만드는 규칙은 필요하지 않다. 따라서 아래의 두 가지 규칙만을 필요로 한다.
a.c
→a.o
- 의존성 : a.c
- 만드는 법 : cc -c a.c
a.o
→a.out
- 의존성 : a.o
- 만드는 법 : cc a.o -o a.out
이를 Makefile 문법으로 나타내면 다음과 같다.
a.o: a.c
cc -c a.c
a.out: a.o
cc a.o -o a.out
위 코드 블럭의 첫 번째 경우, a.o
는 target
, a.c
는 dependency
, cc -c a.c
는 command
라고 부른다.
target
은 command
가 수행되어서 나온 결과 파일을 의미한다.
우선 (target1) (target2) …: (dependency1) (dependency2) …
처럼 의존 관계를 정의하고, 다음 줄부터 탭으로 들여쓰기 해서 한 줄씩 차례대로 쓰면 된다.
2-1. 변수
파일의 이름을 직접 사용하는 것은 좋지 않다.
예를 들어 특정 파일의 이름을 여러번 쓴다면, 파일 이름이 바뀌거나 의존성에 변화가 생길 때마다 그 파일 이름을 쓴 모든 곳을 수정해야 한다. 그 중 한 곳이라도 수정하는 것을 잊으면 makefile을 통해 컴파일을 할 때 문제가 생긴다. 이런 문제를 피하기 위해 변수를 사용할 수 있다.
변수는 변수명 = 값
으로 정의하고, $(변수명)
으로 사용할 수 있다.
변수 하나에 여러 파일을 할당하고 싶으면 \
로 구분하여 할당 가능하다.
아래 예시 코드의 CFLAGS
처럼 플래그도 할당 가능하다.
#변수 정의
CC = cc
SRCS = file1.c \
file2.c \
file3.c
OBJS = file1.o \
file2.o \
file3.o
TARGET = a.out
CFLAGS = -Wall -Wextra -Werror
#변수 사용
$(OBJS): $(SRCS)
$(CC) $(CFLAGS) -c $(SRCS)
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET)
이렇게 변수를 정의해두면 파일명이나 컴파일러가 바뀌어도, 그 파일명이나 컴파일러가 쓰인 모든 곳을 바꿀 필요 없이 변수를 정의한 곳만 바꾸면 된다.
2-2. 자동 변수, 패턴 규칙
위처럼 모든 파일마다 규칙을 만들어야 한다면 Makefile을 쓰더라도 길이가 매우 길어질 것이다. 보다 간편한 작성을 위해 make는 자동 변수와 패턴 규칙을 제공한다.
자동 변수(Automatic Variables)
는 Makefile 안에서 사용할 수 있는, 그때그때 자동으로 만들어지는 변수이다.
자동 변수 | 의미 |
---|---|
$@ | 만들려는 파일(target) 이름 |
$< | 의존성 중 첫번째 |
$^ | 모든 의존성 |
$? | 현재의 target 파일보다 최근에 갱신된 의존성들 |
#예시
a.o: a.c
cc -c $<
a.out: a.o
cc $^ -o $@
패턴 규칙(pattern rule)
은 반복되는 패턴을 하나의 규칙으로 단순화시킬 때 사용된다. 패턴 규칙에서는wildcard %
가 사용된다.%
는 어떠한 것과도 매치될 수 있다(shell에서의 *).
이런 자동 변수와 패턴을 결합하면 여러 파일에 대한 규칙을 간단하게 정의할 수 있다.
TARGET = a.out
%.o: %.c //%.o가 a.o라면 %.c도 a.c가 된다. //모든 .c 파일 → .o 파일
cc -c $<
$(TARGET): a.o
cc $^ -o $@
2-3. 내장 변수, 내장 규칙
GNU Make에는 많은 변수와 규칙이 내장되어있다.
내장 변수 | 의미 |
---|---|
CC | C 컴파일러. default : cc |
CFLAGS | C 컴파일러 플래그 |
CXX | C++ 컴파일러. default : c++ |
CXXFLAGS | C++ 컴파일러 플래그 |
LDFLAGS | 링커 플래그 |
CPPFLAGS | C 전처리기 플래그. C와 C++에 모두 사용 |
이런 내장 규칙을 활용해서 Makefile의 길이를 쉽게 줄일 수 있다.
%.o: %.c
$(CC) $(TARGET_ARCH) $(CPPFLAGS) $(CFLAGS) -o $@ -c $<
%.o: %.cpp
$(CXX) $(TARGET_ARCH) $(CPPFLAGS) $(CXXFLAGS) -o $@ -c $<
%: %.o
$(CC) $(TARGET_ARCH) $(LOADLIBES) $(LDLIBS) $(LDFLAGS) -o $@ $^
2-4. .PHONY 규칙
자주 쓰는 명령어를 make에 등록해서 쓸 수 있다.
clean:
rm -f *.o
하지만 위의 경우, clean이라는 파일이 있으면 make clean이 아무것도 실행하지 않는다. 의존성이 없기 때문에 항상 clean 파일이 최신 상태(변경된 의존성 파일이 없는 경우)라고 생각하기 때문이다.
이런 문제를 해결하기 위해 clean을 가짜를 의미하는 .PHONY 타겟으로 지정할 수 있다.
clean:
rm -f *.o
.PHONY: clean
이렇게 clean을 .PHONY 타겟으로 지정하면 clean이라는 파일이 있어도 make clean이 의도대로 동작한다.
2-5. 매크로 치환(Macro substitution)
필요에 의해 매크로의 내용을 조금 바꾸어야 할 때가 있다. 매크로 내용의 일부만 바꾸기 위해서는 $(MACRO_NAME:OLD=NEW)와 같은 형식을 이용하면 된다.
MY_NAME = HyunsoonIm
NEW_NAME = $(MY_NAME:Im=Kim)
위의 예제에서는 Im
이란 부분이 Kim
으로 바뀌게 된다. 즉 YOUR_NAME
이란 매크로의 값은 HyunsoonKim
이 된다.
매크로 치환을 활용하면 오브젝트 파일들을 쉽게 정의할 수 있다.
SRCS = main.c \
read.c \
write.c
OBJS = $(OBJS:.o=.c)
⚠ OBJS : .o = .c
처럼 가독성을 위해 띄어쓰지 말 것.
띄어쓰면 makefile이 인식하지 못한다.
위 예제의 OBJS에는 SRCS에서 .c가 .o로 바뀐 이름들이 할당된다. 즉 아래와 같다.
OBJS = main.o read.o write.o
2-6. addprefix(접두어 붙이기)
목적에 맞게 폴더를 나눠서 작업을 하거나, 소스 파일들을 어떤 폴더에 옮겨야 하는 상황일 때, makefile이 소스 파일의 위치를 인지할 수 있도록 변수 앞에 경로를 붙여줘야 한다.
변수의 앞에 문자를 붙일 때, addprefix를 사용하면 간편하게 작업할 수 있다.
#예를 들어 main.c, read.c, write.c가 my_files 폴더에 있는 경우,
SRCS = main.c \
read.c \
write.c
PATH_PREFIX = ./my_files/
SRCS_WITH_PATH = $(addprefix $(PATH_PREFIX), $(SRCS))
위 코드는 다음과 같은 의미를 갖는다.
SRCS_WITH_PATH = ./my_files/main.c \
./my_files/read.c \
./my_files/write.c
2-7. 예시
# 디렉토리 구조 : ./headers, ./libft, ./srcs
# headers : pipex.h 헤더파일이 있는 디렉토리
# libft : libft.a 아카이브 파일이 있는 디렉토리
# srcs : main.c, init.c 등 소스파일이 있는 디렉토리
SRCS_NAME = main.c \
init.c \
parse.c \
exec.c \
redirect.c
OBJS = $(SRCS:.c=.o)
PREFIX = ./srcs/
SRCS = $(addprefix $(PREFIX), $(SRCS_NAME))
CC = cc
CFLAGS = -Wall -Wextra -Werror -c
HEADER = ./headers
NAME = pipex
LIBFT = ft
all : $(NAME)
$(NAME) : $(OBJS)
cd libft; make; cd ..
$(CC) $(OBJS) -Llibft -l$(LIBFT) -o $(NAME) -I $(HEADER)
%.o : %.c
$(CC) $(CFLAGS) $< -o $@ -I $(HEADER)
clean :
cd libft; make clean; cd ..
rm -f $(OBJS)
fclean : clean
cd libft; make fclean; cd ..
rm -f $(NAME)
re : fclean all
.PHONY : clean fclean re
Leave a comment