Makefile 만들기

EOH·2023년 4월 10일
0
post-thumbnail

🗿 머리말

처음 Makefile을 접했을 때 친절한 설명글들을 보고도 이해하지 못했고 동료들에게 물어 물어가며 무작정 외워 작성했다.
push_swap과제에서도 가장 마지막에 공부하기로 미뤄두고

gcc push_swap.c check_error.c first_stack.c count.c find.c ft_split.c is_utils.c make_utils.c pivot.c push.c rev_rot.c rotate.c sort_few.c sort.c swap.c utils.c
위 명령어를 드래그 해보시오 ➡️ ➡️ ➡️

이 기나긴 컴파일 명령어를 메모장에 복사해뒀다가 테스트할 때마다 붙여넣는 멍청한 짓을 계속했다😅 하지만 이제는 제대로 작성할 수 있다!

❓ Makefile이란

파일을 관리하는 프로그램이다. 컴파일과 수정된 파일에 대한 재컴파일을 용이하게 해준다. makefile을 작성해두면 gcc를 계속 해주지 않고 make 명령어를 통해 바로 실행파일을 만들 수 있다.

🐝 컴파일이 되는 과정

컴파일이 되는 과정을 알아야 makefile을 이해할 수 있다.
main.c 와 add.c 두 파일이 있다고 가정해보자

#include <stdio.h>

int add(int a, int b);

int main(void)
{
	int a = 3;
    int b = 2;
    int result;
    
    result = add(a, b);
}//main.c 파일의 내용
int add(int a, int b)
{
	return (a + b);
}//add.c 파일의 내용

파일의 내용은 각각 위와 같다.
main을 실행하려면 gcc main.c add.c만 하면 된다고 생각할 것이다.
사실 gcc를 실행하면 그냥 바로 실행되는 것이 아니라
1️⃣ 컴퓨터가 알아들을 수 있는 오브젝트파일(*.o)파일로 변경 ➡️ 2️⃣ 오브젝트파일을 한데 묶어줌(link) ➡️ 3️⃣ a.out 같은 실행파일 생성
순서로 컴파일이 된다. 별도의 옵션이 없는 gcc 명령어는 위에 생성된 파일을 사용 후 실행파일만 남기고 삭제하기 때문에 깔끔하게 컴파일이 되는 것처럼 보인다.

gcc옵션
gcc -c 옵션: *.c 파일을 이용해 *.o파일을 생성
gcc -o test test.c : test.c파일로 test라는 이름의 실행파일 생성
gcc -o test test.o : test.o파일로 test라는 이름의 실행파일 생성


❗️ 간단한 Makefile을 먼저 만들어보자

위 예시 코드를 편리하게 컴파일해주는 makefile을 만들어보자

test : main.o add.o
	gcc -o test main.o add.o
main.o : main.c
	gcc -c main.c
add .o : add.c
	gcc -c add.c

위 코드들을 컴파일해 test라는 실행파일을 만들고 싶어 이 과정을 간단하게 해주는 makefile을 작성해보았다. make를 치면

gcc ~~~를 치는 과정없이 make만 쳤을뿐인데 컴파일 과정을 거쳐 실행파일 test와 *.o 오브젝트 파일들이 생성된 것을 볼 수 있다.

🎛️ Makefile의 구성요소

makefile이 어떻게 작동하는지 알았으니 이제 자세히 뜯어보자
저 코드가 어떻게 작동하는 것일까?
먼저 makefile의 구성요소로는 target, command, prerequisite(전제 조건)이 있다.

  • target : test, main.o, add.o처럼 파란색 글자가 target에 해당한다. target은 원하는 변수명을 넣어서 작성이 가능하며 이 target을 정의하기 위해 command와 prerequisite가 필요하다.
  • command: 분홍색 글자가 command에 해당한다. target이 어떤 동작을 하는지에 대해 정의한다.
    make test를 하면 gcc -o test main.o add.o가 실행될 것이고
    make main.o를하면 gcc -c main.c가 실행될 것이다.
  • prerequisite : target 옆에 하얀 글씨로 적힌 부분이다. target을 만들기 위해 필요한 전제 조건들을 적는다.
    make test를 실행하면 gcc -o test main.o add.o가 실행되어야한다. 하지만 main.o나 add.o는 우리가 미리 가지고 있지 않던 파일들이다. 전제조건으로 main.o와 add.o를 만드는 target을 또 달아줘야한다.

🔆 한 줄 요약
make target을 치면 target에 해당하는 command가 실행되는 것이고 command를 실행하기 위해 필요한 재료는 prerequisite에 있다.


✨ 작성 순서의 중요성과 all

make를 치는 순간 makefile안에 작성된 코드 위에서부터 실행이 시작된다.
위 코드에서는 test를 실행 ➡️ 하려고 봤더니 전제 조건(prerequisite)으로 main.o가 있네? 이거 먼저 실행 ➡️ 또 전제 조건으로 add.o가 있네? 이거 실행 ➡️ 전제 조건 다 실행했네? 이제 command실행
이 순서로 작동한다.
만약 순서를 바꿔서

main.o : main.c
	gcc -c main.c
add .o : add.c
	gcc -c add.c
test : main.o add.o
	gcc -o test main.o add.o

이렇게 작성한다면?
make를 치는 순간 main.o에 해당하는 command실행

서로 연관되어 이어지지 않으면 그것만 실행하고 끝나게 되는 특성을 가지고 있다.
순서에 구애받지 않고 작성하기 위해 'all'이라는 것을 사용할 수 있다.

all : test

main.o : main.c
	gcc -c main.c
add .o : add.c
	gcc -c add.c
test : main.o add.o
	gcc -o test main.o add.o

all을 먼저 적어주면 all의 전제 조건인 test를 먼저 찾아서 실행한다. all을 이용하면 더 편안한 makefile을 만들 수 있다.

🛁 더 편안해지기 위한 매크로 사용법

만약 *.c파일이 많으면? main.o:~~ ,add.o:~~ , check.o:~~.... 10개만 넘어가도 코드가 길어지고 복잡하게 느껴질 것이다.
아래는 내 push_swap과제의 Makefile코드 일부이다.

NAME = push_swap
CC = CC
CFLAGS = -Wall -Wextra -Werror
RM = rm -rf
SRCS = check_error.c count.c find.c first_stack.c \
		ft_split.c is_utils.c make_utils.c pivot.c \
		push_swap.c push.c rev_rot.c rotate.c \
		sort_do.c sort_few.c sort.c swap.c utils.c
OBJS = ${SRCS:.c=.o}
#여기까지가 매크로설정부분

all : $(NAME)

%.o : %.c
	$(CC) $(CFLAGS) -c $^ -o $@

$(NAME) : $(OBJS)
	$(CC) $(CFLAGS) -o $(NAME) $(OBJS)
    
clean :
	$(RM) $(OBJS)

fclean : clean
	$(MAKE) clean
	$(RM) $(NAME)

re : all fclean
	$(MAKE) fclean
	$(MAKE) all

.PHONY : all clean fclean re

위의 간단했던 makefile과 느낌이 많이 다르지만 겁먹지말자!
일단 매크로란 사용자가 정의한 변수에 특정한 것들을 정의해놓는다는 것이다.
makefile에서는 매크로는 대문자로 정의하므로
#여기까지가 매크로설정부분 이라고 쓰인 위의 모든 대문자들(CC, CFLAGS, RM...)들은 매크로라고 보면 되고, 설정된 매크로들은 $(매크로명) 형식으로 사용가능하다.
매크로를 설정해놓으면 어떤것이 좋을까?
코드의 가운데 줄을 보면 쉽게 이해할 수 있을 것이다.

$(NAME) : $(OBJS)
	$(CC) $(CFLAGS) -o $(NAME) $(OBJS)

위 makefile의 가운데 위치한 이 부분을 매크로 없이 쓰면 수많은 오브젝트 파일들을 다 적어주어야 한다. 바로 아래코드처럼 말이다.

push_swap : check_error.o count.o find.o first_stack.c \
		ft_split.o is_utils.o make_utils.o pivot.o \
		push_swap.o push.o rev_rot.o rotate.o \
		sort_do.o sort_few.o sort.o swap.o utils.o
	cc -Wall -Wextra -Werror -o push_swap ft_split.o is_utils.o \
    make_utils.o pivot.o push_swap.o push.o rev_rot.o rotate.o \
    sort_do.o sort_few.o sort.o swap.o utils.o

놀랍게도 위의 코드와 아래의 코드는 똑같이 작동한다. 매크로를 사용하지 않으면 길게 적어야한다는 것 외에도 다른 문제가 있을 수 있다. 만약 check_error.o 의 파일이름이 check_error_error.o로 바뀐다면? 다 찾아서 고쳐줘야한다.

🛀 더 더 편안해지기 위한 여러 규칙들

1️⃣ $@ 와 $^

$(NAME) : $(OBJS)
	$(CC) $(CFLAGS) -o $(NAME) $(OBJS)

이 코드는 더 짧게 아래처럼 줄여 쓸 수 있다.

$(NAME) : $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^
  • $@ : 해당 블록의 target을 의미. 즉 여기서는 $(NAME)을 의미
  • $^ : prerequisite 영역을 의미. 즉 여기서는 $(OBJS)를 의미

2️⃣ %

%.o : %.c
	$(CC) $(CFLAGS) -c $^ -o $@
  • %: %는 wildcard()과 같은 역할이다.
    %.o는
    .o와 같이 작동하며 끝이 .o로 끝나는 모든 파일을 의미한다.

3️⃣ .PHONY

맨 마지막줄에 있는 .PHONY는 특이한 역할을 한다. target으로 작성한 이름과 같은 이름의 파일이 있을 경우 makefile이 제대로 작동하지 않을 수 있는데 .PHONY에 target의 이름을 등록해놓으면 이런 오작동을 방지한다.

🌼 끝으로

makefile은 굉장히 많은 옵션과 기교를 사용할 수 있다. 위에서 설명한 makefile의 원리와 기본 작성법만 알면 여러 옵션을 응용해 자유롭게 makefile을 만들 수 있을 것이다!
makefile manual을 참조하면서 내 makefile을 완성해보자.

profile
에-오

0개의 댓글