본문 바로가기
임베디드 개발/펌웨어

Makefile 작성법

by eteo 2024. 6. 22.

 

 

Makefile이란?

Makefile은 프로젝트의 컴파일, 빌드 및 기타 작업을 자동화하는데 사용되는 파일로 make라는 유틸리티와 함께 사용된다. Makefile은 일련의 규칙과 지시를 포함하고 있어서 해당 규칙에 맞게 작성하면 소스코드를 컴파일하는 과정을 간소화할 수 있다.

 

 

 


왜 Makefile이 필요한가?

  • 자동화: Makefile을 사용하면 여러 파일을 컴파일하고 링크하는 과정을 자동화할 수 있다.
  • 효율성: 의존성 파일의 타임스탬프를 비교하여 변경된 파일만 다시 컴파일하므로 빌드 시간을 줄여준다.
  • 재사용성: 프로젝트마다 반복되는 빌드 작업을 쉽게 재사용할 수 있다.
  • 일관성: 모든 개발자가 동일한 빌드 과정을 따르도록 할 수 있다.

 

 


Makefile 작성법을 익히기 위한 예제 프로젝트

 

 

디렉토리 구성

/project
  ├── include
  │   ├── foo.h
  │   └── bar.h
  ├── main.c
  ├── foo.c
  ├── bar.c
  └── Makefile

 

 

 

main.c

#include <stdio.h>
#include "foo.h"
#include "bar.h"

int main() {
    foo();
    bar();
    return 0;
}

 

foo.c

#include <stdio.h>
#include "foo.h"

void foo() {
    printf("This is foo.\n");
}

 

bar.c

#include <stdio.h>
#include "bar.h"

void bar() {
    printf("This is bar.\n");
}

 

include/foo.h

#ifndef __FOO_H__
#define __FOO_H__

void foo();

#endif

 

include/bar.h

#ifndef __BAR_H__
#define __BAR_H__

void bar();

#endif

 

 

 

 


Makefile 작성

# 컴파일러
CC = gcc

# 컴파일 옵션
CFLAGS = -Wall -g -I./include

# 대상 바이너리 파일
TARGET = example

# 소스 파일
SRCS = main.c foo.c bar.c

# 목적 파일
OBJS = $(SRCS:.c=.o)

# 기본 타겟
all: $(TARGET)

# 타겟을 빌드하는 규칙
$(TARGET): $(OBJS)
        $(CC) $(CFLAGS) -o $@ $^

# .c 파일을 .o 파일로 컴파일하는 규칙
%.o: %.c
        $(CC) $(CFLAGS) -c -o $@ $<

# 정리 규칙
clean:
        rm -f $(OBJS) $(TARGET)

 

 

 

 

 

 


Makefile 하나씩 뜯어보기

 

# 컴파일러
CC = gcc

 

 

여기서 CC 변수는 사용할 컴파일러를 지정한다. 보통 크로스 컴파일러의 경우 gcc 앞에 타겟 아키텍쳐와 로컬 환경 등이 붙기 때문에 CROSS = '크로스 컴파일러 접두사', CC = $(CROSS)gcc 이렇게 사용하기도 한다.

 

 

# 컴파일 옵션
CFLAGS = -Wall -g -I./include

 

CFLAGS 변수는 컴파일러 옵션을 정의한다. 여기서 -Wall은 모든 경고 메시지를 출력하게 하고, -g는 디버깅 정보를 포함시킨다. 그리고 헤더 파일이 다른 디렉토리에 있는 경우 '-I' 옵션 뒤에 디렉토리 경로를 추가하면 된다.

CFLAGS 외에도 링커 옵션인 LDFLAGS도 있을 수 있다.

 

 

# 대상 바이너리 파일
TARGET = example

 

TARGET 변수로 최종적으로 생성될 실행 파일의 이름을 정의하고 있다. 이후 make 명령을 사용해 빌드하면 'example'이란 실행파일이 생긴다. 여기서 변수명은 TARGET이 아닌 다른 이름으로도 설정할 수 있다.

 

 

# 소스 파일
SRCS = main.c foo.c bar.c

 

SRCS 변수는 프로젝트에 포함된 모든 소스 파일을 나열하고 있다.

혹은 이렇게 나열하지 않고 SRCS := $(wildcard *.c)와 같이 wildcard를 사용해서 현재 디렉토리에 있는 모든 .c 파일을 포함시킬 수 있다.

 

 

# 목적 파일
OBJS = $(SRCS:.c=.o)

 

OBJS 변수는 소스 파일을 컴파일한 후 생성될 목적 파일(.o 파일)을 정의한다. SRCS 변수를 참조해서 .c 확장자를 .o로 바꾸기만 하고 있다.

 

 

# 기본 타겟
all: $(TARGET)

 

all은 가장 먼저 등장하는 기본 타겟으로, make 명령어를 실행했을 때 기본적으로 호출되는 타겟이다. 보통 기본 타겟은 all이나 default로 정의되지만, 다른 이름으로도 설정할 수도 있다.

혹은 이 문장을 아예 생략할 수도 있다 그렇게 하면 이 아래 등장하는 타겟인 $(TARGET)이 기본 타겟이 된다.

 

 

# 타겟을 빌드하는 규칙
$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

 

이 규칙은 목적 파일들을 링크해서 최종 실행 파일을 만드는 과정이다. 아랫줄에 TAB을 하고 실행 명령이 등장하는데 CC 변수와 CFLAGS 변수를 참조해 컴파일 한다. 여기서 $@는 윗줄 ':' 왼쪽에 나오는 현재 타겟을, $^는 그 오른쪽에 있는 모든 의존성을 의미한다.

 

 

# .c 파일을 .o 파일로 컴파일하는 규칙
%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

 

이 규칙은 .c 파일을 .o 파일로 컴파일하는 과정이다. 여기서 $@는 현재 타겟을, $<는 첫 번째 의존성을 의미한다.

그리고 사실 이 규칙을 통째로 생략해도 된다. Make 명령은 암시적 규칙에 의해 .o 파일이 없는 경우 같은 이름의 .c 파일을 찾아 자동으로 컴파일 하기 때문이다.

 

 

 

# 정리 규칙
clean:
	rm -f $(OBJS) $(TARGET)

 

clean 타겟은 빌드 결과물인 목적 파일과 실행 파일을 삭제하는 명령을 실행하는 규칙이다.

 

 

 

 


 

Makefile 기본 구조

 

타겟: 의존성
    실행 명령어
  • 타겟(Target): 빌드할 파일 또는 실행할 작업의 이름
  • 의존성(Dependencies): 타겟을 빌드하기 위해 필요한 파일들
  • 실행 명령어(Commands): 타겟을 빌드하거나 작업을 수행하는 명령어들로 반드시 탭(TAB)으로 시작해야 한다.

 

 

✔️ Makefile에서 처음으로 나타나는 타겟이 기본 타겟(Default Target)으로 설정되어, make 명령어를 실행했을 때 기본적으로 이 타겟이 실행된다. 또한, 타겟이 의존성을 가지고 있다면, 그 의존성이 먼저 실행된다.

 

 

✔️   기본 타겟과 기본 타겟의 의존성에 의해 실행 될 타겟들 말고도 다양한 작업을 위해 여러 타겟을 정의할 수 있다. 그런 타겟을 실행할 땐 'make 타겟명'으로 실행한다.

 

 

✔️   make 명령은 암시적 규칙에 의해 기본적으로 .o 파일이 없으면 대응하는 .c 파일을 찾아 컴파일한 후 링크한다.

 

 

✔️  Makefile에서 wildcard를 사용하면 파일 패턴을 매칭하여 해당 패턴에 맞는 파일들의 목록을 반환하는 기능을 한다.

$(wildcard pattern)
$(wildcard *.c)

 

 

 

 

 


 

Makefile 실행

 

터미널에서 make 명령 실행

$ make

 

빌드된 실행파일 실행하여 확인

./example

 

정리

make clean

 

 

만약 깨끗히 재빌드를 하려면 make clean 후 make를 하면 되고, 의존성 파일 중 일부만 수정한 뒤 make clean 없이 바로 make 명령을 사용하면 프로젝트 전체를 컴파일하는게 아니라 수정된 파일만 재컴파일하여 빌드한다.

 

 

 

 

 


 

 

Makefile에서 변수 값 할당 방식

 

Makefile의 앞부분을 보면 CC, CFLAGS, TARGET 등 변수의 값을 할당하고 명령어에서 $(변수)로 해당 변수의 값을 참조하는 것을 볼 수 있다. Makefile에서 변수 값을 할당하는 방식은 주로 등호(=)를 사용하나 그 외에도 다양한 방식이 존재하며 각 방식마다 값 평가시점의 차이가 존재한다.

 

  • 변수 = 값
    • 지연평가 방식으로 변수를 실제 참조할 때 값이 계산된다. 따라서 변수의 값이 평가 시점 전까지 계속 바뀔 수 있다.
  • 변수 := 값
    • 즉시 평가 방식으로 변수 선언 시점에 값을 계산하고, 이후 변수의 값은 고정된다.
  • 변수 ?= 값
    • 조건부 할당 방식으로 변수가 정의되지 않은 경우에만 값을 할당한다. 이미 정의된 변수의 값이 있다면 ?=로 할당한 값은 무시된다.
  • 변수 += 값
    • 값 추가 방식으로 기존 변수의 값에 공백으로 구분된 새 값을 추가하는 방식으로 작동한다. 여러 줄로 나누어 값을 추가할 때 유용한다.