Search
Duplicate

[make] Makefile 개념 및 사용법 정리

간단소개
프로그램 빌드에 사용하는 Makefile 개념, 문법을 정리한 글입니다.
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
Makefile
Scrap
태그
기초지식
9 more properties
목차

make와 Makefile

make란?

make 는 소프트웨어 개발을 위해 유닉스 계열 운영체제에서 사용되는 프로그램 빌드 도구이다.

Makefile이란?

Makefile 은 프로그램을 빌드하기 위해 make 문법에 맞춰 작성하는 문서이다.

빌드 예제

세 개의 소스 파일(main.c, foo.c, bar.c)를 각각 컴파일해서 Object 파일(*.o)을 생성하고, 생성한 Object 파일을 하나로 묶는 링크 과정을 통해 실행 파일인 app.out 을 생성한다. 여기서 foo와 bar에 정의된 함수를 main에서 호출하는 의존성이 존재한다.
make를 사용하지 않고 빌드를 한다면 다음과 같은 명령어를 수행하면 된다.
# 오브젝트 파일 생성 gcc -c main.c foo.c bar.c # 실행 파일 생성 gcc -o app.out main.o foo.o bar.o
Bash
복사
하지만, 소스 파일이 많아지고 복잡해지면 위와 같이 사용하는 것이 어려워진다. 이에 대한 대안으로 쉘 스크립트에 빌드 명령을 작성할 수도 있지만, Makefile 이 제공하는 Incremental build 를 사용할 수 없게 된다.
Incremental build 란 반복적인 빌드 과정에서 변경된 소스코드에 의존성(Dependency)이 있는 대상들만 추려서 다시 빌드하는 기능이다.
Makefile 에서 빌드 대상(Target)별로 의존성을 명시하면 자동으로 Incremental build 를 수행하므로 매우 편리하다.

라이브러리

라이브러리란?

다른 프로그램들과 링크되기 위해 존재하는 하나 이상의 서브루틴이나 함수들의 집합 파일을 의미한다.
링크가 될 수 있도록 컴파일된 형태인 오브젝트 파일(*.o) 형태로 존재한다. 미리 컴파일 되어있기 때문에 전체 컴파일 시간이 단축된다.
라이브러리를 언제 포함시키는 지에 따라 동적 라이브러리와 정적 라이브러리로 구분할 수 있다.

라이브러리 구분

동적 라이브러리 : 완성된 프로그램을 실행할 때 포함시키는 동적 라이브러리
정적 라이브러리 : 프로그램을 컴파일 하는 과정에서 포함시키는 오브젝트 파일들의 모임

확장자별 라이브러리 구분

확장자명
운영체제
라이브러리 구분
.a
리눅스
정적
.so
리눅스
동적
.lib
윈도우
정적
.dll
윈도우
동적

동적 라이브러리(Dynamic Library)

프로그램 실행 시 필요할 때만 라이브러리에서 함수를 참조할 수 있는 라이브러리이다.
프로그램이 시작할 때 같이 로딩되고, 프로그램이 종료될 때 메모리에서 해제되는 암시적 링킹(Implicit LInking)을 사용한다.

정적 라이브러리(Static Library)

프로그램 빌드 시, 라이브러리가 제공하는 코드를 실행 파일에 넣는 방식의 라이브러리이다.
컴파일 링킹 단계에서 실행 파일에 결합된다.
정적 라이브러리를 사용하여 컴파일하면 링커가 프로그램이 필요로 하는 부분을 라이브러리에서 찾아 실행 파일에 바로 복사한다.
유닉스 계열 운영체제에서는 아카이버(ar) 유틸리티를 사용해서 오브젝트 파일의 집합인 아카이브(라이브러리)를 생성할 수 있다.
# 1. 오브젝트 파일 생성 gcc -c foo.c bar.c # 2. 오브젝트 파일로 정적 라이브러리 파일 생성 [ *.o -> *.a ] ar rcs lib.a foo.o bar.o
Bash
복사

문법

ar [options] [라이브러리 이름] [오브젝트 파일들] - 옵션 - r : 새로운 오브젝트 파일이면 아카이브에 추가하고, 기존 파일이면 치환한다. - c : 라이브러리 파일이 존재하지 않아도 안내 메시지를 출력하지 않는다. (ar: creating archive lib.a) - s : 아카이브 인덱스를 생성해서 링크 속도를 빠르게 한다.
Plain Text
복사

Makefile 예제

app.out : main.o foo.o bar.o gcc -o app.out main.o foo.o bar.o main.o : foo.h bar.h main.c gcc -c -o main.o main.c foo.o : foo.h foo.c gcc -c -o foo.o foo.c bar.o : bar.h bar.c gcc -c -o bar.o bar.c
Makefile
복사
소스 코드가 위치하는 디렉토리에 위와 같은 Makefile 을 작성하고 다음 명령어를 실행하면 한번에 실행파일(app.out)을 만든다. 이 때, 실행 파일을 만들기 위해 거치는 단계에서 생성되는 오브젝트 파일들(main.o, foo.o, bar.o)도 함께 생성된다.
make
Bash
복사
make 명령 뒤에 Target 을 명시하면 해당 Target 만 빌드한다.
make foo.o
Bash
복사

Makefile 문법

<Target> : <Dependencies> <Recipe>
Makefile
복사
Target : 빌드 대상 이름을 의미한다. 최종적으로 생성하는 파일명을 작성한다.
Dependencies : 빌드 대상이 의존하는 Target 이나 파일 목록이다. 여기에 나열된 대상들을 먼저 만들고 Recipe 의 명령어를 실행한다.
Recipe : 빌드 대상을 생성하는 명령이다. 여러 줄로 작성할 수 있고, 각 줄 시작에 반드시 Tab 문자로 Indent 가 삽입되어야 한다.
@ : Shell 명령어를 실행하되, 실행하는 명령어를 출력하지 않을 때 사용한다.
hello : echo hello @echo this line printed only once
Makefile
복사
make hello 실행 결과는 다음과 같다.
echo hello hello this line printed only once
Bash
복사

Makefile 내장 규칙 (Built-in Rule)

Make 에서 자주 사용되는 빌드 규칙들은 내장되어 있다. 대표적으로 소스 파일(*.c) 을 컴파일해서 오브젝트 파일(*.o)로 만드는 규칙이 있다.
app.out : main.o foo.o bar.o gcc -o app.out main.o foo.o bar.o
Makefile
복사
하지만 위와 같이 작성할 경우 Incremental build 를 위한 의존성 검사에서 헤더 파일의 변경을 감지하지 못하는 문제가 발생한다. Make 는 소스 파일의 마지막 변경 시점만 확인하지, 소스 코드 내부의 변경 사항을 하나씩 대조하지 않기 때문이다.
따라서 다음과 같이 각 Target 에 대한 Dependencies 까지 명시해주는 것이 바람직하다.
app.out : main.o foo.o bar.o gcc -o gcc.out main.o foo.o bar.o main.o : foo.h bar.h main.c foo.o : foo.h foo.c bar.o : bar.h bar.c
Makefile
복사
위와 같이 작성하면 헤더 파일만 변경되어도 의존성이 올바르게 탐지된다. 마지막 세 줄에 있는 Target 의 Recipe 는 모두 생략되었지만, Make 내부 규칙에 의해 컴파일이 수행된다.

변수 선언

make 에서 변수를 선언하는 방법을 크게 2가지가 있다.

1. Recursively expanded variable

= 기호를 사용해서 변수에 값을 할당하는 방법이다. 변수에 다른 변수를 참조하고 있다면, 다른 변수가 참조하고 있는 값을 참조한다.
foo = $(bar) bar = $(ugh) ugh = Huh? all:;echo $(foo)
Makefile
복사
위의 실행결과로 Huh? 가 출력된다.

장점

여러 변수를 사용해서 의도한 대로 make 를 작동시킬 수 있다.
CFLAGS = $(include_dirs) -O include_dirs = -lfoo -lbar
Makefile
복사
변수 CFLAGS 에는 최종적으로 -lfoo -lbar -O 가 할당된다.

단점

변수 뒤에 다른 것을 추가할 수 없다. 이는 무한 반복에 빠지기 때문이다.
foo = $(foo) -o all:;echo $(foo)
Makefile
복사
위와 같이 Makefile 을 작성하고 터미널에서 make 를 실행시키면 다음과 같은 오류 메세지가 출력된다.
Makefile:1: *** Recursive variable `foo' references itself (eventually). Stop.
Bash
복사
이처럼 make 는 무한 반복을 감지하면 오류를 발생시킨다.

2. Simply expanded variable

재귀적 확장 변수의 단점을 보완하기 위한 변수이다. := 기호를 이용해서 변수에 값을 할당한다. = 와는 달리 재귀적으로 작동하지 않으며, 변수에 대입된 값을 그대로 출력한다.
foo := $(foo) bar all:;echo $(foo)
Makefile
복사
위와 같이 작성하고 실행시킨 결과는 다음과 같다.
echo bar bar
Bash
복사
변수를 참조하는 시점에 저장된 값이 아무 것도 없었기 때문에 변수 대신 어떠한 값도 들어가지 않았다. 또한, foo 변수를 스스로 참조하는 무한 반복에 빠지지 않았다. 이러한 특징 덕분에 변수 뒤에 다른 값을 추가할 수 있는 장점이 있다.
x := hello x := $(x) world all:;echo $(x)
Makefile
복사
실행 결과는 다음과 같다.
echo hello world hello world
Bash
복사

자동 변수(Automatic variables)

$@ : Target 을 지칭한다.
$< : 첫 번째 Dependency 이름을 지칭한다.
$^ : 현재 Target이 의존하는 Dependencies 의 전체 목록 (공백으로 구분)
$? : 현재 Target이 의존하는 대상들 중 Target 보다 새롭게 변경된 Dependencies 의 전체 목록

내장 변수 (Built-in Variable)

$(CC) : 컴파일러
$(CFLAGS) : 컴파일 옵션

리링크(Relink)

리링크는 의존성이 변경되었을 때만 타겟을 생성하는 것을 의미한다. 즉, 의존성이 변경되지 않았다면 리링크가 수행되면 안된다.
bonus: $(OBJ_B) $(AR) $(TARGET) $^
Makefile
복사
위의 코드는 OBJ_B 가 존재하면 TARGET 을 생성한다. OBJ_B 가 변경되었는지 체크하지 않기 때문에 make bonus 를 실행하면 무조건 리링크한다.
ifdef WITH_BONUS OBJ = $(OBJ_O) $(OBJ_B) else OBJ = $(OBJ_O) endif ... bonus: make WITH_BONUS=1 all
Makefile
복사
위의 코드는 make bonus 를 하면 WITH_BONUS 라는 상수를 1로 지정하여 코드를 실행한다. WITH_BONUS 가 1일 때는 OBJOBJ_OOBJ_B 모두 변경사항을 체크하기 때문에 리링크가 방지된다.

패턴 규칙(Pattern Rules)

% 기호는 와일드 카드와 비슷한 역할을 한다.
%.o : %.c $(CC) $(CFLAGS) -c $<
Makefile
복사
오브젝트 파일(.o)는 .c 로 끝나는 파일에 의존한다는 것으로 해석할 수 있다.
.c.o: $(CC) $(CFLAGS) -c $<
Makefile
복사
.c.o 는 Old-fashioned 표현이라고 공식 문서에서 언급하고 있다. 기능은 동일하다.

함수

기본 문법

$(function arguments)
Makefile
복사

치환 함수

1. subst

$(subst ee,EE,feet on the street)
Makefile
복사
위 함수의 실행 결과로 ‘fEEt on the strEEt’ 가 생성된다.

2. 변수를 활용한 치환

$(변수명:pattern=replacement)
Makefile
복사
선언된 변수를 활용해서 문자열을 치환할 수도 있다.
SRCS := first.c second.c third.c OBJS := $(SRCS:.c=.o) # 치환 결과 # OBJS = first.o second.o third.o
Makefile
복사
OBJS 에는 SRCS 에서 .c 와 매칭되는 문자열을 .o 로 치환한다.
이는 patsubst 함수와 동일한 기능을 수행한다.
SRCS := first.c second.c third.c OBJS := $(patsubst %.c,%.o,$(SRCS)) # 치환 결과 # OBJS = first.o second.o third.o
Makefile
복사

파일 이름 관련 함수

1. addprefix

$(addprefix prefix,names...)
Makefile
복사
접미사를 names 앞에 추가하는 함수이다.
# 실행 예제 $(addprefix src/,foo bar) # 실행 결과 src/foo src/bar
Makefile
복사

가짜 목표 파일 .PHONY

.PHONY 는 Target 위치에 작성하지만, 레시피 실행을 위한 이름일 뿐, 실제 파일 이름이 아니라는 것을 알려주기 위해 사용한다. 참고로 phony 는 사전적으로 ‘가짜’라는 의미이다. 맨 윗줄에 작성하는 것이 편하다.

사용 목적

1. 실제 파일 이름과 충돌 해결

디렉토리에 파일명이 test 인 파일이 있다고 해보자.
test : @echo test makefile
Makefile
복사
위와 같이 작성하고 실행하면 결과는 다음과 같다.
$ make test make: 'test' is up to date. # 출력 메시지
Bash
복사
이처럼 make 의 Target 과 디렉토리 내 파일명과 분리하기 위해서 .PHONY 를 사용한다.
.PHONY : test test : @echo test makefile
Makefile
복사
실행 결과는 다음과 같다.
$ make test test makefile # 출력 메시지
Bash
복사

2. 성능 개선

Makefile 안에서 make 를 다시 실행시키는 경우 문제가 발생한다. 다음과 같이 디렉토리가 구성되었다고 해보자.
/main |_ Makefile |_ /foo |_ Makefile |_ ... // other files |_ /bar |_ Makefile |_ ... // other files |_ /koo |_ Makefile |_ ... // other files
Makefile
복사
main 디렉토리의 Makefile 은 다음과 같다.
SUBDIRS = foo bar koo subdirs : for dir in $(SUBDIRS); do \ $(MAKE) -C $$dir; \ done
Makefile
복사
make subdirs 을 실행시키면 다음과 같은 문제가 생긴다.
1.
하위 make 에서 에러가 발생해도 계속 빌드가 진행된다.
2.
하나의 규칙만 실행되기 때문에 make 의 장점인 병렬 수행이 되지 않는다.
이러한 문제를 해결하기 위해 다음과 같이 작성할 수 있다.
SUBDIRS = foo bar koo .PHONY : subdirs $(SUBDIRS) subdirs : $(SUBDIRS) $(SUBDIRS) : $(MAKE) -C $@
Makefile
복사
참고로 make 명령어의 -C 옵션은 makefile 을 읽기 전에 해당 디렉토리로 이동하는 옵션이다.
subsystem: cd subdir && $(MAKE)
Makefile
복사
위와 같은 명령어가 있을 때, 다음과 같이 작성할 수 있다.
subsystem: $(MAKE) -C subdir
Makefile
복사
병렬 수행을 하기 위해서는 make 명령어에서 -j 옵션을 사용하면 된다.

참고자료

ar (유닉스) [위키백과]
[42] libft [티스토리]